Skip to content

Commit

Permalink
get rid of NextEventId altogether
Browse files Browse the repository at this point in the history
  • Loading branch information
Konrad Jamrozik committed Jun 23, 2024
1 parent 3cb893e commit 24b9c3e
Show file tree
Hide file tree
Showing 9 changed files with 23 additions and 111 deletions.
3 changes: 0 additions & 3 deletions src/api/ApplyPlayerActionRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ private static JsonHttpResult<GameSessionTurn> ApplyPlayerActionInternal(
Contract.Assert(playerActionPayload.ActionName != GameEventName.AdvanceTimePlayerAction);

gameSession.CurrentPlayerActionEvents.Add(playerActionPayload.Apply(controller));
// See analogous line in UfoGameLib.Controller.GameSessionController.PlayGameUntilOver
// for explanation why this is needed.
gameSession.CurrentTurn.NextEventId = gameSession.EventIdGen.Value;

JsonHttpResult<GameSessionTurn> result = ApiUtils.ToJsonHttpResult(gameSession.CurrentTurn);
return result;
Expand Down
8 changes: 1 addition & 7 deletions src/game-lib/Controller/GameSessionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,21 +152,15 @@ private void PlayGameUntilOver(IPlayer player, int turnLimit)

GameSession.CurrentTurn.AssertInvariants();
NewTurn(worldEvents, nextTurnStartState);
GameSession.EventIdGen.AssertInvariants(GameSession.Turns);
}

// Ensure NextEventId is set to right value in case we end up in special case of no events -
// see comment on NextEventId for details.
GameSession.CurrentTurn.NextEventId = GameSession.EventIdGen.Value;
}

private void NewTurn(List<WorldEvent> worldEvents, GameState nextTurnStartState)
{
GameSession.Turns.Add(
new GameSessionTurn(
eventsUntilStartState: worldEvents,
startState: nextTurnStartState,
nextEventId: GameSession.EventIdGen.Value));
startState: nextTurnStartState));
}

