init
@@ -361,3 +361,5 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
/.dotnet/.dotnet
|
||||
/.config
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace PokerRogue.Core.Cards;
|
||||
|
||||
public readonly record struct Card(Rank Rank, Suit Suit);
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PokerRogue.Core.Cards;
|
||||
|
||||
public enum Suit
|
||||
{
|
||||
Clubs = 0,
|
||||
Diamonds = 1,
|
||||
Hearts = 2,
|
||||
Spades = 3
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<int>(),
|
||||
LastEnemyTopRanks = Array.Empty<int>(),
|
||||
LastPlayerHandName = "N/A",
|
||||
LastEnemyHandName = "N/A",
|
||||
LastComparisonReason = "No resolved hand yet.",
|
||||
LastPlayerWinningCards = Array.Empty<Card>(),
|
||||
LastEnemyWinningCards = Array.Empty<Card>(),
|
||||
PlayerHoleCards = Array.Empty<Card>(),
|
||||
EnemyHoleCards = Array.Empty<Card>(),
|
||||
BoardCards = Array.Empty<Card>()
|
||||
};
|
||||
|
||||
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<Card>(),
|
||||
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<Card>(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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PokerRogue.Core.Combat;
|
||||
|
||||
public enum HandOutcome
|
||||
{
|
||||
None,
|
||||
PlayerWin,
|
||||
EnemyWin,
|
||||
Tie
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PokerRogue.Core.Combat;
|
||||
|
||||
public enum HandPhase
|
||||
{
|
||||
Deal,
|
||||
Clash,
|
||||
Resolve,
|
||||
End
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PokerRogue.Core.Combat;
|
||||
|
||||
public enum PlayerAction
|
||||
{
|
||||
Strike,
|
||||
Guard,
|
||||
Dodge,
|
||||
Check,
|
||||
Probe,
|
||||
AllIn,
|
||||
Fold,
|
||||
Skill
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using PokerRogue.Core.Cards;
|
||||
|
||||
namespace PokerRogue.Core.Combat;
|
||||
|
||||
public static class PokerHandEvaluator
|
||||
{
|
||||
public static PokerHandValue Evaluate(IReadOnlyList<Card> 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<int> vector)
|
||||
{
|
||||
var encoded = 0;
|
||||
foreach (var rank in vector)
|
||||
{
|
||||
encoded = (encoded * 15) + rank;
|
||||
}
|
||||
|
||||
return encoded;
|
||||
}
|
||||
|
||||
private static int HighestStraight(IEnumerable<int> 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace PokerRogue.Core.Enemies;
|
||||
|
||||
public enum EnemyIntent
|
||||
{
|
||||
Strike,
|
||||
Guard,
|
||||
Dodge
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
}
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -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
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
|
||||
<Asset Type="Graphics:FontDescription">
|
||||
<FontName>Consolas</FontName>
|
||||
<Size>18</Size>
|
||||
<Spacing>1</Spacing>
|
||||
<UseKerning>true</UseKerning>
|
||||
<Style>Regular</Style>
|
||||
<CharacterRegions>
|
||||
<CharacterRegion>
|
||||
<Start> </Start>
|
||||
<End>~</End>
|
||||
</CharacterRegion>
|
||||
<CharacterRegion>
|
||||
<Start>♠</Start>
|
||||
<End>♣</End>
|
||||
</CharacterRegion>
|
||||
</CharacterRegions>
|
||||
</Asset>
|
||||
</XnaContent>
|
||||
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 144 KiB |
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<RollForward>Major</RollForward>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<TieredCompilation>false</TieredCompilation>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Icon.ico" />
|
||||
<None Remove="Icon.bmp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Icon.ico">
|
||||
<LogicalName>Icon.ico</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Icon.bmp">
|
||||
<LogicalName>Icon.bmp</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*" />
|
||||
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PokerRogue.Core\PokerRogue.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Content\Cards\**\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
using var game = new PokerRogue.Game.Game1();
|
||||
game.Run();
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="PokerRogue.Game"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on and is
|
||||
is designed to work with. Uncomment the appropriate elements and Windows will
|
||||
automatically selected the most compatible environment. -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
</assembly>
|
||||
@@ -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<PlayerAction>())
|
||||
{
|
||||
var state = FightEngine.StartFight(config, seed: 123);
|
||||
var result = FightEngine.ApplyPlayerAction(state, action);
|
||||
Assert.True(result.Phase is HandPhase.Clash or HandPhase.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PokerRogue.Core\PokerRogue.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -5,4 +5,7 @@
|
||||
<File Path="../README.md" />
|
||||
<File Path="../ROADMAP.md" />
|
||||
</Folder>
|
||||
<Project Path="PokerRogue.Core/PokerRogue.Core.csproj" />
|
||||
<Project Path="PokerRogue.Game/PokerRogue.Game.csproj" />
|
||||
<Project Path="PokerRogue.Tests/PokerRogue.Tests.csproj" />
|
||||
</Solution>
|
||||
|
||||