Skip to content

Practice mode: replay past matches - #387

Merged
stephenkirk merged 15 commits into
mainfrom
stephenkirk/review-practice-mode
Apr 15, 2026
Merged

Practice mode: replay past matches#387
stephenkirk merged 15 commits into
mainfrom
stephenkirk/review-practice-mode

Conversation

@stephenkirk

Copy link
Copy Markdown
Member

Summary

Practice mode lets you replay past matches. Pick a recorded game, choose which side to play, and the other side's scoring plays back hand-by-hand while you play live. Same seed, same blinds, same ruleset — different you.

Replay sources are Lovely .log files or exported .json. Drop them in replays/ and they show up in the picker.

What's new

A full replay practice flow: browse recorded matches in a two-column picker (match list + detail panel with joker sprites), flip which side you play as, then run the game with MP rulesets and blind progression. Between rounds, open the collection and click to spawn cards. Toggle unlimited slots or edition cycling as needed.

Internals:

  • lib/practice_mode.luaMP.SP state, MP.is_practice_mode()
  • lib/ghost_replay.lua — replay engine: per-ante hand playback, PvP resolution, advance animation
  • lib/log_parser.lua — parses .log files into structured replay data; also accepts .json
  • ui/.../ghost_replay_picker.lua — two-column picker with match details and joker sprites
  • lovely/practice.toml — key/click patches for card spawning (3/click) and edition cycling (Q)
  • MP.is_mp_or_ghost() — replaces MP.LOBBY.code checks across blind, HUD, round, and end-round paths; suppresses network actions behind not MP.GHOST.is_active() guards
  • MP.get_active_gamemode() — centralized resolution (lobby → replay → ruleset). Fixes a prior bug where the replay's gamemode bled into MP.LOBBY.config

State isolation

Replay/practice state must never leak into real MP. action_start_game() (both networking stacks), setup_run_singleplayer(), start_vanilla_sp(), and start_lobby() all clear practice and replay state. Network actions are gated on not MP.GHOST.is_active(). Gamemode reads from replay state, not lobby config.

Known issues

  • Advance animation doesn't pace correctly: when the player outscores the replay mid-round, remaining hands should stagger with 0.6s delays. They fire but blast through instantly. Root cause unknown.
  • should_use_the_order() returns true unconditionally for practice mode. Should check the active ruleset. Intentional deferral.

Test plan

  • Drop .log/.json into replays/, verify they appear in the picker
  • Select a match, verify detail panel (ante breakdown, joker sprites)
  • Flip perspective, verify scores and side swap
  • Play a full replayed match — win and lose paths
  • Spawn cards from collection (click/3), toggle unlimited slots, cycle editions (Q)
  • Exit to real MP lobby or vanilla SP — verify clean state

Use position 2 for better alignment and give Jimbo a more
in-character, playful hint message.
Match recording (snapshot_ante, init, finalize) was dead code —
written but never read. Replays will come from Lovely log files
instead. Removes the table, all methods, and all call sites in
action_handlers and game_state.
Utility predicate doesn't belong in a UI monkey-patch file.
Moved to lib/ where MP.GHOST lives, which loads before ui/.
More descriptive local name for the module table, consistent with
how it's used by callers via pcall/load_mp_file.
The .json path is mainly for verifying the Lua log parser against the
Python tool output. Extracted to load_json_replay() with a comment
explaining its purpose. The .log branch is now checked first as the
primary code path.
Moved is_ruleset_supported, format_score, and build_label from the
picker UI file to MP.GHOST in the lib layer where they belong.
File no longer contains match recording — it's purely ghost replay
load/playback logic now.
…eplay.lua

Move inline ghost replay branching into focused helpers:
- resolve_pvp_hands_exhausted, resolve_pvp_mid_hand, resolve_round_fail
- get_blind_name_ui, current_target, _start_advance_sequence
All replay sources now emit per-hand data. The fallback path that
set a static enemy score from ante-level aggregates was vestigial.

Removes get_enemy_score(), the else-branch in init_playback(), and
the current_target() wrapper — its sole callsite now uses
current_target_big() directly.
@stephenkirk
stephenkirk merged commit 286f56a into main Apr 15, 2026
stephenkirk added a commit that referenced this pull request Apr 16, 2026
* practice mode 1

* fix collection button height in practice mode

* jimbo hint

* Adjust practice collection Jimbo position and copy

Use position 2 for better alignment and give Jimbo a more
in-character, playful hint message.

* Only show practice collection Jimbo hint once per boot

* Remove `log_to_ghost_replay` scripts for now

* Remove MP.MATCH_RECORD code path

Match recording (snapshot_ante, init, finalize) was dead code —
written but never read. Replays will come from Lovely log files
instead. Removes the table, all methods, and all call sites in
action_handlers and game_state.

* Move MP.is_mp_or_ghost() from UI file to lib/match_history.lua

Utility predicate doesn't belong in a UI monkey-patch file.
Moved to lib/ where MP.GHOST lives, which loads before ui/.

* Rename log_parser module table from M to LOG_PARSER

More descriptive local name for the module table, consistent with
how it's used by callers via pcall/load_mp_file.

* Extract JSON replay loading to local function, make .log the main path

The .json path is mainly for verifying the Lua log parser against the
Python tool output. Extracted to load_json_replay() with a comment
explaining its purpose. The .log branch is now checked first as the
primary code path.

* Move data helpers from ghost_replay_picker to lib/match_history

Moved is_ruleset_supported, format_score, and build_label from the
picker UI file to MP.GHOST in the lib layer where they belong.

* Move practice mode logic from UI callbacks to lib/practice_mode.lua

* Rename lib/match_history.lua to lib/ghost_replay.lua

File no longer contains match recording — it's purely ghost replay
load/playback logic now.

* Extract ghost PvP resolution logic from game_state.lua to lib/ghost_replay.lua

Move inline ghost replay branching into focused helpers:
- resolve_pvp_hands_exhausted, resolve_pvp_mid_hand, resolve_round_fail
- get_blind_name_ui, current_target, _start_advance_sequence

* Remove aggregate score fallback from ghost replay

All replay sources now emit per-hand data. The fallback path that
set a static enemy score from ante-level aggregates was vestigial.

Removes get_enemy_score(), the else-branch in init_playback(), and
the current_target() wrapper — its sole callsite now uses
current_target_big() directly.
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.

1 participant