public (PlayerActionEvent advaceTimeEvent, List<WorldEvent> worldEvents) AdvanceTime(GameState? state = null)
Expand Down
39 changes: 3 additions & 36 deletions src/game-lib/Events/EventIdGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,8 @@ public class EventIdGen : IdGen
{
public EventIdGen(List<GameSessionTurn> turns)
{
(int? idFromLastEvent, int? isFromLastTurn) = AssertInvariants(turns);
NextId = idFromLastEvent ?? isFromLastTurn ?? 0;
}

public (int? idFromLastEvent, int? isFromLastTurn) AssertInvariants(List<GameSessionTurn> turns)
{
Contract.Assert(turns.Any());
(int? idFromLastEvent, int? isFromLastTurn) = NextEventIdFromLastEvent(turns);
Contract.Assert(
idFromLastEvent is null || isFromLastTurn is null ||
idFromLastEvent == isFromLastTurn,
$"nextEventIdFromLastEvent: {idFromLastEvent}, nextEventIdFromLastTurn: {isFromLastTurn}");
return (idFromLastEvent, isFromLastTurn);
}

private static (int? idFromLastEvent, int? isFromLastTurn) NextEventIdFromLastEvent(
List<GameSessionTurn> turns)
{
// To compute starting next event ID we first need to determine if there are any events in the input
// game session turns. If yes, we consider as the starting next event ID to be (last event ID + 1).
// If not, we consider the last turn NextEventId value, if set.
// Otherwise, we assume no events, meaning NextEventId is equal to zero.
//
// Notably we must rely on last turn NextEventId when calling REST API to advance turn
// from a turn that has no events. This may happen e.g. when advancing from turn 1 to 2
// when player action made no actions.
// In such case:
// - There will be no "before turn" world events, as this is the first turn.
// - There will be no events in the turn, as the player did nothing.
// - There will be no "advance turn" event, as the player is advancing turn just right now.
GameEvent? lastGameEvent = turns.SelectMany(turn => turn.GameEvents).LastOrDefault();
// ReSharper disable once MergeConditionalExpression
// Disabled because it is not equivalent. Currently null will return null but after merging null would return 1.
int? idFromLastEvent = lastGameEvent != null ? lastGameEvent.Id + 1 : null;
int? isFromLastTurn = turns.Last().NextEventId;
return (idFromLastEvent, isFromLastTurn);
GameEvent lastGameEvent = turns.SelectMany(turn => turn.GameEvents).Last();
int idFromLastEvent = lastGameEvent.Id + 1;
NextId = idFromLastEvent;
}
}
1 change: 1 addition & 0 deletions src/game-lib/Lib/IdGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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
{
if (instances.Any())
Expand Down
26 changes: 5 additions & 21 deletions src/game-lib/State/GameSessionTurn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,20 @@ public class GameSessionTurn

public PlayerActionEvent? AdvanceTimeEvent;

/// <summary>
/// ID of the next event to generate. This is required when:
/// - This is not the first turn in game session, and there was at least one event in previous turns.
/// - This turn has no events.
/// - This turn and this turn only is used as a starting point to continue evaluating game session.
/// Must be ignored if this game session turn has at least one game event in it.
/// See EventIdGen for more details.
/// </summary>
public int? NextEventId;

[JsonConstructor]
public GameSessionTurn(
// Note: at least one param must be mandatory; otherwise JSON deserialization would implicitly call ctor with no args, circumventing validation.
// Specifically I mean this line in ApiUtils.ParseGameSessionTurn
// parsedTurn = (requestBody.FromJsonTo<GameSessionTurn>(GameState.StateJsonSerializerOptions));
// Note: at least one param must be mandatory; see docs/serialization.md for details.
GameState startState,
List<WorldEvent>? eventsUntilStartState = null,
List<PlayerActionEvent>? eventsInTurn = null,
GameState? endState = null,
PlayerActionEvent? advanceTimeEvent = null,
int? nextEventId = null)
PlayerActionEvent? advanceTimeEvent = null)
{
EventsUntilStartState = eventsUntilStartState ?? new List<WorldEvent>();
EventsUntilStartState = eventsUntilStartState ?? [new WorldEvent(0, GameEventName.ReportEvent, [0, 0])];
StartState = startState;
EventsInTurn = eventsInTurn ?? new List<PlayerActionEvent>();
EndState = endState ?? StartState.Clone();
AdvanceTimeEvent = advanceTimeEvent;
// kja make GameSessionTurn.NextEventId be non-nullable. If not provided, derive from events in turn. If none, assume 0.
// Then update frontend to simplify its handling, e.g. removeAdvanceTimeEvent
NextEventId = nextEventId ?? 0;

AssertInvariants();
}
Expand All @@ -66,6 +50,7 @@ public void AssertInvariants()
"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());
StartState.AssertInvariants();
EndState.AssertInvariants();
Expand All @@ -90,7 +75,6 @@ private GameSessionTurn DeepClone()
EventsUntilStartState.Select(gameEvent => gameEvent.Clone()).ToList(),
EventsInTurn.Select(gameEvent => gameEvent.Clone()).ToList(),
EndState.Clone(),
AdvanceTimeEvent?.Clone(),
NextEventId
AdvanceTimeEvent?.Clone()
);
}
14 changes: 0 additions & 14 deletions web/src/lib/codesync/GameSessionTurn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export type GameSessionTurn = {
readonly EventsInTurn: PlayerActionEvent[]
readonly EndState: GameState
readonly AdvanceTimeEvent?: PlayerActionEvent | undefined
readonly NextEventId?: number | undefined
}

