feat: add agent skill documentation

Add new skill documentation files for developing OECS, testing games,
and writing games. Also update the API surface documentation to include
guidance on component and command definitions.
This commit is contained in:
2026-07-20 18:01:46 +08:00
parent b066ac2eba
commit 3b08138580
4 changed files with 610 additions and 2 deletions
+195
View File
@@ -0,0 +1,195 @@
---
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"
```