Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e3c6b06b0 | |||
| f0832f3dfa |
@@ -30,3 +30,8 @@ solitaire_server/e2e/test-results/
|
|||||||
deploy/matomo-secret.yaml
|
deploy/matomo-secret.yaml
|
||||||
deploy/*-secret.yaml
|
deploy/*-secret.yaml
|
||||||
deploy/*-auth-secret.yaml
|
deploy/*-auth-secret.yaml
|
||||||
|
|
||||||
|
# Local agent-tooling artifacts (Codex / claude-flow) — keep out of the repo
|
||||||
|
/.agents/
|
||||||
|
/.codex/
|
||||||
|
/AGENTS.md
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::error::MoveError;
|
use crate::error::MoveError;
|
||||||
use crate::klondike_adapter::{
|
use crate::klondike_adapter::{
|
||||||
DrawMode, KlondikeAdapter, SavedInstruction,
|
DrawMode, KlondikeAdapter, SavedInstruction,
|
||||||
compute_time_bonus as scoring_time_bonus,
|
|
||||||
foundation_from_slot as adapter_foundation_from_slot,
|
foundation_from_slot as adapter_foundation_from_slot,
|
||||||
skip_cards_from_count as adapter_skip_cards_from_count,
|
skip_cards_from_count as adapter_skip_cards_from_count,
|
||||||
tableau_from_index as adapter_tableau_from_index,
|
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.
|
/// Read-only access to the underlying [`card_game::Session`] for this deal.
|
||||||
///
|
///
|
||||||
/// Exposes `session.history()` (deterministic replay) and `session.solve()`
|
/// Exposes `session.history()` (deterministic replay) and `session.solve()`
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ pub use achievements::{
|
|||||||
|
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
pub use progress::{
|
pub use progress::{
|
||||||
PlayerProgress, daily_seed_for, level_for_xp, load_progress_from, progress_file_path,
|
PlayerProgress, XpBreakdown, daily_seed_for, level_for_xp, load_progress_from,
|
||||||
save_progress_to, xp_for_win,
|
progress_file_path, save_progress_to, xp_breakdown, xp_for_win,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod weekly;
|
pub mod weekly;
|
||||||
@@ -172,8 +172,11 @@ pub use replay::{
|
|||||||
ReplayHistory, ReplayMove, append_replay_to_history, load_replay_history_from,
|
ReplayHistory, ReplayMove, append_replay_to_history, load_replay_history_from,
|
||||||
migrate_legacy_latest_replay, replay_history_path, save_replay_history_to,
|
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)]
|
#[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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod matomo_client;
|
pub mod matomo_client;
|
||||||
|
|||||||
@@ -25,12 +25,34 @@ pub fn daily_seed_for(date: NaiveDate) -> u64 {
|
|||||||
y * 10_000 + m * 100 + d
|
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
|
/// Base 50 + scaled fast-win bonus (10..=50 for sub-2-minute wins) + 25 if
|
||||||
/// the player did not use undo.
|
/// the player did not use undo.
|
||||||
pub fn xp_for_win(time_seconds: u64, used_undo: bool) -> u64 {
|
pub fn xp_breakdown(time_seconds: u64, used_undo: bool) -> XpBreakdown {
|
||||||
let base: u64 = 50;
|
|
||||||
let speed_bonus: u64 = if time_seconds >= 120 {
|
let speed_bonus: u64 = if time_seconds >= 120 {
|
||||||
0
|
0
|
||||||
} else {
|
} 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);
|
let scaled = 50_u64.saturating_sub(time_seconds.saturating_mul(40) / 120);
|
||||||
scaled.max(10)
|
scaled.max(10)
|
||||||
};
|
};
|
||||||
let no_undo_bonus: u64 = if used_undo { 0 } else { 25 };
|
XpBreakdown {
|
||||||
base + speed_bonus + no_undo_bonus
|
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`.
|
/// Platform-specific default path for `progress.json`.
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ use std::hash::{Hash, Hasher};
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::RequestRedraw;
|
use bevy::window::RequestRedraw;
|
||||||
use solitaire_core::card::Card;
|
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 solitaire_data::AnimSpeed;
|
||||||
|
|
||||||
use crate::animation_plugin::CardAnim;
|
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)
|
// Unit tests (pure functions only — no Bevy world required)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -90,28 +90,23 @@ pub struct WinSummaryPending {
|
|||||||
|
|
||||||
/// Builds a human-readable XP breakdown string for the win modal.
|
/// Builds a human-readable XP breakdown string for the win modal.
|
||||||
///
|
///
|
||||||
/// Mirrors the logic in `solitaire_data::xp_for_win` so the breakdown always
|
/// Reads the components from `solitaire_data::xp_breakdown` — the single source
|
||||||
/// matches the total shown on the `XpAwardedEvent`.
|
/// of truth shared with `xp_for_win` — so the breakdown can never drift from
|
||||||
|
/// the total shown on the `XpAwardedEvent`.
|
||||||
///
|
///
|
||||||
/// Examples:
|
/// Examples:
|
||||||
/// - slow win, no undo → `"+50 base +25 no-undo"`
|
/// - slow win, no undo → `"+50 base +25 no-undo"`
|
||||||
/// - fast win, undo → `"+50 base +30 speed"`
|
/// - fast win, undo → `"+50 base +30 speed"`
|
||||||
/// - fast win, no undo → `"+50 base +25 no-undo +30 speed"`
|
/// - fast win, no undo → `"+50 base +25 no-undo +30 speed"`
|
||||||
fn build_xp_detail(time_seconds: u64, used_undo: bool) -> String {
|
fn build_xp_detail(time_seconds: u64, used_undo: bool) -> String {
|
||||||
let speed_bonus: u64 = if time_seconds >= 120 {
|
let xp = solitaire_data::xp_breakdown(time_seconds, used_undo);
|
||||||
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 mut parts = vec!["+50 base".to_string()];
|
let mut parts = vec![format!("+{} base", xp.base)];
|
||||||
if no_undo_bonus > 0 {
|
if xp.no_undo_bonus > 0 {
|
||||||
parts.push("+25 no-undo".to_string());
|
parts.push(format!("+{} no-undo", xp.no_undo_bonus));
|
||||||
}
|
}
|
||||||
if speed_bonus > 0 {
|
if xp.speed_bonus > 0 {
|
||||||
parts.push(format!("+{speed_bonus} speed"));
|
parts.push(format!("+{} speed", xp.speed_bonus));
|
||||||
}
|
}
|
||||||
parts.join(" ")
|
parts.join(" ")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user