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 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ use crate::pile::Pile;
|
|||||||
/// [`Pile::claimed_suit`](crate::pile::Pile::claimed_suit)).
|
/// [`Pile::claimed_suit`](crate::pile::Pile::claimed_suit)).
|
||||||
/// - When the pile is non-empty, the next card must match the top card's
|
/// - When the pile is non-empty, the next card must match the top card's
|
||||||
/// suit and be exactly one rank higher.
|
/// suit and be exactly one rank higher.
|
||||||
|
#[must_use]
|
||||||
pub fn can_place_on_foundation(card: &Card, pile: &Pile) -> bool {
|
pub fn can_place_on_foundation(card: &Card, pile: &Pile) -> bool {
|
||||||
match pile.cards.last() {
|
match pile.cards.last() {
|
||||||
None => card.rank.value() == 1,
|
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.
|
/// 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.
|
/// 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 {
|
pub fn can_place_on_tableau(card: &Card, pile: &Pile) -> bool {
|
||||||
match pile.cards.last() {
|
match pile.cards.last() {
|
||||||
None => card.rank.value() == 13,
|
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
|
/// only validates the sequence's *internal* structure, which the tableau
|
||||||
/// move path must enforce so a player can't smuggle an arbitrary stack
|
/// move path must enforce so a player can't smuggle an arbitrary stack
|
||||||
/// onto another column when the bottom card happens to land legally.
|
/// onto another column when the bottom card happens to land legally.
|
||||||
|
#[must_use]
|
||||||
pub fn is_valid_tableau_sequence(cards: &[Card]) -> bool {
|
pub fn is_valid_tableau_sequence(cards: &[Card]) -> bool {
|
||||||
cards.windows(2).all(|w| {
|
cards.windows(2).all(|w| {
|
||||||
w[0].rank.value() == w[1].rank.value() + 1 && w[0].suit.is_red() != w[1].suit.is_red()
|
w[0].rank.value() == w[1].rank.value() + 1 && w[0].suit.is_red() != w[1].suit.is_red()
|
||||||
|
|||||||
@@ -10,12 +10,11 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
pub use solitaire_sync::AchievementRecord;
|
pub use solitaire_sync::AchievementRecord;
|
||||||
|
|
||||||
const APP_DIR_NAME: &str = "ferrous_solitaire";
|
|
||||||
const FILE_NAME: &str = "achievements.json";
|
const FILE_NAME: &str = "achievements.json";
|
||||||
|
|
||||||
/// Platform-specific default path for `achievements.json`.
|
/// Platform-specific default path for `achievements.json`.
|
||||||
pub fn achievements_file_path() -> Option<PathBuf> {
|
pub fn achievements_file_path() -> Option<PathBuf> {
|
||||||
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
|
/// Load achievements from an explicit path. Returns `Vec::new()` if the file
|
||||||
|
|||||||
@@ -168,3 +168,6 @@ pub use matomo_client::MatomoClient;
|
|||||||
|
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub use platform::data_dir;
|
pub use platform::data_dir;
|
||||||
|
|
||||||
|
/// Application data subdirectory name, shared by all persistence modules.
|
||||||
|
pub(crate) const APP_DIR_NAME: &str = "ferrous_solitaire";
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use chrono::{Datelike, NaiveDate};
|
|||||||
pub use solitaire_sync::progress::level_for_xp;
|
pub use solitaire_sync::progress::level_for_xp;
|
||||||
pub use solitaire_sync::PlayerProgress;
|
pub use solitaire_sync::PlayerProgress;
|
||||||
|
|
||||||
const APP_DIR_NAME: &str = "ferrous_solitaire";
|
|
||||||
const FILE_NAME: &str = "progress.json";
|
const FILE_NAME: &str = "progress.json";
|
||||||
|
|
||||||
/// Deterministic seed derived from a date, identical for all players globally.
|
/// 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`.
|
/// Platform-specific default path for `progress.json`.
|
||||||
pub fn progress_file_path() -> Option<PathBuf> {
|
pub fn progress_file_path() -> Option<PathBuf> {
|
||||||
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.
|
/// Load progress from an explicit path. Returns `default()` if missing/corrupt.
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use solitaire_core::game_state::{DrawMode, GameMode};
|
use solitaire_core::game_state::{DrawMode, GameMode};
|
||||||
use solitaire_core::pile::PileType;
|
use solitaire_core::pile::PileType;
|
||||||
|
|
||||||
const APP_DIR_NAME: &str = "ferrous_solitaire";
|
|
||||||
const LATEST_REPLAY_FILE_NAME: &str = "latest_replay.json";
|
const LATEST_REPLAY_FILE_NAME: &str = "latest_replay.json";
|
||||||
const REPLAY_HISTORY_FILE_NAME: &str = "replays.json";
|
const REPLAY_HISTORY_FILE_NAME: &str = "replays.json";
|
||||||
|
|
||||||
@@ -279,14 +278,14 @@ impl ReplayHistory {
|
|||||||
in migrate_legacy_latest_replay"
|
in migrate_legacy_latest_replay"
|
||||||
)]
|
)]
|
||||||
pub fn latest_replay_path() -> Option<PathBuf> {
|
pub fn latest_replay_path() -> Option<PathBuf> {
|
||||||
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
|
/// Returns the platform-specific path to `replays.json`, the rolling
|
||||||
/// history file, or `None` if `crate::data_dir()` is unavailable (e.g.
|
/// history file, or `None` if `crate::data_dir()` is unavailable (e.g.
|
||||||
/// minimal Linux containers).
|
/// minimal Linux containers).
|
||||||
pub fn replay_history_path() -> Option<PathBuf> {
|
pub fn replay_history_path() -> Option<PathBuf> {
|
||||||
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` →
|
/// Save a [`Replay`] atomically to `path` using the standard `.tmp` →
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use std::path::{Path, PathBuf};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use solitaire_core::game_state::{DifficultyLevel, DrawMode};
|
use solitaire_core::game_state::{DifficultyLevel, DrawMode};
|
||||||
|
|
||||||
const APP_DIR_NAME: &str = "ferrous_solitaire";
|
|
||||||
const SETTINGS_FILE_NAME: &str = "settings.json";
|
const SETTINGS_FILE_NAME: &str = "settings.json";
|
||||||
|
|
||||||
/// Animation playback speed for card transitions.
|
/// Animation playback speed for card transitions.
|
||||||
@@ -479,7 +478,7 @@ impl Settings {
|
|||||||
/// Returns the platform-specific path to `settings.json`, or `None` if
|
/// Returns the platform-specific path to `settings.json`, or `None` if
|
||||||
/// the platform's data directory is unavailable.
|
/// the platform's data directory is unavailable.
|
||||||
pub fn settings_file_path() -> Option<PathBuf> {
|
pub fn settings_file_path() -> Option<PathBuf> {
|
||||||
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
|
/// Load settings from an explicit path. Returns `Settings::default()` if the
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use solitaire_core::game_state::{GameState, GAME_STATE_SCHEMA_VERSION};
|
|||||||
|
|
||||||
use crate::stats::StatsSnapshot;
|
use crate::stats::StatsSnapshot;
|
||||||
|
|
||||||
const APP_DIR_NAME: &str = "ferrous_solitaire";
|
|
||||||
const STATS_FILE_NAME: &str = "stats.json";
|
const STATS_FILE_NAME: &str = "stats.json";
|
||||||
const GAME_STATE_FILE_NAME: &str = "game_state.json";
|
const GAME_STATE_FILE_NAME: &str = "game_state.json";
|
||||||
const TIME_ATTACK_SESSION_FILE_NAME: &str = "time_attack_session.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
|
/// Returns the platform-specific path to `stats.json`, or `None` if
|
||||||
/// `crate::data_dir()` is unavailable (e.g. minimal Linux containers).
|
/// `crate::data_dir()` is unavailable (e.g. minimal Linux containers).
|
||||||
pub fn stats_file_path() -> Option<PathBuf> {
|
pub fn stats_file_path() -> Option<PathBuf> {
|
||||||
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
|
/// 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
|
/// Returns the platform-specific path to `game_state.json`, or `None` if
|
||||||
/// `crate::data_dir()` is unavailable.
|
/// `crate::data_dir()` is unavailable.
|
||||||
pub fn game_state_file_path() -> Option<PathBuf> {
|
pub fn game_state_file_path() -> Option<PathBuf> {
|
||||||
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
|
/// 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.
|
/// are silently skipped.
|
||||||
pub fn cleanup_orphaned_tmp_files() -> io::Result<()> {
|
pub fn cleanup_orphaned_tmp_files() -> io::Result<()> {
|
||||||
let dir = match crate::data_dir() {
|
let dir = match crate::data_dir() {
|
||||||
Some(d) => d.join(APP_DIR_NAME),
|
Some(d) => d.join(crate::APP_DIR_NAME),
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,7 +180,7 @@ pub struct TimeAttackSession {
|
|||||||
/// Returns the platform-specific path to `time_attack_session.json`, or
|
/// Returns the platform-specific path to `time_attack_session.json`, or
|
||||||
/// `None` if `crate::data_dir()` is unavailable.
|
/// `None` if `crate::data_dir()` is unavailable.
|
||||||
pub fn time_attack_session_path() -> Option<PathBuf> {
|
pub fn time_attack_session_path() -> Option<PathBuf> {
|
||||||
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
|
/// Save a Time Attack session atomically. Mirrors `save_game_state_to`'s
|
||||||
|
|||||||
Reference in New Issue
Block a user