test: add unit tests for Blackjack and TicTacToe

- Add xUnit test projects for both games
- Implement game flow tests for Blackjack
- Implement game flow tests for TicTacToe
- Rename namespaces and projects to follow `Game.*` convention
- Add documentation for testing games
This commit is contained in:
hypercross 2026-07-20 16:41:10 +08:00
parent 7946f4bafd
commit b4661e714e
38 changed files with 578 additions and 34 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Game.Blackjack.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.Blackjack\Blackjack.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,247 @@
using FluentAssertions;
using Game.Blackjack;
using OECS;
using Xunit;
namespace Game.Blackjack.Tests;
public class GameFlowTests
{
[Fact]
public void NewGame_StartsInBettingPhase()
{
var (world, _) = SetupGame();
var state = world.ReadSingleton<GameState>();
state.Phase.Should().Be(GamePhase.Betting);
state.Chips.Should().Be(100);
state.RoundNumber.Should().Be(1);
}
[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); // Dealing → PlayerTurn happens automatically
state.CurrentBet.Should().Be(10);
state.Chips.Should().Be(90);
}
[Fact]
public void PlaceBet_RejectsInsufficientChips()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 200 });
group.RunLogical();
var state = world.ReadSingleton<GameState>();
state.Phase.Should().Be(GamePhase.Betting); // Should not advance
state.Chips.Should().Be(100);
}
[Fact]
public void PlaceBet_RejectsZeroOrNegative()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 0 });
group.RunLogical();
world.ReadSingleton<GameState>().Phase.Should().Be(GamePhase.Betting);
world.Commands.Enqueue(new PlaceBetCommand { Amount = -5 });
group.RunLogical();
world.ReadSingleton<GameState>().Phase.Should().Be(GamePhase.Betting);
}
[Fact]
public void Dealing_CreatesDeckAndHands()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
// Should have a deck entity.
var deckEntity = FindEntity<Deck>(world);
deckEntity.Should().NotBe(Entity.Null);
// Should have player and dealer hand entities.
var playerHand = FindEntity<PlayerHand>(world);
playerHand.Should().NotBe(Entity.Null);
var dealerHand = FindEntity<DealerHand>(world);
dealerHand.Should().NotBe(Entity.Null);
}
[Fact]
public void Dealing_DealsTwoCardsToEach()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
var playerHand = FindEntity<PlayerHand>(world);
var dealerHand = FindEntity<DealerHand>(world);
CountCardsInHand(world, playerHand).Should().Be(2);
CountCardsInHand(world, dealerHand).Should().Be(2);
}
[Fact]
public void Hit_DrawsOneCard()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
var playerHand = FindEntity<PlayerHand>(world);
var before = CountCardsInHand(world, playerHand);
world.Commands.Enqueue(new HitCommand());
group.RunLogical();
var after = CountCardsInHand(world, playerHand);
after.Should().Be(before + 1);
}
[Fact]
public void Stand_AdvancesToDealerTurn()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
world.Commands.Enqueue(new StandCommand());
group.RunLogical();
var state = world.ReadSingleton<GameState>();
state.Phase.Should().Be(GamePhase.RoundOver);
}
[Fact]
public void NewRound_ResetsPhase()
{
var (world, group) = SetupGame();
// Play a round.
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
world.Commands.Enqueue(new StandCommand());
group.RunLogical();
// Start new round.
world.Commands.Enqueue(new NewRoundCommand());
group.RunLogical();
var state = world.ReadSingleton<GameState>();
state.Phase.Should().Be(GamePhase.Betting);
state.Result.Should().Be(RoundResult.None);
}
[Fact]
public void DeterministicSeed_ProducesSameDeal()
{
var (world1, group1) = SetupGame(seed: 42);
world1.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group1.RunLogical();
var cards1 = GetAllCards(world1);
var (world2, group2) = SetupGame(seed: 42);
world2.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group2.RunLogical();
var cards2 = GetAllCards(world2);
cards1.Should().Equal(cards2);
}
[Fact]
public void PlayerBust_LosesBet()
{
// Use a seed that produces a bust-prone hand, then hit repeatedly.
var (world, group) = SetupGame(seed: 12345);
world.Commands.Enqueue(new PlaceBetCommand { Amount = 10 });
group.RunLogical();
var chipsBefore = world.ReadSingleton<GameState>().Chips;
// Hit until bust or stand.
while (world.ReadSingleton<GameState>().Phase == GamePhase.PlayerTurn)
{
world.Commands.Enqueue(new HitCommand());
group.RunLogical();
}
// Game should have resolved.
var state = world.ReadSingleton<GameState>();
state.Phase.Should().Be(GamePhase.RoundOver);
}
// ── Helpers ───────────────────────────────────────────────────────
private static (World, SystemGroup) SetupGame(uint seed = 42)
{
var world = new World();
var group = new SystemGroup(world);
group.Add(new DeckSetupSystem());
group.Add(new DealSystem());
group.Add(new PlayerBustCheckSystem());
group.Add(new DealerSystem());
world.SetSingleton(new GameState
{
Phase = GamePhase.Betting,
Result = RoundResult.None,
Chips = 100,
CurrentBet = 0,
RoundNumber = 1,
Seed = seed
});
world.PostChanges();
return (world, group);
}
private static Entity FindEntity<T>(World world) where T : struct
{
using var iter = world.Select<T>();
while (iter.MoveNext())
{
if (iter.CurrentEntity != World.SingletonEntity)
return iter.CurrentEntity;
}
return Entity.Null;
}
private static int CountCardsInHand(World world, Entity handEntity)
{
// Holds relationship: Source = card entity, Target = hand entity.
// GetSources returns all card entities that have a Holds pointing to handEntity.
return world.GetSources<Holds>(handEntity).Count;
}
private static List<(Suit, Rank)> GetAllCards(World world)
{
var cards = new List<(Suit, Rank)>();
var query = world.Query().With<Card>().Build();
world.ForEach(query, (Entity e, ref Card card) =>
{
cards.Add((card.Suit, card.Rank));
});
cards.Sort((a, b) =>
{
int cmp = a.Item1.CompareTo(b.Item1);
return cmp != 0 ? cmp : a.Item2.CompareTo(b.Item2);
});
return cards;
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<RootNamespace>Blackjack</RootNamespace>
<RootNamespace>Game.Blackjack</RootNamespace>
</PropertyGroup>
<ItemGroup>

View File

@ -1,7 +1,7 @@
using MessagePack;
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Player hits: draws one card from the deck to the player's hand.

View File

@ -1,7 +1,7 @@
using MessagePack;
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Starts a new round after the previous one ended.

View File

@ -1,7 +1,7 @@
using MessagePack;
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Places a bet and advances the game to the dealing phase.

View File

@ -1,7 +1,7 @@
using MessagePack;
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Player stands: advance to the dealer's turn.

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// A playing card with a suit and rank.

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Tag component marking the dealer's hand entity.

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Tag component marking the deck entity.

View File

@ -1,7 +1,7 @@
using MessagePack;
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Relationship from a hand entity to a card entity,

View File

@ -1,7 +1,7 @@
using MessagePack;
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Relationship from a card entity to the deck entity,

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Tag component marking the player's hand entity.

View File

@ -1,4 +1,4 @@
namespace Blackjack;
namespace Game.Blackjack;
public enum Rank : byte
{

View File

@ -1,4 +1,4 @@
namespace Blackjack;
namespace Game.Blackjack;
public enum Suit : byte
{

View File

@ -1,4 +1,4 @@
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Mulberry32 PRNG — a fast, high-quality 32-bit random number generator.

View File

@ -1,4 +1,4 @@
namespace Blackjack;
namespace Game.Blackjack;
public enum GamePhase : byte
{

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Global game state stored on the singleton entity.

View File

@ -1,4 +1,4 @@
namespace Blackjack;
namespace Game.Blackjack;
public enum RoundResult : byte
{

View File

@ -1,6 +1,6 @@
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Deals initial two cards to player and dealer when entering the dealing phase.

View File

@ -1,6 +1,6 @@
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Dealer draws cards until reaching 17 or higher,

View File

@ -1,6 +1,6 @@
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Creates the deck (52 cards + deck/hand entities) and shuffles

View File

@ -1,6 +1,6 @@
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Helper to calculate the blackjack value of a hand.

View File

@ -1,6 +1,6 @@
using OECS;
namespace Blackjack;
namespace Game.Blackjack;
/// <summary>
/// Evaluates the player's hand total after each hit.

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Game.TicTacToe.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.TicTacToe\TicTacToe.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,213 @@
using FluentAssertions;
using Game.TicTacToe;
using OECS;
using Xunit;
namespace Game.TicTacToe.Tests;
public class GameFlowTests
{
[Fact]
public void NewGame_HasEmptyBoard()
{
var (world, _) = SetupGame();
var query = world.Query().With<Cell>().Without<Mark>().Build();
var emptyCount = 0;
world.ForEach(query, (Entity e, ref Cell cell) => emptyCount++);
emptyCount.Should().Be(9);
}
[Fact]
public void PlaceMark_ClaimsCell()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 });
group.RunLogical();
var query = world.Query().With<Cell>().With<Mark>().Build();
var markedCount = 0;
world.ForEach(query, (Entity e, ref Cell cell, ref Mark mark) =>
{
markedCount++;
cell.Row.Should().Be(0);
cell.Col.Should().Be(0);
mark.Player.Should().Be(Player.X);
});
markedCount.Should().Be(1);
}
[Fact]
public void PlaceMark_TogglesPlayer()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X
group.RunLogical();
world.ReadSingleton<GameState>().CurrentPlayer.Should().Be(Player.O);
world.Commands.Enqueue(new PlaceMarkCommand { Row = 1, Col = 0 }); // O
group.RunLogical();
world.ReadSingleton<GameState>().CurrentPlayer.Should().Be(Player.X);
}
[Fact]
public void PlaceMark_CannotOverwriteClaimedCell()
{
var (world, group) = SetupGame();
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X
group.RunLogical();
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // O tries same cell
group.RunLogical();
// Only one mark should exist at (0,0), and it should still be X.
var query = world.Query().With<Cell>().With<Mark>().Build();
var marks = new List<(int Row, int Col, Player Player)>();
world.ForEach(query, (Entity e, ref Cell cell, ref Mark mark) =>
{
marks.Add((cell.Row, cell.Col, mark.Player));
});
marks.Should().ContainSingle()
.Which.Should().Be((0, 0, Player.X));
}
[Fact]
public void XWins_Row()
{
var (world, group) = SetupGame();
// X: (0,0), O: (1,0), X: (0,1), O: (1,1), X: (0,2) → X wins row 0
PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
}
[Fact]
public void OWins_Column()
{
var (world, group) = SetupGame();
// X: (0,0), O: (1,0), X: (0,1), O: (1,1), X: (2,2), O: (1,2) → O wins col 0
PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (2, 2), (1, 2));
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.OWon);
}
[Fact]
public void XWins_Diagonal()
{
var (world, group) = SetupGame();
// X: (0,0), O: (1,0), X: (1,1), O: (1,2), X: (2,2) → X wins diagonal
PlayMoves(world, group, (0, 0), (1, 0), (1, 1), (1, 2), (2, 2));
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
}
[Fact]
public void Draw_AllCellsFilled()
{
var (world, group) = SetupGame();
// Fill all 9 cells without a winner.
// X: (0,0) (0,2) (1,0) (2,1) (2,2)
// O: (0,1) (1,1) (1,2) (2,0)
PlayMoves(world, group,
(0, 0), (0, 1), (0, 2),
(1, 1), (1, 0), (1, 2),
(2, 1), (2, 0), (2, 2));
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.Draw);
}
[Fact]
public void MovesAfterGameOver_AreIgnored()
{
var (world, group) = SetupGame();
// X wins.
PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
// Try to place another mark — should be ignored.
var moveCountBefore = world.ReadSingleton<GameState>().MoveCount;
world.Commands.Enqueue(new PlaceMarkCommand { Row = 2, Col = 2 });
group.RunLogical();
world.ReadSingleton<GameState>().MoveCount.Should().Be(moveCountBefore);
}
[Fact]
public void SaveLoad_RoundTrips()
{
var (world, group) = SetupGame();
// Play a few moves.
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 });
group.RunLogical();
world.Commands.Enqueue(new PlaceMarkCommand { Row = 1, Col = 0 });
group.RunLogical();
// Save.
using var stream = new MemoryStream();
WorldSerializer.Save(world, stream);
stream.Position = 0;
// Load into a new world.
var world2 = new World();
WorldSerializer.Load(world2, stream);
// Verify state.
var state = world2.ReadSingleton<GameState>();
state.CurrentPlayer.Should().Be(Player.X);
state.MoveCount.Should().Be(2);
var query = world2.Query().With<Cell>().With<Mark>().Build();
var marks = new List<(int Row, int Col, Player Player)>();
world2.ForEach(query, (Entity e, ref Cell cell, ref Mark mark) =>
{
marks.Add((cell.Row, cell.Col, mark.Player));
});
marks.Should().BeEquivalentTo([(0, 0, Player.X), (1, 0, Player.O)]);
}
// ── Helpers ───────────────────────────────────────────────────────
private static (World, SystemGroup) SetupGame()
{
var world = new World();
var group = new SystemGroup(world);
group.Add(new WinCheckSystem());
// Create the 9 cells.
for (int r = 0; r < 3; r++)
for (int c = 0; c < 3; c++)
world.AddComponent(world.CreateEntity(), new Cell { Row = r, Col = c });
world.SetSingleton(new GameState
{
CurrentPlayer = Player.X,
Status = GameStatus.Playing,
MoveCount = 0
});
world.PostChanges();
return (world, group);
}
private static void PlayMoves(World world, SystemGroup group, params (int Row, int Col)[] moves)
{
foreach (var (row, col) in moves)
{
world.Commands.Enqueue(new PlaceMarkCommand { Row = row, Col = col });
group.RunLogical();
}
}
}

