feat(data,engine): add WinUnder-5min and WinDrawThree weekly goal types
Adds two new weekly goals — "Win 1 game in under 5 minutes" and "Win 1 Draw-3 game" — broadening variety beyond the existing three. WeeklyGoalContext gains a draw_mode field so the new WinDrawThree variant can match on draw mode. Existing tests updated to pre-complete new goals where the win conditions overlap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
//! increments matching counters in `PlayerProgress::weekly_goal_progress`.
|
//! increments matching counters in `PlayerProgress::weekly_goal_progress`.
|
||||||
|
|
||||||
use chrono::{Datelike, NaiveDate};
|
use chrono::{Datelike, NaiveDate};
|
||||||
|
use solitaire_core::game_state::DrawMode;
|
||||||
|
|
||||||
/// XP awarded each time a weekly goal is just completed.
|
/// XP awarded each time a weekly goal is just completed.
|
||||||
pub const WEEKLY_GOAL_XP: u64 = 75;
|
pub const WEEKLY_GOAL_XP: u64 = 75;
|
||||||
@@ -17,6 +18,8 @@ pub enum WeeklyGoalKind {
|
|||||||
WinWithoutUndo,
|
WinWithoutUndo,
|
||||||
/// A win in strictly fewer than `seconds` seconds counts.
|
/// A win in strictly fewer than `seconds` seconds counts.
|
||||||
WinUnder { seconds: u64 },
|
WinUnder { seconds: u64 },
|
||||||
|
/// A win in Draw-3 mode counts.
|
||||||
|
WinDrawThree,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Static metadata for a single weekly goal.
|
/// Static metadata for a single weekly goal.
|
||||||
@@ -29,10 +32,11 @@ pub struct WeeklyGoalDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Per-event facts a goal needs to decide whether it matched.
|
/// Per-event facts a goal needs to decide whether it matched.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct WeeklyGoalContext {
|
pub struct WeeklyGoalContext {
|
||||||
pub time_seconds: u64,
|
pub time_seconds: u64,
|
||||||
pub used_undo: bool,
|
pub used_undo: bool,
|
||||||
|
pub draw_mode: DrawMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WeeklyGoalDef {
|
impl WeeklyGoalDef {
|
||||||
@@ -43,6 +47,7 @@ impl WeeklyGoalDef {
|
|||||||
WeeklyGoalKind::WinGame => true,
|
WeeklyGoalKind::WinGame => true,
|
||||||
WeeklyGoalKind::WinWithoutUndo => !ctx.used_undo,
|
WeeklyGoalKind::WinWithoutUndo => !ctx.used_undo,
|
||||||
WeeklyGoalKind::WinUnder { seconds } => ctx.time_seconds < seconds,
|
WeeklyGoalKind::WinUnder { seconds } => ctx.time_seconds < seconds,
|
||||||
|
WeeklyGoalKind::WinDrawThree => ctx.draw_mode == DrawMode::DrawThree,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +72,18 @@ pub const WEEKLY_GOALS: &[WeeklyGoalDef] = &[
|
|||||||
target: 3,
|
target: 3,
|
||||||
kind: WeeklyGoalKind::WinUnder { seconds: 180 },
|
kind: WeeklyGoalKind::WinUnder { seconds: 180 },
|
||||||
},
|
},
|
||||||
|
WeeklyGoalDef {
|
||||||
|
id: "weekly_1_under_five",
|
||||||
|
description: "Win 1 game in under 5 minutes this week",
|
||||||
|
target: 1,
|
||||||
|
kind: WeeklyGoalKind::WinUnder { seconds: 300 },
|
||||||
|
},
|
||||||
|
WeeklyGoalDef {
|
||||||
|
id: "weekly_draw_three",
|
||||||
|
description: "Win 1 Draw-3 game this week",
|
||||||
|
target: 1,
|
||||||
|
kind: WeeklyGoalKind::WinDrawThree,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Stable identifier for the ISO week containing `date`, e.g. `"2026-W17"`.
|
/// Stable identifier for the ISO week containing `date`, e.g. `"2026-W17"`.
|
||||||
@@ -89,6 +106,15 @@ mod tests {
|
|||||||
WeeklyGoalContext {
|
WeeklyGoalContext {
|
||||||
time_seconds: time,
|
time_seconds: time,
|
||||||
used_undo: undo,
|
used_undo: undo,
|
||||||
|
draw_mode: DrawMode::DrawOne,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ctx_d3(time: u64) -> WeeklyGoalContext {
|
||||||
|
WeeklyGoalContext {
|
||||||
|
time_seconds: time,
|
||||||
|
used_undo: false,
|
||||||
|
draw_mode: DrawMode::DrawThree,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,4 +171,20 @@ mod tests {
|
|||||||
assert!(key.starts_with("2026-W"));
|
assert!(key.starts_with("2026-W"));
|
||||||
assert_eq!(key.len(), 8);
|
assert_eq!(key.len(), 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn under_five_matches_wins_below_300_seconds() {
|
||||||
|
let g = weekly_goal_by_id("weekly_1_under_five").unwrap();
|
||||||
|
assert!(g.matches(&ctx(0, false)));
|
||||||
|
assert!(g.matches(&ctx(299, true)));
|
||||||
|
assert!(!g.matches(&ctx(300, false)));
|
||||||
|
assert!(!g.matches(&ctx(999, false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draw_three_goal_matches_only_draw_three_wins() {
|
||||||
|
let g = weekly_goal_by_id("weekly_draw_three").unwrap();
|
||||||
|
assert!(g.matches(&ctx_d3(600)));
|
||||||
|
assert!(!g.matches(&ctx(600, false)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ fn evaluate_weekly_goals(
|
|||||||
let ctx = WeeklyGoalContext {
|
let ctx = WeeklyGoalContext {
|
||||||
time_seconds: ev.time_seconds,
|
time_seconds: ev.time_seconds,
|
||||||
used_undo: game.0.undo_count > 0,
|
used_undo: game.0.undo_count > 0,
|
||||||
|
draw_mode: game.0.draw_mode.clone(),
|
||||||
};
|
};
|
||||||
for def in WEEKLY_GOALS {
|
for def in WEEKLY_GOALS {
|
||||||
if !def.matches(&ctx) {
|
if !def.matches(&ctx) {
|
||||||
@@ -191,11 +192,16 @@ mod tests {
|
|||||||
fn completing_a_goal_fires_event_and_awards_bonus() {
|
fn completing_a_goal_fires_event_and_awards_bonus() {
|
||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
// Pre-set the weekly_3_fast goal to 2/3 so the next fast win completes it.
|
// Pre-set the weekly_3_fast goal to 2/3 so the next fast win completes it.
|
||||||
app.world_mut()
|
// Also pre-complete weekly_1_under_five (target=1) and weekly_5_wins /
|
||||||
.resource_mut::<ProgressResource>()
|
// weekly_3_no_undo at target so a 60-second win only completes weekly_3_fast,
|
||||||
.0
|
// keeping the XP delta predictable.
|
||||||
.weekly_goal_progress
|
{
|
||||||
.insert("weekly_3_fast".to_string(), 2);
|
let mut p = app.world_mut().resource_mut::<ProgressResource>();
|
||||||
|
p.0.weekly_goal_progress.insert("weekly_3_fast".to_string(), 2);
|
||||||
|
p.0.weekly_goal_progress.insert("weekly_1_under_five".to_string(), 1);
|
||||||
|
p.0.weekly_goal_progress.insert("weekly_5_wins".to_string(), 5);
|
||||||
|
p.0.weekly_goal_progress.insert("weekly_3_no_undo".to_string(), 3);
|
||||||
|
}
|
||||||
// Match the current ISO week key so roll_weekly_goals doesn't clear it.
|
// Match the current ISO week key so roll_weekly_goals doesn't clear it.
|
||||||
let key = current_iso_week_key(Local::now().date_naive());
|
let key = current_iso_week_key(Local::now().date_naive());
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
|
|||||||
Reference in New Issue
Block a user