From 24b9c3eac2a58706804561e0f78d8d6c39ccc0a6 Mon Sep 17 00:00:00 2001 From: Konrad Jamrozik Date: Sat, 22 Jun 2024 17:17:34 -0700 Subject: [PATCH] get rid of NextEventId altogether --- src/api/ApplyPlayerActionRoute.cs | 3 -- .../Controller/GameSessionController.cs | 8 +--- src/game-lib/Events/EventIdGen.cs | 39 ++----------------- src/game-lib/Lib/IdGen.cs | 1 + src/game-lib/State/GameSessionTurn.cs | 26 +++---------- web/src/lib/codesync/GameSessionTurn.ts | 14 ------- web/src/lib/gameSession/GameSessionData.ts | 38 ++++++------------ web/src/main.tsx | 3 -- web/test/lib_tests/compression.test.ts | 2 +- 9 files changed, 23 insertions(+), 111 deletions(-) diff --git a/src/api/ApplyPlayerActionRoute.cs b/src/api/ApplyPlayerActionRoute.cs index 7f8ae62b..379fa9a4 100644 --- a/src/api/ApplyPlayerActionRoute.cs +++ b/src/api/ApplyPlayerActionRoute.cs @@ -50,9 +50,6 @@ private static JsonHttpResult 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 result = ApiUtils.ToJsonHttpResult(gameSession.CurrentTurn); return result; diff --git a/src/game-lib/Controller/GameSessionController.cs b/src/game-lib/Controller/GameSessionController.cs index da5de7fa..e2bc82e2 100644 --- a/src/game-lib/Controller/GameSessionController.cs +++ b/src/game-lib/Controller/GameSessionController.cs @@ -152,12 +152,7 @@ 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 worldEvents, GameState nextTurnStartState) @@ -165,8 +160,7 @@ private void NewTurn(List worldEvents, GameState nextTurnStartState) GameSession.Turns.Add( new GameSessionTurn( eventsUntilStartState: worldEvents, - startState: nextTurnStartState, - nextEventId: GameSession.EventIdGen.Value)); + startState: nextTurnStartState)); } public (PlayerActionEvent advaceTimeEvent, List worldEvents) AdvanceTime(GameState? state = null) diff --git a/src/game-lib/Events/EventIdGen.cs b/src/game-lib/Events/EventIdGen.cs index 48402f18..f8648eb3 100644 --- a/src/game-lib/Events/EventIdGen.cs +++ b/src/game-lib/Events/EventIdGen.cs @@ -8,41 +8,8 @@ public class EventIdGen : IdGen { public EventIdGen(List turns) { - (int? idFromLastEvent, int? isFromLastTurn) = AssertInvariants(turns); - NextId = idFromLastEvent ?? isFromLastTurn ?? 0; - } - - public (int? idFromLastEvent, int? isFromLastTurn) AssertInvariants(List 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 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; } } diff --git a/src/game-lib/Lib/IdGen.cs b/src/game-lib/Lib/IdGen.cs index 7bbd16b4..9b6fe3e2 100644 --- a/src/game-lib/Lib/IdGen.cs +++ b/src/game-lib/Lib/IdGen.cs @@ -11,6 +11,7 @@ public class IdGen public int Value => NextId; + // kja need to assert AssertConsecutiveIds across entire game session public static void AssertConsecutiveIds(List instances) where T: IIdentifiable { if (instances.Any()) diff --git a/src/game-lib/State/GameSessionTurn.cs b/src/game-lib/State/GameSessionTurn.cs index 246f7463..43aa03b5 100644 --- a/src/game-lib/State/GameSessionTurn.cs +++ b/src/game-lib/State/GameSessionTurn.cs @@ -17,36 +17,20 @@ public class GameSessionTurn public PlayerActionEvent? AdvanceTimeEvent; - /// - /// 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. - /// - 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(GameState.StateJsonSerializerOptions)); + // Note: at least one param must be mandatory; see docs/serialization.md for details. GameState startState, List? eventsUntilStartState = null, List? eventsInTurn = null, GameState? endState = null, - PlayerActionEvent? advanceTimeEvent = null, - int? nextEventId = null) + PlayerActionEvent? advanceTimeEvent = null) { - EventsUntilStartState = eventsUntilStartState ?? new List(); + EventsUntilStartState = eventsUntilStartState ?? [new WorldEvent(0, GameEventName.ReportEvent, [0, 0])]; StartState = startState; EventsInTurn = eventsInTurn ?? new List(); 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(); } @@ -66,6 +50,7 @@ public void AssertInvariants() "Number of events in turn must match the number of updates between the game states."); IReadOnlyList events = GameEvents; + Contract.Assert(events.First().Type == GameEventName.ReportEvent); IdGen.AssertConsecutiveIds(events.ToList()); StartState.AssertInvariants(); EndState.AssertInvariants(); @@ -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() ); } diff --git a/web/src/lib/codesync/GameSessionTurn.ts b/web/src/lib/codesync/GameSessionTurn.ts index 80563dd4..3c4a0f2e 100644 --- a/web/src/lib/codesync/GameSessionTurn.ts +++ b/web/src/lib/codesync/GameSessionTurn.ts @@ -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[] { @@ -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, } } diff --git a/web/src/lib/gameSession/GameSessionData.ts b/web/src/lib/gameSession/GameSessionData.ts index cafab464..527c1b34 100644 --- a/web/src/lib/gameSession/GameSessionData.ts +++ b/web/src/lib/gameSession/GameSessionData.ts @@ -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( @@ -58,7 +54,15 @@ 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 === @@ -66,12 +70,12 @@ export class GameSessionData { ) ) { 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 ( @@ -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}`, ) } } @@ -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 */ } diff --git a/web/src/main.tsx b/web/src/main.tsx index 37e3d093..ddc63bec 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -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 diff --git a/web/test/lib_tests/compression.test.ts b/web/test/lib_tests/compression.test.ts index 30bea6c7..a0aeaaae 100644 --- a/web/test/lib_tests/compression.test.ts +++ b/web/test/lib_tests/compression.test.ts @@ -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