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:
@@ -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"
|
||||
```
|
||||
Reference in New Issue
Block a user