View File

@ -1,7 +1,7 @@
using MessagePack;
using OECS;
namespace TicTacToe;
namespace Game.TicTacToe;
/// <summary>
/// Places the current player's mark on the cell at (Row, Col).

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace TicTacToe;
namespace Game.TicTacToe;
/// <summary>
/// Identifies a board position. One entity per cell.

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace TicTacToe;
namespace Game.TicTacToe;
/// <summary>
/// A mark placed on a cell by a player.

View File

@ -1,4 +1,4 @@
namespace TicTacToe;
namespace Game.TicTacToe;
public enum Player : byte
{

View File

@ -1,6 +1,6 @@
using OECS;
namespace TicTacToe;
namespace Game.TicTacToe;
/// <summary>
/// Minimal CSV loader that creates entities from a CSV file.

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace TicTacToe;
namespace Game.TicTacToe;
/// <summary>
/// Global game state stored on the singleton entity.

View File

@ -1,4 +1,4 @@
namespace TicTacToe;
namespace Game.TicTacToe;
public enum GameStatus : byte
{

View File

@ -1,6 +1,6 @@
using OECS;
namespace TicTacToe;
namespace Game.TicTacToe;
/// <summary>
/// After each move, checks whether the game has been won or drawn.

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<RootNamespace>TicTacToe</RootNamespace>
<RootNamespace>Game.TicTacToe</RootNamespace>
</PropertyGroup>
<ItemGroup>

View File

@ -8,9 +8,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.SourceGen", "OECS.Sour
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.Tests", "OECS.Tests\OECS.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blackjack", "Blackjack\Blackjack.csproj", "{85631EDE-F984-4DA6-8EE9-0715AF1204E6}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.Blackjack", "Game.Blackjack\Blackjack.csproj", "{85631EDE-F984-4DA6-8EE9-0715AF1204E6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TicTacToe", "TicTacToe\TicTacToe.csproj", "{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.TicTacToe", "Game.TicTacToe\TicTacToe.csproj", "{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.Blackjack.Tests", "Game.Blackjack.Tests\Game.Blackjack.Tests.csproj", "{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.TicTacToe.Tests", "Game.TicTacToe.Tests\Game.TicTacToe.Tests.csproj", "{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -82,6 +86,31 @@ Global
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x64.Build.0 = Release|Any CPU
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x86.ActiveCfg = Release|Any CPU
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x64.ActiveCfg = Debug|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x64.Build.0 = Debug|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x86.ActiveCfg = Debug|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x86.Build.0 = Debug|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|Any CPU.Build.0 = Release|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x64.ActiveCfg = Release|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x64.Build.0 = Release|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x86.ActiveCfg = Release|Any CPU
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x86.Build.0 = Release|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x64.ActiveCfg = Debug|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x64.Build.0 = Debug|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x86.ActiveCfg = Debug|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x86.Build.0 = Debug|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|Any CPU.Build.0 = Release|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x64.ActiveCfg = Release|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x64.Build.0 = Release|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x86.ActiveCfg = Release|Any CPU
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

7
docs/testing-games.md Normal file
View File

@ -0,0 +1,7 @@
# Testing games
1. Games are dlls. Use a standalone test project to test a game.
2. Create baseline game snapshots in textual format.
3. Create baseline logs via the r3 observable api.
4. Read the snapshot/logs manually to identify issues.
5. Make sure serialization roundtrip works.