196 lines
5.4 KiB
Markdown
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 non-singleton entity with component T.
|
|
- `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"
|
|
```
|