142 lines
5.0 KiB
C#
142 lines
5.0 KiB
C#
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;
|
|
}
|
|
}
|
|
} |