export function getEvents(turn: GameSessionTurn): GameEvent[] {
Expand Down Expand Up @@ -45,28 +44,15 @@ export function removeAdvanceTimeEvent(turn: GameSessionTurn): GameSessionTurn {
return {
...turn,
AdvanceTimeEvent: undefined,
// using isNil, because it will be null, not undefined, if the turn is the latest turn
// received from backend.
NextEventId: !_.isNil(turn.AdvanceTimeEvent)
? turn.AdvanceTimeEvent.Id
: turn.NextEventId,
}
}

export function resetTurn(turn: GameSessionTurn): GameSessionTurn {
const eventsRemoved =
turn.EventsInTurn.length + (!_.isNil(turn.AdvanceTimeEvent) ? 1 : 0)
const newNextEventId =
eventsRemoved === 0 ? turn.NextEventId : turn.NextEventId! - eventsRemoved
console.log(
`eventsRemoved: ${eventsRemoved}, turn.NextEventId: ${turn.NextEventId}, newNextEventId: ${newNextEventId}`,
)
return {
EventsUntilStartState: turn.EventsUntilStartState,
StartState: turn.StartState,
EventsInTurn: [],
EndState: turn.StartState,
AdvanceTimeEvent: turn.AdvanceTimeEvent,
NextEventId: newNextEventId,
}
}
38 changes: 12 additions & 26 deletions web/src/lib/gameSession/GameSessionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ export class GameSessionData {
throw new Error(`First turn must be initialTurn of ${initialTurn}`)
}

if (turns.at(0)!.EventsUntilStartState.length > 0) {
throw new Error('EventsUntilStartState must be empty for first turn')
}

for (const turn of turns.slice(0, -1)) {
if (_.isUndefined(turn.AdvanceTimeEvent)) {
throw new TypeError(
Expand All @@ -58,20 +54,28 @@ export class GameSessionData {
}
}

for (const turn of turns) {
for (const [index, turn] of turns.entries()) {
if (
_.isEmpty(turn.EventsUntilStartState) ||
turn.EventsUntilStartState.at(0)?.Type !== 'ReportEvent'
) {
throw new Error(
`First event of any game turn must be ReportEvent. Turn index: ${index}`,
)
}
if (
!(
turn.StartState.Timeline.CurrentTurn ===
turn.EndState.Timeline.CurrentTurn
)
) {
throw new Error(
'All game states in any given turn must have the same turn number',
`All game states in any given turn must have the same turn number. Turn index: ${index}`,
)
}
if (!(turn.EndState.UpdateCount >= turn.StartState.UpdateCount)) {
throw new Error(
'End state must have same or more updates than start state.',
`End state must have same or more updates than start state. Turn index: ${index}`,
)
}
if (
Expand All @@ -81,7 +85,7 @@ export class GameSessionData {
)
) {
throw new Error(
'The number of events in turn must match the number of update count between start and end state.',
`The number of events in turn must match the number of update count between start and end state. Turn index: ${index}`,
)
}
}
Expand Down Expand Up @@ -109,24 +113,6 @@ export class GameSessionData {
}
}
}

const lastGameEvent: GameEvent | undefined = gameEvents.at(-1)
const idFromLastEvent: number | undefined = !_.isUndefined(lastGameEvent)
? lastGameEvent.Id + 1
: undefined
const idFromLastTurn: number | undefined = turns.at(-1)!.NextEventId
if (
!(
_.isUndefined(idFromLastEvent) ||
_.isUndefined(idFromLastTurn) ||
idFromLastEvent === idFromLastTurn
)
) {
throw new Error(
`idFromLastEvent must be equal to idFromLastTurn. But ${idFromLastEvent} != ${idFromLastTurn}`,
)
}

/* c8 ignore stop */
}

Expand Down
3 changes: 0 additions & 3 deletions web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ if (rootElement) {
)
}

// kja got Error: "nextEventIdFromLastEvent: 299, nextEventIdFromLastTurn: 297"
// when reverted turn so that mission is "Active" and then clicked next turn

// kja: when local storage `quota exceeded` is reported, then "Reset game" button is disabled. Why?

// kja need to add support to the new backend stuff like displaying all the mission modifiers and results
2 changes: 1 addition & 1 deletion web/test/lib_tests/compression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const gzip = promisify(zlib.gzip)
const gunzip = promisify(zlib.gunzip)

