Skip to content

Commit

Permalink
Backend: add assertion over consecutive event IDs across entire game …
Browse files Browse the repository at this point in the history
…session; fix game state loading
  • Loading branch information
Konrad Jamrozik committed Jun 23, 2024
1 parent 24b9c3e commit bf56a8f
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 23 deletions.
31 changes: 23 additions & 8 deletions src/game-lib-tests/GameSessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public void Setup()
// - This, coupled with the smart player simulations, ensures test failure on invariant violation.

[Test]
public void BasicHappyPathGameSessionWorks()
public void BasicHappyPath()
{
var session = new GameSession(_randomGen, factions: _factions);
var controller = new GameSessionController(_config, _log, session);
Expand Down Expand Up @@ -102,6 +102,20 @@ public void BasicHappyPathGameSessionWorks()
});
}

[Test]
public void BasicSaveAndLoadOfGameState()
{
var session = new GameSession(_randomGen);
var controller = new GameSessionController(_config, _log, session);

// Act 1: Save game, thus saving initial game state to file
controller.SaveCurrentGameStateToFile();

// Act 2: Load the saved state.
controller.LoadCurrentGameStateFromFile();

// Assert: no exception was thrown.
}

/// <summary>
/// Given:
Expand All @@ -122,7 +136,7 @@ public void LoadingPreviousGameStateOverridesCurrentState()

GameStatePlayerView stateView = controller.CurrentGameStatePlayerView;
int savedTurn = stateView.CurrentTurn;
GameState initialGameState = session.CurrentGameState.Clone();
GameState savedGameState = session.CurrentGameState.Clone();

// Act 1: Save game, thus saving initialGameState to file
controller.SaveCurrentGameStateToFile();
Expand All @@ -141,11 +155,12 @@ public void LoadingPreviousGameStateOverridesCurrentState()
// Assert that after loading, the state view continues to reference current state.
Assert.That(stateView.StateReferenceEquals(controller.CurrentGameStatePlayerView));

// Assert that session.CurrentGameState has been updated to be loadedGameState.
Assert.That(loadedGameState, Is.EqualTo(session.CurrentGameState));
Assert.That(loadedGameState, Is.EqualTo(initialGameState));

Assert.That(stateView.CurrentTurn, Is.EqualTo(savedTurn), "savedTurn");

// Assert that after loading, the loadedGameState is the same as at save time, i.e. savedGameState.
Assert.That(loadedGameState, Is.EqualTo(savedGameState));
Assert.That(stateView.CurrentTurn, Is.EqualTo(savedTurn), "savedTurn");
}

/// <summary>
Expand All @@ -160,7 +175,7 @@ public void LoadingPreviousGameStateOverridesCurrentState()
/// - and some properties have values exactly as expected.
/// </summary>
[Test]
public void RoundTrippingSavingAndLoadingGameStateBehavesCorrectly()
public void SaveAndLoadOfModifiedGameState()
{
var session = new GameSession(_randomGen, factions: _factions);
var controller = new GameSessionController(_config, _log, session);
Expand Down Expand Up @@ -223,11 +238,11 @@ public void RoundTrippingSavingAndLoadingGameStateBehavesCorrectly()
}

