refactor(core): derive score/undo/recycle from upstream session stats
Replace the bespoke WXP scoring engine with the upstream card_game/klondike session stats, eliminating duplicated state that could drift from the single source of truth. score()/undo_count()/recycle_count() now read session.stats(); the -15 undo penalty is configured as SessionConfig::undo_penalty and applied by the upstream score formula. Save schema bumped v4 -> v5 (the three counters are no longer persisted -- they are rebuilt by replaying the forward instruction history on load). - Remove GameState fields score, undo_count, recycle_count (#87) - Remove score_history / is_recycle_history undo journal (#86) - Remove KlondikeAdapter::apply_undo_score and the score_for_* helpers, plus pre_instruction_score_delta / will_flip_tableau_source (#84) These three issues are a single atomic change: each removed field/helper is consumed by the same draw/apply_instruction/undo/serde/PartialEq paths, so they cannot compile or pass tests in isolation. Behaviour changes (intentional): the escalating recycle penalty and per-step score floor are gone (upstream linear scoring, floored once at 0); recycle_count is now cumulative; undo_count resets across save/load. Refs #84, #86, #87 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -497,15 +497,15 @@ mod tests {
|
||||
/// replays all `saved_moves` to reconstruct every pile.
|
||||
///
|
||||
/// A fresh-game test (zero moves) never exercises that replay path, so this
|
||||
/// test plays several real moves — including an undo — before saving, then
|
||||
/// asserts the full pile layout round-trips exactly.
|
||||
/// test plays several real moves — including an undo — before saving.
|
||||
///
|
||||
/// `GameState::PartialEq` covers stock, waste, all four foundations, all
|
||||
/// seven tableau columns, `score`, `move_count`, `undo_count`, and
|
||||
/// `recycle_count`. Any breakage in the upstream serde or replay path
|
||||
/// will cause at least one pile to disagree.
|
||||
/// Since schema v5 no longer persists `score`/`undo_count`/`recycle_count`
|
||||
/// (they are derived from the replayed session stats), round-trip fidelity is
|
||||
/// verified by **re-save idempotency**: reloading the save and serialising it
|
||||
/// again must reproduce byte-identical JSON. `undo_count` deliberately resets
|
||||
/// to 0 on load because only the forward instruction history is persisted.
|
||||
#[test]
|
||||
fn game_state_v4_mid_game_round_trip() {
|
||||
fn game_state_v5_mid_game_round_trip() {
|
||||
use solitaire_core::KlondikeInstruction;
|
||||
use solitaire_core::game_state::GameState;
|
||||
|
||||
@@ -546,19 +546,40 @@ mod tests {
|
||||
|
||||
save_game_state_to(&path, &gs).expect("save");
|
||||
|
||||
// Verify the file contains the v4 schema marker (tolerates pretty-print whitespace).
|
||||
// Verify the file carries the v5 schema marker.
|
||||
let json = fs::read_to_string(&path).expect("read json");
|
||||
assert!(
|
||||
json.contains("schema_version") && json.contains('4') && !json.contains(": 3"),
|
||||
"saved file must use schema version 4",
|
||||
json.contains("\"schema_version\"") && json.contains('5'),
|
||||
"saved file must use schema version 5",
|
||||
);
|
||||
|
||||
let loaded = load_game_state_from(&path)
|
||||
.expect("a valid in-progress game must load without error");
|
||||
|
||||
// The forward instruction history round-trips, so the reconstructed board
|
||||
// re-serialises to byte-identical JSON.
|
||||
let path_reload = gs_path("v5_mid_game_reload");
|
||||
let _ = fs::remove_file(&path_reload);
|
||||
save_game_state_to(&path_reload, &loaded).expect("re-save loaded");
|
||||
assert_eq!(
|
||||
loaded, gs,
|
||||
"all pile layouts and counters must be identical after schema-v4 round-trip",
|
||||
fs::read_to_string(&path).expect("read original save"),
|
||||
fs::read_to_string(&path_reload).expect("read re-saved"),
|
||||
"re-saving the loaded game must reproduce the original save exactly",
|
||||
);
|
||||
|
||||
// Derived board reads match the live game (move count + recycle count are
|
||||
// both rebuilt from the replayed forward history).
|
||||
assert_eq!(loaded.move_count(), gs.move_count(), "move_count round-trips");
|
||||
assert_eq!(
|
||||
loaded.recycle_count(),
|
||||
gs.recycle_count(),
|
||||
"recycle_count round-trips",
|
||||
);
|
||||
// undo_count is intentionally not persisted: it resets to 0 on load.
|
||||
assert_eq!(
|
||||
loaded.undo_count(),
|
||||
0,
|
||||
"undo_count resets across save/load under schema v5",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user