This commit is contained in:
Michal Vrba
2026-02-11 14:12:30 +01:00
parent 1535628412
commit 7b77754bc7
2 changed files with 521 additions and 115 deletions
+61 -49
View File
@@ -1,77 +1,89 @@
# Ako Hrat PokerRogue (Prototyp) # Ako hrat PokerRogue (prototyp)
PokerRogue je singleplayer hra, kde sa boj riesi pokerom. PokerRogue je singleplayer poker-combat hra. Kazda handa je jeden suboj.
Kazde kolo boja je jedna poker handa.
## Ciel ## Ciel
- Znizit HP nepriatela na 0 skor, ako nepriatel znizi tvoje HP na 0. - Znizit HP nepriatela na 0 skor, ako nepriatel znizi tvoje HP na 0.
- Boj trva obmedzeny pocet hand (`Hand X/Y` hore na obrazovke). - Boj ma limit hand (`Hand X/Y` v hornom paneli).
## Zakladny priebeh ## Priebeh handy (aktualna logika)
1. Zobrazi sa tvoja ruka (2 karty), board (5 kariet) a zamer nepriatela. 1. Dostanes 2 karty, nepriatel ma 2 karty, na stole je board 5 kariet.
2. Vyberies akciu (`1`, `2`, `3`). 2. Rozhodujes sa po streetoch:
3. Vyhodnoti sa handa: - `Flop` (3 karty),
- kto ma silnejsiu kombinaciu, vyhrava handu, - `Turn` (4 karty),
- podla Pot/Edge/Shield sa vypocita damage. - `River` (5 kariet).
4. Na 5 sekund sa ukaze showdown: 3. Na kazdom kroku vyberas akciu (`1` az `7`), okrem `Fold`, ktory handu okamzite ukonci.
- nepriatelove karty, 4. Po River sa z troch vybranych akcii vypocita finalna bojova akcia.
- zlate obrysy vitaznej 5-kartovej kombinacie. 5. Vyhodnoti sa showdown (najlepsich 5 zo 7 kariet), aplikuje sa damage.
5. Zacne dalsia handa. 6. Vysledok ostane na obrazovke, pokracujes az po `Space`/`Enter`.
## Ovládanie ## Ovládanie
- `1` = `Strike` (agresivne) - `1` = `Strike`
- `2` = `Guard` (defenzivne) - `2` = `Guard`
- `3` = `Dodge` (znizenie rizika) - `3` = `Dodge`
- `R` = restart boja - `4` = `Check`
- `Esc` = ukoncit hru - `5` = `Probe`
- `6` = `All-In`
- `7` = `Fold` (okamzite skonci handu)
- `Space` / `Enter` = pokracovat po showdowne
- `R` = novy run
- `Esc` = koniec hry
## Co robia akcie ## Co robia akcie
### Strike (`1`) - `Strike`: `Pot +3`, `PlayerEdge +10`
- `Guard`: `Shield +6`, `PlayerEdge +5`
- `Dodge`: `Pot -2` (minimum 1), `Shield +2`
- `Check`: `Shield +1`, `PlayerEdge +2`
- `Probe`: `Pot +1`, `PlayerEdge +6`
- `All-In`: `Pot +8`, `PlayerEdge +18`, `EnemyEdge +8`
- `Fold`: okamzita prehra handy, ziskavas vacsi `Shield`, damage je znizeny oproti beznej prehre
- zvysi `Pot` ## Edge a Shield (dolezite)
- zvysi tvoj `Edge`
- vhodne, ked chces tlacit na vysoky damage
### Guard (`2`) ### Edge
- prida `Shield` `Edge` je percent bonus k damage:
- mierne zvysi tvoj `Edge`
- vhodne, ked cakas silny utok nepriatela
### Dodge (`3`) - ak vyhras handu ty: `EnemyDamage = Pot * (100 + PlayerEdge) / 100`
- ak vyhra nepriatel: `RawPlayerDamage = Pot * (100 + EnemyEdge) / 100`
- znizi `Pot` (mensie riziko aj odmena) Cim vyssi `Edge`, tym vacsie potencialne zranenie.
- da mensi `Shield`
- vhodne na stabilizaciu pri nevyhodnej hande ### Shield
`Shield` znizuje damage, ktory dostanes pri prehratej hande:
- `PlayerDamage = max(0, RawPlayerDamage - Shield)`
`Shield` sa pocita len pri damage do hraca v danej hande.
## Ako sa urcuje vitaz handy ## Ako sa urcuje vitaz handy
Hra vyhodnocuje najlepsiu 5-kartovu kombinaciu zo 7 kariet: Vyhodnocuje sa najlepsia 5-kartova kombinacia zo 7 kariet:
- 2 hole cards + 5 board cards - tvoje 2 karty + 5 kariet na stole,
- nepriatelove 2 karty + tych istych 5 kariet na stole.
Porovnanie funguje ako v pokri: Porovnanie:
1. najprv typ kombinacie (napr. Pair, Two Pair, Straight, Flush...) 1. Typ kombinacie (Pair, Two Pair, Straight, Flush, ...)
2. pri rovnakom type rozhoduju kickery 2. Pri rovnosti rozhoduju kickery (rank vector).
Panel vpravo ukazuje: ## Vizualne pomocky v hre
- nazov kombinacie hraca a nepriatela, - Hore vpravo su animovane bary:
- dovod vyhry/prehry, - `Player Edge`,
- vysledny vypocet damage. - `Enemy Edge`,
- `Shield`.
- Po showdowne su vitazne karty zvyraznene zlatym obrysom.
- Nepriatelove karty sa odkryju pri showdowne.
## Tipy pre zaciatok ## Tipy
- Ak mas dobru kombinaciu, casto sa oplati `Strike`. - Silna handa + vyssi `PlayerEdge` = velky tlak na HP nepriatela.
- Ak je nepriatel agresivny, `Guard` ti casto zachrani HP. - Pri riziku prehry pomaha `Guard`, `Check` alebo `Dodge` (kvoli `Shield`).
- `Dodge` pouzi, ked nechces prehrat velky pot. - `Fold` je takticky ustupok: prehras handu, ale casto prezijes lacnejsie.
- Sleduj zlate obrysy po hande, rychlo sa naucis, ktore karty rozhodli.
## Poznamka k verzii
Toto je prototyp. Balans, grafika a pravidla sa budu postupne menil.
+458 -64
View File
@@ -24,13 +24,14 @@ public class Game1 : Microsoft.Xna.Framework.Game
private FightState _fightState = null!; private FightState _fightState = null!;
private KeyboardState _previousKeyboardState; private KeyboardState _previousKeyboardState;
private string _feedbackText = "Press 1/2/3 to act."; private string _feedbackText = "Press 1..7 to act.";
private float _feedbackSecondsRemaining; private float _feedbackSecondsRemaining;
private float _showdownSecondsRemaining; private float _showdownSecondsRemaining;
private bool _awaitingContinue; private bool _awaitingContinue;
private const float ShowdownDurationSeconds = 5.0f; private const float ShowdownDurationSeconds = 5.0f;
private const float TurnRevealAtSeconds = 1.2f; private const float TurnRevealAtSeconds = 1.2f;
private const float RiverRevealAtSeconds = 2.4f; private const float RiverRevealAtSeconds = 2.4f;
private const float EnemyTurnDelaySeconds = 0.55f;
private Card[] _showdownPlayerCards = Array.Empty<Card>(); private Card[] _showdownPlayerCards = Array.Empty<Card>();
private Card[] _showdownEnemyCards = Array.Empty<Card>(); private Card[] _showdownEnemyCards = Array.Empty<Card>();
@@ -42,11 +43,37 @@ public class Game1 : Microsoft.Xna.Framework.Game
private int _runMaxHp = 100; private int _runMaxHp = 100;
private int _runPlayerHp = 100; private int _runPlayerHp = 100;
private int _runPotBonus; private int _runPotBonus;
private int _runPlayerDexterity = 10;
private string _encounterText = string.Empty; private string _encounterText = string.Empty;
private DecisionStage _decisionStage = DecisionStage.Flop; private DecisionStage _decisionStage = DecisionStage.Preflop;
private TurnActor _activeTurn = TurnActor.Player;
private bool _playerActsSecond;
private bool _playerActedThisStreet;
private bool _enemyActedThisStreet;
private float _enemyTurnSecondsRemaining;
private TurnActor _lastSecondActor = TurnActor.None;
private readonly List<string> _currentHandActionLog = new();
private int _playerDexterity = 10;
private int _enemyDexterity = 10;
private int _lastPlayerInitiative;
private int _lastEnemyInitiative;
private readonly Random _initiativeRng = new();
private PlayerAction? _preflopAction;
private PlayerAction? _flopAction; private PlayerAction? _flopAction;
private PlayerAction? _turnAction; private PlayerAction? _turnAction;
private PlayerAction? _riverAction; private float _displayPlayerEdge;
private float _displayEnemyEdge;
private float _displayShield;
private int _lastPlayerEdge = int.MinValue;
private int _lastEnemyEdge = int.MinValue;
private int _lastShield = int.MinValue;
private float _playerEdgePulse;
private float _enemyEdgePulse;
private float _shieldPulse;
private int _pendingPlayerEdgeDelta;
private int _pendingEnemyEdgeDelta;
private int _pendingShieldDelta;
private readonly List<FloatingStatText> _floatingStatTexts = new();
public Game1() public Game1()
{ {
@@ -79,6 +106,7 @@ public class Game1 : Microsoft.Xna.Framework.Game
protected override void Update(GameTime gameTime) protected override void Update(GameTime gameTime)
{ {
var dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
var keyboardState = Keyboard.GetState(); var keyboardState = Keyboard.GetState();
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || keyboardState.IsKeyDown(Keys.Escape)) if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || keyboardState.IsKeyDown(Keys.Escape))
{ {
@@ -127,6 +155,11 @@ public class Game1 : Microsoft.Xna.Framework.Game
_runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 20); _runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 20);
AdvanceToNextNode(); AdvanceToNextNode();
} }
else if (Pressed(keyboardState, Keys.D4) || Pressed(keyboardState, Keys.NumPad4))
{
_runPlayerDexterity += 1;
AdvanceToNextNode();
}
break; break;
case RunScreen.Fight: case RunScreen.Fight:
UpdateFightScreen(keyboardState, gameTime); UpdateFightScreen(keyboardState, gameTime);
@@ -140,14 +173,17 @@ public class Game1 : Microsoft.Xna.Framework.Game
break; break;
} }
UpdateHudAnimations(dt);
UpdateFloatingTexts(dt);
if (_feedbackSecondsRemaining > 0f) if (_feedbackSecondsRemaining > 0f)
{ {
_feedbackSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds; _feedbackSecondsRemaining -= dt;
} }
if (_showdownSecondsRemaining > 0f) if (_showdownSecondsRemaining > 0f)
{ {
_showdownSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds; _showdownSecondsRemaining -= dt;
if (_showdownSecondsRemaining <= 0f) if (_showdownSecondsRemaining <= 0f)
{ {
_showdownSecondsRemaining = 0f; _showdownSecondsRemaining = 0f;
@@ -209,12 +245,16 @@ public class Game1 : Microsoft.Xna.Framework.Game
_runMaxHp = 100; _runMaxHp = 100;
_runPlayerHp = 100; _runPlayerHp = 100;
_runPotBonus = 0; _runPotBonus = 0;
_runPlayerDexterity = 10;
_runScreen = RunScreen.Map; _runScreen = RunScreen.Map;
_encounterText = string.Empty; _encounterText = string.Empty;
_decisionStage = DecisionStage.Flop; _decisionStage = DecisionStage.Preflop;
_activeTurn = TurnActor.Player;
_preflopAction = null;
_flopAction = null; _flopAction = null;
_turnAction = null; _turnAction = null;
_riverAction = null; _currentHandActionLog.Clear();
ResetPendingHudDeltas();
ResetShowdownState(); ResetShowdownState();
ShowFeedback("New run started."); ShowFeedback("New run started.");
} }
@@ -243,7 +283,8 @@ public class Game1 : Microsoft.Xna.Framework.Game
} }
_runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 18); _runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 18);
_encounterText = "Shop: Purchased provisions (+18 HP)."; _runPlayerDexterity += 1;
_encounterText = "Shop: Purchased light gear (+18 HP, +1 DEX).";
_runScreen = RunScreen.Encounter; _runScreen = RunScreen.Encounter;
} }
@@ -277,12 +318,17 @@ public class Game1 : Microsoft.Xna.Framework.Game
MaxHands: maxHands, MaxHands: maxHands,
Enemy: enemy); Enemy: enemy);
_enemyDexterity = node.Type switch
{
NodeType.Boss => 13,
NodeType.Elite => 12,
_ => 10
};
_playerDexterity = _runPlayerDexterity;
_fightState = FightEngine.StartFight(config, seed: Environment.TickCount); _fightState = FightEngine.StartFight(config, seed: Environment.TickCount);
_decisionStage = DecisionStage.Flop;
_flopAction = null;
_turnAction = null;
_riverAction = null;
ResetShowdownState(); ResetShowdownState();
SetupHandOrder();
_runScreen = RunScreen.Fight; _runScreen = RunScreen.Fight;
} }
@@ -291,33 +337,34 @@ public class Game1 : Microsoft.Xna.Framework.Game
var showdownActive = IsShowdownVisible; var showdownActive = IsShowdownVisible;
if (_fightState.Phase != HandPhase.End && !showdownActive) if (_fightState.Phase != HandPhase.End && !showdownActive)
{ {
var pickedAction = ReadActionKey(keyboardState); if (_activeTurn == TurnActor.Enemy)
if (pickedAction is not null)
{ {
if (_decisionStage == DecisionStage.Flop) _enemyTurnSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds;
if (_enemyTurnSecondsRemaining <= 0f)
{ {
_flopAction = pickedAction.Value; ResolveEnemyStreetAction();
_decisionStage = DecisionStage.Turn;
ShowFeedback($"Flop action: {_flopAction.Value}. Pick turn action.");
} }
else if (_decisionStage == DecisionStage.Turn)
{
_turnAction = pickedAction.Value;
_decisionStage = DecisionStage.River;
ShowFeedback($"Turn action: {_turnAction.Value}. Pick river action.");
} }
else else
{ {
_riverAction = pickedAction.Value; var pickedAction = ReadActionKey(keyboardState);
var combined = CombineActions( if (pickedAction is not null)
_flopAction ?? PlayerAction.Check, {
_turnAction ?? PlayerAction.Check, if (pickedAction.Value == PlayerAction.Fold)
_riverAction.Value); {
ApplyActionAndFeedback(combined, _flopAction, _turnAction, _riverAction); _currentHandActionLog.Add($"{StageLabel(_decisionStage)}: Player Fold");
_flopAction = null; SpawnActionFloaters(PlayerAction.Fold);
_turnAction = null; ApplyActionAndFeedback(PlayerAction.Fold);
_riverAction = null; ResetStreetState();
_decisionStage = DecisionStage.Flop; return;
}
StorePlayerAction(pickedAction.Value);
ApplyPendingDeltaForAction(pickedAction.Value);
SpawnActionFloaters(pickedAction.Value);
_currentHandActionLog.Add($"{StageLabel(_decisionStage)}: Player {pickedAction.Value}");
_playerActedThisStreet = true;
AdvanceStreetIfReady();
} }
} }
} }
@@ -328,10 +375,11 @@ public class Game1 : Microsoft.Xna.Framework.Game
_showdownPlayerCards = Array.Empty<Card>(); _showdownPlayerCards = Array.Empty<Card>();
_showdownEnemyCards = Array.Empty<Card>(); _showdownEnemyCards = Array.Empty<Card>();
_showdownBoardCards = Array.Empty<Card>(); _showdownBoardCards = Array.Empty<Card>();
_decisionStage = DecisionStage.Flop; _currentHandActionLog.Clear();
_flopAction = null; if (_fightState.Phase != HandPhase.End)
_turnAction = null; {
_riverAction = null; SetupHandOrder();
}
ShowFeedback("Next hand."); ShowFeedback("Next hand.");
} }
@@ -376,7 +424,7 @@ public class Game1 : Microsoft.Xna.Framework.Game
private void DrawMapScreen() private void DrawMapScreen()
{ {
DrawLine(new Vector2(40, 24), $"Map - Node {_currentNodeIndex + 1}/{_runNodes.Count} | HP {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus}"); DrawLine(new Vector2(40, 24), $"Map - Node {_currentNodeIndex + 1}/{_runNodes.Count} | HP {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus} | DEX {_runPlayerDexterity}");
for (var i = 0; i < _runNodes.Count; i++) for (var i = 0; i < _runNodes.Count; i++)
{ {
var node = _runNodes[i]; var node = _runNodes[i];
@@ -391,17 +439,18 @@ public class Game1 : Microsoft.Xna.Framework.Game
{ {
DrawLine(new Vector2(50, 60), "Node Result"); DrawLine(new Vector2(50, 60), "Node Result");
DrawLine(new Vector2(50, 110), _encounterText); DrawLine(new Vector2(50, 110), _encounterText);
DrawLine(new Vector2(50, 160), $"Current HP: {_runPlayerHp}/{_runMaxHp}"); DrawLine(new Vector2(50, 160), $"Current HP: {_runPlayerHp}/{_runMaxHp} | DEX: {_runPlayerDexterity}");
DrawLine(new Vector2(50, 220), "Press Space to continue."); DrawLine(new Vector2(50, 220), "Press Space to continue.");
} }
private void DrawRewardScreen() private void DrawRewardScreen()
{ {
DrawLine(new Vector2(50, 44), "Reward Choice"); DrawLine(new Vector2(50, 44), "Reward Choice");
DrawLine(new Vector2(50, 88), $"HP: {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus}"); DrawLine(new Vector2(50, 88), $"HP: {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus} | DEX {_runPlayerDexterity}");
DrawLine(new Vector2(50, 148), "1 - Max HP +10 and heal +12"); DrawLine(new Vector2(50, 148), "1 - Max HP +10 and heal +12");
DrawLine(new Vector2(50, 182), "2 - Pot bonus +2 for next fights"); DrawLine(new Vector2(50, 182), "2 - Pot bonus +2 for next fights");
DrawLine(new Vector2(50, 216), "3 - Immediate heal +20"); DrawLine(new Vector2(50, 216), "3 - Immediate heal +20");
DrawLine(new Vector2(50, 250), "4 - Dexterity +1 (better initiative)");
} }
private void DrawEndScreen(string title, string subtitle) private void DrawEndScreen(string title, string subtitle)
@@ -418,10 +467,7 @@ public class Game1 : Microsoft.Xna.Framework.Game
_showdownPlayerCards = Array.Empty<Card>(); _showdownPlayerCards = Array.Empty<Card>();
_showdownEnemyCards = Array.Empty<Card>(); _showdownEnemyCards = Array.Empty<Card>();
_showdownBoardCards = Array.Empty<Card>(); _showdownBoardCards = Array.Empty<Card>();
_decisionStage = DecisionStage.Flop; ResetStreetState();
_flopAction = null;
_turnAction = null;
_riverAction = null;
} }
private Layout BuildLayout() private Layout BuildLayout()
@@ -483,21 +529,30 @@ public class Game1 : Microsoft.Xna.Framework.Game
private void DrawTopHud(Layout layout) private void DrawTopHud(Layout layout)
{ {
var x = layout.Margin; var x = layout.Margin;
var viewportWidth = GraphicsDevice.Viewport.Width;
var statsTotalWidth = 240 + 16 + 240 + 16 + 170;
var statsX = Math.Max(x + 520, viewportWidth - layout.Margin - statsTotalWidth);
const int statsY = 124;
DrawLine(new Vector2(x, 16), $"Hand {_fightState.HandNumber}/{_fightState.Config.MaxHands} | Phase: {_fightState.Phase}"); DrawLine(new Vector2(x, 16), $"Hand {_fightState.HandNumber}/{_fightState.Config.MaxHands} | Phase: {_fightState.Phase}");
DrawLine(new Vector2(x, 44), $"Player HP: {_fightState.PlayerHp} | Enemy HP: {_fightState.EnemyHp} | Pot: {_fightState.Pot}"); DrawLine(new Vector2(x, 44), $"Player HP: {_fightState.PlayerHp} | Enemy HP: {_fightState.EnemyHp} | Pot: {_fightState.Pot}");
DrawLine(new Vector2(x, 72), $"Edge: Player {_fightState.PlayerEdge} | Enemy {_fightState.EnemyEdge} | Enemy Intent: {_fightState.EnemyIntent}"); DrawLine(new Vector2(x, 72), $"Edge: Player {_fightState.PlayerEdge} | Enemy {_fightState.EnemyEdge} | Enemy Intent: {_fightState.EnemyIntent}");
DrawLine(new Vector2(x, 100), $"Decision stage: {BuildDecisionStageText()}"); DrawLine(new Vector2(x, 100), $"Decision stage: {BuildDecisionStageText()} | Turn: {_activeTurn}");
DrawLine(new Vector2(x, 124), "Action Tooltips", Color.LightGray); DrawLine(new Vector2(x, 124), $"Initiative P:{_lastPlayerInitiative} E:{_lastEnemyInitiative} | Player position: {(_playerActsSecond ? "Second" : "First")}", Color.LightGray);
DrawLine(new Vector2(x, 148), "1 Strike: Pot +3, Edge +10 | 2 Guard: Shield +6, Edge +5", Color.LightGray); DrawStatBar("Player Edge", _displayPlayerEdge, GetTargetPlayerEdge(), 0, 100, new Rectangle(statsX, statsY, 240, 12), new Color(210, 170, 60), _playerEdgePulse);
DrawLine(new Vector2(x, 172), "3 Dodge: Pot -2 (min 1), Shield +2 | 4 Check: Shield +1, Edge +2", Color.LightGray); DrawStatBar("Enemy Edge", _displayEnemyEdge, GetTargetEnemyEdge(), 0, 100, new Rectangle(statsX + 256, statsY, 240, 12), new Color(184, 90, 90), _enemyEdgePulse);
DrawLine(new Vector2(x, 196), "5 Probe: Pot +1, Edge +6 | 6 All-In: Pot +8, Edge +18, EnemyEdge +8", Color.LightGray); DrawStatBar("Shield", _displayShield, GetTargetShield(), 0, 12, new Rectangle(statsX + 512, statsY, 170, 12), new Color(88, 176, 212), _shieldPulse);
DrawLine(new Vector2(x, 220), "7 Fold: Lose hand early, reduced damage, keep HP for later", Color.LightGray); DrawFloatingTexts();
DrawLine(new Vector2(x, 244), "Per hand: pick Flop, Turn, River action", Color.LightGray); DrawLine(new Vector2(x, 152), "Action Tooltips", Color.LightGray);
DrawLine(new Vector2(x, 268), "Global: Space Continue, R Restart, Esc Exit", Color.LightGray); DrawLine(new Vector2(x, 176), "1 Strike: Pot +3, Edge +10 | 2 Guard: Shield +6, Edge +5", Color.LightGray);
DrawLine(new Vector2(x, 200), "3 Dodge: Pot -2 (min 1), Shield +2 | 4 Check: Shield +1, Edge +2", Color.LightGray);
DrawLine(new Vector2(x, 224), "5 Probe: Pot +1, Edge +6 | 6 All-In: Pot +8, Edge +18, EnemyEdge +8", Color.LightGray);
DrawLine(new Vector2(x, 248), "7 Fold: instant hand end, enemy wins this hand, reduced incoming damage", Color.LightGray);
DrawLine(new Vector2(x, 272), "Flow: each street is Player <-> Enemy (position set by initiative)", Color.LightGray);
DrawLine(new Vector2(x, 296), "Global: Space Continue, R Restart, Esc Exit", Color.LightGray);
if (_feedbackSecondsRemaining > 0f) if (_feedbackSecondsRemaining > 0f)
{ {
DrawLine(new Vector2(x, 292), _feedbackText, Color.Gold); DrawLine(new Vector2(x, 320), _feedbackText, Color.Gold);
} }
} }
@@ -542,6 +597,24 @@ public class Game1 : Microsoft.Xna.Framework.Game
DrawLine(new Vector2(x, y), "Why this result"); DrawLine(new Vector2(x, y), "Why this result");
y += 30; y += 30;
DrawLine(new Vector2(x, y), "Action timeline:");
y += 22;
if (_currentHandActionLog.Count == 0)
{
DrawLine(new Vector2(x, y), "-");
y += 20;
}
else
{
var start = Math.Max(0, _currentHandActionLog.Count - 6);
for (var i = start; i < _currentHandActionLog.Count; i++)
{
DrawLine(new Vector2(x, y), _currentHandActionLog[i], Color.LightGray);
y += 18;
}
}
y += 12;
DrawLine(new Vector2(x, y), "Poker evaluator (best 5 of 7):"); DrawLine(new Vector2(x, y), "Poker evaluator (best 5 of 7):");
y += 24; y += 24;
DrawLine(new Vector2(x, y), "1) Detect category (pair/straight/flush...)"); DrawLine(new Vector2(x, y), "1) Detect category (pair/straight/flush...)");
@@ -648,9 +721,9 @@ public class Game1 : Microsoft.Xna.Framework.Game
private void ApplyActionAndFeedback( private void ApplyActionAndFeedback(
PlayerAction action, PlayerAction action,
PlayerAction? flopAction = null, PlayerAction? firstAction = null,
PlayerAction? turnAction = null, PlayerAction? secondAction = null,
PlayerAction? riverAction = null) PlayerAction? thirdAction = null)
{ {
_showdownPlayerCards = _fightState.PlayerHoleCards.ToArray(); _showdownPlayerCards = _fightState.PlayerHoleCards.ToArray();
_showdownEnemyCards = _fightState.EnemyHoleCards.ToArray(); _showdownEnemyCards = _fightState.EnemyHoleCards.ToArray();
@@ -659,9 +732,9 @@ public class Game1 : Microsoft.Xna.Framework.Game
_fightState = FightEngine.ApplyPlayerAction(_fightState, action); _fightState = FightEngine.ApplyPlayerAction(_fightState, action);
_showdownSecondsRemaining = ShowdownDurationSeconds; _showdownSecondsRemaining = ShowdownDurationSeconds;
_awaitingContinue = false; _awaitingContinue = false;
if (flopAction is not null && turnAction is not null && riverAction is not null) if (firstAction is not null && secondAction is not null && thirdAction is not null)
{ {
ShowFeedback($"Flop {flopAction.Value} + Turn {turnAction.Value} + River {riverAction.Value} => {action} | {_fightState.LastOutcome}"); ShowFeedback($"Preflop {firstAction.Value} + Flop {secondAction.Value} + Turn {thirdAction.Value} => {action} | {_fightState.LastOutcome}");
} }
else else
{ {
@@ -669,6 +742,135 @@ public class Game1 : Microsoft.Xna.Framework.Game
} }
} }
private void SetupHandOrder()
{
ResetStreetState();
_currentHandActionLog.Clear();
_lastPlayerInitiative = _playerDexterity + _initiativeRng.Next(0, 6);
_lastEnemyInitiative = _enemyDexterity + _initiativeRng.Next(0, 6);
var secondActor = _lastPlayerInitiative >= _lastEnemyInitiative ? TurnActor.Player : TurnActor.Enemy;
if (_lastSecondActor != TurnActor.None && secondActor == _lastSecondActor)
{
secondActor = secondActor == TurnActor.Player ? TurnActor.Enemy : TurnActor.Player;
}
_lastSecondActor = secondActor;
_playerActsSecond = secondActor == TurnActor.Player;
_activeTurn = _playerActsSecond ? TurnActor.Enemy : TurnActor.Player;
_enemyTurnSecondsRemaining = _activeTurn == TurnActor.Enemy ? EnemyTurnDelaySeconds : 0f;
_currentHandActionLog.Add($"Initiative: Player {_lastPlayerInitiative} vs Enemy {_lastEnemyInitiative}");
_currentHandActionLog.Add($"Position: Player {(_playerActsSecond ? "Second" : "First")} this hand");
}
private void ResetStreetState()
{
_decisionStage = DecisionStage.Preflop;
_preflopAction = null;
_flopAction = null;
_turnAction = null;
_playerActedThisStreet = false;
_enemyActedThisStreet = false;
_enemyTurnSecondsRemaining = 0f;
_activeTurn = TurnActor.Player;
ResetPendingHudDeltas();
}
private void StorePlayerAction(PlayerAction action)
{
if (_decisionStage == DecisionStage.Preflop)
{
_preflopAction = action;
return;
}
if (_decisionStage == DecisionStage.Flop)
{
_flopAction = action;
return;
}
_turnAction = action;
}
private void ResolveEnemyStreetAction()
{
var enemyAction = GetEnemyActionForStage(_decisionStage, _fightState.EnemyIntent);
_currentHandActionLog.Add($"{StageLabel(_decisionStage)}: Enemy {enemyAction}");
_enemyActedThisStreet = true;
AdvanceStreetIfReady();
}
private void AdvanceStreetIfReady()
{
if (!_playerActedThisStreet || !_enemyActedThisStreet)
{
_activeTurn = _playerActedThisStreet ? TurnActor.Enemy : TurnActor.Player;
if (_activeTurn == TurnActor.Enemy)
{
_enemyTurnSecondsRemaining = EnemyTurnDelaySeconds;
ShowFeedback("Enemy turn...");
}
else
{
ShowFeedback("Your turn.");
}
return;
}
if (_decisionStage == DecisionStage.Preflop)
{
_decisionStage = DecisionStage.Flop;
_playerActedThisStreet = false;
_enemyActedThisStreet = false;
_activeTurn = _playerActsSecond ? TurnActor.Enemy : TurnActor.Player;
_enemyTurnSecondsRemaining = _activeTurn == TurnActor.Enemy ? EnemyTurnDelaySeconds : 0f;
ShowFeedback("Flop revealed.");
return;
}
if (_decisionStage == DecisionStage.Flop)
{
_decisionStage = DecisionStage.Turn;
_playerActedThisStreet = false;
_enemyActedThisStreet = false;
_activeTurn = _playerActsSecond ? TurnActor.Enemy : TurnActor.Player;
_enemyTurnSecondsRemaining = _activeTurn == TurnActor.Enemy ? EnemyTurnDelaySeconds : 0f;
ShowFeedback("Turn revealed.");
return;
}
var combined = CombineActions(
_preflopAction ?? PlayerAction.Check,
_flopAction ?? PlayerAction.Check,
_turnAction ?? PlayerAction.Check);
ApplyActionAndFeedback(combined, _preflopAction, _flopAction, _turnAction);
ResetStreetState();
}
private static string StageLabel(DecisionStage stage)
{
return stage switch
{
DecisionStage.Preflop => "Preflop",
DecisionStage.Flop => "Flop",
DecisionStage.Turn => "Turn",
_ => "Street"
};
}
private static PlayerAction GetEnemyActionForStage(DecisionStage stage, EnemyIntent intent)
{
return intent switch
{
EnemyIntent.Strike => stage == DecisionStage.Turn ? PlayerAction.AllIn : PlayerAction.Strike,
EnemyIntent.Guard => stage == DecisionStage.Preflop ? PlayerAction.Check : PlayerAction.Guard,
EnemyIntent.Dodge => stage == DecisionStage.Turn ? PlayerAction.Check : PlayerAction.Dodge,
_ => PlayerAction.Check
};
}
private void ShowFeedback(string text) private void ShowFeedback(string text)
{ {
_feedbackText = text; _feedbackText = text;
@@ -717,6 +919,189 @@ public class Game1 : Microsoft.Xna.Framework.Game
_spriteBatch.Draw(_pixel, rect, color); _spriteBatch.Draw(_pixel, rect, color);
} }
private void DrawStatBar(string label, float displayValue, int actualValue, int min, int max, Rectangle barRect, Color fillColor, float pulse)
{
var pulseAmount = Math.Clamp(pulse / 0.4f, 0f, 1f);
var glow = Color.Lerp(Color.Black, Color.White, pulseAmount * 0.45f);
var expanded = new Rectangle(barRect.X - (int)(2 * pulseAmount), barRect.Y - (int)(2 * pulseAmount), barRect.Width + (int)(4 * pulseAmount), barRect.Height + (int)(4 * pulseAmount));
DrawFilled(expanded, new Color(20, 20, 20, 180));
DrawBorder(expanded, glow);
var clamped = Math.Clamp(displayValue, min, max);
var normalized = max > min ? (clamped - min) / (float)(max - min) : 0f;
var fillWidth = Math.Max(0, (int)((expanded.Width - 2) * normalized));
if (fillWidth > 0)
{
var animatedFill = Color.Lerp(fillColor, Color.White, pulseAmount * 0.25f);
DrawFilled(new Rectangle(expanded.X + 1, expanded.Y + 1, fillWidth, expanded.Height - 2), animatedFill);
}
var labelSize = _debugFont.MeasureString(label);
_spriteBatch.DrawString(_debugFont, label, new Vector2(expanded.X, expanded.Y - labelSize.Y - 2), Color.LightGray);
_spriteBatch.DrawString(_debugFont, actualValue.ToString(), new Vector2(expanded.Right + 6, expanded.Y - 4), Color.White);
}
private void UpdateHudAnimations(float dt)
{
var targetPlayerEdge = GetTargetPlayerEdge();
var targetEnemyEdge = GetTargetEnemyEdge();
var targetShield = GetTargetShield();
if (_lastPlayerEdge == int.MinValue)
{
_displayPlayerEdge = targetPlayerEdge;
_displayEnemyEdge = targetEnemyEdge;
_displayShield = targetShield;
_lastPlayerEdge = targetPlayerEdge;
_lastEnemyEdge = targetEnemyEdge;
_lastShield = targetShield;
}
if (_lastPlayerEdge != targetPlayerEdge)
{
_playerEdgePulse = 0.4f;
_lastPlayerEdge = targetPlayerEdge;
}
if (_lastEnemyEdge != targetEnemyEdge)
{
_enemyEdgePulse = 0.4f;
_lastEnemyEdge = targetEnemyEdge;
}
if (_lastShield != targetShield)
{
_shieldPulse = 0.4f;
_lastShield = targetShield;
}
_displayPlayerEdge = SmoothTowards(_displayPlayerEdge, targetPlayerEdge, dt, 12f);
_displayEnemyEdge = SmoothTowards(_displayEnemyEdge, targetEnemyEdge, dt, 12f);
_displayShield = SmoothTowards(_displayShield, targetShield, dt, 18f);
_playerEdgePulse = Math.Max(0f, _playerEdgePulse - dt);
_enemyEdgePulse = Math.Max(0f, _enemyEdgePulse - dt);
_shieldPulse = Math.Max(0f, _shieldPulse - dt);
}
private int GetTargetPlayerEdge()
{
return _fightState.PlayerEdge + _pendingPlayerEdgeDelta;
}
private int GetTargetEnemyEdge()
{
return _fightState.EnemyEdge + _pendingEnemyEdgeDelta;
}
private int GetTargetShield()
{
return _fightState.PlayerShield + _pendingShieldDelta;
}
private void ResetPendingHudDeltas()
{
_pendingPlayerEdgeDelta = 0;
_pendingEnemyEdgeDelta = 0;
_pendingShieldDelta = 0;
}
private void ApplyPendingDeltaForAction(PlayerAction action)
{
var (edge, enemyEdge, shield) = GetActionDelta(action);
_pendingPlayerEdgeDelta += edge;
_pendingEnemyEdgeDelta += enemyEdge;
_pendingShieldDelta += shield;
}
private static (int Edge, int EnemyEdge, int Shield) GetActionDelta(PlayerAction action)
{
return action switch
{
PlayerAction.Strike => (10, 0, 0),
PlayerAction.Guard => (5, 0, 6),
PlayerAction.Dodge => (0, 0, 2),
PlayerAction.Check => (2, 0, 1),
PlayerAction.Probe => (6, 0, 0),
PlayerAction.AllIn => (18, 8, 0),
PlayerAction.Fold => (0, 0, 10),
_ => (0, 0, 0)
};
}
private void SpawnActionFloaters(PlayerAction action)
{
var viewportWidth = GraphicsDevice.Viewport.Width;
var margin = Math.Max(14, viewportWidth / 90);
var statsTotalWidth = 240 + 16 + 240 + 16 + 170;
var statsX = Math.Max(margin + 520, viewportWidth - margin - statsTotalWidth);
const int statsY = 124;
var (edge, enemyEdge, shield) = GetActionDelta(action);
if (edge != 0)
{
SpawnFloatingText(FormatDelta(edge), new Vector2(statsX + 112, statsY - 18), edge > 0 ? Color.LawnGreen : Color.OrangeRed);
}
if (enemyEdge != 0)
{
SpawnFloatingText(FormatDelta(enemyEdge), new Vector2(statsX + 368, statsY - 18), enemyEdge > 0 ? Color.Orange : Color.CornflowerBlue);
}
if (shield != 0)
{
SpawnFloatingText(FormatDelta(shield), new Vector2(statsX + 564, statsY - 18), shield > 0 ? Color.LightSkyBlue : Color.OrangeRed);
}
}
private void SpawnFloatingText(string text, Vector2 position, Color color)
{
_floatingStatTexts.Add(new FloatingStatText(text, position, color, 0.9f, 0.9f));
}
private void UpdateFloatingTexts(float dt)
{
for (var i = _floatingStatTexts.Count - 1; i >= 0; i--)
{
var item = _floatingStatTexts[i];
var updated = item with
{
Remaining = item.Remaining - dt,
Position = item.Position + new Vector2(0f, -20f * dt)
};
if (updated.Remaining <= 0f)
{
_floatingStatTexts.RemoveAt(i);
continue;
}
_floatingStatTexts[i] = updated;
}
}
private void DrawFloatingTexts()
{
for (var i = 0; i < _floatingStatTexts.Count; i++)
{
var item = _floatingStatTexts[i];
var alpha = Math.Clamp(item.Remaining / item.Lifetime, 0f, 1f);
_spriteBatch.DrawString(_debugFont, item.Text, item.Position, item.Color * alpha);
}
}
private static string FormatDelta(int value)
{
return value >= 0 ? $"+{value}" : value.ToString();
}
private static float SmoothTowards(float current, float target, float dt, float speed)
{
var alpha = 1f - MathF.Exp(-speed * Math.Max(0f, dt));
return current + ((target - current) * alpha);
}
private void DrawBorder(Rectangle rect, Color color) private void DrawBorder(Rectangle rect, Color color)
{ {
const int thickness = 2; const int thickness = 2;
@@ -820,9 +1205,9 @@ public class Game1 : Microsoft.Xna.Framework.Game
return _decisionStage switch return _decisionStage switch
{ {
DecisionStage.Flop => "Flop action (1st)", DecisionStage.Preflop => "Preflop action (1st)",
DecisionStage.Turn => "Turn action (2nd)", DecisionStage.Flop => "Flop action (2nd)",
DecisionStage.River => "River action (3rd)", DecisionStage.Turn => "Turn action (3rd)",
_ => "-" _ => "-"
}; };
} }
@@ -928,9 +1313,9 @@ public class Game1 : Microsoft.Xna.Framework.Game
{ {
return _decisionStage switch return _decisionStage switch
{ {
DecisionStage.Preflop => 0,
DecisionStage.Flop => 3, DecisionStage.Flop => 3,
DecisionStage.Turn => 4, DecisionStage.Turn => 4,
DecisionStage.River => 5,
_ => 3 _ => 3
}; };
} }
@@ -985,9 +1370,16 @@ public class Game1 : Microsoft.Xna.Framework.Game
private enum DecisionStage private enum DecisionStage
{ {
Preflop,
Flop, Flop,
Turn, Turn
River }
private enum TurnActor
{
None,
Player,
Enemy
} }
private enum RunScreen private enum RunScreen
@@ -1025,4 +1417,6 @@ public class Game1 : Microsoft.Xna.Framework.Game
Vector2 BoardCardsPos, Vector2 BoardCardsPos,
Vector2 PlayerLabelPos, Vector2 PlayerLabelPos,
Vector2 PlayerCardsPos); Vector2 PlayerCardsPos);
private readonly record struct FloatingStatText(string Text, Vector2 Position, Color Color, float Remaining, float Lifetime);
} }