diff --git a/.gitignore b/.gitignore
index 9491a2f..9aa5ae5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -360,4 +360,6 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
-FodyWeavers.xsd
\ No newline at end of file
+FodyWeavers.xsd
+/.dotnet/.dotnet
+/.config
diff --git a/AKO_HRAT.md b/AKO_HRAT.md
new file mode 100644
index 0000000..c47f795
--- /dev/null
+++ b/AKO_HRAT.md
@@ -0,0 +1,77 @@
+# Ako Hrat PokerRogue (Prototyp)
+
+PokerRogue je singleplayer hra, kde sa boj riesi pokerom.
+Kazde kolo boja je jedna poker handa.
+
+## 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).
+
+## Zakladny priebeh
+
+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.
+
+## Ovládanie
+
+- `1` = `Strike` (agresivne)
+- `2` = `Guard` (defenzivne)
+- `3` = `Dodge` (znizenie rizika)
+- `R` = restart boja
+- `Esc` = ukoncit hru
+
+## Co robia akcie
+
+### Strike (`1`)
+
+- zvysi `Pot`
+- zvysi tvoj `Edge`
+- vhodne, ked chces tlacit na vysoky damage
+
+### Guard (`2`)
+
+- prida `Shield`
+- mierne zvysi tvoj `Edge`
+- vhodne, ked cakas silny utok nepriatela
+
+### Dodge (`3`)
+
+- znizi `Pot` (mensie riziko aj odmena)
+- da mensi `Shield`
+- vhodne na stabilizaciu pri nevyhodnej hande
+
+## Ako sa urcuje vitaz handy
+
+Hra vyhodnocuje najlepsiu 5-kartovu kombinaciu zo 7 kariet:
+
+- 2 hole cards + 5 board cards
+
+Porovnanie funguje ako v pokri:
+
+1. najprv typ kombinacie (napr. Pair, Two Pair, Straight, Flush...)
+2. pri rovnakom type rozhoduju kickery
+
+Panel vpravo ukazuje:
+
+- nazov kombinacie hraca a nepriatela,
+- dovod vyhry/prehry,
+- vysledny vypocet damage.
+
+## Tipy pre zaciatok
+
+- 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.
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..765346e
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/PokerRogue.Core/Cards/Card.cs b/src/PokerRogue.Core/Cards/Card.cs
new file mode 100644
index 0000000..ac70a22
--- /dev/null
+++ b/src/PokerRogue.Core/Cards/Card.cs
@@ -0,0 +1,3 @@
+namespace PokerRogue.Core.Cards;
+
+public readonly record struct Card(Rank Rank, Suit Suit);
diff --git a/src/PokerRogue.Core/Cards/Rank.cs b/src/PokerRogue.Core/Cards/Rank.cs
new file mode 100644
index 0000000..553c14c
--- /dev/null
+++ b/src/PokerRogue.Core/Cards/Rank.cs
@@ -0,0 +1,18 @@
+namespace PokerRogue.Core.Cards;
+
+public enum Rank
+{
+ Two = 2,
+ Three = 3,
+ Four = 4,
+ Five = 5,
+ Six = 6,
+ Seven = 7,
+ Eight = 8,
+ Nine = 9,
+ Ten = 10,
+ Jack = 11,
+ Queen = 12,
+ King = 13,
+ Ace = 14
+}
diff --git a/src/PokerRogue.Core/Cards/Suit.cs b/src/PokerRogue.Core/Cards/Suit.cs
new file mode 100644
index 0000000..0450e9c
--- /dev/null
+++ b/src/PokerRogue.Core/Cards/Suit.cs
@@ -0,0 +1,9 @@
+namespace PokerRogue.Core.Cards;
+
+public enum Suit
+{
+ Clubs = 0,
+ Diamonds = 1,
+ Hearts = 2,
+ Spades = 3
+}
diff --git a/src/PokerRogue.Core/Combat/FightConfig.cs b/src/PokerRogue.Core/Combat/FightConfig.cs
new file mode 100644
index 0000000..f8556c1
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/FightConfig.cs
@@ -0,0 +1,19 @@
+using PokerRogue.Core.Enemies;
+
+namespace PokerRogue.Core.Combat;
+
+public sealed record FightConfig(
+ int StartingPlayerHp,
+ int StartingEnemyHp,
+ int StartingPot,
+ int MaxHands,
+ EnemyProfile Enemy)
+{
+ public static FightConfig Default(EnemyProfile enemy) =>
+ new(
+ StartingPlayerHp: 100,
+ StartingEnemyHp: 100,
+ StartingPot: 10,
+ MaxHands: 5,
+ Enemy: enemy);
+}
diff --git a/src/PokerRogue.Core/Combat/FightEngine.cs b/src/PokerRogue.Core/Combat/FightEngine.cs
new file mode 100644
index 0000000..32fdff5
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/FightEngine.cs
@@ -0,0 +1,301 @@
+using PokerRogue.Core.Cards;
+using PokerRogue.Core.Enemies;
+
+namespace PokerRogue.Core.Combat;
+
+public static class FightEngine
+{
+ private const ulong Multiplier = 6364136223846793005UL;
+ private const ulong Increment = 1442695040888963407UL;
+
+ public static FightState StartFight(FightConfig config, int seed)
+ {
+ var state = new FightState
+ {
+ Config = config,
+ RngState = unchecked((ulong)(uint)seed),
+ Phase = HandPhase.Deal,
+ HandNumber = 1,
+ PlayerHp = config.StartingPlayerHp,
+ EnemyHp = config.StartingEnemyHp,
+ Pot = config.StartingPot,
+ PlayerEdge = 0,
+ EnemyEdge = 0,
+ PlayerShield = 0,
+ EnemyIntent = EnemyIntent.Guard,
+ LastOutcome = HandOutcome.None,
+ LastPlayerAction = null,
+ LastDamageToEnemy = 0,
+ LastDamageToPlayer = 0,
+ LastPlayerScore = 0,
+ LastEnemyScore = 0,
+ LastPlayerTopRanks = Array.Empty(),
+ LastEnemyTopRanks = Array.Empty(),
+ LastPlayerHandName = "N/A",
+ LastEnemyHandName = "N/A",
+ LastComparisonReason = "No resolved hand yet.",
+ LastPlayerWinningCards = Array.Empty(),
+ LastEnemyWinningCards = Array.Empty(),
+ PlayerHoleCards = Array.Empty(),
+ EnemyHoleCards = Array.Empty(),
+ BoardCards = Array.Empty()
+ };
+
+ return DealHand(state);
+ }
+
+ public static FightState ApplyPlayerAction(FightState state, PlayerAction action)
+ {
+ if (state.Phase != HandPhase.Clash)
+ {
+ return state;
+ }
+
+ var updated = state with
+ {
+ Phase = HandPhase.Resolve,
+ LastPlayerAction = action,
+ PlayerShield = 0
+ };
+ var folded = action == PlayerAction.Fold;
+
+ updated = action switch
+ {
+ PlayerAction.Strike => updated with
+ {
+ Pot = updated.Pot + 3,
+ PlayerEdge = updated.PlayerEdge + 10
+ },
+ PlayerAction.Guard => updated with
+ {
+ PlayerShield = 6,
+ PlayerEdge = updated.PlayerEdge + 5
+ },
+ PlayerAction.Dodge => updated with
+ {
+ Pot = Math.Max(1, updated.Pot - 2),
+ PlayerShield = 2
+ },
+ PlayerAction.Check => updated with
+ {
+ PlayerShield = 1,
+ PlayerEdge = updated.PlayerEdge + 2
+ },
+ PlayerAction.Probe => updated with
+ {
+ Pot = updated.Pot + 1,
+ PlayerEdge = updated.PlayerEdge + 6
+ },
+ PlayerAction.AllIn => updated with
+ {
+ Pot = updated.Pot + 8,
+ PlayerEdge = updated.PlayerEdge + 18,
+ EnemyEdge = updated.EnemyEdge + 8
+ },
+ PlayerAction.Fold => updated with
+ {
+ Pot = Math.Max(1, updated.Pot - 3),
+ PlayerShield = 10
+ },
+ PlayerAction.Skill => updated with
+ {
+ PlayerEdge = updated.PlayerEdge + 12
+ },
+ _ => updated
+ };
+
+ updated = updated.EnemyIntent switch
+ {
+ EnemyIntent.Strike => updated with
+ {
+ EnemyEdge = updated.EnemyEdge + 10,
+ Pot = updated.Pot + 2
+ },
+ EnemyIntent.Guard => updated with
+ {
+ EnemyEdge = updated.EnemyEdge + 4
+ },
+ EnemyIntent.Dodge => updated with
+ {
+ Pot = Math.Max(1, updated.Pot - 1)
+ },
+ _ => updated
+ };
+
+ var completedHand = folded ? ResolveFold(updated) : ResolveHand(updated);
+
+ if (completedHand.PlayerHp <= 0 || completedHand.EnemyHp <= 0 || completedHand.HandNumber >= completedHand.Config.MaxHands)
+ {
+ return completedHand with { Phase = HandPhase.End };
+ }
+
+ return DealHand(completedHand with { HandNumber = completedHand.HandNumber + 1 });
+ }
+
+ private static FightState DealHand(FightState state)
+ {
+ var rngState = state.RngState;
+ var deck = CreateShuffledDeck(ref rngState);
+
+ var playerHole = new[] { deck[0], deck[1] };
+ var enemyHole = new[] { deck[2], deck[3] };
+ var board = new[] { deck[4], deck[5], deck[6], deck[7], deck[8] };
+
+ var intent = RollEnemyIntent(state.Config.Enemy, ref rngState);
+
+ return state with
+ {
+ RngState = rngState,
+ Phase = HandPhase.Clash,
+ EnemyIntent = intent,
+ PlayerHoleCards = playerHole,
+ EnemyHoleCards = enemyHole,
+ BoardCards = board
+ };
+ }
+
+ private static FightState ResolveHand(FightState state)
+ {
+ var playerCards = state.PlayerHoleCards.Concat(state.BoardCards).ToArray();
+ var enemyCards = state.EnemyHoleCards.Concat(state.BoardCards).ToArray();
+
+ var playerStrength = PokerHandEvaluator.Evaluate(playerCards);
+ var enemyStrength = PokerHandEvaluator.Evaluate(enemyCards);
+ var comparison = PokerHandEvaluator.Compare(playerStrength, enemyStrength);
+
+ HandOutcome outcome;
+ if (comparison > 0)
+ {
+ outcome = HandOutcome.PlayerWin;
+ }
+ else if (comparison < 0)
+ {
+ outcome = HandOutcome.EnemyWin;
+ }
+ else
+ {
+ outcome = HandOutcome.Tie;
+ }
+
+ var playerHp = state.PlayerHp;
+ var enemyHp = state.EnemyHp;
+ var damageToEnemy = 0;
+ var damageToPlayer = 0;
+
+ if (outcome == HandOutcome.PlayerWin)
+ {
+ var damage = ComputeDamage(state.Pot, state.PlayerEdge);
+ enemyHp = Math.Max(0, enemyHp - damage);
+ damageToEnemy = damage;
+ }
+ else if (outcome == HandOutcome.EnemyWin)
+ {
+ var rawDamage = ComputeDamage(state.Pot, state.EnemyEdge);
+ var damage = Math.Max(0, rawDamage - state.PlayerShield);
+ playerHp = Math.Max(0, playerHp - damage);
+ damageToPlayer = damage;
+ }
+
+ return state with
+ {
+ PlayerHp = playerHp,
+ EnemyHp = enemyHp,
+ LastOutcome = outcome,
+ LastDamageToEnemy = damageToEnemy,
+ LastDamageToPlayer = damageToPlayer,
+ LastPlayerScore = playerStrength.Score,
+ LastEnemyScore = enemyStrength.Score,
+ LastPlayerTopRanks = playerStrength.RankVector,
+ LastEnemyTopRanks = enemyStrength.RankVector,
+ LastPlayerHandName = playerStrength.DisplayName,
+ LastEnemyHandName = enemyStrength.DisplayName,
+ LastComparisonReason = PokerHandEvaluator.DescribeComparison(playerStrength, enemyStrength),
+ LastPlayerWinningCards = playerStrength.WinningCards,
+ LastEnemyWinningCards = enemyStrength.WinningCards,
+ Phase = HandPhase.Resolve
+ };
+ }
+
+ private static FightState ResolveFold(FightState state)
+ {
+ var playerCards = state.PlayerHoleCards.Concat(state.BoardCards).ToArray();
+ var enemyCards = state.EnemyHoleCards.Concat(state.BoardCards).ToArray();
+
+ var playerStrength = PokerHandEvaluator.Evaluate(playerCards);
+ var enemyStrength = PokerHandEvaluator.Evaluate(enemyCards);
+
+ var rawDamage = ComputeDamage(state.Pot, state.EnemyEdge);
+ var reduced = rawDamage / 2; // Folding intentionally trades tempo for lower damage.
+ var damageToPlayer = Math.Max(0, reduced - state.PlayerShield);
+ var playerHp = Math.Max(0, state.PlayerHp - damageToPlayer);
+
+ return state with
+ {
+ PlayerHp = playerHp,
+ EnemyHp = state.EnemyHp,
+ LastOutcome = HandOutcome.EnemyWin,
+ LastDamageToEnemy = 0,
+ LastDamageToPlayer = damageToPlayer,
+ LastPlayerScore = playerStrength.Score,
+ LastEnemyScore = enemyStrength.Score,
+ LastPlayerTopRanks = playerStrength.RankVector,
+ LastEnemyTopRanks = enemyStrength.RankVector,
+ LastPlayerHandName = "Folded",
+ LastEnemyHandName = enemyStrength.DisplayName,
+ LastComparisonReason = "Player folded the hand.",
+ LastPlayerWinningCards = Array.Empty(),
+ LastEnemyWinningCards = enemyStrength.WinningCards,
+ Phase = HandPhase.Resolve
+ };
+ }
+
+ private static int ComputeDamage(int pot, int edge)
+ {
+ return (pot * (100 + edge)) / 100;
+ }
+
+ private static EnemyIntent RollEnemyIntent(EnemyProfile profile, ref ulong rngState)
+ {
+ var total = profile.Aggression + profile.Trickery + profile.Cowardice;
+ var roll = NextInt(ref rngState, Math.Max(1, total));
+
+ if (roll < profile.Aggression)
+ {
+ return EnemyIntent.Strike;
+ }
+
+ if (roll < profile.Aggression + profile.Trickery)
+ {
+ return EnemyIntent.Guard;
+ }
+
+ return EnemyIntent.Dodge;
+ }
+
+ private static Card[] CreateShuffledDeck(ref ulong rngState)
+ {
+ var deck = new List(52);
+ for (var suit = 0; suit < 4; suit++)
+ {
+ for (var rank = 2; rank <= 14; rank++)
+ {
+ deck.Add(new Card((Rank)rank, (Suit)suit));
+ }
+ }
+
+ for (var i = deck.Count - 1; i > 0; i--)
+ {
+ var j = NextInt(ref rngState, i + 1);
+ (deck[i], deck[j]) = (deck[j], deck[i]);
+ }
+
+ return deck.ToArray();
+ }
+
+ private static int NextInt(ref ulong rngState, int maxExclusive)
+ {
+ rngState = (rngState * Multiplier) + Increment;
+ var sample = (int)((rngState >> 33) & int.MaxValue);
+ return sample % maxExclusive;
+ }
+}
diff --git a/src/PokerRogue.Core/Combat/FightState.cs b/src/PokerRogue.Core/Combat/FightState.cs
new file mode 100644
index 0000000..99d5078
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/FightState.cs
@@ -0,0 +1,35 @@
+using PokerRogue.Core.Cards;
+using PokerRogue.Core.Enemies;
+
+namespace PokerRogue.Core.Combat;
+
+public sealed record FightState
+{
+ public required FightConfig Config { get; init; }
+ public required ulong RngState { get; init; }
+ public required HandPhase Phase { get; init; }
+ public required int HandNumber { get; init; }
+ public required int PlayerHp { get; init; }
+ public required int EnemyHp { get; init; }
+ public required int Pot { get; init; }
+ public required int PlayerEdge { get; init; }
+ public required int EnemyEdge { get; init; }
+ public required int PlayerShield { get; init; }
+ public required EnemyIntent EnemyIntent { get; init; }
+ public required HandOutcome LastOutcome { get; init; }
+ public required PlayerAction? LastPlayerAction { get; init; }
+ public required int LastDamageToEnemy { get; init; }
+ public required int LastDamageToPlayer { get; init; }
+ public required int LastPlayerScore { get; init; }
+ public required int LastEnemyScore { get; init; }
+ public required int[] LastPlayerTopRanks { get; init; }
+ public required int[] LastEnemyTopRanks { get; init; }
+ public required string LastPlayerHandName { get; init; }
+ public required string LastEnemyHandName { get; init; }
+ public required string LastComparisonReason { get; init; }
+ public required Card[] LastPlayerWinningCards { get; init; }
+ public required Card[] LastEnemyWinningCards { get; init; }
+ public required Card[] PlayerHoleCards { get; init; }
+ public required Card[] EnemyHoleCards { get; init; }
+ public required Card[] BoardCards { get; init; }
+}
diff --git a/src/PokerRogue.Core/Combat/HandOutcome.cs b/src/PokerRogue.Core/Combat/HandOutcome.cs
new file mode 100644
index 0000000..1b9d8a6
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/HandOutcome.cs
@@ -0,0 +1,9 @@
+namespace PokerRogue.Core.Combat;
+
+public enum HandOutcome
+{
+ None,
+ PlayerWin,
+ EnemyWin,
+ Tie
+}
diff --git a/src/PokerRogue.Core/Combat/HandPhase.cs b/src/PokerRogue.Core/Combat/HandPhase.cs
new file mode 100644
index 0000000..50bb901
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/HandPhase.cs
@@ -0,0 +1,9 @@
+namespace PokerRogue.Core.Combat;
+
+public enum HandPhase
+{
+ Deal,
+ Clash,
+ Resolve,
+ End
+}
diff --git a/src/PokerRogue.Core/Combat/PlayerAction.cs b/src/PokerRogue.Core/Combat/PlayerAction.cs
new file mode 100644
index 0000000..0382671
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/PlayerAction.cs
@@ -0,0 +1,13 @@
+namespace PokerRogue.Core.Combat;
+
+public enum PlayerAction
+{
+ Strike,
+ Guard,
+ Dodge,
+ Check,
+ Probe,
+ AllIn,
+ Fold,
+ Skill
+}
diff --git a/src/PokerRogue.Core/Combat/PokerHandCategory.cs b/src/PokerRogue.Core/Combat/PokerHandCategory.cs
new file mode 100644
index 0000000..1bde433
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/PokerHandCategory.cs
@@ -0,0 +1,14 @@
+namespace PokerRogue.Core.Combat;
+
+public enum PokerHandCategory
+{
+ HighCard = 0,
+ Pair = 1,
+ TwoPair = 2,
+ ThreeOfAKind = 3,
+ Straight = 4,
+ Flush = 5,
+ FullHouse = 6,
+ FourOfAKind = 7,
+ StraightFlush = 8
+}
diff --git a/src/PokerRogue.Core/Combat/PokerHandEvaluator.cs b/src/PokerRogue.Core/Combat/PokerHandEvaluator.cs
new file mode 100644
index 0000000..070516b
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/PokerHandEvaluator.cs
@@ -0,0 +1,208 @@
+using PokerRogue.Core.Cards;
+
+namespace PokerRogue.Core.Combat;
+
+public static class PokerHandEvaluator
+{
+ public static PokerHandValue Evaluate(IReadOnlyList cards)
+ {
+ if (cards.Count != 7)
+ {
+ throw new ArgumentException("PokerHandEvaluator expects exactly 7 cards.", nameof(cards));
+ }
+
+ PokerHandValue? best = null;
+ for (var a = 0; a < cards.Count - 4; a++)
+ {
+ for (var b = a + 1; b < cards.Count - 3; b++)
+ {
+ for (var c = b + 1; c < cards.Count - 2; c++)
+ {
+ for (var d = c + 1; d < cards.Count - 1; d++)
+ {
+ for (var e = d + 1; e < cards.Count; e++)
+ {
+ var five = new[] { cards[a], cards[b], cards[c], cards[d], cards[e] };
+ var value = EvaluateFive(five);
+ if (best is null || Compare(value, best) > 0)
+ {
+ best = value;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return best!;
+ }
+
+ public static int Compare(PokerHandValue left, PokerHandValue right)
+ {
+ if (left.Category != right.Category)
+ {
+ return left.Category > right.Category ? 1 : -1;
+ }
+
+ var length = Math.Max(left.RankVector.Length, right.RankVector.Length);
+ for (var i = 0; i < length; i++)
+ {
+ var a = i < left.RankVector.Length ? left.RankVector[i] : 0;
+ var b = i < right.RankVector.Length ? right.RankVector[i] : 0;
+ if (a != b)
+ {
+ return a > b ? 1 : -1;
+ }
+ }
+
+ return 0;
+ }
+
+ public static string DescribeComparison(PokerHandValue player, PokerHandValue enemy)
+ {
+ if (player.Category != enemy.Category)
+ {
+ return $"Higher category: {player.DisplayName} vs {enemy.DisplayName}.";
+ }
+
+ for (var i = 0; i < Math.Max(player.RankVector.Length, enemy.RankVector.Length); i++)
+ {
+ var a = i < player.RankVector.Length ? player.RankVector[i] : 0;
+ var b = i < enemy.RankVector.Length ? enemy.RankVector[i] : 0;
+ if (a != b)
+ {
+ return $"Same category ({player.DisplayName}), kicker step {i + 1}: {a} vs {b}.";
+ }
+ }
+
+ return $"Exact tie: same {player.DisplayName} and identical kickers.";
+ }
+
+ private static PokerHandValue EvaluateFive(Card[] cards)
+ {
+ var ranks = cards.Select(card => (int)card.Rank).OrderByDescending(value => value).ToArray();
+ var rankGroups = ranks
+ .GroupBy(rank => rank)
+ .Select(group => new { Rank = group.Key, Count = group.Count() })
+ .OrderByDescending(x => x.Count)
+ .ThenByDescending(x => x.Rank)
+ .ToArray();
+
+ var isFlush = cards.Select(card => card.Suit).Distinct().Count() == 1;
+ var straightHigh = HighestStraight(ranks);
+ var isStraight = straightHigh > 0;
+
+ if (isFlush && isStraight)
+ {
+ return BuildValue(PokerHandCategory.StraightFlush, new[] { straightHigh }, cards);
+ }
+
+ if (rankGroups[0].Count == 4)
+ {
+ var quad = rankGroups[0].Rank;
+ var kicker = rankGroups[1].Rank;
+ return BuildValue(PokerHandCategory.FourOfAKind, new[] { quad, kicker }, cards);
+ }
+
+ if (rankGroups[0].Count == 3 && rankGroups[1].Count == 2)
+ {
+ return BuildValue(PokerHandCategory.FullHouse, new[] { rankGroups[0].Rank, rankGroups[1].Rank }, cards);
+ }
+
+ if (isFlush)
+ {
+ return BuildValue(PokerHandCategory.Flush, ranks, cards);
+ }
+
+ if (isStraight)
+ {
+ return BuildValue(PokerHandCategory.Straight, new[] { straightHigh }, cards);
+ }
+
+ if (rankGroups[0].Count == 3)
+ {
+ var trip = rankGroups[0].Rank;
+ var kickers = rankGroups.Where(x => x.Count == 1).Select(x => x.Rank).OrderByDescending(x => x).ToArray();
+ return BuildValue(PokerHandCategory.ThreeOfAKind, new[] { trip, kickers[0], kickers[1] }, cards);
+ }
+
+ if (rankGroups[0].Count == 2 && rankGroups[1].Count == 2)
+ {
+ var pairA = Math.Max(rankGroups[0].Rank, rankGroups[1].Rank);
+ var pairB = Math.Min(rankGroups[0].Rank, rankGroups[1].Rank);
+ var kicker = rankGroups.Single(x => x.Count == 1).Rank;
+ return BuildValue(PokerHandCategory.TwoPair, new[] { pairA, pairB, kicker }, cards);
+ }
+
+ if (rankGroups[0].Count == 2)
+ {
+ var pair = rankGroups[0].Rank;
+ var kickers = rankGroups.Where(x => x.Count == 1).Select(x => x.Rank).OrderByDescending(x => x).ToArray();
+ return BuildValue(PokerHandCategory.Pair, new[] { pair, kickers[0], kickers[1], kickers[2] }, cards);
+ }
+
+ return BuildValue(PokerHandCategory.HighCard, ranks, cards);
+ }
+
+ private static PokerHandValue BuildValue(PokerHandCategory category, int[] vector, Card[] winningCards)
+ {
+ var score = ((int)category * 1_000_000) + EncodeVector(vector);
+ return new PokerHandValue(category, vector, score, DisplayName(category), winningCards);
+ }
+
+ private static int EncodeVector(IReadOnlyList vector)
+ {
+ var encoded = 0;
+ foreach (var rank in vector)
+ {
+ encoded = (encoded * 15) + rank;
+ }
+
+ return encoded;
+ }
+
+ private static int HighestStraight(IEnumerable ranks)
+ {
+ var distinct = ranks.Distinct().OrderByDescending(value => value).ToList();
+ if (distinct.Contains(14))
+ {
+ distinct.Add(1);
+ }
+
+ var run = 1;
+ for (var i = 1; i < distinct.Count; i++)
+ {
+ if (distinct[i - 1] - 1 == distinct[i])
+ {
+ run++;
+ if (run >= 5)
+ {
+ return distinct[i - 4];
+ }
+ }
+ else
+ {
+ run = 1;
+ }
+ }
+
+ return 0;
+ }
+
+ private static string DisplayName(PokerHandCategory category)
+ {
+ return category switch
+ {
+ PokerHandCategory.HighCard => "High Card",
+ PokerHandCategory.Pair => "Pair",
+ PokerHandCategory.TwoPair => "Two Pair",
+ PokerHandCategory.ThreeOfAKind => "Three of a Kind",
+ PokerHandCategory.Straight => "Straight",
+ PokerHandCategory.Flush => "Flush",
+ PokerHandCategory.FullHouse => "Full House",
+ PokerHandCategory.FourOfAKind => "Four of a Kind",
+ PokerHandCategory.StraightFlush => "Straight Flush",
+ _ => category.ToString()
+ };
+ }
+}
diff --git a/src/PokerRogue.Core/Combat/PokerHandValue.cs b/src/PokerRogue.Core/Combat/PokerHandValue.cs
new file mode 100644
index 0000000..f80ffe8
--- /dev/null
+++ b/src/PokerRogue.Core/Combat/PokerHandValue.cs
@@ -0,0 +1,10 @@
+using PokerRogue.Core.Cards;
+
+namespace PokerRogue.Core.Combat;
+
+public sealed record PokerHandValue(
+ PokerHandCategory Category,
+ int[] RankVector,
+ int Score,
+ string DisplayName,
+ Card[] WinningCards);
diff --git a/src/PokerRogue.Core/Enemies/EnemyIntent.cs b/src/PokerRogue.Core/Enemies/EnemyIntent.cs
new file mode 100644
index 0000000..fec3bf1
--- /dev/null
+++ b/src/PokerRogue.Core/Enemies/EnemyIntent.cs
@@ -0,0 +1,8 @@
+namespace PokerRogue.Core.Enemies;
+
+public enum EnemyIntent
+{
+ Strike,
+ Guard,
+ Dodge
+}
diff --git a/src/PokerRogue.Core/Enemies/EnemyProfile.cs b/src/PokerRogue.Core/Enemies/EnemyProfile.cs
new file mode 100644
index 0000000..3e14aab
--- /dev/null
+++ b/src/PokerRogue.Core/Enemies/EnemyProfile.cs
@@ -0,0 +1,6 @@
+namespace PokerRogue.Core.Enemies;
+
+public sealed record EnemyProfile(string Name, int Aggression, int Trickery, int Cowardice)
+{
+ public static EnemyProfile GoblinManiac => new("Goblin Maniac", Aggression: 65, Trickery: 10, Cowardice: 25);
+}
diff --git a/src/PokerRogue.Core/PokerRogue.Core.csproj b/src/PokerRogue.Core/PokerRogue.Core.csproj
new file mode 100644
index 0000000..fa71b7a
--- /dev/null
+++ b/src/PokerRogue.Core/PokerRogue.Core.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/src/PokerRogue.Game/.config/dotnet-tools.json b/src/PokerRogue.Game/.config/dotnet-tools.json
new file mode 100644
index 0000000..a07174a
--- /dev/null
+++ b/src/PokerRogue.Game/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PokerRogue.Game/.vscode/launch.json b/src/PokerRogue.Game/.vscode/launch.json
new file mode 100644
index 0000000..faf83a1
--- /dev/null
+++ b/src/PokerRogue.Game/.vscode/launch.json
@@ -0,0 +1,14 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "C#: PokerRogue.Game Debug",
+ "type": "dotnet",
+ "request": "launch",
+ "projectPath": "${workspaceFolder}/PokerRogue.Game.csproj"
+ }
+ ],
+ }
\ No newline at end of file
diff --git a/src/PokerRogue.Game/Content/Cards/2C.png b/src/PokerRogue.Game/Content/Cards/2C.png
new file mode 100644
index 0000000..d23b316
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/2C.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/2D.png b/src/PokerRogue.Game/Content/Cards/2D.png
new file mode 100644
index 0000000..b88fccf
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/2D.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/2H.png b/src/PokerRogue.Game/Content/Cards/2H.png
new file mode 100644
index 0000000..f6fcf76
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/2H.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/2S.png b/src/PokerRogue.Game/Content/Cards/2S.png
new file mode 100644
index 0000000..70281e6
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/2S.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/3C.png b/src/PokerRogue.Game/Content/Cards/3C.png
new file mode 100644
index 0000000..5c4d93b
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/3C.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/3D.png b/src/PokerRogue.Game/Content/Cards/3D.png
new file mode 100644
index 0000000..7a65ea5
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/3D.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/3H.png b/src/PokerRogue.Game/Content/Cards/3H.png
new file mode 100644
index 0000000..e8b508c
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/3H.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/3S.png b/src/PokerRogue.Game/Content/Cards/3S.png
new file mode 100644
index 0000000..e432839
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/3S.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/4C.png b/src/PokerRogue.Game/Content/Cards/4C.png
new file mode 100644
index 0000000..8766ea7
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/4C.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/4D.png b/src/PokerRogue.Game/Content/Cards/4D.png
new file mode 100644
index 0000000..725a210
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/4D.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/4H.png b/src/PokerRogue.Game/Content/Cards/4H.png
new file mode 100644
index 0000000..25677ca
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/4H.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/4S.png b/src/PokerRogue.Game/Content/Cards/4S.png
new file mode 100644
index 0000000..be5f98e
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/4S.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/5C.png b/src/PokerRogue.Game/Content/Cards/5C.png
new file mode 100644
index 0000000..9b3dc8b
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/5C.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/5D.png b/src/PokerRogue.Game/Content/Cards/5D.png
new file mode 100644
index 0000000..a62c769
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/5D.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/5H.png b/src/PokerRogue.Game/Content/Cards/5H.png
new file mode 100644
index 0000000..fc9b6e5
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/5H.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/5S.png b/src/PokerRogue.Game/Content/Cards/5S.png
new file mode 100644
index 0000000..7104a56
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/5S.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/6C.png b/src/PokerRogue.Game/Content/Cards/6C.png
new file mode 100644
index 0000000..a2bdce9
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/6C.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/6D.png b/src/PokerRogue.Game/Content/Cards/6D.png
new file mode 100644
index 0000000..aaed4f7
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/6D.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/6H.png b/src/PokerRogue.Game/Content/Cards/6H.png
new file mode 100644
index 0000000..90b2f0b
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/6H.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/6S.png b/src/PokerRogue.Game/Content/Cards/6S.png
new file mode 100644
index 0000000..637670c
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/6S.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/7C.png b/src/PokerRogue.Game/Content/Cards/7C.png
new file mode 100644
index 0000000..d2086be
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/7C.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/7D.png b/src/PokerRogue.Game/Content/Cards/7D.png
new file mode 100644
index 0000000..5e1fbc7
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/7D.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/7H.png b/src/PokerRogue.Game/Content/Cards/7H.png
new file mode 100644
index 0000000..aaf3f51
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/7H.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/7S.png b/src/PokerRogue.Game/Content/Cards/7S.png
new file mode 100644
index 0000000..152e7ce
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/7S.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/8C.png b/src/PokerRogue.Game/Content/Cards/8C.png
new file mode 100644
index 0000000..3a7d997
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/8C.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/8D.png b/src/PokerRogue.Game/Content/Cards/8D.png
new file mode 100644
index 0000000..d3895b2
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/8D.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/8H.png b/src/PokerRogue.Game/Content/Cards/8H.png
new file mode 100644
index 0000000..901e4eb
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/8H.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/8S.png b/src/PokerRogue.Game/Content/Cards/8S.png
new file mode 100644
index 0000000..8b3ef36
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/8S.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/9C.png b/src/PokerRogue.Game/Content/Cards/9C.png
new file mode 100644
index 0000000..bc42583
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/9C.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/9D.png b/src/PokerRogue.Game/Content/Cards/9D.png
new file mode 100644
index 0000000..84f6646
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/9D.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/9H.png b/src/PokerRogue.Game/Content/Cards/9H.png
new file mode 100644
index 0000000..2d4f8c1
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/9H.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/9S.png b/src/PokerRogue.Game/Content/Cards/9S.png
new file mode 100644
index 0000000..622224a
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/9S.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/AC.png b/src/PokerRogue.Game/Content/Cards/AC.png
new file mode 100644
index 0000000..8591d05
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/AC.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/AD.png b/src/PokerRogue.Game/Content/Cards/AD.png
new file mode 100644
index 0000000..35e0b33
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/AD.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/AH.png b/src/PokerRogue.Game/Content/Cards/AH.png
new file mode 100644
index 0000000..4ec13bb
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/AH.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/AS.png b/src/PokerRogue.Game/Content/Cards/AS.png
new file mode 100644
index 0000000..9f4c226
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/AS.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/BACK.png b/src/PokerRogue.Game/Content/Cards/BACK.png
new file mode 100644
index 0000000..8be6637
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/BACK.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/JC.png b/src/PokerRogue.Game/Content/Cards/JC.png
new file mode 100644
index 0000000..0a094e7
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/JC.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/JD.png b/src/PokerRogue.Game/Content/Cards/JD.png
new file mode 100644
index 0000000..5d60ea4
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/JD.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/JH.png b/src/PokerRogue.Game/Content/Cards/JH.png
new file mode 100644
index 0000000..09c0b28
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/JH.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/JS.png b/src/PokerRogue.Game/Content/Cards/JS.png
new file mode 100644
index 0000000..c3aa638
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/JS.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/KC.png b/src/PokerRogue.Game/Content/Cards/KC.png
new file mode 100644
index 0000000..9a9b3a2
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/KC.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/KD.png b/src/PokerRogue.Game/Content/Cards/KD.png
new file mode 100644
index 0000000..9e10a73
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/KD.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/KH.png b/src/PokerRogue.Game/Content/Cards/KH.png
new file mode 100644
index 0000000..c9fca0d
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/KH.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/KS.png b/src/PokerRogue.Game/Content/Cards/KS.png
new file mode 100644
index 0000000..73865f9
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/KS.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/QC.png b/src/PokerRogue.Game/Content/Cards/QC.png
new file mode 100644
index 0000000..b2ae7a4
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/QC.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/QD.png b/src/PokerRogue.Game/Content/Cards/QD.png
new file mode 100644
index 0000000..0fc1976
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/QD.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/QH.png b/src/PokerRogue.Game/Content/Cards/QH.png
new file mode 100644
index 0000000..b8077d1
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/QH.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/QS.png b/src/PokerRogue.Game/Content/Cards/QS.png
new file mode 100644
index 0000000..6826231
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/QS.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/TC.png b/src/PokerRogue.Game/Content/Cards/TC.png
new file mode 100644
index 0000000..9a42920
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/TC.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/TD.png b/src/PokerRogue.Game/Content/Cards/TD.png
new file mode 100644
index 0000000..5f31e86
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/TD.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/TH.png b/src/PokerRogue.Game/Content/Cards/TH.png
new file mode 100644
index 0000000..446420a
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/TH.png differ
diff --git a/src/PokerRogue.Game/Content/Cards/TS.png b/src/PokerRogue.Game/Content/Cards/TS.png
new file mode 100644
index 0000000..09035cf
Binary files /dev/null and b/src/PokerRogue.Game/Content/Cards/TS.png differ
diff --git a/src/PokerRogue.Game/Content/Content.mgcb b/src/PokerRogue.Game/Content/Content.mgcb
new file mode 100644
index 0000000..2c973f1
--- /dev/null
+++ b/src/PokerRogue.Game/Content/Content.mgcb
@@ -0,0 +1,19 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin DebugFont.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/build:DebugFont.spritefont
diff --git a/src/PokerRogue.Game/Content/DebugFont.spritefont b/src/PokerRogue.Game/Content/DebugFont.spritefont
new file mode 100644
index 0000000..9c7f39e
--- /dev/null
+++ b/src/PokerRogue.Game/Content/DebugFont.spritefont
@@ -0,0 +1,20 @@
+
+
+
+ Consolas
+ 18
+ 1
+ true
+
+
+
+
+ ~
+
+
+ ♠
+ ♣
+
+
+
+
diff --git a/src/PokerRogue.Game/Game1.cs b/src/PokerRogue.Game/Game1.cs
new file mode 100644
index 0000000..4e9f3ce
--- /dev/null
+++ b/src/PokerRogue.Game/Game1.cs
@@ -0,0 +1,1028 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using PokerRogue.Core.Cards;
+using PokerRogue.Core.Combat;
+using PokerRogue.Core.Enemies;
+
+namespace PokerRogue.Game;
+
+public class Game1 : Microsoft.Xna.Framework.Game
+{
+ private const float CardAspect = 300f / 420f;
+
+ private readonly GraphicsDeviceManager _graphics;
+ private SpriteBatch _spriteBatch = null!;
+ private SpriteFont _debugFont = null!;
+ private Texture2D _pixel = null!;
+ private readonly Dictionary _cardTextures = new();
+
+ private FightState _fightState = null!;
+ private KeyboardState _previousKeyboardState;
+
+ private string _feedbackText = "Press 1/2/3 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 Card[] _showdownPlayerCards = Array.Empty();
+ private Card[] _showdownEnemyCards = Array.Empty();
+ private Card[] _showdownBoardCards = Array.Empty();
+
+ private RunScreen _runScreen = RunScreen.Title;
+ private readonly List _runNodes = new();
+ private int _currentNodeIndex;
+ private int _runMaxHp = 100;
+ private int _runPlayerHp = 100;
+ private int _runPotBonus;
+ private string _encounterText = string.Empty;
+ private DecisionStage _decisionStage = DecisionStage.Flop;
+ private PlayerAction? _flopAction;
+ private PlayerAction? _turnAction;
+ private PlayerAction? _riverAction;
+
+ public Game1()
+ {
+ _graphics = new GraphicsDeviceManager(this);
+ _graphics.PreferredBackBufferWidth = 1366;
+ _graphics.PreferredBackBufferHeight = 768;
+ _graphics.ApplyChanges();
+
+ Content.RootDirectory = "Content";
+ IsMouseVisible = true;
+ Window.AllowUserResizing = true;
+ }
+
+ protected override void Initialize()
+ {
+ _fightState = FightEngine.StartFight(FightConfig.Default(EnemyProfile.GoblinManiac), seed: 42);
+ _previousKeyboardState = Keyboard.GetState();
+ UpdateWindowTitle();
+ base.Initialize();
+ }
+
+ protected override void LoadContent()
+ {
+ _spriteBatch = new SpriteBatch(GraphicsDevice);
+ _debugFont = Content.Load("DebugFont");
+ _pixel = new Texture2D(GraphicsDevice, 1, 1);
+ _pixel.SetData(new[] { Color.White });
+ LoadCardTextures();
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ var keyboardState = Keyboard.GetState();
+ if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || keyboardState.IsKeyDown(Keys.Escape))
+ {
+ Exit();
+ }
+
+ if (Pressed(keyboardState, Keys.R))
+ {
+ StartNewRun();
+ }
+
+ switch (_runScreen)
+ {
+ case RunScreen.Title:
+ if (Pressed(keyboardState, Keys.Space) || Pressed(keyboardState, Keys.Enter))
+ {
+ StartNewRun();
+ }
+ break;
+ case RunScreen.Map:
+ if (Pressed(keyboardState, Keys.Space) || Pressed(keyboardState, Keys.Enter))
+ {
+ EnterCurrentNode();
+ }
+ break;
+ case RunScreen.Encounter:
+ if (Pressed(keyboardState, Keys.Space) || Pressed(keyboardState, Keys.Enter))
+ {
+ _runScreen = _currentNodeIndex >= _runNodes.Count - 1 ? RunScreen.Victory : RunScreen.Reward;
+ }
+ break;
+ case RunScreen.Reward:
+ if (Pressed(keyboardState, Keys.D1) || Pressed(keyboardState, Keys.NumPad1))
+ {
+ _runPlayerHp = Math.Min(_runMaxHp + 10, _runPlayerHp + 12);
+ _runMaxHp += 10;
+ AdvanceToNextNode();
+ }
+ else if (Pressed(keyboardState, Keys.D2) || Pressed(keyboardState, Keys.NumPad2))
+ {
+ _runPotBonus += 2;
+ AdvanceToNextNode();
+ }
+ else if (Pressed(keyboardState, Keys.D3) || Pressed(keyboardState, Keys.NumPad3))
+ {
+ _runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 20);
+ AdvanceToNextNode();
+ }
+ break;
+ case RunScreen.Fight:
+ UpdateFightScreen(keyboardState, gameTime);
+ break;
+ case RunScreen.Victory:
+ case RunScreen.Defeat:
+ if (Pressed(keyboardState, Keys.Space) || Pressed(keyboardState, Keys.Enter))
+ {
+ _runScreen = RunScreen.Title;
+ }
+ break;
+ }
+
+ if (_feedbackSecondsRemaining > 0f)
+ {
+ _feedbackSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds;
+ }
+
+ if (_showdownSecondsRemaining > 0f)
+ {
+ _showdownSecondsRemaining -= (float)gameTime.ElapsedGameTime.TotalSeconds;
+ if (_showdownSecondsRemaining <= 0f)
+ {
+ _showdownSecondsRemaining = 0f;
+ _awaitingContinue = true;
+ }
+ }
+
+ _previousKeyboardState = keyboardState;
+ UpdateWindowTitle();
+ base.Update(gameTime);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ GraphicsDevice.Clear(new Color(15, 72, 51));
+ _spriteBatch.Begin();
+ switch (_runScreen)
+ {
+ case RunScreen.Title:
+ DrawTitleScreen();
+ break;
+ case RunScreen.Map:
+ DrawMapScreen();
+ break;
+ case RunScreen.Encounter:
+ DrawEncounterScreen();
+ break;
+ case RunScreen.Reward:
+ DrawRewardScreen();
+ break;
+ case RunScreen.Fight:
+ var layout = BuildLayout();
+ DrawTopHud(layout);
+ DrawCardSections(layout);
+ DrawExplanationPanel(layout.PanelRect);
+ break;
+ case RunScreen.Victory:
+ DrawEndScreen("Run Completed", "Boss defeated. Press Space to return to title.");
+ break;
+ case RunScreen.Defeat:
+ DrawEndScreen("Defeat", "You were eliminated. Press Space to return to title.");
+ break;
+ }
+ _spriteBatch.End();
+
+ base.Draw(gameTime);
+ }
+
+ private void StartNewRun()
+ {
+ _runNodes.Clear();
+ _runNodes.Add(new RunNode(NodeType.Fight, "Forest Ambush"));
+ _runNodes.Add(new RunNode(NodeType.Event, "Campfire Event"));
+ _runNodes.Add(new RunNode(NodeType.Shop, "Traveling Merchant"));
+ _runNodes.Add(new RunNode(NodeType.Elite, "Knight Duel"));
+ _runNodes.Add(new RunNode(NodeType.Boss, "The Tax Collector"));
+
+ _currentNodeIndex = 0;
+ _runMaxHp = 100;
+ _runPlayerHp = 100;
+ _runPotBonus = 0;
+ _runScreen = RunScreen.Map;
+ _encounterText = string.Empty;
+ _decisionStage = DecisionStage.Flop;
+ _flopAction = null;
+ _turnAction = null;
+ _riverAction = null;
+ ResetShowdownState();
+ ShowFeedback("New run started.");
+ }
+
+ private void EnterCurrentNode()
+ {
+ if (_currentNodeIndex >= _runNodes.Count)
+ {
+ _runScreen = RunScreen.Victory;
+ return;
+ }
+
+ var node = _runNodes[_currentNodeIndex];
+ if (node.Type is NodeType.Fight or NodeType.Elite or NodeType.Boss)
+ {
+ StartFightForNode(node);
+ return;
+ }
+
+ if (node.Type == NodeType.Event)
+ {
+ _runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 10);
+ _encounterText = "Event: Campfire healed 10 HP.";
+ _runScreen = RunScreen.Encounter;
+ return;
+ }
+
+ _runPlayerHp = Math.Min(_runMaxHp, _runPlayerHp + 18);
+ _encounterText = "Shop: Purchased provisions (+18 HP).";
+ _runScreen = RunScreen.Encounter;
+ }
+
+ private void StartFightForNode(RunNode node)
+ {
+ var enemy = node.Type switch
+ {
+ NodeType.Elite => new EnemyProfile("Knight Captain", 55, 25, 20),
+ NodeType.Boss => new EnemyProfile("The Tax Collector", 70, 20, 10),
+ _ => EnemyProfile.GoblinManiac
+ };
+
+ var enemyHp = node.Type switch
+ {
+ NodeType.Boss => 130,
+ NodeType.Elite => 115,
+ _ => 90
+ };
+
+ var maxHands = node.Type switch
+ {
+ NodeType.Boss => 7,
+ NodeType.Elite => 6,
+ _ => 5
+ };
+
+ var config = new FightConfig(
+ StartingPlayerHp: _runPlayerHp,
+ StartingEnemyHp: enemyHp,
+ StartingPot: 10 + _runPotBonus,
+ MaxHands: maxHands,
+ Enemy: enemy);
+
+ _fightState = FightEngine.StartFight(config, seed: Environment.TickCount);
+ _decisionStage = DecisionStage.Flop;
+ _flopAction = null;
+ _turnAction = null;
+ _riverAction = null;
+ ResetShowdownState();
+ _runScreen = RunScreen.Fight;
+ }
+
+ private void UpdateFightScreen(KeyboardState keyboardState, GameTime gameTime)
+ {
+ var showdownActive = IsShowdownVisible;
+ if (_fightState.Phase != HandPhase.End && !showdownActive)
+ {
+ var pickedAction = ReadActionKey(keyboardState);
+ if (pickedAction is not null)
+ {
+ if (_decisionStage == DecisionStage.Flop)
+ {
+ _flopAction = pickedAction.Value;
+ _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
+ {
+ _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 (_awaitingContinue && (Pressed(keyboardState, Keys.Space) || Pressed(keyboardState, Keys.Enter)))
+ {
+ _awaitingContinue = false;
+ _showdownPlayerCards = Array.Empty();
+ _showdownEnemyCards = Array.Empty();
+ _showdownBoardCards = Array.Empty();
+ _decisionStage = DecisionStage.Flop;
+ _flopAction = null;
+ _turnAction = null;
+ _riverAction = null;
+ ShowFeedback("Next hand.");
+ }
+
+ if (_fightState.Phase == HandPhase.End && !IsShowdownVisible)
+ {
+ _runPlayerHp = _fightState.PlayerHp;
+ if (_runPlayerHp <= 0)
+ {
+ _runScreen = RunScreen.Defeat;
+ return;
+ }
+
+ if (_currentNodeIndex >= _runNodes.Count - 1)
+ {
+ _runScreen = RunScreen.Victory;
+ return;
+ }
+
+ _runScreen = RunScreen.Reward;
+ }
+ }
+
+ private void AdvanceToNextNode()
+ {
+ _currentNodeIndex++;
+ if (_currentNodeIndex >= _runNodes.Count)
+ {
+ _runScreen = RunScreen.Victory;
+ return;
+ }
+
+ _runScreen = RunScreen.Map;
+ }
+
+ private void DrawTitleScreen()
+ {
+ DrawLine(new Vector2(70, 90), "PokerRogue - Vertical Slice Prototype");
+ DrawLine(new Vector2(70, 140), "Arcade poker combat run: Fight -> Event -> Shop -> Elite -> Boss");
+ DrawLine(new Vector2(70, 200), "Press Space to start a run.");
+ DrawLine(new Vector2(70, 240), "Press Esc to quit.");
+ }
+
+ private void DrawMapScreen()
+ {
+ DrawLine(new Vector2(40, 24), $"Map - Node {_currentNodeIndex + 1}/{_runNodes.Count} | HP {_runPlayerHp}/{_runMaxHp} | Pot bonus +{_runPotBonus}");
+ for (var i = 0; i < _runNodes.Count; i++)
+ {
+ var node = _runNodes[i];
+ var prefix = i == _currentNodeIndex ? ">" : " ";
+ DrawLine(new Vector2(64, 80 + (i * 34)), $"{prefix} {i + 1}. {node.Type} - {node.Name}");
+ }
+
+ DrawLine(new Vector2(40, 300), "Press Space to enter current node. Press R to restart run.");
+ }
+
+ private void DrawEncounterScreen()
+ {
+ 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, 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, 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");
+ }
+
+ private void DrawEndScreen(string title, string subtitle)
+ {
+ DrawLine(new Vector2(70, 100), title);
+ DrawLine(new Vector2(70, 150), subtitle);
+ DrawLine(new Vector2(70, 210), $"Final progression: node {_currentNodeIndex + 1}/{_runNodes.Count}");
+ }
+
+ private void ResetShowdownState()
+ {
+ _showdownSecondsRemaining = 0f;
+ _awaitingContinue = false;
+ _showdownPlayerCards = Array.Empty();
+ _showdownEnemyCards = Array.Empty();
+ _showdownBoardCards = Array.Empty();
+ _decisionStage = DecisionStage.Flop;
+ _flopAction = null;
+ _turnAction = null;
+ _riverAction = null;
+ }
+
+ private Layout BuildLayout()
+ {
+ var viewport = GraphicsDevice.Viewport;
+ var margin = Math.Max(14, viewport.Width / 90);
+ var topHudHeight = Math.Max(200, viewport.Height / 3);
+
+ var panelWidth = Math.Clamp((int)(viewport.Width * 0.34f), 360, 620);
+ var panelX = viewport.Width - margin - panelWidth;
+ var panelRect = new Rectangle(panelX, topHudHeight + margin, panelWidth, viewport.Height - topHudHeight - (margin * 2));
+
+ var leftX = margin;
+ var leftWidth = Math.Max(260, panelX - (margin * 2) - leftX);
+ var cardsTop = topHudHeight + margin + 18;
+ var cardsBottom = viewport.Height - margin;
+ var cardsHeight = cardsBottom - cardsTop;
+
+ var rowGap = Math.Max(18, viewport.Height / 48);
+ var labelHeight = 30;
+ var rowHeight = (cardsHeight - (rowGap * 2)) / 3;
+ var cardGap = Math.Max(8, viewport.Width / 140);
+
+ var maxCardHeightByRow = Math.Max(48, rowHeight - labelHeight - 8);
+ var maxCardWidthByBoard = Math.Max(60, (leftWidth - (cardGap * 4)) / 5);
+ var cardWidth = Math.Min(maxCardWidthByBoard, (int)(maxCardHeightByRow * CardAspect));
+ cardWidth = Math.Max(56, cardWidth);
+ var cardHeight = (int)(cardWidth / CardAspect);
+
+ if (cardHeight > maxCardHeightByRow)
+ {
+ cardHeight = maxCardHeightByRow;
+ cardWidth = (int)(cardHeight * CardAspect);
+ }
+
+ var enemyRowTop = cardsTop;
+ var boardRowTop = enemyRowTop + rowHeight + rowGap;
+ var playerRowTop = boardRowTop + rowHeight + rowGap;
+
+ var enemyCardsX = leftX + (leftWidth - ((cardWidth * 2) + cardGap)) / 2;
+ var boardCardsX = leftX + (leftWidth - ((cardWidth * 5) + (cardGap * 4))) / 2;
+ var playerCardsX = leftX + (leftWidth - ((cardWidth * 2) + cardGap)) / 2;
+
+ return new Layout(
+ Margin: margin,
+ TopHudHeight: topHudHeight,
+ PanelRect: panelRect,
+ CardWidth: cardWidth,
+ CardHeight: cardHeight,
+ CardGap: cardGap,
+ EnemyLabelPos: new Vector2(leftX, enemyRowTop),
+ EnemyCardsPos: new Vector2(enemyCardsX, enemyRowTop + labelHeight),
+ BoardLabelPos: new Vector2(leftX, boardRowTop),
+ BoardCardsPos: new Vector2(boardCardsX, boardRowTop + labelHeight),
+ PlayerLabelPos: new Vector2(leftX, playerRowTop),
+ PlayerCardsPos: new Vector2(playerCardsX, playerRowTop + labelHeight));
+ }
+
+ private void DrawTopHud(Layout layout)
+ {
+ var x = layout.Margin;
+ 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);
+
+ if (_feedbackSecondsRemaining > 0f)
+ {
+ DrawLine(new Vector2(x, 292), _feedbackText, Color.Gold);
+ }
+ }
+
+ private void DrawCardSections(Layout layout)
+ {
+ DrawLine(layout.EnemyLabelPos, "Enemy Cards");
+ DrawLine(layout.BoardLabelPos, "Board");
+ DrawLine(layout.PlayerLabelPos, "Player Cards");
+
+ var enemyCards = IsShowdownVisible ? _showdownEnemyCards : _fightState.EnemyHoleCards;
+ var boardCards = IsShowdownVisible
+ ? _showdownBoardCards
+ : _fightState.BoardCards.Take(GetBoardCountForDecisionStage()).ToArray();
+ var playerCards = IsShowdownVisible ? _showdownPlayerCards : _fightState.PlayerHoleCards;
+ var shownBoardCards = GetRevealedBoardCards(boardCards);
+ var isRiverRevealed = IsRiverRevealed;
+ var showEnemyFaces = IsShowdownVisible && isRiverRevealed;
+
+ var playerHighlight = BuildHighlightSet(_fightState.LastPlayerWinningCards);
+ var enemyHighlight = BuildHighlightSet(_fightState.LastEnemyWinningCards);
+ var boardHighlight = _fightState.LastOutcome switch
+ {
+ HandOutcome.PlayerWin => playerHighlight,
+ HandOutcome.EnemyWin => enemyHighlight,
+ HandOutcome.Tie => new HashSet(playerHighlight.Concat(enemyHighlight)),
+ _ => new HashSet()
+ };
+
+ DrawCardRow(layout.EnemyCardsPos, enemyCards, hideFaces: !showEnemyFaces, layout, enemyHighlight, enableHighlight: isRiverRevealed);
+ DrawCardRow(layout.BoardCardsPos, shownBoardCards, hideFaces: false, layout, boardHighlight, enableHighlight: isRiverRevealed);
+ DrawCardRow(layout.PlayerCardsPos, playerCards, hideFaces: false, layout, playerHighlight, enableHighlight: isRiverRevealed);
+ }
+
+ private void DrawExplanationPanel(Rectangle panel)
+ {
+ DrawFilled(panel, new Color(8, 34, 24, 210));
+ DrawBorder(panel, Color.Black);
+
+ var x = panel.X + 14;
+ var y = panel.Y + 16;
+
+ DrawLine(new Vector2(x, y), "Why this result");
+ y += 30;
+
+ DrawLine(new Vector2(x, y), "Poker evaluator (best 5 of 7):");
+ y += 24;
+ DrawLine(new Vector2(x, y), "1) Detect category (pair/straight/flush...)");
+ y += 22;
+ DrawLine(new Vector2(x, y), "2) If same category, compare kicker vector");
+ y += 22;
+ DrawLine(new Vector2(x, y), "3) Higher category/vector wins");
+ y += 30;
+
+ DrawLine(new Vector2(x, y), $"Player hand: {_fightState.LastPlayerHandName}");
+ y += 22;
+ DrawLine(new Vector2(x, y), $"Enemy hand: {_fightState.LastEnemyHandName}");
+ y += 22;
+ DrawLine(new Vector2(x, y), $"Player top ranks: {FormatRanks(_fightState.LastPlayerTopRanks)}");
+ y += 22;
+ DrawLine(new Vector2(x, y), $"Enemy top ranks: {FormatRanks(_fightState.LastEnemyTopRanks)}");
+ y += 22;
+ DrawLine(new Vector2(x, y), $"Player score: {_fightState.LastPlayerScore}");
+ y += 22;
+ DrawLine(new Vector2(x, y), $"Enemy score: {_fightState.LastEnemyScore}");
+ y += 30;
+
+ DrawLine(new Vector2(x, y), BuildWinnerReason());
+ y += 30;
+ DrawWrappedText(_fightState.LastComparisonReason, x, y, panel.Width - 28, 20);
+ y += 48;
+
+ DrawLine(new Vector2(x, y), "Damage logic:");
+ y += 22;
+ if (_fightState.LastOutcome == HandOutcome.PlayerWin)
+ {
+ DrawLine(new Vector2(x, y), $"EnemyDamage = Pot * (100 + PlayerEdge) / 100 = {_fightState.LastDamageToEnemy}");
+ }
+ else if (_fightState.LastOutcome == HandOutcome.EnemyWin)
+ {
+ DrawLine(new Vector2(x, y), $"PlayerDamage = max(0, Pot*(100+EnemyEdge)/100 - Shield) = {_fightState.LastDamageToPlayer}");
+ }
+ else
+ {
+ DrawLine(new Vector2(x, y), "No damage on tie/no-resolve.");
+ }
+
+ if (_fightState.Phase == HandPhase.End)
+ {
+ DrawLine(new Vector2(x, panel.Bottom - 34), "Fight ended. Press R to restart.", Color.OrangeRed);
+ }
+ else if (IsShowdownVisible)
+ {
+ DrawLine(new Vector2(x, panel.Bottom - 34), BuildRevealStageText(), Color.LightGreen);
+ }
+ if (_awaitingContinue)
+ {
+ DrawLine(new Vector2(x, panel.Bottom - 58), "Stlac Space pre dalsiu handu.", Color.Gold);
+ }
+ }
+
+ private void DrawCardRow(
+ Vector2 origin,
+ IReadOnlyList cards,
+ bool hideFaces,
+ Layout layout,
+ HashSet highlightSet,
+ bool enableHighlight)
+ {
+ for (var i = 0; i < cards.Count; i++)
+ {
+ var rect = new Rectangle(
+ (int)origin.X + i * (layout.CardWidth + layout.CardGap),
+ (int)origin.Y,
+ layout.CardWidth,
+ layout.CardHeight);
+ var highlight = enableHighlight && IsShowdownVisible && !hideFaces && highlightSet.Contains(CardKey(cards[i]));
+ DrawCard(rect, cards[i], hideFaces, highlight);
+ }
+ }
+
+ private void DrawCard(Rectangle rect, Card card, bool hideFace, bool highlight)
+ {
+ var shadow = new Rectangle(rect.X + 3, rect.Y + 4, rect.Width, rect.Height);
+ DrawFilled(shadow, new Color(0, 0, 0, 90));
+
+ if (TryGetCardTexture(card, hideFace, out var texture))
+ {
+ _spriteBatch.Draw(texture, rect, Color.White);
+ DrawBorder(rect, new Color(15, 15, 15));
+ if (highlight)
+ {
+ DrawHighlight(rect);
+ }
+ return;
+ }
+
+ DrawFilled(rect, hideFace ? new Color(30, 30, 30) : Color.WhiteSmoke);
+ DrawBorder(rect, Color.Black);
+ if (!hideFace)
+ {
+ DrawRankCorners(rect, card);
+ }
+ if (highlight)
+ {
+ DrawHighlight(rect);
+ }
+ }
+
+ private void ApplyActionAndFeedback(
+ PlayerAction action,
+ PlayerAction? flopAction = null,
+ PlayerAction? turnAction = null,
+ PlayerAction? riverAction = null)
+ {
+ _showdownPlayerCards = _fightState.PlayerHoleCards.ToArray();
+ _showdownEnemyCards = _fightState.EnemyHoleCards.ToArray();
+ _showdownBoardCards = _fightState.BoardCards.ToArray();
+
+ _fightState = FightEngine.ApplyPlayerAction(_fightState, action);
+ _showdownSecondsRemaining = ShowdownDurationSeconds;
+ _awaitingContinue = false;
+ if (flopAction is not null && turnAction is not null && riverAction is not null)
+ {
+ ShowFeedback($"Flop {flopAction.Value} + Turn {turnAction.Value} + River {riverAction.Value} => {action} | {_fightState.LastOutcome}");
+ }
+ else
+ {
+ ShowFeedback($"{action} -> {_fightState.LastOutcome} | Dealt {_fightState.LastDamageToEnemy}, Took {_fightState.LastDamageToPlayer}");
+ }
+ }
+
+ private void ShowFeedback(string text)
+ {
+ _feedbackText = text;
+ _feedbackSecondsRemaining = 2.2f;
+ }
+
+ private bool Pressed(KeyboardState currentState, Keys key)
+ {
+ return currentState.IsKeyDown(key) && !_previousKeyboardState.IsKeyDown(key);
+ }
+
+ private void DrawLine(Vector2 position, string text, Color? color = null)
+ {
+ _spriteBatch.DrawString(_debugFont, text, position, color ?? Color.White);
+ }
+
+ private void DrawWrappedText(string text, float x, float y, float maxWidth, float lineHeight)
+ {
+ var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ var line = string.Empty;
+ var cursorY = y;
+
+ foreach (var word in words)
+ {
+ var candidate = string.IsNullOrEmpty(line) ? word : $"{line} {word}";
+ if (_debugFont.MeasureString(candidate).X > maxWidth && !string.IsNullOrEmpty(line))
+ {
+ DrawLine(new Vector2(x, cursorY), line);
+ cursorY += lineHeight;
+ line = word;
+ }
+ else
+ {
+ line = candidate;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(line))
+ {
+ DrawLine(new Vector2(x, cursorY), line);
+ }
+ }
+
+ private void DrawFilled(Rectangle rect, Color color)
+ {
+ _spriteBatch.Draw(_pixel, rect, color);
+ }
+
+ private void DrawBorder(Rectangle rect, Color color)
+ {
+ const int thickness = 2;
+ _spriteBatch.Draw(_pixel, new Rectangle(rect.X, rect.Y, rect.Width, thickness), color);
+ _spriteBatch.Draw(_pixel, new Rectangle(rect.X, rect.Bottom - thickness, rect.Width, thickness), color);
+ _spriteBatch.Draw(_pixel, new Rectangle(rect.X, rect.Y, thickness, rect.Height), color);
+ _spriteBatch.Draw(_pixel, new Rectangle(rect.Right - thickness, rect.Y, thickness, rect.Height), color);
+ }
+
+ private void DrawHighlight(Rectangle rect)
+ {
+ const int thickness = 4;
+ var color = new Color(255, 214, 64);
+ _spriteBatch.Draw(_pixel, new Rectangle(rect.X - 1, rect.Y - 1, rect.Width + 2, thickness), color);
+ _spriteBatch.Draw(_pixel, new Rectangle(rect.X - 1, rect.Bottom - thickness + 1, rect.Width + 2, thickness), color);
+ _spriteBatch.Draw(_pixel, new Rectangle(rect.X - 1, rect.Y - 1, thickness, rect.Height + 2), color);
+ _spriteBatch.Draw(_pixel, new Rectangle(rect.Right - thickness + 1, rect.Y - 1, thickness, rect.Height + 2), color);
+ }
+
+ private void DrawRankCorners(Rectangle rect, Card card)
+ {
+ var rank = FormatRankToken((int)card.Rank);
+ var color = card.Suit is Suit.Hearts or Suit.Diamonds ? new Color(172, 24, 24) : new Color(20, 20, 20);
+
+ var scale = Math.Clamp(rect.Width / 120f, 0.5f, 1.0f);
+ var originOffset = Math.Max(4, (int)(6 * scale));
+ var size = _debugFont.MeasureString(rank) * scale;
+
+ _spriteBatch.DrawString(_debugFont, rank, new Vector2(rect.X + originOffset, rect.Y + originOffset), color, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f);
+ _spriteBatch.DrawString(
+ _debugFont,
+ rank,
+ new Vector2(rect.Right - size.X - originOffset, rect.Bottom - size.Y - originOffset),
+ color,
+ 0f,
+ Vector2.Zero,
+ scale,
+ SpriteEffects.None,
+ 0f);
+ }
+
+ private PlayerAction? ReadActionKey(KeyboardState keyboardState)
+ {
+ if (Pressed(keyboardState, Keys.D1) || Pressed(keyboardState, Keys.NumPad1)) return PlayerAction.Strike;
+ if (Pressed(keyboardState, Keys.D2) || Pressed(keyboardState, Keys.NumPad2)) return PlayerAction.Guard;
+ if (Pressed(keyboardState, Keys.D3) || Pressed(keyboardState, Keys.NumPad3)) return PlayerAction.Dodge;
+ if (Pressed(keyboardState, Keys.D4) || Pressed(keyboardState, Keys.NumPad4)) return PlayerAction.Check;
+ if (Pressed(keyboardState, Keys.D5) || Pressed(keyboardState, Keys.NumPad5)) return PlayerAction.Probe;
+ if (Pressed(keyboardState, Keys.D6) || Pressed(keyboardState, Keys.NumPad6)) return PlayerAction.AllIn;
+ if (Pressed(keyboardState, Keys.D7) || Pressed(keyboardState, Keys.NumPad7)) return PlayerAction.Fold;
+ return null;
+ }
+
+ private PlayerAction CombineActions(PlayerAction flopAction, PlayerAction turnAction, PlayerAction riverAction)
+ {
+ if (flopAction == PlayerAction.Fold || turnAction == PlayerAction.Fold || riverAction == PlayerAction.Fold)
+ {
+ return PlayerAction.Fold;
+ }
+
+ if (flopAction == PlayerAction.AllIn || turnAction == PlayerAction.AllIn || riverAction == PlayerAction.AllIn)
+ {
+ return PlayerAction.AllIn;
+ }
+
+ var score = ActionScore(flopAction) + ActionScore(turnAction) + ActionScore(riverAction);
+ if (score <= -4) return PlayerAction.Dodge;
+ if (score <= -2) return PlayerAction.Check;
+ if (score <= 0) return PlayerAction.Guard;
+ if (score <= 2) return PlayerAction.Probe;
+ if (score <= 4) return PlayerAction.Strike;
+ return PlayerAction.AllIn;
+ }
+
+ private static int ActionScore(PlayerAction action)
+ {
+ return action switch
+ {
+ PlayerAction.Dodge => -2,
+ PlayerAction.Check => -1,
+ PlayerAction.Guard => 0,
+ PlayerAction.Probe => 1,
+ PlayerAction.Strike => 2,
+ PlayerAction.AllIn => 3,
+ PlayerAction.Fold => -4,
+ _ => 0
+ };
+ }
+
+ private string BuildDecisionStageText()
+ {
+ if (_runScreen != RunScreen.Fight || _fightState.Phase == HandPhase.End)
+ {
+ return "-";
+ }
+
+ if (IsShowdownVisible)
+ {
+ return "Resolve/Showdown";
+ }
+
+ return _decisionStage switch
+ {
+ DecisionStage.Flop => "Flop action (1st)",
+ DecisionStage.Turn => "Turn action (2nd)",
+ DecisionStage.River => "River action (3rd)",
+ _ => "-"
+ };
+ }
+
+ private void UpdateWindowTitle()
+ {
+ if (_runScreen == RunScreen.Fight)
+ {
+ Window.Title =
+ $"PokerRogue | Fight {_currentNodeIndex + 1}/{Math.Max(1, _runNodes.Count)} | " +
+ $"Hand {_fightState.HandNumber}/{_fightState.Config.MaxHands} | PHP {_fightState.PlayerHp} EHP {_fightState.EnemyHp}";
+ return;
+ }
+
+ Window.Title = $"PokerRogue | {_runScreen} | HP {_runPlayerHp}/{_runMaxHp}";
+ }
+
+ private string BuildWinnerReason()
+ {
+ return _fightState.LastOutcome switch
+ {
+ HandOutcome.PlayerWin => $"Player wins because {_fightState.LastPlayerScore} > {_fightState.LastEnemyScore}.",
+ HandOutcome.EnemyWin => $"Enemy wins because {_fightState.LastEnemyScore} > {_fightState.LastPlayerScore}.",
+ HandOutcome.Tie => $"Tie because {_fightState.LastPlayerScore} == {_fightState.LastEnemyScore}.",
+ _ => "No resolved hand yet. Press 1..7."
+ };
+ }
+
+ private static string FormatRanks(IReadOnlyList ranks)
+ {
+ return ranks.Count == 0 ? "-" : string.Join(", ", ranks.Select(FormatRankNumber));
+ }
+
+ private static string FormatRankNumber(int rank)
+ {
+ return rank switch
+ {
+ 14 => "A",
+ 13 => "K",
+ 12 => "Q",
+ 11 => "J",
+ 10 => "T",
+ _ => rank.ToString()
+ };
+ }
+
+ private static string FormatRankToken(int rank)
+ {
+ return rank switch
+ {
+ 14 => "A",
+ 13 => "K",
+ 12 => "Q",
+ 11 => "J",
+ _ => rank.ToString()
+ };
+ }
+
+ private static string FormatSuit(Suit suit)
+ {
+ return suit switch
+ {
+ Suit.Clubs => "C",
+ Suit.Diamonds => "D",
+ Suit.Hearts => "H",
+ Suit.Spades => "S",
+ _ => "?"
+ };
+ }
+
+ private static string CardKey(Card card)
+ {
+ return $"{(int)card.Rank}_{(int)card.Suit}";
+ }
+
+ private static HashSet BuildHighlightSet(IEnumerable cards)
+ {
+ return new HashSet(cards.Select(CardKey));
+ }
+
+ private Card[] GetRevealedBoardCards(IReadOnlyList boardCards)
+ {
+ if (!IsShowdownVisible)
+ {
+ return boardCards.Take(3).ToArray();
+ }
+
+ var elapsed = ShowdownDurationSeconds - _showdownSecondsRemaining;
+ if (elapsed < TurnRevealAtSeconds)
+ {
+ return boardCards.Take(3).ToArray();
+ }
+
+ if (elapsed < RiverRevealAtSeconds)
+ {
+ return boardCards.Take(4).ToArray();
+ }
+
+ return boardCards.Take(5).ToArray();
+ }
+
+ private int GetBoardCountForDecisionStage()
+ {
+ return _decisionStage switch
+ {
+ DecisionStage.Flop => 3,
+ DecisionStage.Turn => 4,
+ DecisionStage.River => 5,
+ _ => 3
+ };
+ }
+
+ private string BuildRevealStageText()
+ {
+ var elapsed = ShowdownDurationSeconds - _showdownSecondsRemaining;
+ if (elapsed < TurnRevealAtSeconds)
+ {
+ return "Showdown: Flop (3 karty)";
+ }
+
+ if (elapsed < RiverRevealAtSeconds)
+ {
+ return "Showdown: Turn (4. karta)";
+ }
+
+ return "Showdown: River + vitazna kombinacia";
+ }
+
+ private void LoadCardTextures()
+ {
+ _cardTextures.Clear();
+ var cardsDir = Path.Combine(AppContext.BaseDirectory, "Content", "Cards");
+ if (!Directory.Exists(cardsDir))
+ {
+ return;
+ }
+
+ foreach (var path in Directory.EnumerateFiles(cardsDir, "*.png", SearchOption.TopDirectoryOnly))
+ {
+ using var stream = File.OpenRead(path);
+ var texture = Texture2D.FromStream(GraphicsDevice, stream);
+ var key = Path.GetFileNameWithoutExtension(path).ToUpperInvariant();
+ _cardTextures[key] = texture;
+ }
+ }
+
+ private bool TryGetCardTexture(Card card, bool hideFace, out Texture2D texture)
+ {
+ if (hideFace)
+ {
+ return _cardTextures.TryGetValue("BACK", out texture!);
+ }
+
+ var key = $"{FormatRankNumber((int)card.Rank)}{FormatSuit(card.Suit)}";
+ return _cardTextures.TryGetValue(key, out texture!);
+ }
+
+ private bool IsShowdownVisible => _showdownSecondsRemaining > 0f || _awaitingContinue;
+ private bool IsRiverRevealed => _awaitingContinue || (IsShowdownVisible && (ShowdownDurationSeconds - _showdownSecondsRemaining) >= RiverRevealAtSeconds);
+
+ private enum DecisionStage
+ {
+ Flop,
+ Turn,
+ River
+ }
+
+ private enum RunScreen
+ {
+ Title,
+ Map,
+ Encounter,
+ Reward,
+ Fight,
+ Victory,
+ Defeat
+ }
+
+ private enum NodeType
+ {
+ Fight,
+ Event,
+ Shop,
+ Elite,
+ Boss
+ }
+
+ private sealed record RunNode(NodeType Type, string Name);
+
+ private sealed record Layout(
+ int Margin,
+ int TopHudHeight,
+ Rectangle PanelRect,
+ int CardWidth,
+ int CardHeight,
+ int CardGap,
+ Vector2 EnemyLabelPos,
+ Vector2 EnemyCardsPos,
+ Vector2 BoardLabelPos,
+ Vector2 BoardCardsPos,
+ Vector2 PlayerLabelPos,
+ Vector2 PlayerCardsPos);
+}
diff --git a/src/PokerRogue.Game/Icon.bmp b/src/PokerRogue.Game/Icon.bmp
new file mode 100644
index 0000000..2b48165
Binary files /dev/null and b/src/PokerRogue.Game/Icon.bmp differ
diff --git a/src/PokerRogue.Game/Icon.ico b/src/PokerRogue.Game/Icon.ico
new file mode 100644
index 0000000..7d9dec1
Binary files /dev/null and b/src/PokerRogue.Game/Icon.ico differ
diff --git a/src/PokerRogue.Game/PokerRogue.Game.csproj b/src/PokerRogue.Game/PokerRogue.Game.csproj
new file mode 100644
index 0000000..2ef5655
--- /dev/null
+++ b/src/PokerRogue.Game/PokerRogue.Game.csproj
@@ -0,0 +1,38 @@
+
+
+ WinExe
+ net8.0
+ x64
+ Major
+ false
+ false
+
+
+ app.manifest
+ Icon.ico
+
+
+
+
+
+
+
+ Icon.ico
+
+
+ Icon.bmp
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/src/PokerRogue.Game/Program.cs b/src/PokerRogue.Game/Program.cs
new file mode 100644
index 0000000..bdedc6d
--- /dev/null
+++ b/src/PokerRogue.Game/Program.cs
@@ -0,0 +1,2 @@
+using var game = new PokerRogue.Game.Game1();
+game.Run();
diff --git a/src/PokerRogue.Game/app.manifest b/src/PokerRogue.Game/app.manifest
new file mode 100644
index 0000000..4edae28
--- /dev/null
+++ b/src/PokerRogue.Game/app.manifest
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
+
diff --git a/src/PokerRogue.Tests/FightEngineTests.cs b/src/PokerRogue.Tests/FightEngineTests.cs
new file mode 100644
index 0000000..2188d0d
--- /dev/null
+++ b/src/PokerRogue.Tests/FightEngineTests.cs
@@ -0,0 +1,62 @@
+using PokerRogue.Core.Combat;
+using PokerRogue.Core.Enemies;
+using System;
+
+namespace PokerRogue.Tests;
+
+public sealed class FightEngineTests
+{
+ [Fact]
+ public void StartFight_InitializesClashPhaseAndFlop()
+ {
+ var config = FightConfig.Default(EnemyProfile.GoblinManiac);
+
+ var state = FightEngine.StartFight(config, seed: 42);
+
+ Assert.Equal(HandPhase.Clash, state.Phase);
+ Assert.Equal(1, state.HandNumber);
+ Assert.Equal(2, state.PlayerHoleCards.Length);
+ Assert.Equal(2, state.EnemyHoleCards.Length);
+ Assert.Equal(5, state.BoardCards.Length);
+ }
+
+ [Fact]
+ public void StartFight_WithSameSeed_ProducesSameOpeningState()
+ {
+ var config = FightConfig.Default(EnemyProfile.GoblinManiac);
+
+ var a = FightEngine.StartFight(config, seed: 1337);
+ var b = FightEngine.StartFight(config, seed: 1337);
+
+ Assert.Equal(a.EnemyIntent, b.EnemyIntent);
+ Assert.Equal(a.PlayerHoleCards, b.PlayerHoleCards);
+ Assert.Equal(a.EnemyHoleCards, b.EnemyHoleCards);
+ Assert.Equal(a.BoardCards, b.BoardCards);
+ }
+
+ [Fact]
+ public void ApplyPlayerAction_AdvancesFightOrEnds()
+ {
+ var config = FightConfig.Default(EnemyProfile.GoblinManiac) with { MaxHands = 2 };
+ var state = FightEngine.StartFight(config, seed: 7);
+
+ var afterFirst = FightEngine.ApplyPlayerAction(state, PlayerAction.Strike);
+ var afterSecond = FightEngine.ApplyPlayerAction(afterFirst, PlayerAction.Guard);
+
+ Assert.True(afterFirst.Phase is HandPhase.Clash or HandPhase.End);
+ Assert.Equal(HandPhase.End, afterSecond.Phase);
+ }
+
+ [Fact]
+ public void ApplyPlayerAction_AllActionsAreAccepted()
+ {
+ var config = FightConfig.Default(EnemyProfile.GoblinManiac) with { MaxHands = 10 };
+
+ foreach (var action in Enum.GetValues())
+ {
+ var state = FightEngine.StartFight(config, seed: 123);
+ var result = FightEngine.ApplyPlayerAction(state, action);
+ Assert.True(result.Phase is HandPhase.Clash or HandPhase.End);
+ }
+ }
+}
diff --git a/src/PokerRogue.Tests/PokerHandEvaluatorTests.cs b/src/PokerRogue.Tests/PokerHandEvaluatorTests.cs
new file mode 100644
index 0000000..19d74f9
--- /dev/null
+++ b/src/PokerRogue.Tests/PokerHandEvaluatorTests.cs
@@ -0,0 +1,143 @@
+using PokerRogue.Core.Cards;
+using PokerRogue.Core.Combat;
+
+namespace PokerRogue.Tests;
+
+public sealed class PokerHandEvaluatorTests
+{
+ [Fact]
+ public void Pair_Beats_HighCard()
+ {
+ var pair = Eval("AS", "AD", "KC", "8H", "7D", "4C", "2S");
+ var high = Eval("AS", "KD", "QC", "9H", "7D", "4C", "2S");
+
+ Assert.True(PokerHandEvaluator.Compare(pair, high) > 0);
+ }
+
+ [Fact]
+ public void TwoPair_Beats_Pair()
+ {
+ var twoPair = Eval("AS", "AD", "KC", "KH", "7D", "4C", "2S");
+ var pair = Eval("QS", "QD", "AC", "9H", "7D", "4C", "2S");
+
+ Assert.True(PokerHandEvaluator.Compare(twoPair, pair) > 0);
+ }
+
+ [Fact]
+ public void Straight_Beats_ThreeOfAKind()
+ {
+ var straight = Eval("9S", "8D", "7C", "6H", "5D", "2C", "AS");
+ var trips = Eval("QS", "QD", "QH", "9C", "8D", "2C", "AS");
+
+ Assert.True(PokerHandEvaluator.Compare(straight, trips) > 0);
+ }
+
+ [Fact]
+ public void Flush_Beats_Straight()
+ {
+ var flush = Eval("AS", "QS", "9S", "6S", "3S", "KD", "2C");
+ var straight = Eval("9H", "8D", "7C", "6H", "5D", "2C", "AS");
+
+ Assert.True(PokerHandEvaluator.Compare(flush, straight) > 0);
+ }
+
+ [Fact]
+ public void FullHouse_Beats_Flush()
+ {
+ var fullHouse = Eval("AS", "AD", "AH", "KC", "KD", "2C", "3S");
+ var flush = Eval("QS", "TS", "8S", "6S", "3S", "KD", "2C");
+
+ Assert.True(PokerHandEvaluator.Compare(fullHouse, flush) > 0);
+ }
+
+ [Fact]
+ public void FourOfAKind_Beats_FullHouse()
+ {
+ var quads = Eval("AS", "AD", "AH", "AC", "KD", "2C", "3S");
+ var fullHouse = Eval("KS", "KD", "KH", "QC", "QD", "2C", "3S");
+
+ Assert.True(PokerHandEvaluator.Compare(quads, fullHouse) > 0);
+ }
+
+ [Fact]
+ public void StraightFlush_Beats_FourOfAKind()
+ {
+ var straightFlush = Eval("9S", "8S", "7S", "6S", "5S", "2C", "AD");
+ var quads = Eval("AS", "AD", "AH", "AC", "KD", "2C", "3S");
+
+ Assert.True(PokerHandEvaluator.Compare(straightFlush, quads) > 0);
+ }
+
+ [Fact]
+ public void Pair_Kicker_Decides()
+ {
+ var pairWithAceKicker = Eval("KS", "KD", "AH", "TC", "8D", "3S", "2C");
+ var pairWithQueenKicker = Eval("KH", "KC", "QH", "TC", "8D", "3S", "2C");
+
+ Assert.True(PokerHandEvaluator.Compare(pairWithAceKicker, pairWithQueenKicker) > 0);
+ }
+
+ [Fact]
+ public void TwoPair_SecondPair_Decides()
+ {
+ var acesAndKings = Eval("AS", "AD", "KH", "KC", "8D", "3S", "2C");
+ var acesAndQueens = Eval("AH", "AC", "QH", "QC", "8D", "3S", "2C");
+
+ Assert.True(PokerHandEvaluator.Compare(acesAndKings, acesAndQueens) > 0);
+ }
+
+ [Fact]
+ public void ExactTie_ReturnsZero()
+ {
+ var a = Eval("AS", "KD", "QH", "9C", "7D", "4S", "2C");
+ var b = Eval("AH", "KC", "QD", "9S", "7H", "4D", "2S");
+
+ Assert.Equal(0, PokerHandEvaluator.Compare(a, b));
+ }
+
+ [Fact]
+ public void WheelStraight_IsDetected()
+ {
+ var wheel = Eval("AS", "2D", "3H", "4C", "5D", "KH", "QC");
+
+ Assert.Equal(PokerHandCategory.Straight, wheel.Category);
+ Assert.Equal(5, wheel.RankVector[0]);
+ }
+
+ private static PokerHandValue Eval(params string[] cards)
+ {
+ return PokerHandEvaluator.Evaluate(cards.Select(ParseCard).ToArray());
+ }
+
+ private static Card ParseCard(string code)
+ {
+ var rank = code[0] switch
+ {
+ 'A' => Rank.Ace,
+ 'K' => Rank.King,
+ 'Q' => Rank.Queen,
+ 'J' => Rank.Jack,
+ 'T' => Rank.Ten,
+ '9' => Rank.Nine,
+ '8' => Rank.Eight,
+ '7' => Rank.Seven,
+ '6' => Rank.Six,
+ '5' => Rank.Five,
+ '4' => Rank.Four,
+ '3' => Rank.Three,
+ '2' => Rank.Two,
+ _ => throw new ArgumentOutOfRangeException(nameof(code), $"Unknown rank {code[0]}")
+ };
+
+ var suit = code[1] switch
+ {
+ 'S' => Suit.Spades,
+ 'H' => Suit.Hearts,
+ 'D' => Suit.Diamonds,
+ 'C' => Suit.Clubs,
+ _ => throw new ArgumentOutOfRangeException(nameof(code), $"Unknown suit {code[1]}")
+ };
+
+ return new Card(rank, suit);
+ }
+}
diff --git a/src/PokerRogue.Tests/PokerRogue.Tests.csproj b/src/PokerRogue.Tests/PokerRogue.Tests.csproj
new file mode 100644
index 0000000..dcefe90
--- /dev/null
+++ b/src/PokerRogue.Tests/PokerRogue.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PokerRogue.slnx b/src/PokerRogue.slnx
index de8701b..b19e117 100644
--- a/src/PokerRogue.slnx
+++ b/src/PokerRogue.slnx
@@ -5,4 +5,7 @@
+
+
+