Skip to content

Add undo/redo, move history and ranked-mode undo rules to the game engine - #488

Merged
Mkalbani merged 22 commits into
MindFlowInteractive:mainfrom
akargi:feature/undo-redo-move-history
Oct 20, 2026
Merged

Mkalbani merged 22 commits into
MindFlowInteractive:mainfrom
akargi:feature/undo-redo-move-history

Conversation

@akargi

@akargi akargi commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

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.ts defines 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 penalty
  • MoveHistory — the ordered action log, the retained state snapshots, the cursor into them, and the effective undo depth
  • UndoRedoPolicy — the rules that apply to a session
  • GameMode — practice, challenge, timed, zen, ranked
  • DEFAULT_UNDO_DEPTH_BY_PUZZLE_TYPE — the shared per-type depth defaults

How undo and redo work

The history is a list of state snapshots with a cursor into it:

  • a new move truncates anything ahead of the cursor (the redo branch is gone)
  • undo moves the cursor back one step and returns that snapshot
  • redo moves the cursor forward one step and returns that snapshot

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:

Puzzle type Undo depth
logic_grid 5
sequence 5
spatial 8
mathematical 10
pattern_matching 15
word_puzzle 20
custom 10

The 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

Mode Undo
practice allowed, free
zen allowed, free
challenge allowed, penalised
timed allowed, penalised
ranked disabled (403)

The penalty is a ratio of the base score, applied by ScoringService in 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

Method Path
POST /puzzles/:puzzleId/player/:playerId/undo
POST /puzzles/:puzzleId/player/:playerId/redo
GET /puzzles/:puzzleId/player/:playerId/move-history

Each 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.
  • The undo/redo endpoints accept an optional replayId and mirror the action onto that replay, so the replay shows exactly what the player did — including their undos.
  • GET /move-history?includeReplayActions=true returns the derived actions ready to import.
  • REDO was added to the replay action type (the column is already varchar(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.ts never provided EnergyService, so its dependency graph could not be resolved and the spec could not compile. It now provides it (and MoveHistoryService).

Notes

  • No schema migration is required: every new value is stored in existing jsonb columns.
  • npm install, npm test and CI were not run in this environment — please run npm run verify (lint + format + typecheck + test) locally.
  • npm run lint:check and npm run format:check are already failing on main for unrelated files and are continue-on-error in CI; every file touched here is prettier-clean and the new code introduces no new lint findings.

Closes #461

akargi added 21 commits October 20, 2026 17:01
…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.
@drips-wave

drips-wave Bot commented Oct 20, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@Mkalbani

Copy link
Copy Markdown
Contributor

@akargi please resolve conflict

@akargi

akargi commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

@Mkalbani Tafaddal

@Mkalbani
Mkalbani merged commit 91f4ec9 into MindFlowInteractive:main Oct 20, 2026
1 check failed
@grantfox-oss grantfox-oss Bot mentioned this pull request Oct 20, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Undo/Redo and Move History

2 participants