Compare commits

..

2 Commits

Author SHA1 Message Date
funman300 9e3c6b06b0 chore: gitignore local agent-tooling artifacts
Keep Codex / claude-flow scaffolding (.agents/, .codex/, AGENTS.md) out
of the repo — these are locally generated and not project sources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:17:04 -07:00
funman300 f0832f3dfa refactor: remove leftover redundancies after card_game migration
Post-migration audit found the card_game/klondike migration essentially
complete; these are the four small redundancies that remained:

- core: delete dead GameState::compute_time_bonus (zero callers; engine
  uses the klondike_adapter free fn directly)
- data: drop dead public re-exports load_latest_replay_from /
  save_latest_replay_to (no callers outside replay.rs); keep
  latest_replay_path (engine legacy migration still uses it)
- data+engine: lift win-XP scoring into a shared XpBreakdown so the
  win-summary modal breakdown and xp_for_win share one source of truth
  instead of duplicating the speed/no-undo constants
- engine: replace feedback_anim_plugin's private foundation_from_slot
  copy with the canonical klondike_adapter::foundation_from_slot

cargo test --workspace + clippy -D warnings green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:12:03 -07:00
6 changed files with 57 additions and 39 deletions
+5
View File
@@ -30,3 +30,8 @@ solitaire_server/e2e/test-results/
deploy/matomo-secret.yaml
deploy/*-secret.yaml
deploy/*-auth-secret.yaml
# Local agent-tooling artifacts (Codex / claude-flow) — keep out of the repo
/.agents/
/.codex/
/AGENTS.md
-6
View File
@@ -1,7 +1,6 @@
use crate::error::MoveError;
use crate::klondike_adapter::{
DrawMode, KlondikeAdapter, SavedInstruction,
compute_time_bonus as scoring_time_bonus,
foundation_from_slot as adapter_foundation_from_slot,
skip_cards_from_count as adapter_skip_cards_from_count,
tableau_from_index as adapter_tableau_from_index,
@@ -1118,11 +1117,6 @@ impl GameState {
})
}
/// Time bonus added to score on win: `700_000 / elapsed_seconds` (0 if elapsed is 0).
pub fn compute_time_bonus(&self) -> i32 {
scoring_time_bonus(self.elapsed_seconds)
}
/// Read-only access to the underlying [`card_game::Session`] for this deal.
///
/// Exposes `session.history()` (deterministic replay) and `session.solve()`
+6 -3
View File
@@ -124,8 +124,8 @@ pub use achievements::{
pub mod progress;
pub use progress::{
PlayerProgress, daily_seed_for, level_for_xp, load_progress_from, progress_file_path,
save_progress_to, xp_for_win,
PlayerProgress, XpBreakdown, daily_seed_for, level_for_xp, load_progress_from,
progress_file_path, save_progress_to, xp_breakdown, xp_for_win,
};
pub mod weekly;
@@ -172,8 +172,11 @@ pub use replay::{
ReplayHistory, ReplayMove, append_replay_to_history, load_replay_history_from,
migrate_legacy_latest_replay, replay_history_path, save_replay_history_to,
};
// `latest_replay_path` is still consumed by the engine's one-shot legacy
// migration; `load_latest_replay_from`/`save_latest_replay_to` had no callers
// outside `replay.rs` and were dropped from the public surface.
#[allow(deprecated)]
pub use replay::{latest_replay_path, load_latest_replay_from, save_latest_replay_to};
pub use replay::latest_replay_path;
#[cfg(not(target_arch = "wasm32"))]
pub mod matomo_client;
+35 -5
View File
@@ -25,12 +25,34 @@ pub fn daily_seed_for(date: NaiveDate) -> u64 {
y * 10_000 + m * 100 + d
}
/// XP awarded for winning a game.
/// Component breakdown of the XP awarded for a win.
///
/// This is the single source of truth for win-XP scoring: [`xp_for_win`] sums
/// it for the total, and UI that displays the individual lines (the win-summary
/// modal) reads the parts from here so the breakdown can never drift from the
/// total.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XpBreakdown {
/// Flat base XP granted for any win.
pub base: u64,
/// Scaled fast-win bonus (10..=50 for sub-2-minute wins, else 0).
pub speed_bonus: u64,
/// Bonus for winning without using undo (25, else 0).
pub no_undo_bonus: u64,
}
impl XpBreakdown {
/// Total XP awarded: `base + speed_bonus + no_undo_bonus`.
pub fn total(self) -> u64 {
self.base + self.speed_bonus + self.no_undo_bonus
}
}
/// Component breakdown of the XP awarded for a win.
///
/// Base 50 + scaled fast-win bonus (10..=50 for sub-2-minute wins) + 25 if
/// the player did not use undo.
pub fn xp_for_win(time_seconds: u64, used_undo: bool) -> u64 {
let base: u64 = 50;
pub fn xp_breakdown(time_seconds: u64, used_undo: bool) -> XpBreakdown {
let speed_bonus: u64 = if time_seconds >= 120 {
0
} else {
@@ -39,8 +61,16 @@ pub fn xp_for_win(time_seconds: u64, used_undo: bool) -> u64 {
let scaled = 50_u64.saturating_sub(time_seconds.saturating_mul(40) / 120);
scaled.max(10)
};
let no_undo_bonus: u64 = if used_undo { 0 } else { 25 };
base + speed_bonus + no_undo_bonus
XpBreakdown {
base: 50,
speed_bonus,
no_undo_bonus: if used_undo { 0 } else { 25 },
}
}
/// XP awarded for winning a game. See [`xp_breakdown`] for the components.
pub fn xp_for_win(time_seconds: u64, used_undo: bool) -> u64 {
xp_breakdown(time_seconds, used_undo).total()
}
/// Platform-specific default path for `progress.json`.
+2 -11
View File
@@ -44,7 +44,8 @@ use std::hash::{Hash, Hasher};
use bevy::prelude::*;
use bevy::window::RequestRedraw;
use solitaire_core::card::Card;
use solitaire_core::{Foundation, KlondikePile};
use solitaire_core::KlondikePile;
use solitaire_core::klondike_adapter::foundation_from_slot;
use solitaire_data::AnimSpeed;
use crate::animation_plugin::CardAnim;
@@ -645,16 +646,6 @@ fn pile_cards(
}
}
fn foundation_from_slot(slot: u8) -> Option<Foundation> {
match slot {
0 => Some(Foundation::Foundation1),
1 => Some(Foundation::Foundation2),
2 => Some(Foundation::Foundation3),
3 => Some(Foundation::Foundation4),
_ => None,
}
}
// ---------------------------------------------------------------------------
// Unit tests (pure functions only — no Bevy world required)
// ---------------------------------------------------------------------------
+9 -14
View File
@@ -90,28 +90,23 @@ pub struct WinSummaryPending {
/// Builds a human-readable XP breakdown string for the win modal.
///
/// Mirrors the logic in `solitaire_data::xp_for_win` so the breakdown always
/// matches the total shown on the `XpAwardedEvent`.
/// Reads the components from `solitaire_data::xp_breakdown` the single source
/// of truth shared with `xp_for_win` — so the breakdown can never drift from
/// the total shown on the `XpAwardedEvent`.
///
/// Examples:
/// - slow win, no undo → `"+50 base +25 no-undo"`
/// - fast win, undo → `"+50 base +30 speed"`
/// - fast win, no undo → `"+50 base +25 no-undo +30 speed"`
fn build_xp_detail(time_seconds: u64, used_undo: bool) -> String {
let speed_bonus: u64 = if time_seconds >= 120 {
0
} else {
let scaled = 50_u64.saturating_sub(time_seconds.saturating_mul(40) / 120);
scaled.max(10)
};
let no_undo_bonus: u64 = if used_undo { 0 } else { 25 };
let xp = solitaire_data::xp_breakdown(time_seconds, used_undo);
let mut parts = vec!["+50 base".to_string()];
if no_undo_bonus > 0 {
parts.push("+25 no-undo".to_string());
let mut parts = vec![format!("+{} base", xp.base)];
if xp.no_undo_bonus > 0 {
parts.push(format!("+{} no-undo", xp.no_undo_bonus));
}
if speed_bonus > 0 {
parts.push(format!("+{speed_bonus} speed"));
if xp.speed_bonus > 0 {
parts.push(format!("+{} speed", xp.speed_bonus));
}
parts.join(" ")
}