/// <summary>
/// This test is like RoundTrippingSavingAndLoadingGameStateBehavesCorrectly
/// This test is like SaveAndLoadOfModifiedGameState
/// but when the game is saved there is an active mission.
/// </summary>
[Test]
public void RoundTrippingSavingAndLoadingGameStateWithActiveMissionBehavesCorrectly()
public void SaveAndLoadOfModifiedGameStateWithActiveMission()
{
var session = new GameSession(_randomGen, factions: _factions);
var controller = new GameSessionController(_config, _log, session);
Expand Down
4 changes: 2 additions & 2 deletions src/game-lib/Controller/GameSessionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ private void PlayGameUntilOver(IPlayer player, int turnLimit)

private void NewTurn(List<WorldEvent> worldEvents, GameState nextTurnStartState)
{
GameSession.Turns.Add(
GameSession.AddTurn(
new GameSessionTurn(
eventsUntilStartState: worldEvents,
startState: nextTurnStartState));
Expand All @@ -176,7 +176,7 @@ public GameState SaveCurrentGameStateToFile()
public GameState LoadCurrentGameStateFromFile()
{
GameState loadedGameState = GameState.FromJsonFile(_config.SaveFile);
GameSession.Turns.Add(new GameSessionTurn(startState: loadedGameState));
GameSession.ReplaceCurrentTurnWithState(loadedGameState);
_log.Info($"Loaded game state from {_config.SaveFile.FullPath}");
return GameSession.CurrentGameState;
}
Expand Down
6 changes: 4 additions & 2 deletions src/game-lib/Lib/IdGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ public class IdGen
public int Value => NextId;

// kja need to assert AssertConsecutiveIds across entire game session
public static void AssertConsecutiveIds<T>(List<T> instances) where T: IIdentifiable
public static void AssertConsecutiveIds<T>(List<T> instances, int? expectedFirstId = null) where T: IIdentifiable
{
if (instances.Any())
{
int firstId = instances[0].Id;
if (expectedFirstId != null)
Contract.Assert(firstId == expectedFirstId);
for (int i = 0; i < instances.Count; i++)
{
int expectedId = firstId + i;
Contract.Assert(
instances[i].Id == expectedId,
$"Instance with id {instances[i].Id} is not equal to expected {expectedId}.");
$"Expected consecutive ID of {expectedId} but got {instances[i].Id}.");
}
}
}
Expand Down
30 changes: 27 additions & 3 deletions src/game-lib/State/GameSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,41 @@ public GameSession(IRandomGen randomGen, List<GameSessionTurn>? turns = null, Fa
{
RandomGen = randomGen;
Turns = turns ?? [new GameSessionTurn(startState: GameState.NewInitialGameState(randomGen, factions))];
Contract.Assert(Turns.Any());
Turns.ForEach(turn => turn.AssertInvariants());

EventIdGen = new EventIdGen(Turns);
AgentIdGen = new AgentIdGen(Turns);
MissionSiteIdGen = new MissionSiteIdGen(Turns);
MissionIdGen = new MissionIdGen(Turns);
AssertInvariants();
}

public IReadOnlyList<GameState> GameStates
=> Turns.SelectMany<GameSessionTurn, GameState>(turn => [turn.StartState, turn.EndState])
.ToList()
.AsReadOnly();

public void AssertInvariants()
{
Contract.Assert(Turns.Any());
Turns.ForEach(turn => turn.AssertInvariants());
List<GameEvent> gameEvents = Turns.SelectMany(turn => turn.GameEvents).ToList();
List<GameState> gameStates = Turns.SelectMany(turn => turn.GameStates).ToList();
IdGen.AssertConsecutiveIds(gameEvents);
gameStates.ForEach(gs => gs.AssertInvariants());
}

public void AddTurn(GameSessionTurn turn)
{
Turns.Add(turn);
AssertInvariants();
}

public void ReplaceCurrentTurnWithState(GameState gs)
{
var currentTurn = Turns.Last();
Turns.RemoveAt(Turns.Count - 1);
AddTurn(
new GameSessionTurn(
eventsUntilStartState: currentTurn.EventsUntilStartState,
startState: gs));
}
}
15 changes: 9 additions & 6 deletions src/game-lib/State/GameSessionTurn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public void AssertInvariants()
{
Contract.Assert(
StartState.Timeline.CurrentTurn == EndState.Timeline.CurrentTurn,
"Both game states in the turn must denote the same turn.");
$"Both game states in the turn must denote the same turn. " +
$"StartState turn: {StartState.Timeline.CurrentTurn}, EndState turn: {EndState.Timeline.CurrentTurn}");

Contract.Assert(
EndState.UpdateCount >= StartState.UpdateCount,
Expand All @@ -49,9 +50,8 @@ public void AssertInvariants()
EventsInTurn.Count == EndState.UpdateCount - StartState.UpdateCount,
"Number of events in turn must match the number of updates between the game states.");

IReadOnlyList<GameEvent> events = GameEvents;
Contract.Assert(events.First().Type == GameEventName.ReportEvent);
IdGen.AssertConsecutiveIds(events.ToList());
Contract.Assert(EventsUntilStartState.Last().Type == GameEventName.ReportEvent);
IdGen.AssertConsecutiveIds(GameEvents.ToList());
StartState.AssertInvariants();
EndState.AssertInvariants();
}
Expand All @@ -63,8 +63,11 @@ public IReadOnlyList<GameEvent> GameEvents
..EventsUntilStartState,
..EventsInTurn,
..(AdvanceTimeEvent != null ? (List<GameEvent>) [AdvanceTimeEvent] : [])
])
.ToList().AsReadOnly();
]).AsReadOnly();

[JsonIgnore]
public IReadOnlyList<GameState> GameStates
=> ((List<GameState>)[StartState, EndState]).AsReadOnly();

public GameSessionTurn Clone()
=> DeepClone();
Expand Down
4 changes: 2 additions & 2 deletions web/src/lib/gameSession/GameSessionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ export class GameSessionData {
for (const [index, turn] of turns.entries()) {
if (
_.isEmpty(turn.EventsUntilStartState) ||
turn.EventsUntilStartState.at(0)?.Type !== 'ReportEvent'
turn.EventsUntilStartState.at(-1)?.Type !== 'ReportEvent'
) {
throw new Error(
`First event of any game turn must be ReportEvent. Turn index: ${index}`,
`Last world event of any game turn must be ReportEvent. Turn index: ${index}`,
)
}
if (
Expand Down

0 comments on commit bf56a8f

Please sign in to comment.