oecs-sharp/.agents/skills/testing-games/SKILL.md

196 lines
5.4 KiB
Markdown

---
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
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Game.YourGame.Tests</RootNamespace>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Game.YourGame\YourGame.csproj" />
</ItemGroup>
</Project>
```
## 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<GameState>();
state.Phase.Should().Be(GamePhase.PlayerTurn);
state.Chips.Should().Be(90);
}
```
Common test helpers:
- `SetupGame(seed?)` — create world, register systems, set initial singletons.
- `FindEntity<T>(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<string>();
world.ObserveComponentChanges<Card>().Subscribe(change =>
log.Add($"[card] {change}"));
world.ObserveComponentChanges<GameState>().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<GameState>().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<GameState>();
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"
```