56e3b62269
Phase 3 of the in-place card_game rewrite. Two bugs on undo: 1. recycle_count was incremented when recycling but never decremented on undo, causing the free-recycle allowance to be exhausted faster than it should be after undo+redo cycles. 2. undoing a penalised recycle applied the −15 undo penalty on top of the post-penalty (post-recycle) score rather than on the pre-recycle score, compounding the −100 / −20 penalty rather than reversing it. Fix: - Add score_history: Vec<i32> and is_recycle_history: Vec<bool> to GameState, both parallel to session.history() at all times. - Extract pre_instruction_score_delta() helper — single source of truth for all scoring logic, called from draw(), move_cards(), and the Deserialize replay. - draw() and move_cards() push to both stacks before processing. - undo() pops from both stacks: uses the popped pre-move score as the base for apply_undo_score() and decrements recycle_count if the undone instruction was a recycle. - Deserialize rebuilds is_recycle_history and recycle_count from the instruction replay (recycle detection needs only pre-instruction session state, so it is always correct across save/load cycles). score_history is not rebuilt on load (undo-penalty history is absent from saved_moves); undo falls back to old behaviour for pre-load moves, but is fully correct for moves made in the current session. - Remove recycle_count from PersistedGameStateIn (now rebuilt; serde silently ignores the field in existing JSON saves). Tests added: - recycle_count_decrements_when_recycle_is_undone - score_recycle_penalty_is_reversed_on_undo All 71 solitaire_core tests and full-workspace suite pass; clippy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>