const inputDataStr =
'{"turns":[{"EventsUntilStartState":[],"StartState":{"IsGameOver":false,"IsGameLost":false,"IsGameWon":false,"Timeline":{"CurrentTurn":1},"Assets":{"Money":500,"Intel":0,"Funding":20,"Support":30,"CurrentTransportCapacity":4,"MaxTransportCapacity":4,"Agents":[]},"MissionSites":[],"Missions":[],"TerminatedAgents":[],"Factions":[{"Id":0,"Name":"Black Lotus cult","Power":200,"MissionSiteCountdown":3,"PowerIncrease":4,"PowerAcceleration":8,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":1,"Name":"Red Dawn remnants","Power":300,"MissionSiteCountdown":3,"PowerIncrease":5,"PowerAcceleration":5,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":2,"Name":"EXALT","Power":400,"MissionSiteCountdown":9,"PowerIncrease":6,"PowerAcceleration":4,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":3,"Name":"Zombies","Power":100,"MissionSiteCountdown":6,"PowerIncrease":1,"PowerAcceleration":20,"AccumulatedPowerAcceleration":0,"IntelInvested":0}],"UpdateCount":0},"EventsInTurn":[],"EndState":{"IsGameOver":false,"IsGameLost":false,"IsGameWon":false,"Timeline":{"CurrentTurn":1},"Assets":{"Money":500,"Intel":0,"Funding":20,"Support":30,"CurrentTransportCapacity":4,"MaxTransportCapacity":4,"Agents":[]},"MissionSites":[],"Missions":[],"TerminatedAgents":[],"Factions":[{"Id":0,"Name":"Black Lotus cult","Power":200,"MissionSiteCountdown":3,"PowerIncrease":4,"PowerAcceleration":8,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":1,"Name":"Red Dawn remnants","Power":300,"MissionSiteCountdown":3,"PowerIncrease":5,"PowerAcceleration":5,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":2,"Name":"EXALT","Power":400,"MissionSiteCountdown":9,"PowerIncrease":6,"PowerAcceleration":4,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":3,"Name":"Zombies","Power":100,"MissionSiteCountdown":6,"PowerIncrease":1,"PowerAcceleration":20,"AccumulatedPowerAcceleration":0,"IntelInvested":0}],"UpdateCount":0},"AdvanceTimeEvent":null,"NextEventId":0}]}'
'{"turns":[{"EventsUntilStartState":[],"StartState":{"IsGameOver":false,"IsGameLost":false,"IsGameWon":false,"Timeline":{"CurrentTurn":1},"Assets":{"Money":500,"Intel":0,"Funding":20,"Support":30,"CurrentTransportCapacity":4,"MaxTransportCapacity":4,"Agents":[]},"MissionSites":[],"Missions":[],"TerminatedAgents":[],"Factions":[{"Id":0,"Name":"Black Lotus cult","Power":200,"MissionSiteCountdown":3,"PowerIncrease":4,"PowerAcceleration":8,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":1,"Name":"Red Dawn remnants","Power":300,"MissionSiteCountdown":3,"PowerIncrease":5,"PowerAcceleration":5,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":2,"Name":"EXALT","Power":400,"MissionSiteCountdown":9,"PowerIncrease":6,"PowerAcceleration":4,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":3,"Name":"Zombies","Power":100,"MissionSiteCountdown":6,"PowerIncrease":1,"PowerAcceleration":20,"AccumulatedPowerAcceleration":0,"IntelInvested":0}],"UpdateCount":0},"EventsInTurn":[],"EndState":{"IsGameOver":false,"IsGameLost":false,"IsGameWon":false,"Timeline":{"CurrentTurn":1},"Assets":{"Money":500,"Intel":0,"Funding":20,"Support":30,"CurrentTransportCapacity":4,"MaxTransportCapacity":4,"Agents":[]},"MissionSites":[],"Missions":[],"TerminatedAgents":[],"Factions":[{"Id":0,"Name":"Black Lotus cult","Power":200,"MissionSiteCountdown":3,"PowerIncrease":4,"PowerAcceleration":8,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":1,"Name":"Red Dawn remnants","Power":300,"MissionSiteCountdown":3,"PowerIncrease":5,"PowerAcceleration":5,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":2,"Name":"EXALT","Power":400,"MissionSiteCountdown":9,"PowerIncrease":6,"PowerAcceleration":4,"AccumulatedPowerAcceleration":0,"IntelInvested":0},{"Id":3,"Name":"Zombies","Power":100,"MissionSiteCountdown":6,"PowerIncrease":1,"PowerAcceleration":20,"AccumulatedPowerAcceleration":0,"IntelInvested":0}],"UpdateCount":0},"AdvanceTimeEvent":null}]}'

describe('compression tests', () => {
// eslint-disable-next-line max-statements
Expand Down

0 comments on commit 24b9c3e

Please sign in to comment.