From 10c4caed9093da00d97ae1521613b8c003098b05 Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 20 Jul 2026 17:37:23 +0800 Subject: [PATCH] test: improve blackjack play tests and logging Refactor `RunRound` to `RunUntilDone` to allow for multi-round simulations until a win/loss condition is met. Added round summaries, win/loss/push tracking, and improved log formatting to provide better visibility into agent performance. --- Game.Blackjack.Tests/PlayTests.cs | 130 ++++++++++++++++----- Game.Blackjack/Commands/NewRoundCommand.cs | 1 + 2 files changed, 103 insertions(+), 28 deletions(-) diff --git a/Game.Blackjack.Tests/PlayTests.cs b/Game.Blackjack.Tests/PlayTests.cs index d71a02b..cf98df0 100644 --- a/Game.Blackjack.Tests/PlayTests.cs +++ b/Game.Blackjack.Tests/PlayTests.cs @@ -8,6 +8,9 @@ namespace Game.Blackjack.Tests; public class PlayTests { + private const int StartingChips = 100; + private const int BetAmount = 10; + [Fact] public void Play_BasicStrategy() { @@ -25,10 +28,10 @@ public class PlayTests log.Reactivity.Add($"{change.Kind} GameState = {state}"); }); - log.Header = "Blackjack: Basic Strategy (seed=42)"; - var agent = new BasicStrategyAgent(); - RunRound(world, group, agent, log); + log.Header = $"Blackjack: Basic Strategy (seed=42, agent={agent})"; + + RunUntilDone(world, group, agent, log); log.FinalSnapshot = SnapshotWorld(world); @@ -53,10 +56,10 @@ public class PlayTests log.Reactivity.Add($"{change.Kind} GameState = {state}"); }); - log.Header = "Blackjack: Random Agent (seed=123)"; - var agent = new RandomBlackjackAgent(); - RunRound(world, group, agent, log); + log.Header = $"Blackjack: Random Agent (seed=123, agent={agent})"; + + RunUntilDone(world, group, agent, log); log.FinalSnapshot = SnapshotWorld(world); @@ -85,10 +88,10 @@ public class PlayTests pool.Add(new BasicStrategyAgent(), 7); pool.Add(new RandomBlackjackAgent(), 3); - log.Header = $"Blackjack: Weighted Pool (seed=77, agent={pool.LastPicked})"; - var agent = pool.Pick(); - RunRound(world, group, agent, log); + log.Header = $"Blackjack: Weighted Pool (seed=77, agent={agent})"; + + RunUntilDone(world, group, agent, log); log.FinalSnapshot = SnapshotWorld(world); @@ -101,6 +104,7 @@ public class PlayTests 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; } = ""; @@ -111,6 +115,10 @@ public class PlayTests 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]}"); @@ -140,7 +148,7 @@ public class PlayTests { Phase = GamePhase.Betting, Result = RoundResult.None, - Chips = 100, + Chips = StartingChips, CurrentBet = 0, RoundNumber = 1, Seed = seed @@ -150,27 +158,96 @@ public class PlayTests return (world, group); } - private static void RunRound(World world, SystemGroup group, IBlackjackAgent agent, PlayLog log) + /// + /// 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) { - world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 }); - group.RunLogical(); + int totalRounds = 0; + int wins = 0; + int losses = 0; + int pushes = 0; - while (world.ReadSingleton().Phase == GamePhase.PlayerTurn) + while (true) { - var total = HandUtil.CalculateHand(world, new PlayerHand()); - var decision = agent.Decide(world); - log.Decisions.Add($"Hand={total}, Decision={decision}"); + var state = world.ReadSingleton(); + int chips = state.Chips; - if (decision == BlackjackDecision.Hit) - world.Commands.Enqueue(new HitCommand()); - else - world.Commands.Enqueue(new StandCommand()); + // Stop conditions. + if (chips < BetAmount) + { + log.Header += $" — BUSTED after {totalRounds} rounds"; + break; + } + if (chips >= StartingChips * 2) + { + 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; + while (world.ReadSingleton().Phase == GamePhase.PlayerTurn) + { + var total = HandUtil.CalculateHand(world, new PlayerHand()); + var decision = agent.Decide(world); + log.Decisions.Add($"R{totalRounds + 1} Hand={total}, Decision={decision}"); + + if (decision == BlackjackDecision.Hit) + { + world.Commands.Enqueue(new HitCommand()); + roundHits++; + } + else + { + world.Commands.Enqueue(new StandCommand()); + } + + 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); + + switch (state.Result) + { + case RoundResult.PlayerWin: + case RoundResult.DealerBust: + wins++; + break; + case RoundResult.DealerWin: + case RoundResult.PlayerBust: + losses++; + break; + case RoundResult.Push: + pushes++; + break; + } + + totalRounds++; + + // Start next round. + world.Commands.Enqueue(new NewRoundCommand()); group.RunLogical(); } - var state = world.ReadSingleton(); - log.Header += $" — Result: {state.Result}"; + log.Header += $" | W:{wins} L:{losses} P:{pushes}"; } // ── Snapshot ────────────────────────────────────────────────────── @@ -181,6 +258,7 @@ public class PlayTests 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); @@ -237,6 +315,7 @@ public class PlayTests { var content = File.ReadAllText(path); + content.Should().Contain("--- Round Summaries ---"); content.Should().Contain("--- Decisions ---"); content.Should().Contain("--- Reactivity ---"); content.Should().Contain("--- Final State ---"); @@ -279,7 +358,6 @@ public class PlayTests private readonly List<(IBlackjackAgent Agent, int Weight)> _agents = new(); private int _totalWeight; private static readonly Random _rng = new(); - public string LastPicked { get; private set; } = ""; public void Add(IBlackjackAgent agent, int weight) { @@ -295,12 +373,8 @@ public class PlayTests { cumulative += weight; if (roll < cumulative) - { - LastPicked = agent.ToString()!; return agent; - } } - LastPicked = _agents[^1].Agent.ToString()!; return _agents[^1].Agent; } } diff --git a/Game.Blackjack/Commands/NewRoundCommand.cs b/Game.Blackjack/Commands/NewRoundCommand.cs index 3ef5f91..bd41b2b 100644 --- a/Game.Blackjack/Commands/NewRoundCommand.cs +++ b/Game.Blackjack/Commands/NewRoundCommand.cs @@ -46,6 +46,7 @@ public struct NewRoundCommand : ICommand } } + state.RoundNumber++; state.Phase = GamePhase.Betting; state.Result = RoundResult.None; world.MarkModified(World.SingletonEntity);