This commit is contained in:
Michal Vrba
2026-02-11 14:12:22 +01:00
parent 48099e9de7
commit 1535628412
85 changed files with 2191 additions and 1 deletions
+2
View File
@@ -361,3 +361,5 @@ MigrationBackup/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
/.dotnet/.dotnet
/.config
+77
View File
@@ -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.
+7
View File
@@ -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>
+3
View File
@@ -0,0 +1,3 @@
namespace PokerRogue.Core.Cards;
public readonly record struct Card(Rank Rank, Suit Suit);
+18
View File
@@ -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
}
+9
View File
@@ -0,0 +1,9 @@
namespace PokerRogue.Core.Cards;
public enum Suit
{
Clubs = 0,
Diamonds = 1,
Hearts = 2,
Spades = 3
}
+19
View File
@@ -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);
}
+301
View File
@@ -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;
}
}
+35
View File
@@ -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
}
+9
View File
@@ -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"
]
}
}
}
+14
View File
@@ -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"
}
],
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+19
View File
@@ -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>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
<CharacterRegion>
<Start>&#9824;</Start>
<End>&#9827;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

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>
+2
View File
@@ -0,0 +1,2 @@
using var game = new PokerRogue.Game.Game1();
game.Run();
+43
View File
@@ -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>
+62
View File
@@ -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>
+3
View File
@@ -5,4 +5,7 @@
<File Path="../README.md" /> <File Path="../README.md" />
<File Path="../ROADMAP.md" /> <File Path="../ROADMAP.md" />
</Folder> </Folder>
<Project Path="PokerRogue.Core/PokerRogue.Core.csproj" />
<Project Path="PokerRogue.Game/PokerRogue.Game.csproj" />
<Project Path="PokerRogue.Tests/PokerRogue.Tests.csproj" />
</Solution> </Solution>