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:
funman300
2026-04-25 17:18:32 -07:00
parent 09d62f4255
commit 788ac9f65a
8 changed files with 346 additions and 9 deletions
+17 -4
View File
@@ -24,10 +24,12 @@ use solitaire_core::pile::PileType;
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
use crate::card_plugin::{CardEntity, TABLEAU_FAN_FRAC};
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
use crate::events::{
DrawRequestEvent, MoveRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
};
use crate::game_plugin::GameMutation;
use crate::progress_plugin::ProgressResource;
use crate::layout::{Layout, LayoutResource};
use crate::resources::{DragState, GameStateResource};
@@ -61,6 +63,7 @@ impl Plugin for InputPlugin {
fn handle_keyboard(
keys: Res<ButtonInput<KeyCode>>,
progress: Option<Res<ProgressResource>>,
mut undo: EventWriter<UndoRequestEvent>,
mut new_game: EventWriter<NewGameRequestEvent>,
mut draw: EventWriter<DrawRequestEvent>,
@@ -72,10 +75,20 @@ fn handle_keyboard(
new_game.send(NewGameRequestEvent::default());
}
if keys.just_pressed(KeyCode::KeyZ) {
new_game.send(NewGameRequestEvent {
seed: None,
mode: Some(solitaire_core::game_state::GameMode::Zen),
});
// Zen / Challenge / Time Attack are gated to level >= CHALLENGE_UNLOCK_LEVEL.
// X is gated separately by ChallengePlugin.
let level = progress.as_ref().map_or(0, |p| p.0.level);
if level >= CHALLENGE_UNLOCK_LEVEL {
new_game.send(NewGameRequestEvent {
seed: None,
mode: Some(solitaire_core::game_state::GameMode::Zen),
});
} else {
info!(
"Zen mode locked — reach level {} (currently {}).",
CHALLENGE_UNLOCK_LEVEL, level
);
}
}
if keys.just_pressed(KeyCode::KeyD) {
draw.send(DrawRequestEvent);