oecs-sharp/Game.TicTacToe.Tests/PlayTests.cs

142 lines
5.0 KiB
C#
Raw Permalink Normal View History

using FluentAssertions;
using Game.TicTacToe;
using OECS;
using OECS.PlayTest;
using Xunit;
namespace Game.TicTacToe.Tests;
public class PlayTests
{
[Fact]
public void Play_GreedyVsRandom()
{
var (world, group) = TestHelpers.SetupGame();
var agentX = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock);
var agentO = new RandomTicTacToeAgent();
var log = RunGame(world, group, agentX, agentO, "TicTacToe: Greedy X vs Random O");
var path = log.SaveTo("tic_tac_toe_greedy_vs_random.playlog");
ReadAndVerify(path);
}
[Fact]
public void Play_RandomVsRandom()
{
var (world, group) = TestHelpers.SetupGame();
var agentX = new RandomTicTacToeAgent();
var agentO = new RandomTicTacToeAgent();
var log = RunGame(world, group, agentX, agentO, "TicTacToe: Random X vs Random O");
var path = log.SaveTo("tic_tac_toe_random_vs_random.playlog");
ReadAndVerify(path);
}
[Fact]
public void Play_GreedyVsGreedy()
{
var (world, group) = TestHelpers.SetupGame();
var agentX = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock);
var agentO = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock);
var log = RunGame(world, group, agentX, agentO, "TicTacToe: Greedy X vs Greedy O");
var path = log.SaveTo("tic_tac_toe_greedy_vs_greedy.playlog");
ReadAndVerify(path);
}
// ── Game Runner ───────────────────────────────────────────────────
private static PlayLog RunGame(World world, SystemGroup group,
IAgent<PlaceMarkCommand> agentX, IAgent<PlaceMarkCommand> agentO, string header)
{
var log = new PlayLog { Header = header };
using var capture = new ObservableCapture(world);
capture.FormatWith<Mark>(m => m.Player.ToString());
var moves = new List<string>();
while (world.ReadSingleton<GameState>().Status == GameStatus.Playing)
{
var state = world.ReadSingleton<GameState>();
var agent = state.CurrentPlayer == Player.X ? agentX : agentO;
var cmd = agent.Decide(world);
world.Commands.Enqueue(cmd);
group.RunLogical();
moves.Add($"{state.CurrentPlayer} → ({cmd.Row},{cmd.Col})");
}
var final = world.ReadSingleton<GameState>();
log.Header += $" — Result: {final.Status}";
log.AddSection("Moves", moves.Select((m, i) => $"{i + 1}. {m}"));
log.AddSection("Reactivity", capture.GetLogLines());
log.FinalSnapshot = TestHelpers.SnapshotWorld(world);
return log;
}
// ── File I/O ──────────────────────────────────────────────────────
private static void ReadAndVerify(string path)
{
var content = File.ReadAllText(path);
content.Should().Contain("--- Moves ---");
content.Should().Contain("--- Reactivity ---");
content.Should().Contain("--- Final State ---");
content.Should().Contain("Result:");
content.Should().NotContain("Result: Playing");
}
// ── Agents ────────────────────────────────────────────────────────
private sealed class RandomTicTacToeAgent : GreedyAgent<PlaceMarkCommand>
{
protected override List<PlaceMarkCommand> GetLegalActions(World world)
{
return TestHelpers.GetEmptyCells(world)
.Select(c => new PlaceMarkCommand { Row = c.Row, Col = c.Col })
.ToList();
}
}
private sealed class GreedyTicTacToeAgent : GreedyAgent<PlaceMarkCommand>
{
public enum Strategy { WinOrBlock }
private readonly Strategy _strategy;
private static readonly Random _rng = new();
public GreedyTicTacToeAgent(Strategy strategy) => _strategy = strategy;
protected override List<PlaceMarkCommand> GetLegalActions(World world)
{
return TestHelpers.GetEmptyCells(world)
.Select(c => new PlaceMarkCommand { Row = c.Row, Col = c.Col })
.ToList();
}
protected override float ScoreAction(World world, PlaceMarkCommand action)
{
if (_strategy != Strategy.WinOrBlock)
return 0f;
var state = world.ReadSingleton<GameState>();
var me = state.CurrentPlayer;
var opponent = me == Player.X ? Player.O : Player.X;
if (TestHelpers.WouldWin(world, action.Row, action.Col, me))
return 100f;
if (TestHelpers.WouldWin(world, action.Row, action.Col, opponent))
return 50f;
if (action.Row == 1 && action.Col == 1)
return 10f;
return 0f;
}
}
}