feat(engine): add Play-by-Seed dialog with solver preview

Adds a numeric-input modal (PlayBySeedPlugin) that lets the player type
a decimal seed and receive an instant solver-verified verdict before the
hand is dealt.  A new HomeMode::PlayBySeed card surfaces it in the home
overlay, matched by the StartPlayBySeedRequestEvent carrier.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-08 20:19:02 -07:00
parent 395a322adc
commit 0cb15872b1
5 changed files with 713 additions and 11 deletions
+26 -5
View File
@@ -23,8 +23,8 @@ use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
use crate::daily_challenge_plugin::DailyChallengeResource;
use crate::events::{
InfoToastEvent, NewGameRequestEvent, StartChallengeRequestEvent,
StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent, StartZenRequestEvent,
ToggleProfileRequestEvent,
StartDailyChallengeRequestEvent, StartPlayBySeedRequestEvent, StartTimeAttackRequestEvent,
StartZenRequestEvent, ToggleProfileRequestEvent,
};
use crate::font_plugin::FontResource;
use crate::progress_plugin::ProgressResource;
@@ -96,6 +96,7 @@ enum HomeMode {
Zen,
Challenge,
TimeAttack,
PlayBySeed,
}
impl HomeMode {
@@ -107,6 +108,7 @@ impl HomeMode {
HomeMode::Zen => "Zen Mode",
HomeMode::Challenge => "Challenge",
HomeMode::TimeAttack => "Time Attack",
HomeMode::PlayBySeed => "Play by Seed",
}
}
@@ -118,6 +120,7 @@ impl HomeMode {
HomeMode::Zen => "No timer, no score. Just the cards.",
HomeMode::Challenge => "Hand-picked hard deals. No undo. Win to advance.",
HomeMode::TimeAttack => "How many can you finish in ten minutes?",
HomeMode::PlayBySeed => "Enter any number to play a specific deal.",
}
}
@@ -150,6 +153,9 @@ impl HomeMode {
// ships ▲ (up triangle) but evidently not the sideways
// siblings.
HomeMode::TimeAttack => "\u{2192}",
// Number sign — ASCII, universally available. Reads as
// "a specific number / seed ID".
HomeMode::PlayBySeed => "#",
}
}
@@ -162,6 +168,7 @@ impl HomeMode {
HomeMode::Zen => "Z",
HomeMode::Challenge => "X",
HomeMode::TimeAttack => "T",
HomeMode::PlayBySeed => "6",
}
}
@@ -238,6 +245,7 @@ impl Plugin for HomePlugin {
.add_message::<StartChallengeRequestEvent>()
.add_message::<StartTimeAttackRequestEvent>()
.add_message::<StartDailyChallengeRequestEvent>()
.add_message::<StartPlayBySeedRequestEvent>()
.add_message::<InfoToastEvent>()
.add_message::<ToggleProfileRequestEvent>()
.add_message::<SettingsChangedEvent>()
@@ -423,6 +431,7 @@ fn handle_home_card_click(
mut challenge: MessageWriter<StartChallengeRequestEvent>,
mut time_attack: MessageWriter<StartTimeAttackRequestEvent>,
mut daily: MessageWriter<StartDailyChallengeRequestEvent>,
mut play_by_seed: MessageWriter<StartPlayBySeedRequestEvent>,
mut info_toast: MessageWriter<InfoToastEvent>,
) {
let level = progress.as_ref().map_or(0, |p| p.0.level);
@@ -457,6 +466,9 @@ fn handle_home_card_click(
HomeMode::TimeAttack => {
time_attack.write(StartTimeAttackRequestEvent);
}
HomeMode::PlayBySeed => {
play_by_seed.write(StartPlayBySeedRequestEvent);
}
}
// Close the modal after dispatching the launch event.
@@ -619,6 +631,7 @@ fn digit_to_home_mode(key: KeyCode) -> Option<HomeMode> {
KeyCode::Digit3 => Some(HomeMode::Zen),
KeyCode::Digit4 => Some(HomeMode::Challenge),
KeyCode::Digit5 => Some(HomeMode::TimeAttack),
KeyCode::Digit6 => Some(HomeMode::PlayBySeed),
_ => None,
}
}
@@ -646,6 +659,7 @@ fn handle_home_digit_keys(
mut challenge: MessageWriter<StartChallengeRequestEvent>,
mut time_attack: MessageWriter<StartTimeAttackRequestEvent>,
mut daily: MessageWriter<StartDailyChallengeRequestEvent>,
mut play_by_seed: MessageWriter<StartPlayBySeedRequestEvent>,
) {
// Modal-scoped: do nothing when the Mode Launcher isn't open.
if screens.is_empty() {
@@ -658,6 +672,7 @@ fn handle_home_digit_keys(
KeyCode::Digit3,
KeyCode::Digit4,
KeyCode::Digit5,
KeyCode::Digit6,
]
.into_iter()
.find(|k| keys.just_pressed(*k))
@@ -687,6 +702,9 @@ fn handle_home_digit_keys(
HomeMode::TimeAttack => {
time_attack.write(StartTimeAttackRequestEvent);
}
HomeMode::PlayBySeed => {
play_by_seed.write(StartPlayBySeedRequestEvent);
}
}
// Close the modal after dispatching the launch event — same shape as
@@ -784,6 +802,7 @@ fn spawn_home_screen(commands: &mut Commands, ctx: HomeContext<'_>) {
HomeMode::Zen,
HomeMode::Challenge,
HomeMode::TimeAttack,
HomeMode::PlayBySeed,
] {
spawn_mode_card(grid, mode, &ctx);
}
@@ -999,6 +1018,7 @@ fn home_mode_focus_order(mode: HomeMode) -> i32 {
HomeMode::Zen => 2,
HomeMode::Challenge => 3,
HomeMode::TimeAttack => 4,
HomeMode::PlayBySeed => 5,
}
}
@@ -1402,13 +1422,14 @@ mod tests {
HomeMode::Zen,
HomeMode::Challenge,
HomeMode::TimeAttack,
HomeMode::PlayBySeed,
] {
assert!(
modes.contains(&expected),
"missing card for {expected:?}; found {modes:?}"
);
}
assert_eq!(modes.len(), 5, "exactly five cards expected");
assert_eq!(modes.len(), 6, "exactly six cards expected");
}
#[test]
@@ -1600,7 +1621,7 @@ mod tests {
.map(|(c, f)| (c.0, *f))
.collect();
assert_eq!(cards.len(), 5, "all five cards must carry a Focusable");
assert_eq!(cards.len(), 6, "all six cards must carry a Focusable");
for (mode, focusable) in &cards {
assert_eq!(
focusable.group,
@@ -1626,7 +1647,7 @@ mod tests {
for (mode, disabled) in states {
match mode {
HomeMode::Classic | HomeMode::Daily => assert!(
HomeMode::Classic | HomeMode::Daily | HomeMode::PlayBySeed => assert!(
!disabled,
"{mode:?} must not be Disabled at level 0 (it's never locked)"
),