Repository navigation
Add undo/redo, move history and ranked-mode undo rules to the game engine - #488
Merged
Mkalbani merged 22 commits intoOct 20, 2026
Merged
Conversation
…pths Introduces the move history model: the ordered action log, the state snapshots, the game modes and the per-puzzle-type undo depth defaults. The defaults live here so the Nest configuration and the in-memory puzzle implementations share a single source of truth.
Adds an undoRedo section to the game engine configuration: a master switch, the per-puzzle-type undo depth limits, the modes in which undo is disabled or penalised, and the penalty ratios. Every value is environment driven.
Tracks the move history of a puzzle session and implements undo/redo on top of it. - the history is a list of state snapshots plus a cursor, so undo and redo restore the exact prior state rather than replaying moves - a new move discards the redo branch - undo depth is limited per puzzle type, which also bounds the memory held - ranked modes have undo disabled, penalised modes charge a score penalty - the history converts into replay actions for the replay system
Provides and exports MoveHistoryService from the game engine module.
A redo is a first-class replay action, just like an undo.
Keeps the DTO in step with the entity's action types.
Adds recordActions so a whole move history can be imported into a replay in one go.
Wires the move history into the puzzle engine. - creating a puzzle starts a move history session with the puzzle type and the session mode, so the per-type undo depth applies from the first move - makeMove records the move and persists the resulting state - undoMove and redoMove restore the exact prior state and persist it - resetPuzzle clears the history
The session mode and puzzle type are fixed when the puzzle is created, so they are deliberately not accepted on an undo request - otherwise a client could downgrade a ranked session to dodge the undo restrictions.
Adds three endpoints: - POST /puzzles/:puzzleId/player/:playerId/undo - POST /puzzles/:puzzleId/player/:playerId/redo - GET /puzzles/:puzzleId/player/:playerId/move-history The undo and redo endpoints accept an optional replayId and mirror the action onto that replay, so the replay shows what the player actually did.
The mode decides whether undo is free, penalised or disabled, so it is fixed when the puzzle is created.
The in-memory undo stack used a hardcoded limit of 50 steps. It now uses the same per-puzzle-type depth as the session move history, so complex puzzles keep a shallower history.
Ranked sessions have undo disabled, so the mode has to be expressible in the session configuration.
…rics Adds the number of undos and redos to the performance metrics, together with the mode the session was played in.
Undo is free in practice and zen sessions, but competitive modes charge a ratio of the base score for every undo, in the same way hints do. The penalty is reported in the score breakdown.
…ode rules Covers the happy path, the per-puzzle-type undo depth, the ranked mode restrictions, the penalised modes, the redo branch and the replay conversion.
Covers the responses, the replay mirroring and the propagation of a rejected undo.
Covers recording a batch of actions in order and the undo counter on the replay.
…egration spec The testing module never provided EnergyService, so its dependency graph could not be resolved and the spec could not compile. It also now provides MoveHistoryService.
Explains how the move history works, the undo depth per puzzle type, the ranked mode rules, the endpoints and the configuration. Closes MindFlowInteractive#461
Adds the game engine undo/redo settings to the environment template.
|
@akargi Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Contributor
|
@akargi please resolve conflict |
Contributor
Author
|
@Mkalbani Tafaddal |
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #461 — Undo/Redo and Move History for the game engine.
Every puzzle session now keeps an ordered move history: the moves the player made, the state before and after each of them, and any undo/redo actions in between. That history is the single source of truth for undo/redo, for replays, and for analytics.
Move history structure
New
src/game-engine/types/move-history.types.tsdefines the model:MoveHistoryEntry— one action (move / undo / redo / reset) with the move, the state before, the state after, and the running undo/redo counters and score penaltyMoveHistory— the ordered action log, the retained state snapshots, the cursor into them, and the effective undo depthUndoRedoPolicy— the rules that apply to a sessionGameMode—practice,challenge,timed,zen,rankedDEFAULT_UNDO_DEPTH_BY_PUZZLE_TYPE— the shared per-type depth defaultsHow undo and redo work
The history is a list of state snapshots with a cursor into it:
Because the snapshot is returned as-is, undo and redo restore the exact prior state rather than replaying moves and hoping for the same result.
MoveHistoryService(src/game-engine/services/move-history.service.ts) owns this, plus the rules below.Undo depth per puzzle type
Complex puzzles hold large state snapshots, so they keep a shallower history:
logic_gridsequencespatialmathematicalpattern_matchingword_puzzlecustomThe retained snapshot buffer is trimmed to
maxUndoDepth + 1, so the limit bounds both how far a player can go back and the memory held per session.BasePuzzle's in-memory undo stack previously used a hardcoded 50 steps and now uses the same per-type depth, so the two layers agree.All depths are environment driven (
GAME_MAX_UNDO_DEPTH_*, see.env.example).Ranked modes respect the undo rules
practicezenchallengetimedrankedThe penalty is a ratio of the base score, applied by
ScoringServicein exactly the same way the hint penalty is, and reported in the score breakdown.The mode and puzzle type are fixed when the puzzle is created and are locked once moves exist — a client cannot downgrade a ranked session to dodge the restrictions. The undo/redo endpoints deliberately do not accept a
mode.Endpoints
POST/puzzles/:puzzleId/player/:playerId/undoPOST/puzzles/:puzzleId/player/:playerId/redoGET/puzzles/:puzzleId/player/:playerId/move-historyEach response includes the restored state, the history entry, the score penalty charged, and the effective policy.
Move history feeds the replay system
MoveHistoryService.toReplayActions()converts the history into replay actions (MOVE/UNDO/REDO) with relative timestamps. The shape is structurally compatible with the replay module's action DTO, so neither module has to depend on the other.ReplayService.recordActions()imports a whole history into a replay in one go.replayIdand mirror the action onto that replay, so the replay shows exactly what the player did — including their undos.GET /move-history?includeReplayActions=truereturns the derived actions ready to import.REDOwas added to the replay action type (the column is alreadyvarchar(50), so no migration is needed).Tests
src/game-engine/services/move-history.service.spec.ts— the happy path, exact-state restoration, repeated undo/redo, the redo branch being discarded, the per-type depth limit, the retained-buffer bound, ranked-mode rejection, penalised vs free modes, the policy report and the replay conversion.src/game-engine/controllers/move-history.controller.spec.ts— the three endpoints, the replay mirroring, and the propagation of a rejected undo.src/replay/tests/replay.service.spec.ts— bulk action recording and the replay's undo counter.Also fixed
src/game-engine/tests/puzzle-engine.integration.spec.tsnever providedEnergyService, so its dependency graph could not be resolved and the spec could not compile. It now provides it (andMoveHistoryService).Notes
jsonbcolumns.npm install,npm testand CI were not run in this environment — please runnpm run verify(lint + format + typecheck + test) locally.npm run lint:checkandnpm run format:checkare already failing onmainfor unrelated files and arecontinue-on-errorin CI; every file touched here is prettier-clean and the new code introduces no new lint findings.Closes #461