feat(engine,core,data): add Challenge mode with seed list and level-5 gate
Phase 6 part 4b (partial): - GameMode::Challenge variant in solitaire_core. undo() returns RuleViolation when mode is Challenge so the player commits to each decision. - solitaire_data::challenge defines a stable CHALLENGE_SEEDS list with challenge_seed_for(index) wrapping modulo length. - PlayerProgress.challenge_index (serde-default for older saves) tracks how far the player has progressed. - ChallengePlugin advances the cursor on Challenge-mode wins, persists, and emits ChallengeAdvancedEvent. Pressing X starts a Challenge-mode game with the current seed; gated to level >= CHALLENGE_UNLOCK_LEVEL (5). - InputPlugin's Z key (Zen mode) is now also gated to level >= 5. Time Attack and unlock UI still deferred. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -16,15 +16,18 @@ pub enum DrawMode {
|
||||
DrawThree,
|
||||
}
|
||||
|
||||
/// Top-level game mode. Affects scoring and (eventually) timer behaviour.
|
||||
/// Top-level game mode. Affects scoring, undo, and (eventually) timer behaviour.
|
||||
///
|
||||
/// - `Classic`: standard Klondike scoring and timer.
|
||||
/// - `Zen`: scoring suppressed (stays at 0); intended for relaxed play.
|
||||
/// - `Classic`: standard Klondike scoring, undo allowed.
|
||||
/// - `Zen`: scoring suppressed (stays at 0); undo allowed; intended for relaxed play.
|
||||
/// - `Challenge`: standard scoring, **undo disabled** (returns
|
||||
/// `MoveError::RuleViolation`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum GameMode {
|
||||
#[default]
|
||||
Classic,
|
||||
Zen,
|
||||
Challenge,
|
||||
}
|
||||
|
||||
/// Snapshot of game state used for undo.
|
||||
@@ -261,10 +264,16 @@ impl GameState {
|
||||
}
|
||||
|
||||
/// Restore the most recent undo snapshot and apply the undo score penalty (-15).
|
||||
/// Disabled in `GameMode::Challenge` — returns `MoveError::RuleViolation`.
|
||||
pub fn undo(&mut self) -> Result<(), MoveError> {
|
||||
if self.is_won {
|
||||
return Err(MoveError::GameAlreadyWon);
|
||||
}
|
||||
if self.mode == GameMode::Challenge {
|
||||
return Err(MoveError::RuleViolation(
|
||||
"undo is disabled in Challenge mode".into(),
|
||||
));
|
||||
}
|
||||
let snapshot = self.undo_stack.pop().ok_or(MoveError::UndoStackEmpty)?;
|
||||
self.piles = snapshot.piles;
|
||||
self.score = if self.mode == GameMode::Zen {
|
||||
@@ -559,6 +568,34 @@ mod tests {
|
||||
assert_eq!(g.draw_mode, DrawMode::DrawThree);
|
||||
}
|
||||
|
||||
// --- GameMode: Challenge ---
|
||||
|
||||
#[test]
|
||||
fn challenge_mode_disables_undo() {
|
||||
let mut g = GameState::new_with_mode(42, DrawMode::DrawOne, GameMode::Challenge);
|
||||
g.draw().unwrap();
|
||||
let result = g.undo();
|
||||
assert!(matches!(result, Err(MoveError::RuleViolation(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn challenge_mode_still_allows_normal_moves() {
|
||||
let g = GameState::new_with_mode(42, DrawMode::DrawOne, GameMode::Challenge);
|
||||
// Just verify the game initialises cleanly with Challenge mode.
|
||||
assert_eq!(g.mode, GameMode::Challenge);
|
||||
assert_eq!(g.score, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn challenge_mode_scoring_applies_normally() {
|
||||
// Challenge uses Classic scoring; only undo is disabled.
|
||||
let g = GameState::new_with_mode(42, DrawMode::DrawOne, GameMode::Challenge);
|
||||
assert_eq!(g.score, 0);
|
||||
// Note: Verifying score increases on actual moves would require
|
||||
// hand-crafting a legal move from the dealt state. We rely on the
|
||||
// fact that move_cards' score path is identical to Classic.
|
||||
}
|
||||
|
||||
// --- Auto-complete ---
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user