From 67271266e1650742e1e1e54e4331de617656cf93 Mon Sep 17 00:00:00 2001 From: funman300 Date: Sun, 17 May 2026 20:43:47 -0700 Subject: [PATCH] refactor(data,core): consolidate APP_DIR_NAME and add #[must_use] on pure fns - Hoist APP_DIR_NAME = "ferrous_solitaire" to solitaire_data crate root as pub(crate); remove 5 duplicate local definitions across achievements, progress, settings, storage, replay modules (L-9) - Add #[must_use] to can_place_on_foundation, can_place_on_tableau, and is_valid_tableau_sequence in solitaire_core::rules so callers that accidentally discard the result get a compile-time warning (L-6) Co-Authored-By: Claude Sonnet 4.6 --- solitaire_core/src/rules.rs | 3 +++ solitaire_data/src/achievements.rs | 3 +-- solitaire_data/src/lib.rs | 3 +++ solitaire_data/src/progress.rs | 3 +-- solitaire_data/src/replay.rs | 5 ++--- solitaire_data/src/settings.rs | 3 +-- solitaire_data/src/storage.rs | 9 ++++----- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/solitaire_core/src/rules.rs b/solitaire_core/src/rules.rs index 66b0b9c..9073844 100644 --- a/solitaire_core/src/rules.rs +++ b/solitaire_core/src/rules.rs @@ -9,6 +9,7 @@ use crate::pile::Pile; /// [`Pile::claimed_suit`](crate::pile::Pile::claimed_suit)). /// - When the pile is non-empty, the next card must match the top card's /// suit and be exactly one rank higher. +#[must_use] pub fn can_place_on_foundation(card: &Card, pile: &Pile) -> bool { match pile.cards.last() { None => card.rank.value() == 1, @@ -19,6 +20,7 @@ pub fn can_place_on_foundation(card: &Card, pile: &Pile) -> bool { /// Returns `true` if `card` (or the bottom card of a sequence) can be placed on `pile` in the tableau. /// /// Tableau rules: Kings go on empty piles; otherwise alternating colour, one rank lower. +#[must_use] pub fn can_place_on_tableau(card: &Card, pile: &Pile) -> bool { match pile.cards.last() { None => card.rank.value() == 13, @@ -36,6 +38,7 @@ pub fn can_place_on_tableau(card: &Card, pile: &Pile) -> bool { /// only validates the sequence's *internal* structure, which the tableau /// move path must enforce so a player can't smuggle an arbitrary stack /// onto another column when the bottom card happens to land legally. +#[must_use] pub fn is_valid_tableau_sequence(cards: &[Card]) -> bool { cards.windows(2).all(|w| { w[0].rank.value() == w[1].rank.value() + 1 && w[0].suit.is_red() != w[1].suit.is_red() diff --git a/solitaire_data/src/achievements.rs b/solitaire_data/src/achievements.rs index 40cdaa7..3619795 100644 --- a/solitaire_data/src/achievements.rs +++ b/solitaire_data/src/achievements.rs @@ -10,12 +10,11 @@ use std::path::{Path, PathBuf}; pub use solitaire_sync::AchievementRecord; -const APP_DIR_NAME: &str = "ferrous_solitaire"; const FILE_NAME: &str = "achievements.json"; /// Platform-specific default path for `achievements.json`. pub fn achievements_file_path() -> Option { - crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(FILE_NAME)) + crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(FILE_NAME)) } /// Load achievements from an explicit path. Returns `Vec::new()` if the file diff --git a/solitaire_data/src/lib.rs b/solitaire_data/src/lib.rs index 1f0e623..61eea90 100644 --- a/solitaire_data/src/lib.rs +++ b/solitaire_data/src/lib.rs @@ -168,3 +168,6 @@ pub use matomo_client::MatomoClient; pub mod platform; pub use platform::data_dir; + +/// Application data subdirectory name, shared by all persistence modules. +pub(crate) const APP_DIR_NAME: &str = "ferrous_solitaire"; diff --git a/solitaire_data/src/progress.rs b/solitaire_data/src/progress.rs index 9dccdf3..137db3f 100644 --- a/solitaire_data/src/progress.rs +++ b/solitaire_data/src/progress.rs @@ -14,7 +14,6 @@ use chrono::{Datelike, NaiveDate}; pub use solitaire_sync::progress::level_for_xp; pub use solitaire_sync::PlayerProgress; -const APP_DIR_NAME: &str = "ferrous_solitaire"; const FILE_NAME: &str = "progress.json"; /// Deterministic seed derived from a date, identical for all players globally. @@ -46,7 +45,7 @@ pub fn xp_for_win(time_seconds: u64, used_undo: bool) -> u64 { /// Platform-specific default path for `progress.json`. pub fn progress_file_path() -> Option { - crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(FILE_NAME)) + crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(FILE_NAME)) } /// Load progress from an explicit path. Returns `default()` if missing/corrupt. diff --git a/solitaire_data/src/replay.rs b/solitaire_data/src/replay.rs index 82d97a3..3514fbb 100644 --- a/solitaire_data/src/replay.rs +++ b/solitaire_data/src/replay.rs @@ -29,7 +29,6 @@ use serde::{Deserialize, Serialize}; use solitaire_core::game_state::{DrawMode, GameMode}; use solitaire_core::pile::PileType; -const APP_DIR_NAME: &str = "ferrous_solitaire"; const LATEST_REPLAY_FILE_NAME: &str = "latest_replay.json"; const REPLAY_HISTORY_FILE_NAME: &str = "replays.json"; @@ -279,14 +278,14 @@ impl ReplayHistory { in migrate_legacy_latest_replay" )] pub fn latest_replay_path() -> Option { - crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(LATEST_REPLAY_FILE_NAME)) + crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(LATEST_REPLAY_FILE_NAME)) } /// Returns the platform-specific path to `replays.json`, the rolling /// history file, or `None` if `crate::data_dir()` is unavailable (e.g. /// minimal Linux containers). pub fn replay_history_path() -> Option { - crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(REPLAY_HISTORY_FILE_NAME)) + crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(REPLAY_HISTORY_FILE_NAME)) } /// Save a [`Replay`] atomically to `path` using the standard `.tmp` → diff --git a/solitaire_data/src/settings.rs b/solitaire_data/src/settings.rs index 4818f15..cbbe8ab 100644 --- a/solitaire_data/src/settings.rs +++ b/solitaire_data/src/settings.rs @@ -11,7 +11,6 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use solitaire_core::game_state::{DifficultyLevel, DrawMode}; -const APP_DIR_NAME: &str = "ferrous_solitaire"; const SETTINGS_FILE_NAME: &str = "settings.json"; /// Animation playback speed for card transitions. @@ -479,7 +478,7 @@ impl Settings { /// Returns the platform-specific path to `settings.json`, or `None` if /// the platform's data directory is unavailable. pub fn settings_file_path() -> Option { - crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(SETTINGS_FILE_NAME)) + crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(SETTINGS_FILE_NAME)) } /// Load settings from an explicit path. Returns `Settings::default()` if the diff --git a/solitaire_data/src/storage.rs b/solitaire_data/src/storage.rs index e2d7a14..a66b09d 100644 --- a/solitaire_data/src/storage.rs +++ b/solitaire_data/src/storage.rs @@ -13,7 +13,6 @@ use solitaire_core::game_state::{GameState, GAME_STATE_SCHEMA_VERSION}; use crate::stats::StatsSnapshot; -const APP_DIR_NAME: &str = "ferrous_solitaire"; const STATS_FILE_NAME: &str = "stats.json"; const GAME_STATE_FILE_NAME: &str = "game_state.json"; const TIME_ATTACK_SESSION_FILE_NAME: &str = "time_attack_session.json"; @@ -21,7 +20,7 @@ const TIME_ATTACK_SESSION_FILE_NAME: &str = "time_attack_session.json"; /// Returns the platform-specific path to `stats.json`, or `None` if /// `crate::data_dir()` is unavailable (e.g. minimal Linux containers). pub fn stats_file_path() -> Option { - crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(STATS_FILE_NAME)) + crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(STATS_FILE_NAME)) } /// Load stats from an explicit path. Returns `StatsSnapshot::default()` if @@ -71,7 +70,7 @@ pub fn save_stats(stats: &StatsSnapshot) -> io::Result<()> { /// Returns the platform-specific path to `game_state.json`, or `None` if /// `crate::data_dir()` is unavailable. pub fn game_state_file_path() -> Option { - crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(GAME_STATE_FILE_NAME)) + crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(GAME_STATE_FILE_NAME)) } /// Load an in-progress `GameState` from `path`. Returns `None` if the file is @@ -130,7 +129,7 @@ pub fn delete_game_state_at(path: &Path) -> io::Result<()> { /// are silently skipped. pub fn cleanup_orphaned_tmp_files() -> io::Result<()> { let dir = match crate::data_dir() { - Some(d) => d.join(APP_DIR_NAME), + Some(d) => d.join(crate::APP_DIR_NAME), None => return Ok(()), }; @@ -181,7 +180,7 @@ pub struct TimeAttackSession { /// Returns the platform-specific path to `time_attack_session.json`, or /// `None` if `crate::data_dir()` is unavailable. pub fn time_attack_session_path() -> Option { - crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(TIME_ATTACK_SESSION_FILE_NAME)) + crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(TIME_ATTACK_SESSION_FILE_NAME)) } /// Save a Time Attack session atomically. Mirrors `save_game_state_to`'s