diff --git a/Game.Blackjack.Tests/Game.Blackjack.Tests.csproj b/Game.Blackjack.Tests/Game.Blackjack.Tests.csproj
index a898773..3755345 100644
--- a/Game.Blackjack.Tests/Game.Blackjack.Tests.csproj
+++ b/Game.Blackjack.Tests/Game.Blackjack.Tests.csproj
@@ -19,6 +19,7 @@
+
diff --git a/Game.Blackjack.Tests/GameFlowTests.cs b/Game.Blackjack.Tests/GameFlowTests.cs
index 96ca1ee..27d551d 100644
--- a/Game.Blackjack.Tests/GameFlowTests.cs
+++ b/Game.Blackjack.Tests/GameFlowTests.cs
@@ -10,7 +10,7 @@ public class GameFlowTests
[Fact]
public void NewGame_StartsInBettingPhase()
{
- var (world, _) = SetupGame();
+ var (world, _) = TestHelpers.SetupGame();
var state = world.ReadSingleton();
state.Phase.Should().Be(GamePhase.Betting);
@@ -21,13 +21,13 @@ public class GameFlowTests
[Fact]
public void PlaceBet_AdvancesToDealing()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
var state = world.ReadSingleton();
- state.Phase.Should().Be(GamePhase.PlayerTurn); // Dealing → PlayerTurn happens automatically
+ state.Phase.Should().Be(GamePhase.PlayerTurn);
state.CurrentBet.Should().Be(10);
state.Chips.Should().Be(90);
}
@@ -35,20 +35,20 @@ public class GameFlowTests
[Fact]
public void PlaceBet_RejectsInsufficientChips()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 200 });
group.RunLogical();
var state = world.ReadSingleton();
- state.Phase.Should().Be(GamePhase.Betting); // Should not advance
+ state.Phase.Should().Be(GamePhase.Betting);
state.Chips.Should().Be(100);
}
[Fact]
public void PlaceBet_RejectsZeroOrNegative()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 0 });
group.RunLogical();
@@ -62,60 +62,53 @@ public class GameFlowTests
[Fact]
public void Dealing_CreatesDeckAndHands()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
- // Should have a deck entity.
- var deckEntity = FindEntity(world);
- deckEntity.Should().NotBe(Entity.Null);
-
- // Should have player and dealer hand entities.
- var playerHand = FindEntity(world);
- playerHand.Should().NotBe(Entity.Null);
-
- var dealerHand = FindEntity(world);
- dealerHand.Should().NotBe(Entity.Null);
+ TestHelpers.FindEntity(world).Should().NotBe(Entity.Null);
+ TestHelpers.FindEntity(world).Should().NotBe(Entity.Null);
+ TestHelpers.FindEntity(world).Should().NotBe(Entity.Null);
}
[Fact]
public void Dealing_DealsTwoCardsToEach()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
- var playerHand = FindEntity(world);
- var dealerHand = FindEntity(world);
+ var playerHand = TestHelpers.FindEntity(world);
+ var dealerHand = TestHelpers.FindEntity(world);
- CountCardsInHand(world, playerHand).Should().Be(2);
- CountCardsInHand(world, dealerHand).Should().Be(2);
+ TestHelpers.CountCardsInHand(world, playerHand).Should().Be(2);
+ TestHelpers.CountCardsInHand(world, dealerHand).Should().Be(2);
}
[Fact]
public void Hit_DrawsOneCard()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
- var playerHand = FindEntity(world);
- var before = CountCardsInHand(world, playerHand);
+ var playerHand = TestHelpers.FindEntity(world);
+ var before = TestHelpers.CountCardsInHand(world, playerHand);
world.Commands.Enqueue(new HitCommand());
group.RunLogical();
- var after = CountCardsInHand(world, playerHand);
+ var after = TestHelpers.CountCardsInHand(world, playerHand);
after.Should().Be(before + 1);
}
[Fact]
public void Stand_AdvancesToDealerTurn()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
@@ -130,15 +123,13 @@ public class GameFlowTests
[Fact]
public void NewRound_ResetsPhase()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
- // Play a round.
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
world.Commands.Enqueue(new StandCommand());
group.RunLogical();
- // Start new round.
world.Commands.Enqueue(new NewRoundCommand());
group.RunLogical();
@@ -150,15 +141,15 @@ public class GameFlowTests
[Fact]
public void DeterministicSeed_ProducesSameDeal()
{
- var (world1, group1) = SetupGame(seed: 42);
+ var (world1, group1) = TestHelpers.SetupGame(seed: 42);
world1.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
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 });
group2.RunLogical();
- var cards2 = GetAllCards(world2);
+ var cards2 = TestHelpers.GetAllCards(world2);
cards1.Should().Equal(cards2);
}
@@ -166,20 +157,17 @@ public class GameFlowTests
[Fact]
public void PlayerBust_LosesBet()
{
- // Use a seed that produces a bust-prone hand, then hit repeatedly.
- var (world, group) = SetupGame(seed: 12345);
+ var (world, group) = TestHelpers.SetupGame(seed: 12345);
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
- // Hit until bust or stand.
while (world.ReadSingleton().Phase == GamePhase.PlayerTurn)
{
world.Commands.Enqueue(new HitCommand());
group.RunLogical();
}
- // Game should have resolved.
var state = world.ReadSingleton();
state.Phase.Should().Be(GamePhase.RoundOver);
}
@@ -187,7 +175,7 @@ public class GameFlowTests
[Fact]
public void Serialization_RoundTrips()
{
- var (world, group) = SetupGame(seed: 42);
+ var (world, group) = TestHelpers.SetupGame(seed: 42);
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
@@ -204,60 +192,4 @@ public class GameFlowTests
state.CurrentBet.Should().Be(10);
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(World world) where T : struct
- {
- return world.FindEntity();
- }
-
- 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(handEntity).Count;
- }
-
- private static List<(Suit, Rank)> GetAllCards(World world)
- {
- var cards = new List<(Suit, Rank)>();
- using (var iter = world.Select())
- {
- 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;
- }
-}
+}
\ No newline at end of file
diff --git a/Game.Blackjack.Tests/PlayTests.cs b/Game.Blackjack.Tests/PlayTests.cs
index bf175e6..11bad81 100644
--- a/Game.Blackjack.Tests/PlayTests.cs
+++ b/Game.Blackjack.Tests/PlayTests.cs
@@ -1,7 +1,7 @@
using FluentAssertions;
using Game.Blackjack;
using OECS;
-using R3;
+using OECS.PlayTest;
using Xunit;
namespace Game.Blackjack.Tests;
@@ -14,167 +14,73 @@ public class PlayTests
[Fact]
public void Play_BasicStrategy()
{
- var log = new PlayLog();
- var (world, group) = SetupGame(seed: 42);
-
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var card = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} Card = {card} on {change.Entity}");
- });
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var state = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} GameState = {state}");
- });
-
+ var (world, group) = TestHelpers.SetupGame(seed: 42);
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 = SavePlayLog("blackjack_basic_strategy.playlog", log);
+ var path = log.SaveTo("blackjack_basic_strategy.playlog");
ReadAndVerify(path);
}
[Fact]
public void Play_RandomAgent()
{
- var log = new PlayLog();
- var (world, group) = SetupGame(seed: 123);
-
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var card = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} Card = {card} on {change.Entity}");
- });
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var state = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} GameState = {state}");
- });
-
+ var (world, group) = TestHelpers.SetupGame(seed: 123);
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 = SavePlayLog("blackjack_random_agent.playlog", log);
+ var path = log.SaveTo("blackjack_random_agent.playlog");
ReadAndVerify(path);
}
[Fact]
public void Play_WeightedPool()
{
- var log = new PlayLog();
- var (world, group) = SetupGame(seed: 77);
+ var (world, group) = TestHelpers.SetupGame(seed: 77);
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var card = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} Card = {card} on {change.Entity}");
- });
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var state = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} GameState = {state}");
- });
-
- var pool = new WeightedAgentPool();
+ var pool = new WeightedAgentPool();
pool.Add(new BasicStrategyAgent(), 7);
pool.Add(new RandomBlackjackAgent(), 3);
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);
-
- log.FinalSnapshot = SnapshotWorld(world);
-
- var path = SavePlayLog("blackjack_weighted_pool.playlog", log);
+ var path = log.SaveTo("blackjack_weighted_pool.playlog");
ReadAndVerify(path);
}
- // ── Play Log ──────────────────────────────────────────────────────
-
- private sealed class PlayLog
- {
- public string Header { get; set; } = "";
- public readonly List RoundSummaries = new();
- public readonly List Decisions = new();
- public readonly List 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 ──────────────────────────────────────────────────
- 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);
- }
-
///
/// Runs rounds until the player runs out of chips (can't afford minimum bet)
/// or doubles their starting chips.
///
- private static void RunUntilDone(World world, SystemGroup group, IBlackjackAgent agent, PlayLog log)
+ private static PlayLog RunUntilDone(World world, SystemGroup group,
+ IAgent agent, string header)
{
+ var log = new PlayLog { Header = header };
+
+ using var capture = new ObservableCapture(world);
+ capture.FormatWith(c => $"{c.Rank} of {c.Suit}");
+ capture.FormatWith(s => $"{s.Phase} Chips={s.Chips} Bet={s.CurrentBet}");
+
int totalRounds = 0;
int wins = 0;
int losses = 0;
int pushes = 0;
+ var roundSummaries = new List();
+ var decisions = new List();
while (true)
{
var state = world.ReadSingleton();
int chips = state.Chips;
- // Stop conditions.
if (chips < BetAmount)
{
log.Header += $" — BUSTED after {totalRounds} rounds";
@@ -185,19 +91,15 @@ public class PlayTests
log.Header += $" — DOUBLED after {totalRounds} rounds";
break;
}
-
- // Safety: max 200 rounds to prevent infinite loops.
if (totalRounds >= 200)
{
log.Header += $" — MAX ROUNDS ({totalRounds})";
break;
}
- // Place bet and run dealing.
world.Commands.Enqueue(new PlaceBetCommand { Amount = BetAmount });
group.RunLogical();
- // Player turn: agent decides hit/stand.
int roundHits = 0;
int playerTurnSafety = 0;
while (world.ReadSingleton().Phase == GamePhase.PlayerTurn && playerTurnSafety < 52)
@@ -205,7 +107,7 @@ public class PlayTests
playerTurnSafety++;
var total = HandUtil.CalculateHand(world, new PlayerHand());
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)
{
@@ -220,12 +122,10 @@ public class PlayTests
group.RunLogical();
}
- // Record round result.
state = world.ReadSingleton();
int newChips = state.Chips;
int delta = newChips - chips;
- string summary = $"{state.Result} | Bet={BetAmount} | Chips: {chips}→{newChips} ({delta:+0;-#}) | Hits: {roundHits}";
- log.RoundSummaries.Add(summary);
+ roundSummaries.Add($"Round {totalRounds + 1}: {state.Result} | Bet={BetAmount} | Chips: {chips}→{newChips} ({delta:+0;-#}) | Hits: {roundHits}");
switch (state.Result)
{
@@ -244,71 +144,22 @@ public class PlayTests
totalRounds++;
- // Start next round.
world.Commands.Enqueue(new NewRoundCommand());
group.RunLogical();
}
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)
- {
- var sb = new System.Text.StringBuilder();
- var state = world.ReadSingleton();
- 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(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(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(World world) where T : struct
- {
- return world.FindEntity();
- }
-
- private static List GetHandCards(World world, Entity hand)
- {
- var cards = new List();
- foreach (var cardEntity in world.GetSources(hand))
- {
- var card = world.ReadComponent(cardEntity);
- cards.Add($"{card.Rank} of {card.Suit}");
- }
- return cards;
+ return log;
}
// ── 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)
{
var content = File.ReadAllText(path);
@@ -324,12 +175,7 @@ public class PlayTests
private enum BlackjackDecision { Hit, Stand }
- private interface IBlackjackAgent
- {
- BlackjackDecision Decide(World world);
- }
-
- private sealed class BasicStrategyAgent : IBlackjackAgent
+ private sealed class BasicStrategyAgent : IAgent
{
public BlackjackDecision Decide(World world)
{
@@ -339,7 +185,7 @@ public class PlayTests
public override string ToString() => "BasicStrategy";
}
- private sealed class RandomBlackjackAgent : IBlackjackAgent
+ private sealed class RandomBlackjackAgent : IAgent
{
private static readonly Random _rng = new();
public BlackjackDecision Decide(World world)
@@ -350,30 +196,4 @@ public class PlayTests
}
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;
- }
- }
-}
+}
\ No newline at end of file
diff --git a/Game.Blackjack.Tests/SnapshotTests.cs b/Game.Blackjack.Tests/SnapshotTests.cs
index b53310e..e745816 100644
--- a/Game.Blackjack.Tests/SnapshotTests.cs
+++ b/Game.Blackjack.Tests/SnapshotTests.cs
@@ -1,7 +1,7 @@
using FluentAssertions;
using Game.Blackjack;
using OECS;
-using R3;
+using OECS.PlayTest;
using Xunit;
using Xunit.Abstractions;
@@ -24,9 +24,9 @@ public class SnapshotTests
[Fact]
public void Snapshot_InitialState()
{
- var (world, _) = SetupGame();
+ var (world, _) = TestHelpers.SetupGame();
- var snapshot = SnapshotWorld(world);
+ var snapshot = TestHelpers.SnapshotWorld(world);
_output.WriteLine(snapshot);
snapshot.Should().Contain("Phase: Betting");
@@ -37,12 +37,12 @@ public class SnapshotTests
[Fact]
public void Snapshot_AfterDeal()
{
- var (world, group) = SetupGame(seed: 42);
+ var (world, group) = TestHelpers.SetupGame(seed: 42);
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
- var snapshot = SnapshotWorld(world);
+ var snapshot = TestHelpers.SnapshotWorld(world);
_output.WriteLine(snapshot);
snapshot.Should().Contain("Phase: PlayerTurn");
@@ -56,7 +56,7 @@ public class SnapshotTests
[Fact]
public void Snapshot_AfterHit()
{
- var (world, group) = SetupGame(seed: 42);
+ var (world, group) = TestHelpers.SetupGame(seed: 42);
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
@@ -64,17 +64,16 @@ public class SnapshotTests
world.Commands.Enqueue(new HitCommand());
group.RunLogical();
- var snapshot = SnapshotWorld(world);
+ var snapshot = TestHelpers.SnapshotWorld(world);
_output.WriteLine(snapshot);
- // With seed 42, one hit may bust. Just verify the game resolved.
snapshot.Should().NotContain("Phase: Dealing");
}
[Fact]
public void Snapshot_AfterStand()
{
- var (world, group) = SetupGame(seed: 42);
+ var (world, group) = TestHelpers.SetupGame(seed: 42);
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
@@ -82,7 +81,7 @@ public class SnapshotTests
world.Commands.Enqueue(new StandCommand());
group.RunLogical();
- var snapshot = SnapshotWorld(world);
+ var snapshot = TestHelpers.SnapshotWorld(world);
_output.WriteLine(snapshot);
snapshot.Should().Contain("Phase: RoundOver");
@@ -92,23 +91,11 @@ public class SnapshotTests
[Fact]
public void Log_ReactivityDuringRound()
{
- var (world, group) = SetupGame(seed: 42);
- var log = new List();
+ var (world, group) = TestHelpers.SetupGame(seed: 42);
- world.ObserveEntityChanges().Subscribe(change =>
- {
- log.Add($"[entity] {change}");
- });
-
- world.ObserveComponentChanges().Subscribe(change =>
- {
- log.Add($"[card] {change}");
- });
-
- world.ObserveComponentChanges().Subscribe(change =>
- {
- log.Add($"[gamestate] {change}");
- });
+ using var capture = new ObservableCapture(world);
+ capture.FormatWith(c => $"{c.Rank} of {c.Suit}");
+ capture.FormatWith(s => $"{s.Phase} Chips={s.Chips} Bet={s.CurrentBet} Round={s.RoundNumber}");
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
@@ -116,24 +103,23 @@ public class SnapshotTests
world.Commands.Enqueue(new StandCommand());
group.RunLogical();
- var logText = string.Join("\n", log);
+ var logText = string.Join("\n", capture.GetLogLines());
_output.WriteLine(logText);
- // Should have entity creations (deck, hands, cards) and component changes.
- log.Should().Contain(l => l.Contains("EntityAdded"));
- log.Should().Contain(l => l.Contains("ComponentAdded") && l.Contains("Card"));
- log.Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState"));
+ capture.GetLogLines().Should().Contain(l => l.Contains("EntityAdded"));
+ capture.GetLogLines().Should().Contain(l => l.Contains("ComponentAdded") && l.Contains("Card"));
+ capture.GetLogLines().Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState"));
}
[Fact]
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 });
group.RunLogical();
- var before = SnapshotWorld(world);
+ var before = TestHelpers.SnapshotWorld(world);
using var stream = new MemoryStream();
WorldSerializer.Save(world, stream);
@@ -142,112 +128,19 @@ public class SnapshotTests
var world2 = new World();
WorldSerializer.Load(world2, stream);
- var after = SnapshotWorld(world2);
+ var after = TestHelpers.SnapshotWorld(world2);
_output.WriteLine("=== Before ===");
_output.WriteLine(before);
_output.WriteLine("=== After ===");
_output.WriteLine(after);
- // Core game state must round-trip.
var state = world2.ReadSingleton();
state.Phase.Should().Be(GamePhase.PlayerTurn);
state.Chips.Should().Be(90);
state.CurrentBet.Should().Be(10);
state.RoundNumber.Should().Be(1);
- // Card entities must be preserved.
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);
- }
-
- ///
- /// Produces a human-readable textual snapshot of the world state.
- ///
- private static string SnapshotWorld(World world)
- {
- var sb = new System.Text.StringBuilder();
-
- // GameState singleton.
- var state = world.ReadSingleton();
- 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(world);
- if (deckEntity != Entity.Null)
- {
- var cardsInDeck = world.GetSources(deckEntity).ToList();
- sb.AppendLine($"Cards in deck: {cardsInDeck.Count}");
- }
-
- // Player hand.
- var playerHand = FindEntity(world);
- if (playerHand != Entity.Null)
- {
- var cards = world.GetSources(playerHand).ToList();
- sb.AppendLine($"Player hand: {cards.Count} cards");
- foreach (var cardEntity in cards)
- {
- var card = world.ReadComponent(cardEntity);
- sb.AppendLine($" {card.Rank} of {card.Suit}");
- }
- }
-
- // Dealer hand.
- var dealerHand = FindEntity(world);
- if (dealerHand != Entity.Null)
- {
- var cards = world.GetSources(dealerHand).ToList();
- sb.AppendLine($"Dealer hand: {cards.Count} cards");
- foreach (var cardEntity in cards)
- {
- var card = world.ReadComponent(cardEntity);
- sb.AppendLine($" {card.Rank} of {card.Suit}");
- }
- }
-
- // All entities.
- int entityCount = 0;
- using (var iter = world.Select())
- {
- while (iter.MoveNext()) entityCount++;
- }
- sb.AppendLine($"Card entities: {entityCount}");
-
- return sb.ToString().TrimEnd();
- }
-
- private static Entity FindEntity(World world) where T : struct
- {
- return world.FindEntity();
- }
-}
+}
\ No newline at end of file