refactor(test): migrate blackjack tests to use TestHelpers
Refactor the blackjack test suite to utilize the centralized `TestHelpers` from `OECS.PlayTest` instead of local implementations. This includes: - Replacing local `SetupGame`, `FindEntity`, and `SnapshotWorld` with `TestHelpers` equivalents. - Moving `PlayLog` logic and round running into the `OECS.PlayTest` framework. - Updating project references to include `OECS.PlayTest`.
This commit is contained in:
parent
9ef387f010
commit
94a9cc9519
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Game.Blackjack\Blackjack.csproj" />
|
<ProjectReference Include="..\Game.Blackjack\Blackjack.csproj" />
|
||||||
|
<ProjectReference Include="..\OECS.PlayTest\OECS.PlayTest.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NewGame_StartsInBettingPhase()
|
public void NewGame_StartsInBettingPhase()
|
||||||
{
|
{
|
||||||
var (world, _) = SetupGame();
|
var (world, _) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
state.Phase.Should().Be(GamePhase.Betting);
|
state.Phase.Should().Be(GamePhase.Betting);
|
||||||
|
|
@ -21,13 +21,13 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlaceBet_AdvancesToDealing()
|
public void PlaceBet_AdvancesToDealing()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
state.Phase.Should().Be(GamePhase.PlayerTurn); // Dealing → PlayerTurn happens automatically
|
state.Phase.Should().Be(GamePhase.PlayerTurn);
|
||||||
state.CurrentBet.Should().Be(10);
|
state.CurrentBet.Should().Be(10);
|
||||||
state.Chips.Should().Be(90);
|
state.Chips.Should().Be(90);
|
||||||
}
|
}
|
||||||
|
|
@ -35,20 +35,20 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlaceBet_RejectsInsufficientChips()
|
public void PlaceBet_RejectsInsufficientChips()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 200 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 200 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
state.Phase.Should().Be(GamePhase.Betting); // Should not advance
|
state.Phase.Should().Be(GamePhase.Betting);
|
||||||
state.Chips.Should().Be(100);
|
state.Chips.Should().Be(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlaceBet_RejectsZeroOrNegative()
|
public void PlaceBet_RejectsZeroOrNegative()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 0 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 0 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -62,60 +62,53 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Dealing_CreatesDeckAndHands()
|
public void Dealing_CreatesDeckAndHands()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
// Should have a deck entity.
|
TestHelpers.FindEntity<Deck>(world).Should().NotBe(Entity.Null);
|
||||||
var deckEntity = FindEntity<Deck>(world);
|
TestHelpers.FindEntity<PlayerHand>(world).Should().NotBe(Entity.Null);
|
||||||
deckEntity.Should().NotBe(Entity.Null);
|
TestHelpers.FindEntity<DealerHand>(world).Should().NotBe(Entity.Null);
|
||||||
|
|
||||||
// Should have player and dealer hand entities.
|
|
||||||
var playerHand = FindEntity<PlayerHand>(world);
|
|
||||||
playerHand.Should().NotBe(Entity.Null);
|
|
||||||
|
|
||||||
var dealerHand = FindEntity<DealerHand>(world);
|
|
||||||
dealerHand.Should().NotBe(Entity.Null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Dealing_DealsTwoCardsToEach()
|
public void Dealing_DealsTwoCardsToEach()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var playerHand = FindEntity<PlayerHand>(world);
|
var playerHand = TestHelpers.FindEntity<PlayerHand>(world);
|
||||||
var dealerHand = FindEntity<DealerHand>(world);
|
var dealerHand = TestHelpers.FindEntity<DealerHand>(world);
|
||||||
|
|
||||||
CountCardsInHand(world, playerHand).Should().Be(2);
|
TestHelpers.CountCardsInHand(world, playerHand).Should().Be(2);
|
||||||
CountCardsInHand(world, dealerHand).Should().Be(2);
|
TestHelpers.CountCardsInHand(world, dealerHand).Should().Be(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Hit_DrawsOneCard()
|
public void Hit_DrawsOneCard()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var playerHand = FindEntity<PlayerHand>(world);
|
var playerHand = TestHelpers.FindEntity<PlayerHand>(world);
|
||||||
var before = CountCardsInHand(world, playerHand);
|
var before = TestHelpers.CountCardsInHand(world, playerHand);
|
||||||
|
|
||||||
world.Commands.Enqueue(new HitCommand());
|
world.Commands.Enqueue(new HitCommand());
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var after = CountCardsInHand(world, playerHand);
|
var after = TestHelpers.CountCardsInHand(world, playerHand);
|
||||||
after.Should().Be(before + 1);
|
after.Should().Be(before + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Stand_AdvancesToDealerTurn()
|
public void Stand_AdvancesToDealerTurn()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -130,15 +123,13 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NewRound_ResetsPhase()
|
public void NewRound_ResetsPhase()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
// Play a round.
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
world.Commands.Enqueue(new StandCommand());
|
world.Commands.Enqueue(new StandCommand());
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
// Start new round.
|
|
||||||
world.Commands.Enqueue(new NewRoundCommand());
|
world.Commands.Enqueue(new NewRoundCommand());
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
|
|
@ -150,15 +141,15 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DeterministicSeed_ProducesSameDeal()
|
public void DeterministicSeed_ProducesSameDeal()
|
||||||
{
|
{
|
||||||
var (world1, group1) = SetupGame(seed: 42);
|
var (world1, group1) = TestHelpers.SetupGame(seed: 42);
|
||||||
world1.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world1.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group1.RunLogical();
|
group1.RunLogical();
|
||||||
var cards1 = GetAllCards(world1);
|
var cards1 = TestHelpers.GetAllCards(world1);
|
||||||
|
|
||||||
var (world2, group2) = SetupGame(seed: 42);
|
var (world2, group2) = TestHelpers.SetupGame(seed: 42);
|
||||||
world2.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world2.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group2.RunLogical();
|
group2.RunLogical();
|
||||||
var cards2 = GetAllCards(world2);
|
var cards2 = TestHelpers.GetAllCards(world2);
|
||||||
|
|
||||||
cards1.Should().Equal(cards2);
|
cards1.Should().Equal(cards2);
|
||||||
}
|
}
|
||||||
|
|
@ -166,20 +157,17 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlayerBust_LosesBet()
|
public void PlayerBust_LosesBet()
|
||||||
{
|
{
|
||||||
// Use a seed that produces a bust-prone hand, then hit repeatedly.
|
var (world, group) = TestHelpers.SetupGame(seed: 12345);
|
||||||
var (world, group) = SetupGame(seed: 12345);
|
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
// Hit until bust or stand.
|
|
||||||
while (world.ReadSingleton<GameState>().Phase == GamePhase.PlayerTurn)
|
while (world.ReadSingleton<GameState>().Phase == GamePhase.PlayerTurn)
|
||||||
{
|
{
|
||||||
world.Commands.Enqueue(new HitCommand());
|
world.Commands.Enqueue(new HitCommand());
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Game should have resolved.
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
state.Phase.Should().Be(GamePhase.RoundOver);
|
state.Phase.Should().Be(GamePhase.RoundOver);
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +175,7 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Serialization_RoundTrips()
|
public void Serialization_RoundTrips()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame(seed: 42);
|
var (world, group) = TestHelpers.SetupGame(seed: 42);
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -204,60 +192,4 @@ public class GameFlowTests
|
||||||
state.CurrentBet.Should().Be(10);
|
state.CurrentBet.Should().Be(10);
|
||||||
state.RoundNumber.Should().Be(1);
|
state.RoundNumber.Should().Be(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// ── Helpers ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private static (World, SystemGroup) SetupGame(uint seed = 42)
|
|
||||||
{
|
|
||||||
var world = new World();
|
|
||||||
var group = new SystemGroup(world);
|
|
||||||
group.Add(new DeckSetupSystem());
|
|
||||||
group.Add(new DealSystem());
|
|
||||||
group.Add(new PlayerBustCheckSystem());
|
|
||||||
group.Add(new DealerSystem());
|
|
||||||
|
|
||||||
world.SetSingleton(new GameState
|
|
||||||
{
|
|
||||||
Phase = GamePhase.Betting,
|
|
||||||
Result = RoundResult.None,
|
|
||||||
Chips = 100,
|
|
||||||
CurrentBet = 0,
|
|
||||||
RoundNumber = 1,
|
|
||||||
Seed = seed
|
|
||||||
});
|
|
||||||
world.PostChanges();
|
|
||||||
|
|
||||||
return (world, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Entity FindEntity<T>(World world) where T : struct
|
|
||||||
{
|
|
||||||
return world.FindEntity<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CountCardsInHand(World world, Entity handEntity)
|
|
||||||
{
|
|
||||||
// Holds relationship: the card entity (which Holds points to the hand entity).
|
|
||||||
// GetSources returns all card entities that have a Holds pointing to handEntity.
|
|
||||||
return world.GetSources<Holds>(handEntity).Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<(Suit, Rank)> GetAllCards(World world)
|
|
||||||
{
|
|
||||||
var cards = new List<(Suit, Rank)>();
|
|
||||||
using (var iter = world.Select<Card>())
|
|
||||||
{
|
|
||||||
while (iter.MoveNext())
|
|
||||||
{
|
|
||||||
var card = iter.Val1;
|
|
||||||
cards.Add((card.Suit, card.Rank));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cards.Sort((a, b) =>
|
|
||||||
{
|
|
||||||
int cmp = a.Item1.CompareTo(b.Item1);
|
|
||||||
return cmp != 0 ? cmp : a.Item2.CompareTo(b.Item2);
|
|
||||||
});
|
|
||||||
return cards;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Game.Blackjack;
|
using Game.Blackjack;
|
||||||
using OECS;
|
using OECS;
|
||||||
using R3;
|
using OECS.PlayTest;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Game.Blackjack.Tests;
|
namespace Game.Blackjack.Tests;
|
||||||
|
|
@ -14,167 +14,73 @@ public class PlayTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Play_BasicStrategy()
|
public void Play_BasicStrategy()
|
||||||
{
|
{
|
||||||
var log = new PlayLog();
|
var (world, group) = TestHelpers.SetupGame(seed: 42);
|
||||||
var (world, group) = SetupGame(seed: 42);
|
|
||||||
|
|
||||||
world.ObserveComponentChanges<Card>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
var card = world.ReadComponent<Card>(change.Entity);
|
|
||||||
log.Reactivity.Add($"{change.Kind} Card = {card} on {change.Entity}");
|
|
||||||
});
|
|
||||||
world.ObserveComponentChanges<GameState>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
var state = world.ReadComponent<GameState>(change.Entity);
|
|
||||||
log.Reactivity.Add($"{change.Kind} GameState = {state}");
|
|
||||||
});
|
|
||||||
|
|
||||||
var agent = new BasicStrategyAgent();
|
var agent = new BasicStrategyAgent();
|
||||||
log.Header = $"Blackjack: Basic Strategy (seed=42, agent={agent})";
|
|
||||||
|
|
||||||
RunUntilDone(world, group, agent, log);
|
var log = RunUntilDone(world, group, agent,
|
||||||
|
$"Blackjack: Basic Strategy (seed=42, agent={agent})");
|
||||||
|
|
||||||
log.FinalSnapshot = SnapshotWorld(world);
|
var path = log.SaveTo("blackjack_basic_strategy.playlog");
|
||||||
|
|
||||||
var path = SavePlayLog("blackjack_basic_strategy.playlog", log);
|
|
||||||
ReadAndVerify(path);
|
ReadAndVerify(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Play_RandomAgent()
|
public void Play_RandomAgent()
|
||||||
{
|
{
|
||||||
var log = new PlayLog();
|
var (world, group) = TestHelpers.SetupGame(seed: 123);
|
||||||
var (world, group) = SetupGame(seed: 123);
|
|
||||||
|
|
||||||
world.ObserveComponentChanges<Card>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
var card = world.ReadComponent<Card>(change.Entity);
|
|
||||||
log.Reactivity.Add($"{change.Kind} Card = {card} on {change.Entity}");
|
|
||||||
});
|
|
||||||
world.ObserveComponentChanges<GameState>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
var state = world.ReadComponent<GameState>(change.Entity);
|
|
||||||
log.Reactivity.Add($"{change.Kind} GameState = {state}");
|
|
||||||
});
|
|
||||||
|
|
||||||
var agent = new RandomBlackjackAgent();
|
var agent = new RandomBlackjackAgent();
|
||||||
log.Header = $"Blackjack: Random Agent (seed=123, agent={agent})";
|
|
||||||
|
|
||||||
RunUntilDone(world, group, agent, log);
|
var log = RunUntilDone(world, group, agent,
|
||||||
|
$"Blackjack: Random Agent (seed=123, agent={agent})");
|
||||||
|
|
||||||
log.FinalSnapshot = SnapshotWorld(world);
|
var path = log.SaveTo("blackjack_random_agent.playlog");
|
||||||
|
|
||||||
var path = SavePlayLog("blackjack_random_agent.playlog", log);
|
|
||||||
ReadAndVerify(path);
|
ReadAndVerify(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Play_WeightedPool()
|
public void Play_WeightedPool()
|
||||||
{
|
{
|
||||||
var log = new PlayLog();
|
var (world, group) = TestHelpers.SetupGame(seed: 77);
|
||||||
var (world, group) = SetupGame(seed: 77);
|
|
||||||
|
|
||||||
world.ObserveComponentChanges<Card>().Subscribe(change =>
|
var pool = new WeightedAgentPool<BlackjackDecision>();
|
||||||
{
|
|
||||||
var card = world.ReadComponent<Card>(change.Entity);
|
|
||||||
log.Reactivity.Add($"{change.Kind} Card = {card} on {change.Entity}");
|
|
||||||
});
|
|
||||||
world.ObserveComponentChanges<GameState>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
var state = world.ReadComponent<GameState>(change.Entity);
|
|
||||||
log.Reactivity.Add($"{change.Kind} GameState = {state}");
|
|
||||||
});
|
|
||||||
|
|
||||||
var pool = new WeightedAgentPool();
|
|
||||||
pool.Add(new BasicStrategyAgent(), 7);
|
pool.Add(new BasicStrategyAgent(), 7);
|
||||||
pool.Add(new RandomBlackjackAgent(), 3);
|
pool.Add(new RandomBlackjackAgent(), 3);
|
||||||
|
|
||||||
var agent = pool.Pick();
|
var agent = pool.Pick();
|
||||||
log.Header = $"Blackjack: Weighted Pool (seed=77, agent={agent})";
|
var log = RunUntilDone(world, group, agent,
|
||||||
|
$"Blackjack: Weighted Pool (seed=77, agent={agent})");
|
||||||
|
|
||||||
RunUntilDone(world, group, agent, log);
|
var path = log.SaveTo("blackjack_weighted_pool.playlog");
|
||||||
|
|
||||||
log.FinalSnapshot = SnapshotWorld(world);
|
|
||||||
|
|
||||||
var path = SavePlayLog("blackjack_weighted_pool.playlog", log);
|
|
||||||
ReadAndVerify(path);
|
ReadAndVerify(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Play Log ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private sealed class PlayLog
|
|
||||||
{
|
|
||||||
public string Header { get; set; } = "";
|
|
||||||
public readonly List<string> RoundSummaries = new();
|
|
||||||
public readonly List<string> Decisions = new();
|
|
||||||
public readonly List<string> Reactivity = new();
|
|
||||||
public string FinalSnapshot { get; set; } = "";
|
|
||||||
|
|
||||||
public string Build()
|
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder();
|
|
||||||
sb.AppendLine(Header);
|
|
||||||
sb.AppendLine(new string('=', Header.Length));
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("--- Round Summaries ---");
|
|
||||||
for (int i = 0; i < RoundSummaries.Count; i++)
|
|
||||||
sb.AppendLine($" Round {i + 1}: {RoundSummaries[i]}");
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("--- Decisions ---");
|
|
||||||
for (int i = 0; i < Decisions.Count; i++)
|
|
||||||
sb.AppendLine($" {i + 1}. {Decisions[i]}");
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("--- Reactivity ---");
|
|
||||||
foreach (var entry in Reactivity)
|
|
||||||
sb.AppendLine($" {entry}");
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("--- Final State ---");
|
|
||||||
sb.AppendLine(FinalSnapshot);
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Round Runner ──────────────────────────────────────────────────
|
// ── Round Runner ──────────────────────────────────────────────────
|
||||||
|
|
||||||
private static (World, SystemGroup) SetupGame(uint seed)
|
|
||||||
{
|
|
||||||
var world = new World();
|
|
||||||
var group = new SystemGroup(world);
|
|
||||||
group.Add(new DeckSetupSystem());
|
|
||||||
group.Add(new DealSystem());
|
|
||||||
group.Add(new PlayerBustCheckSystem());
|
|
||||||
group.Add(new DealerSystem());
|
|
||||||
|
|
||||||
world.SetSingleton(new GameState
|
|
||||||
{
|
|
||||||
Phase = GamePhase.Betting,
|
|
||||||
Result = RoundResult.None,
|
|
||||||
Chips = StartingChips,
|
|
||||||
CurrentBet = 0,
|
|
||||||
RoundNumber = 1,
|
|
||||||
Seed = seed
|
|
||||||
});
|
|
||||||
world.PostChanges();
|
|
||||||
|
|
||||||
return (world, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs rounds until the player runs out of chips (can't afford minimum bet)
|
/// Runs rounds until the player runs out of chips (can't afford minimum bet)
|
||||||
/// or doubles their starting chips.
|
/// or doubles their starting chips.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void RunUntilDone(World world, SystemGroup group, IBlackjackAgent agent, PlayLog log)
|
private static PlayLog RunUntilDone(World world, SystemGroup group,
|
||||||
|
IAgent<BlackjackDecision> agent, string header)
|
||||||
{
|
{
|
||||||
|
var log = new PlayLog { Header = header };
|
||||||
|
|
||||||
|
using var capture = new ObservableCapture(world);
|
||||||
|
capture.FormatWith<Card>(c => $"{c.Rank} of {c.Suit}");
|
||||||
|
capture.FormatWith<GameState>(s => $"{s.Phase} Chips={s.Chips} Bet={s.CurrentBet}");
|
||||||
|
|
||||||
int totalRounds = 0;
|
int totalRounds = 0;
|
||||||
int wins = 0;
|
int wins = 0;
|
||||||
int losses = 0;
|
int losses = 0;
|
||||||
int pushes = 0;
|
int pushes = 0;
|
||||||
|
var roundSummaries = new List<string>();
|
||||||
|
var decisions = new List<string>();
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
int chips = state.Chips;
|
int chips = state.Chips;
|
||||||
|
|
||||||
// Stop conditions.
|
|
||||||
if (chips < BetAmount)
|
if (chips < BetAmount)
|
||||||
{
|
{
|
||||||
log.Header += $" — BUSTED after {totalRounds} rounds";
|
log.Header += $" — BUSTED after {totalRounds} rounds";
|
||||||
|
|
@ -185,19 +91,15 @@ public class PlayTests
|
||||||
log.Header += $" — DOUBLED after {totalRounds} rounds";
|
log.Header += $" — DOUBLED after {totalRounds} rounds";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety: max 200 rounds to prevent infinite loops.
|
|
||||||
if (totalRounds >= 200)
|
if (totalRounds >= 200)
|
||||||
{
|
{
|
||||||
log.Header += $" — MAX ROUNDS ({totalRounds})";
|
log.Header += $" — MAX ROUNDS ({totalRounds})";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place bet and run dealing.
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = BetAmount });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = BetAmount });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
// Player turn: agent decides hit/stand.
|
|
||||||
int roundHits = 0;
|
int roundHits = 0;
|
||||||
int playerTurnSafety = 0;
|
int playerTurnSafety = 0;
|
||||||
while (world.ReadSingleton<GameState>().Phase == GamePhase.PlayerTurn && playerTurnSafety < 52)
|
while (world.ReadSingleton<GameState>().Phase == GamePhase.PlayerTurn && playerTurnSafety < 52)
|
||||||
|
|
@ -205,7 +107,7 @@ public class PlayTests
|
||||||
playerTurnSafety++;
|
playerTurnSafety++;
|
||||||
var total = HandUtil.CalculateHand(world, new PlayerHand());
|
var total = HandUtil.CalculateHand(world, new PlayerHand());
|
||||||
var decision = agent.Decide(world);
|
var decision = agent.Decide(world);
|
||||||
log.Decisions.Add($"R{totalRounds + 1} Hand={total}, Decision={decision}");
|
decisions.Add($"R{totalRounds + 1} Hand={total}, Decision={decision}");
|
||||||
|
|
||||||
if (decision == BlackjackDecision.Hit)
|
if (decision == BlackjackDecision.Hit)
|
||||||
{
|
{
|
||||||
|
|
@ -220,12 +122,10 @@ public class PlayTests
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record round result.
|
|
||||||
state = world.ReadSingleton<GameState>();
|
state = world.ReadSingleton<GameState>();
|
||||||
int newChips = state.Chips;
|
int newChips = state.Chips;
|
||||||
int delta = newChips - chips;
|
int delta = newChips - chips;
|
||||||
string summary = $"{state.Result} | Bet={BetAmount} | Chips: {chips}→{newChips} ({delta:+0;-#}) | Hits: {roundHits}";
|
roundSummaries.Add($"Round {totalRounds + 1}: {state.Result} | Bet={BetAmount} | Chips: {chips}→{newChips} ({delta:+0;-#}) | Hits: {roundHits}");
|
||||||
log.RoundSummaries.Add(summary);
|
|
||||||
|
|
||||||
switch (state.Result)
|
switch (state.Result)
|
||||||
{
|
{
|
||||||
|
|
@ -244,71 +144,22 @@ public class PlayTests
|
||||||
|
|
||||||
totalRounds++;
|
totalRounds++;
|
||||||
|
|
||||||
// Start next round.
|
|
||||||
world.Commands.Enqueue(new NewRoundCommand());
|
world.Commands.Enqueue(new NewRoundCommand());
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Header += $" | W:{wins} L:{losses} P:{pushes}";
|
log.Header += $" | W:{wins} L:{losses} P:{pushes}";
|
||||||
}
|
|
||||||
|
|
||||||
// ── Snapshot ──────────────────────────────────────────────────────
|
log.AddSection("Round Summaries", roundSummaries);
|
||||||
|
log.AddSection("Decisions", decisions);
|
||||||
|
log.AddSection("Reactivity", capture.GetLogLines());
|
||||||
|
log.FinalSnapshot = TestHelpers.SnapshotWorld(world);
|
||||||
|
|
||||||
private static string SnapshotWorld(World world)
|
return log;
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder();
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
sb.AppendLine($"Phase: {state.Phase}");
|
|
||||||
sb.AppendLine($"Chips: {state.Chips}, Bet: {state.CurrentBet}");
|
|
||||||
sb.AppendLine($"Round: {state.RoundNumber}");
|
|
||||||
sb.AppendLine($"Result: {state.Result}");
|
|
||||||
|
|
||||||
var playerHand = FindEntity<PlayerHand>(world);
|
|
||||||
if (playerHand != Entity.Null)
|
|
||||||
{
|
|
||||||
sb.AppendLine($"Player ({HandUtil.CalculateHand(world, new PlayerHand())}):");
|
|
||||||
foreach (var card in GetHandCards(world, playerHand))
|
|
||||||
sb.AppendLine($" {card}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var dealerHand = FindEntity<DealerHand>(world);
|
|
||||||
if (dealerHand != Entity.Null)
|
|
||||||
{
|
|
||||||
sb.AppendLine($"Dealer ({HandUtil.CalculateHand(world, new DealerHand())}):");
|
|
||||||
foreach (var card in GetHandCards(world, dealerHand))
|
|
||||||
sb.AppendLine($" {card}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString().TrimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Entity FindEntity<T>(World world) where T : struct
|
|
||||||
{
|
|
||||||
return world.FindEntity<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<string> GetHandCards(World world, Entity hand)
|
|
||||||
{
|
|
||||||
var cards = new List<string>();
|
|
||||||
foreach (var cardEntity in world.GetSources<Holds>(hand))
|
|
||||||
{
|
|
||||||
var card = world.ReadComponent<Card>(cardEntity);
|
|
||||||
cards.Add($"{card.Rank} of {card.Suit}");
|
|
||||||
}
|
|
||||||
return cards;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── File I/O ──────────────────────────────────────────────────────
|
// ── File I/O ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static string SavePlayLog(string filename, PlayLog log)
|
|
||||||
{
|
|
||||||
var dir = Path.Combine(AppContext.BaseDirectory, "playlogs");
|
|
||||||
Directory.CreateDirectory(dir);
|
|
||||||
var path = Path.Combine(dir, filename);
|
|
||||||
File.WriteAllText(path, log.Build());
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadAndVerify(string path)
|
private static void ReadAndVerify(string path)
|
||||||
{
|
{
|
||||||
var content = File.ReadAllText(path);
|
var content = File.ReadAllText(path);
|
||||||
|
|
@ -324,12 +175,7 @@ public class PlayTests
|
||||||
|
|
||||||
private enum BlackjackDecision { Hit, Stand }
|
private enum BlackjackDecision { Hit, Stand }
|
||||||
|
|
||||||
private interface IBlackjackAgent
|
private sealed class BasicStrategyAgent : IAgent<BlackjackDecision>
|
||||||
{
|
|
||||||
BlackjackDecision Decide(World world);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BasicStrategyAgent : IBlackjackAgent
|
|
||||||
{
|
{
|
||||||
public BlackjackDecision Decide(World world)
|
public BlackjackDecision Decide(World world)
|
||||||
{
|
{
|
||||||
|
|
@ -339,7 +185,7 @@ public class PlayTests
|
||||||
public override string ToString() => "BasicStrategy";
|
public override string ToString() => "BasicStrategy";
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RandomBlackjackAgent : IBlackjackAgent
|
private sealed class RandomBlackjackAgent : IAgent<BlackjackDecision>
|
||||||
{
|
{
|
||||||
private static readonly Random _rng = new();
|
private static readonly Random _rng = new();
|
||||||
public BlackjackDecision Decide(World world)
|
public BlackjackDecision Decide(World world)
|
||||||
|
|
@ -350,30 +196,4 @@ public class PlayTests
|
||||||
}
|
}
|
||||||
public override string ToString() => "Random";
|
public override string ToString() => "Random";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private sealed class WeightedAgentPool
|
|
||||||
{
|
|
||||||
private readonly List<(IBlackjackAgent Agent, int Weight)> _agents = new();
|
|
||||||
private int _totalWeight;
|
|
||||||
private static readonly Random _rng = new();
|
|
||||||
|
|
||||||
public void Add(IBlackjackAgent agent, int weight)
|
|
||||||
{
|
|
||||||
_agents.Add((agent, weight));
|
|
||||||
_totalWeight += weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IBlackjackAgent Pick()
|
|
||||||
{
|
|
||||||
int roll = _rng.Next(_totalWeight);
|
|
||||||
int cumulative = 0;
|
|
||||||
foreach (var (agent, weight) in _agents)
|
|
||||||
{
|
|
||||||
cumulative += weight;
|
|
||||||
if (roll < cumulative)
|
|
||||||
return agent;
|
|
||||||
}
|
|
||||||
return _agents[^1].Agent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Game.Blackjack;
|
using Game.Blackjack;
|
||||||
using OECS;
|
using OECS;
|
||||||
using R3;
|
using OECS.PlayTest;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
|
@ -24,9 +24,9 @@ public class SnapshotTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Snapshot_InitialState()
|
public void Snapshot_InitialState()
|
||||||
{
|
{
|
||||||
var (world, _) = SetupGame();
|
var (world, _) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
var snapshot = SnapshotWorld(world);
|
var snapshot = TestHelpers.SnapshotWorld(world);
|
||||||
_output.WriteLine(snapshot);
|
_output.WriteLine(snapshot);
|
||||||
|
|
||||||
snapshot.Should().Contain("Phase: Betting");
|
snapshot.Should().Contain("Phase: Betting");
|
||||||
|
|
@ -37,12 +37,12 @@ public class SnapshotTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Snapshot_AfterDeal()
|
public void Snapshot_AfterDeal()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame(seed: 42);
|
var (world, group) = TestHelpers.SetupGame(seed: 42);
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var snapshot = SnapshotWorld(world);
|
var snapshot = TestHelpers.SnapshotWorld(world);
|
||||||
_output.WriteLine(snapshot);
|
_output.WriteLine(snapshot);
|
||||||
|
|
||||||
snapshot.Should().Contain("Phase: PlayerTurn");
|
snapshot.Should().Contain("Phase: PlayerTurn");
|
||||||
|
|
@ -56,7 +56,7 @@ public class SnapshotTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Snapshot_AfterHit()
|
public void Snapshot_AfterHit()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame(seed: 42);
|
var (world, group) = TestHelpers.SetupGame(seed: 42);
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -64,17 +64,16 @@ public class SnapshotTests
|
||||||
world.Commands.Enqueue(new HitCommand());
|
world.Commands.Enqueue(new HitCommand());
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var snapshot = SnapshotWorld(world);
|
var snapshot = TestHelpers.SnapshotWorld(world);
|
||||||
_output.WriteLine(snapshot);
|
_output.WriteLine(snapshot);
|
||||||
|
|
||||||
// With seed 42, one hit may bust. Just verify the game resolved.
|
|
||||||
snapshot.Should().NotContain("Phase: Dealing");
|
snapshot.Should().NotContain("Phase: Dealing");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Snapshot_AfterStand()
|
public void Snapshot_AfterStand()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame(seed: 42);
|
var (world, group) = TestHelpers.SetupGame(seed: 42);
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -82,7 +81,7 @@ public class SnapshotTests
|
||||||
world.Commands.Enqueue(new StandCommand());
|
world.Commands.Enqueue(new StandCommand());
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var snapshot = SnapshotWorld(world);
|
var snapshot = TestHelpers.SnapshotWorld(world);
|
||||||
_output.WriteLine(snapshot);
|
_output.WriteLine(snapshot);
|
||||||
|
|
||||||
snapshot.Should().Contain("Phase: RoundOver");
|
snapshot.Should().Contain("Phase: RoundOver");
|
||||||
|
|
@ -92,23 +91,11 @@ public class SnapshotTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Log_ReactivityDuringRound()
|
public void Log_ReactivityDuringRound()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame(seed: 42);
|
var (world, group) = TestHelpers.SetupGame(seed: 42);
|
||||||
var log = new List<string>();
|
|
||||||
|
|
||||||
world.ObserveEntityChanges().Subscribe(change =>
|
using var capture = new ObservableCapture(world);
|
||||||
{
|
capture.FormatWith<Card>(c => $"{c.Rank} of {c.Suit}");
|
||||||
log.Add($"[entity] {change}");
|
capture.FormatWith<GameState>(s => $"{s.Phase} Chips={s.Chips} Bet={s.CurrentBet} Round={s.RoundNumber}");
|
||||||
});
|
|
||||||
|
|
||||||
world.ObserveComponentChanges<Card>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
log.Add($"[card] {change}");
|
|
||||||
});
|
|
||||||
|
|
||||||
world.ObserveComponentChanges<GameState>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
log.Add($"[gamestate] {change}");
|
|
||||||
});
|
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -116,24 +103,23 @@ public class SnapshotTests
|
||||||
world.Commands.Enqueue(new StandCommand());
|
world.Commands.Enqueue(new StandCommand());
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var logText = string.Join("\n", log);
|
var logText = string.Join("\n", capture.GetLogLines());
|
||||||
_output.WriteLine(logText);
|
_output.WriteLine(logText);
|
||||||
|
|
||||||
// Should have entity creations (deck, hands, cards) and component changes.
|
capture.GetLogLines().Should().Contain(l => l.Contains("EntityAdded"));
|
||||||
log.Should().Contain(l => l.Contains("EntityAdded"));
|
capture.GetLogLines().Should().Contain(l => l.Contains("ComponentAdded") && l.Contains("Card"));
|
||||||
log.Should().Contain(l => l.Contains("ComponentAdded") && l.Contains("Card"));
|
capture.GetLogLines().Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState"));
|
||||||
log.Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Serialization_RoundTrip_PreservesCoreState()
|
public void Serialization_RoundTrip_PreservesCoreState()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame(seed: 42);
|
var (world, group) = TestHelpers.SetupGame(seed: 42);
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
var before = SnapshotWorld(world);
|
var before = TestHelpers.SnapshotWorld(world);
|
||||||
|
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
WorldSerializer.Save(world, stream);
|
WorldSerializer.Save(world, stream);
|
||||||
|
|
@ -142,112 +128,19 @@ public class SnapshotTests
|
||||||
var world2 = new World();
|
var world2 = new World();
|
||||||
WorldSerializer.Load(world2, stream);
|
WorldSerializer.Load(world2, stream);
|
||||||
|
|
||||||
var after = SnapshotWorld(world2);
|
var after = TestHelpers.SnapshotWorld(world2);
|
||||||
|
|
||||||
_output.WriteLine("=== Before ===");
|
_output.WriteLine("=== Before ===");
|
||||||
_output.WriteLine(before);
|
_output.WriteLine(before);
|
||||||
_output.WriteLine("=== After ===");
|
_output.WriteLine("=== After ===");
|
||||||
_output.WriteLine(after);
|
_output.WriteLine(after);
|
||||||
|
|
||||||
// Core game state must round-trip.
|
|
||||||
var state = world2.ReadSingleton<GameState>();
|
var state = world2.ReadSingleton<GameState>();
|
||||||
state.Phase.Should().Be(GamePhase.PlayerTurn);
|
state.Phase.Should().Be(GamePhase.PlayerTurn);
|
||||||
state.Chips.Should().Be(90);
|
state.Chips.Should().Be(90);
|
||||||
state.CurrentBet.Should().Be(10);
|
state.CurrentBet.Should().Be(10);
|
||||||
state.RoundNumber.Should().Be(1);
|
state.RoundNumber.Should().Be(1);
|
||||||
|
|
||||||
// Card entities must be preserved.
|
|
||||||
after.Should().Contain("Card entities: 52");
|
after.Should().Contain("Card entities: 52");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// ── Helpers ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private static (World, SystemGroup) SetupGame(uint seed = 42)
|
|
||||||
{
|
|
||||||
var world = new World();
|
|
||||||
var group = new SystemGroup(world);
|
|
||||||
group.Add(new DeckSetupSystem());
|
|
||||||
group.Add(new DealSystem());
|
|
||||||
group.Add(new PlayerBustCheckSystem());
|
|
||||||
group.Add(new DealerSystem());
|
|
||||||
|
|
||||||
world.SetSingleton(new GameState
|
|
||||||
{
|
|
||||||
Phase = GamePhase.Betting,
|
|
||||||
Result = RoundResult.None,
|
|
||||||
Chips = 100,
|
|
||||||
CurrentBet = 0,
|
|
||||||
RoundNumber = 1,
|
|
||||||
Seed = seed
|
|
||||||
});
|
|
||||||
world.PostChanges();
|
|
||||||
|
|
||||||
return (world, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Produces a human-readable textual snapshot of the world state.
|
|
||||||
/// </summary>
|
|
||||||
private static string SnapshotWorld(World world)
|
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder();
|
|
||||||
|
|
||||||
// GameState singleton.
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
sb.AppendLine($"Phase: {state.Phase}");
|
|
||||||
sb.AppendLine($"Chips: {state.Chips}");
|
|
||||||
sb.AppendLine($"Bet: {state.CurrentBet}");
|
|
||||||
sb.AppendLine($"Round: {state.RoundNumber}");
|
|
||||||
sb.AppendLine($"Result: {state.Result}");
|
|
||||||
sb.AppendLine($"Seed: {state.Seed}");
|
|
||||||
|
|
||||||
// Deck entity.
|
|
||||||
var deckEntity = FindEntity<Deck>(world);
|
|
||||||
if (deckEntity != Entity.Null)
|
|
||||||
{
|
|
||||||
var cardsInDeck = world.GetSources<InDeck>(deckEntity).ToList();
|
|
||||||
sb.AppendLine($"Cards in deck: {cardsInDeck.Count}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player hand.
|
|
||||||
var playerHand = FindEntity<PlayerHand>(world);
|
|
||||||
if (playerHand != Entity.Null)
|
|
||||||
{
|
|
||||||
var cards = world.GetSources<Holds>(playerHand).ToList();
|
|
||||||
sb.AppendLine($"Player hand: {cards.Count} cards");
|
|
||||||
foreach (var cardEntity in cards)
|
|
||||||
{
|
|
||||||
var card = world.ReadComponent<Card>(cardEntity);
|
|
||||||
sb.AppendLine($" {card.Rank} of {card.Suit}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dealer hand.
|
|
||||||
var dealerHand = FindEntity<DealerHand>(world);
|
|
||||||
if (dealerHand != Entity.Null)
|
|
||||||
{
|
|
||||||
var cards = world.GetSources<Holds>(dealerHand).ToList();
|
|
||||||
sb.AppendLine($"Dealer hand: {cards.Count} cards");
|
|
||||||
foreach (var cardEntity in cards)
|
|
||||||
{
|
|
||||||
var card = world.ReadComponent<Card>(cardEntity);
|
|
||||||
sb.AppendLine($" {card.Rank} of {card.Suit}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All entities.
|
|
||||||
int entityCount = 0;
|
|
||||||
using (var iter = world.Select<Card>())
|
|
||||||
{
|
|
||||||
while (iter.MoveNext()) entityCount++;
|
|
||||||
}
|
|
||||||
sb.AppendLine($"Card entities: {entityCount}");
|
|
||||||
|
|
||||||
return sb.ToString().TrimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Entity FindEntity<T>(World world) where T : struct
|
|
||||||
{
|
|
||||||
return world.FindEntity<T>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue