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 @@ + + +