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.
This commit is contained in:
parent
f8d0fe314c
commit
10c4caed90
|
|
@ -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<string> RoundSummaries = new();
|
||||
public readonly List<string> Decisions = new();
|
||||
public readonly List<string> 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)
|
||||
/// <summary>
|
||||
/// Runs rounds until the player runs out of chips (can't afford minimum bet)
|
||||
/// or doubles their starting chips.
|
||||
/// </summary>
|
||||
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<GameState>().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<GameState>();
|
||||
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<GameState>().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<GameState>();
|
||||
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<GameState>();
|
||||
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<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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public struct NewRoundCommand : ICommand
|
|||
}
|
||||
}
|
||||
|
||||
state.RoundNumber++;
|
||||
state.Phase = GamePhase.Betting;
|
||||
state.Result = RoundResult.None;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
|
|
|
|||
Loading…
Reference in New Issue