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.
Kazde kolo boja je jedna poker handa.
PokerRogue je singleplayer poker-combat hra. Kazda handa je jeden suboj.
## Ciel
- 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.
2. Vyberies akciu (`1`, `2`, `3`).
3. Vyhodnoti sa handa:
- kto ma silnejsiu kombinaciu, vyhrava handu,
- podla Pot/Edge/Shield sa vypocita damage.
4. Na 5 sekund sa ukaze showdown:
- nepriatelove karty,
- zlate obrysy vitaznej 5-kartovej kombinacie.
5. Zacne dalsia handa.
1. Dostanes 2 karty, nepriatel ma 2 karty, na stole je board 5 kariet.
2. Rozhodujes sa po streetoch:
- `Flop` (3 karty),
- `Turn` (4 karty),
- `River` (5 kariet).
3. Na kazdom kroku vyberas akciu (`1` az `7`), okrem `Fold`, ktory handu okamzite ukonci.
4. Po River sa z troch vybranych akcii vypocita finalna bojova akcia.
5. Vyhodnoti sa showdown (najlepsich 5 zo 7 kariet), aplikuje sa damage.
6. Vysledok ostane na obrazovke, pokracujes az po `Space`/`Enter`.
## Ovládanie
- `1` = `Strike` (agresivne)
- `2` = `Guard` (defenzivne)
- `3` = `Dodge` (znizenie rizika)
- `R` = restart boja
- `Esc` = ukoncit hru
- `1` = `Strike`
- `2` = `Guard`
- `3` = `Dodge`
- `4` = `Check`
- `5` = `Probe`
- `6` = `All-In`
- `7` = `Fold` (okamzite skonci handu)
- `Space` / `Enter` = pokracovat po showdowne
- `R` = novy run
- `Esc` = koniec hry
## 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`
- zvysi tvoj `Edge`
- vhodne, ked chces tlacit na vysoky damage
## Edge a Shield (dolezite)
### Guard (`2`)
### Edge
- prida `Shield`
- mierne zvysi tvoj `Edge`
- vhodne, ked cakas silny utok nepriatela
`Edge` je percent bonus k damage:
### 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)
- da mensi `Shield`
- vhodne na stabilizaciu pri nevyhodnej hande
Cim vyssi `Edge`, tym vacsie potencialne zranenie.
### 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
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...)
2. pri rovnakom type rozhoduju kickery
1. Typ kombinacie (Pair, Two Pair, Straight, Flush, ...)
2. Pri rovnosti rozhoduju kickery (rank vector).
Panel vpravo ukazuje:
## Vizualne pomocky v hre
- nazov kombinacie hraca a nepriatela,
- dovod vyhry/prehry,
- vysledny vypocet damage.
- Hore vpravo su animovane bary:
- `Player Edge`,
- `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`.
- Ak je nepriatel agresivny, `Guard` ti casto zachrani HP.
- `Dodge` pouzi, ked nechces prehrat velky pot.
- 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.
- Silna handa + vyssi `PlayerEdge` = velky tlak na HP nepriatela.
- Pri riziku prehry pomaha `Guard`, `Check` alebo `Dodge` (kvoli `Shield`).
- `Fold` je takticky ustupok: prehras handu, ale casto prezijes lacnejsie.
+460 -66
View File
@@ -24,13 +24,14 @@ public class Game1 : Microsoft.Xna.Framework.Game
private FightState _fightState = null!;
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 _showdownSecondsRemaining;
private bool _awaitingContinue;
private const float ShowdownDurationSeconds = 5.0f;
private const float TurnRevealAtSeconds = 1.2f;
private const float RiverRevealAtSeconds = 2.4f;
private const float EnemyTurnDelaySeconds = 0.55f;
private Card[] _showdownPlayerCards = 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 _runPlayerHp = 100;
private int _runPotBonus;
private int _runPlayerDexterity = 10;
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? _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()
{
@@ -79,6 +106,7 @@ public class Game1 : Microsoft.Xna.Framework.Game
protected override void Update(GameTime gameTime)
{
var dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
var keyboardState = Keyboard.GetState();
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);
AdvanceToNextNode();
}
else if (Pressed(keyboardState, Keys.D4) || Pressed(keyboardState, Keys.NumPad4))
{
_runPlayerDexterity += 1;
AdvanceToNextNode();
}
break;
case RunScreen.Fight:
UpdateFightScreen(keyboardState, gameTime);
@@ -140,14 +173,17 @@ public class Game1 : Microsoft.Xna.Framework.Game
break;
}
UpdateHudAnimations(dt);
UpdateFloatingTexts(dt);
if (_feedbackSecondsRemaining > 0f)
{
_feedbackSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds;
_feedbackSecondsRemaining -= dt;
}
if (_showdownSecondsRemaining > 0f)
{
_showdownSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds;
_showdownSecondsRemaining -= dt;
if (_showdownSecondsRemaining <= 0f)
{
_showdownSecondsRemaining = 0f;
@@ -209,12 +245,16 @@ public class Game1 : Microsoft.Xna.Framework.Game
_runMaxHp = 100;
_runPlayerHp = 100;
_runPotBonus = 0;
_runPlayerDexterity = 10;
_runScreen = RunScreen.Map;
_encounterText = string.Empty;
_decisionStage = DecisionStage.Flop;
_decisionStage = DecisionStage.Preflop;
_activeTurn = TurnActor.Player;
_preflopAction = null;
_flopAction = null;
_turnAction = null;
_riverAction = null;
_currentHandActionLog.Clear();
ResetPendingHudDeltas();
ResetShowdownState();
ShowFeedback("New run started.");
}
@@ -243,7 +283,8 @@ public class Game1 : Microsoft.Xna.Framework.Game
}
_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;
}
@@ -277,12 +318,17 @@ public class Game1 : Microsoft.Xna.Framework.Game
MaxHands: maxHands,
Enemy: enemy);
_enemyDexterity = node.Type switch
{
NodeType.Boss => 13,
NodeType.Elite => 12,
_ => 10
};
_playerDexterity = _runPlayerDexterity;
_fightState = FightEngine.StartFight(config, seed: Environment.TickCount);
_decisionStage = DecisionStage.Flop;
_flopAction = null;
_turnAction = null;
_riverAction = null;
ResetShowdownState();
SetupHandOrder();
_runScreen = RunScreen.Fight;
}
@@ -291,33 +337,34 @@ public class Game1 : Microsoft.Xna.Framework.Game
var showdownActive = IsShowdownVisible;
if (_fightState.Phase != HandPhase.End && !showdownActive)
{
var pickedAction = ReadActionKey(keyboardState);
if (pickedAction is not null)
if (_activeTurn == TurnActor.Enemy)
{
if (_decisionStage == DecisionStage.Flop)
_enemyTurnSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds;
if (_enemyTurnSecondsRemaining <= 0f)
{
_flopAction = pickedAction.Value;
_decisionStage = DecisionStage.Turn;
ShowFeedback($"Flop action: {_flopAction.Value}. Pick turn action.");
ResolveEnemyStreetAction();
}
else if (_decisionStage == DecisionStage.Turn)
}
else
{
var pickedAction = ReadActionKey(keyboardState);
if (pickedAction is not null)
{
_turnAction = pickedAction.Value;
_decisionStage = DecisionStage.River;
ShowFeedback($"Turn action: {_turnAction.Value}. Pick river action.");
}
else
{
_riverAction = pickedAction.Value;
var combined = CombineActions(
_flopAction ?? PlayerAction.Check,
_turnAction ?? PlayerAction.Check,
_riverAction.Value);
ApplyActionAndFeedback(combined, _flopAction, _turnAction, _riverAction);
_flopAction = null;
_turnAction = null;
_riverAction = null;
_decisionStage = DecisionStage.Flop;
if (pickedAction.Value == PlayerAction.Fold)
{
_currentHandActionLog.Add($"{StageLabel(_decisionStage)}: Player Fold");
SpawnActionFloaters(PlayerAction.Fold);
ApplyActionAndFeedback(PlayerAction.Fold);
ResetStreetState();
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>();
_showdownEnemyCards = Array.Empty<Card>();
_showdownBoardCards = Array.Empty<Card>();
_decisionStage = DecisionStage.Flop;
_flopAction = null;
_turnAction = null;
_riverAction = null;
_currentHandActionLog.Clear();
if (_fightState.Phase != HandPhase.End)
{
SetupHandOrder();
}
ShowFeedback("Next hand.");
}
@@ -376,7 +424,7 @@ public class Game1 : Microsoft.Xna.Framework.Game
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++)
{
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, 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.");
}
private void DrawRewardScreen()
{
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, 182), "2 - Pot bonus +2 for next fights");
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)
@@ -418,10 +467,7 @@ public class Game1 : Microsoft.Xna.Framework.Game
_showdownPlayerCards = Array.Empty<Card>();
_showdownEnemyCards = Array.Empty<Card>();
_showdownBoardCards = Array.Empty<Card>();
_decisionStage = DecisionStage.Flop;
_flopAction = null;
_turnAction = null;
_riverAction = null;
ResetStreetState();
}
private Layout BuildLayout()
@@ -483,21 +529,30 @@ public class Game1 : Microsoft.Xna.Framework.Game
private void DrawTopHud(Layout layout)
{
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, 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, 100), $"Decision stage: {BuildDecisionStageText()}");
DrawLine(new Vector2(x, 124), "Action Tooltips", Color.LightGray);
DrawLine(new Vector2(x, 148), "1 Strike: Pot +3, Edge +10 | 2 Guard: Shield +6, Edge +5", Color.LightGray);
DrawLine(new Vector2(x, 172), "3 Dodge: Pot -2 (min 1), Shield +2 | 4 Check: Shield +1, Edge +2", Color.LightGray);
DrawLine(new Vector2(x, 196), "5 Probe: Pot +1, Edge +6 | 6 All-In: Pot +8, Edge +18, EnemyEdge +8", Color.LightGray);
DrawLine(new Vector2(x, 220), "7 Fold: Lose hand early, reduced damage, keep HP for later", Color.LightGray);
DrawLine(new Vector2(x, 244), "Per hand: pick Flop, Turn, River action", Color.LightGray);
DrawLine(new Vector2(x, 268), "Global: Space Continue, R Restart, Esc Exit", Color.LightGray);
DrawLine(new Vector2(x, 100), $"Decision stage: {BuildDecisionStageText()} | Turn: {_activeTurn}");
DrawLine(new Vector2(x, 124), $"Initiative P:{_lastPlayerInitiative} E:{_lastEnemyInitiative} | Player position: {(_playerActsSecond ? "Second" : "First")}", Color.LightGray);
DrawStatBar("Player Edge", _displayPlayerEdge, GetTargetPlayerEdge(), 0, 100, new Rectangle(statsX, statsY, 240, 12), new Color(210, 170, 60), _playerEdgePulse);
DrawStatBar("Enemy Edge", _displayEnemyEdge, GetTargetEnemyEdge(), 0, 100, new Rectangle(statsX + 256, statsY, 240, 12), new Color(184, 90, 90), _enemyEdgePulse);
DrawStatBar("Shield", _displayShield, GetTargetShield(), 0, 12, new Rectangle(statsX + 512, statsY, 170, 12), new Color(88, 176, 212), _shieldPulse);
DrawFloatingTexts();
DrawLine(new Vector2(x, 152), "Action Tooltips", 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)
{
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");
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):");
y += 24;
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(
PlayerAction action,
PlayerAction? flopAction = null,
PlayerAction? turnAction = null,
PlayerAction? riverAction = null)
PlayerAction? firstAction = null,
PlayerAction? secondAction = null,
PlayerAction? thirdAction = null)
{
_showdownPlayerCards = _fightState.PlayerHoleCards.ToArray();
_showdownEnemyCards = _fightState.EnemyHoleCards.ToArray();
@@ -659,9 +732,9 @@ public class Game1 : Microsoft.Xna.Framework.Game
_fightState = FightEngine.ApplyPlayerAction(_fightState, action);
_showdownSecondsRemaining = ShowdownDurationSeconds;
_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
{
@@ -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)
{
_feedbackText = text;
@@ -717,6 +919,189 @@ public class Game1 : Microsoft.Xna.Framework.Game
_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)
{
const int thickness = 2;
@@ -820,9 +1205,9 @@ public class Game1 : Microsoft.Xna.Framework.Game
return _decisionStage switch
{
DecisionStage.Flop => "Flop action (1st)",
DecisionStage.Turn => "Turn action (2nd)",
DecisionStage.River => "River action (3rd)",
DecisionStage.Preflop => "Preflop action (1st)",
DecisionStage.Flop => "Flop action (2nd)",
DecisionStage.Turn => "Turn action (3rd)",
_ => "-"
};
}
@@ -928,9 +1313,9 @@ public class Game1 : Microsoft.Xna.Framework.Game
{
return _decisionStage switch
{
DecisionStage.Preflop => 0,
DecisionStage.Flop => 3,
DecisionStage.Turn => 4,
DecisionStage.River => 5,
_ => 3
};
}
@@ -985,9 +1370,16 @@ public class Game1 : Microsoft.Xna.Framework.Game
private enum DecisionStage
{
Preflop,
Flop,
Turn,
River
Turn
}
private enum TurnActor
{
None,
Player,
Enemy
}
private enum RunScreen
@@ -1025,4 +1417,6 @@ public class Game1 : Microsoft.Xna.Framework.Game
Vector2 BoardCardsPos,
Vector2 PlayerLabelPos,
Vector2 PlayerCardsPos);
private readonly record struct FloatingStatText(string Text, Vector2 Position, Color Color, float Remaining, float Lifetime);
}