--- name: testing-games description: Test OECS-based games via unit tests, snapshots, playtests with AI agents, reactivity logs, and serialization round-trips. Use this when writing or running tests for a game built on OECS. --- # Testing Games with OECS Read `docs/testing-games.md` for the testing strategy overview. This skill provides the detailed patterns and conventions. ## Test Project Setup A test project references the game DLL plus xUnit and FluentAssertions: ```xml Game.YourGame.Tests false true runtime; build; native; contentfiles; analyzers; buildtransitive all ``` ## Three Test Categories ### 1. Unit / Game Flow Tests Test individual game rules in isolation. Each test sets up a fresh `World`, enqueues commands, runs a tick, and asserts state: ```csharp [Fact] public void PlaceBet_AdvancesToDealing() { var (world, group) = SetupGame(); world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 }); group.RunLogical(); var state = world.ReadSingleton(); state.Phase.Should().Be(GamePhase.PlayerTurn); state.Chips.Should().Be(90); } ``` Common test helpers: - `SetupGame(seed?)` — create world, register systems, set initial singletons. - `FindEntity(world)` — find first entity with component T (singletons are automatically excluded by query results). - `CountCardsInHand(world, handEntity)` or `GetHandCards(world, handEntity)`. Always keep helpers in the test class (or a shared base) rather than in the game DLL — they are test infrastructure. ### 2. Snapshot & Log Tests Capture world state as text and R3 change logs for manual review: ```csharp [Fact] public void Snapshot_AfterDeal() { var (world, group) = SetupGame(seed: 42); world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 }); group.RunLogical(); var snapshot = SnapshotWorld(world); _output.WriteLine(snapshot); // ITestOutputHelper snapshot.Should().Contain("Phase: PlayerTurn"); snapshot.Should().Contain("Player hand:"); snapshot.Should().Contain("Dealer hand:"); } ``` `SnapshotWorld(world)` should produce a human-readable text block containing all singletons, entity counts, hand contents, and any other debug-relevant state. Reactivity log test pattern: ```csharp var log = new List(); world.ObserveComponentChanges().Subscribe(change => log.Add($"[card] {change}")); world.ObserveComponentChanges().Subscribe(change => log.Add($"[gamestate] {change}")); // ... run game ... log.Should().Contain(l => l.Contains("EntityAdded")); log.Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState")); ``` ### 3. Play Tests AI-driven multi-round playtests. See `docs/testing-games.md` for the full strategy. Key patterns: **Agents** implement a simple interface: ```csharp private interface IYourGameAgent { Decision Decide(World world); } ``` Built-in agent types: - **Greedy/Basic Strategy:** score actions, pick the highest. For blackjack: hit if total < 17, stand otherwise. - **Random:** evenly choose moves randomly (but never do illegal moves). - **Weighted Pool:** pick from a pool of agents by weight each round. **Round runner** loops until a terminal condition: ```csharp while (true) { int chips = world.ReadSingleton().Chips; if (chips < BetAmount) { /* busted */ break; } if (chips >= StartingChips * 2) { /* doubled */ break; } if (totalRounds >= 200) { /* safety cap */ break; } // Place bet, deal, agent decides hit/stand, resolve, new round. } ``` **Play logs** are saved as `.playlog` files to `AppContext.BaseDirectory/playlogs/`: ``` Blackjack: Basic Strategy (seed=42, agent=BasicStrategy) — BUSTED after 38 rounds | W:12 L:22 P:4 ================================================================================================= --- Round Summaries --- Round 1: PlayerBust | Bet=10 | Chips: 100→90 (-10) | Hits: 1 ... --- Decisions --- 1. R1 Hand=12, Decision=Hit ... --- Reactivity --- ComponentModified GameState = ... ... --- Final State --- Phase: Betting Chips: 0, Bet: 10 ... ``` ## Serialization Round-Trip Every game must have a serialization test: ```csharp [Fact] public void Serialization_RoundTrips() { var (world, group) = SetupGame(seed: 42); // ... run some game state ... using var stream = new MemoryStream(); WorldSerializer.Save(world, stream); stream.Position = 0; var world2 = new World(); WorldSerializer.Load(world2, stream); // Assert key state survived: var state = world2.ReadSingleton(); state.Chips.Should().Be(expected); } ``` ## Running Tests ```bash dotnet test "E:/projects/oecs-sharp/Game.YourGame.Tests/Game.YourGame.Tests.csproj" ``` To run only playtests: ```bash dotnet test ... --filter "FullyQualifiedName~PlayTests" ```