From 060180579ded7989fe19da7a0ac1f4fe03025a6f Mon Sep 17 00:00:00 2001 From: emilis Date: Sun, 1 Feb 2026 22:58:15 +0000 Subject: [PATCH] skip prompt button --- werewolves-proto/src/game/mod.rs | 4 ++ werewolves-proto/src/game/night.rs | 17 +++++ werewolves-proto/src/game/story.rs | 3 +- werewolves-proto/src/game_test/mod.rs | 22 ++++++ werewolves-proto/src/game_test/skip.rs | 78 ++++++++++++++++++++++ werewolves-proto/src/message/host.rs | 1 + werewolves-proto/src/message/night.rs | 4 +- werewolves/src/clients/host/host.rs | 28 ++++++++ werewolves/src/components/action/result.rs | 2 +- werewolves/src/test_util/mod.rs | 5 +- werewolves/src/test_util/result.rs | 3 +- 11 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 werewolves-proto/src/game_test/skip.rs diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index bd00616..e7bcf9f 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -171,6 +171,10 @@ impl Game { settings: village.settings(), }) } + (GameState::Night { night }, HostGameMessage::Night(HostNightMessage::SkipAction)) => { + night.skip_action()?; + self.process(HostGameMessage::GetState) + } (GameState::Night { night }, HostGameMessage::GetState) => { if let Some(res) = night.current_result() { return Ok(ServerToHostMessage::ActionResult( diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 6b31808..df5bc9a 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -421,6 +421,23 @@ impl Night { }) } + pub fn skip_action(&mut self) -> Result<()> { + match &mut self.night_state { + NightState::Active { + current_result, + current_changes, + .. + } => { + *current_result = CurrentResult::GoBackToSleepAfterShown { + result_with_data: ActionResult::SkippedByHost, + }; + current_changes.clear(); + } + NightState::Complete => return Err(GameError::NightOver), + } + self.next() + } + fn remove_reverted_prompts( mut action_queue: VecDeque, reverting: CharacterId, diff --git a/werewolves-proto/src/game/story.rs b/werewolves-proto/src/game/story.rs index 15598fe..506097b 100644 --- a/werewolves-proto/src/game/story.rs +++ b/werewolves-proto/src/game/story.rs @@ -69,7 +69,7 @@ pub struct NightChoice { impl NightChoice { pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option { - Some(Self { + (result != ActionResult::SkippedByHost).then_some(Self { prompt: StoryActionPrompt::new(prompt, village)?, result: StoryActionResult::new(result), }) @@ -107,6 +107,7 @@ pub enum StoryActionResult { impl StoryActionResult { pub fn new(result: ActionResult) -> Option { Some(match result { + ActionResult::SkippedByHost => return None, ActionResult::ShiftFailed => Self::ShiftFailed, ActionResult::BeholderSawNothing => Self::BeholderSawNothing, ActionResult::BeholderSawEverything => Self::BeholderSawEverything, diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 6d40640..f6b14c4 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -18,6 +18,7 @@ mod night_order; mod previous; mod revert; mod role; +mod skip; use crate::{ character::{Character, CharacterId}, @@ -339,9 +340,30 @@ pub trait GameExt { fn next_expect_game_over(&mut self) -> GameOver; fn prev(&mut self) -> ServerToHostMessage; fn mark_villager(&mut self) -> ActionPrompt; + fn skip(&mut self) -> ActionPrompt; + fn skip_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); } impl GameExt for Game { + fn skip(&mut self) -> ActionPrompt { + self.process(HostGameMessage::Night(HostNightMessage::SkipAction)) + .unwrap() + .prompt() + } + fn skip_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { + match self + .process(HostGameMessage::Night(HostNightMessage::SkipAction)) + .unwrap() + { + ServerToHostMessage::Daytime { + characters, + marked, + day, + .. + } => (characters, marked, day), + other => panic!("expected daytime, got {other:?}"), + } + } fn prev(&mut self) -> ServerToHostMessage { self.process(HostGameMessage::PreviousState).unwrap() } diff --git a/werewolves-proto/src/game_test/skip.rs b/werewolves-proto/src/game_test/skip.rs new file mode 100644 index 0000000..47c14d7 --- /dev/null +++ b/werewolves-proto/src/game_test/skip.rs @@ -0,0 +1,78 @@ +// Copyright (C) 2026 Emilis Bliūdžius +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + game::{Game, GameSettings, SetupRole}, + game_test::{ + ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log, + }, + message::night::ActionPromptTitle, +}; + +#[test] +pub fn skip_wolf_kill_all_villagers() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let wolf = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.fill_remaining_slots_with_villagers(players.len()); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + game.next_expect_day(); + game.execute().title().wolf_pack_kill(); + game.skip_expect_day(); + + assert!(game.village().characters().into_iter().all(|c| c.alive())) +} + +#[test] +pub fn skip_wolf_kill_has_seer() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let wolf = player_ids.next().unwrap(); + let seer = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Seer, seer); + settings.fill_remaining_slots_with_villagers(players.len()); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(wolf).character_id()); + game.r#continue(); + game.r#continue().sleep(); + + game.next_expect_day(); + game.execute().title().wolf_pack_kill(); + game.skip().title().seer(); + game.mark(game.character_by_player_id(wolf).character_id()); + game.r#continue(); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert!(game.village().characters().into_iter().all(|c| c.alive())) +} diff --git a/werewolves-proto/src/message/host.rs b/werewolves-proto/src/message/host.rs index 8948452..3e7eff0 100644 --- a/werewolves-proto/src/message/host.rs +++ b/werewolves-proto/src/message/host.rs @@ -60,6 +60,7 @@ pub enum HostNightMessage { ActionResponse(ActionResponse), Next, NextPage, + SkipAction, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index 4208341..2b8c20a 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -623,6 +623,7 @@ pub enum ActionResult { GoBackToSleep, ShiftFailed, Continue, + SkippedByHost, } impl ActionResult { @@ -660,7 +661,8 @@ impl ActionResult { | ActionResult::Mortician(_, _) | ActionResult::Insomniac(_) | ActionResult::GoBackToSleep - | ActionResult::Continue => return None, + | ActionResult::Continue + | ActionResult::SkippedByHost => return None, }) } } diff --git a/werewolves/src/clients/host/host.rs b/werewolves/src/clients/host/host.rs index 18672b2..416f2dd 100644 --- a/werewolves/src/clients/host/host.rs +++ b/werewolves/src/clients/host/host.rs @@ -560,12 +560,40 @@ impl Component for Host { } _ => None, }; + let skip_btn = match &self.state { + HostState::Prompt(_, _) | HostState::Result(_, _) => { + let on_skip_click = callback::send_message( + HostMessage::InGame(HostGameMessage::Night(HostNightMessage::SkipAction)), + self.send.clone(), + ); + let on_skip_click = { + Callback::from(move |_| { + on_skip_click.emit(()); + crate::components::modal::close_modal_by_id("skip-button"); + }) + }; + + Some(html! { + +

{"skip the current prompt?"}

+

{"if this is the final prompt of the night, you may not be able to go back"}

+ +
+ }) + } + _ => None, + }; let nav = self.big_screen.not().then(|| { html! { } }); diff --git a/werewolves/src/components/action/result.rs b/werewolves/src/components/action/result.rs index 3453139..2e10fad 100644 --- a/werewolves/src/components/action/result.rs +++ b/werewolves/src/components/action/result.rs @@ -124,7 +124,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { }; } - ActionResult::Continue => { + ActionResult::SkippedByHost | ActionResult::Continue => { props.on_complete.emit(HostMessage::GetState); return html! { diff --git a/werewolves/src/test_util/mod.rs b/werewolves/src/test_util/mod.rs index acc3c08..4bc43c8 100644 --- a/werewolves/src/test_util/mod.rs +++ b/werewolves/src/test_util/mod.rs @@ -223,6 +223,7 @@ pub enum TestScreen { impl From for TestScreen { fn from(value: ActionResultTitle) -> Self { TestScreen::Result(match value { + ActionResultTitle::SkippedByHost => ActionResult::SkippedByHost, ActionResultTitle::RoleBlocked => ActionResult::RoleBlocked, ActionResultTitle::Drunk => ActionResult::Drunk, ActionResultTitle::Seer => ActionResult::Seer(identity(), Alignment::Village), @@ -421,7 +422,9 @@ fn result_class(result: &ActionResult) -> Option<&'static str> { | ActionResultTitle::BeholderSawEverything | ActionResultTitle::Seer => Some("intel"), ActionResultTitle::ShiftFailed => Some("wolves"), - ActionResultTitle::GoBackToSleep | ActionResultTitle::Continue => None, + ActionResultTitle::GoBackToSleep + | ActionResultTitle::Continue + | ActionResultTitle::SkippedByHost => None, } } diff --git a/werewolves/src/test_util/result.rs b/werewolves/src/test_util/result.rs index 9497d24..0959b41 100644 --- a/werewolves/src/test_util/result.rs +++ b/werewolves/src/test_util/result.rs @@ -48,7 +48,8 @@ pub fn ResultScreenTest( | ActionResult::ShiftFailed | ActionResult::Continue | ActionResult::Drunk - | ActionResult::RoleBlocked => html! {}, + | ActionResult::RoleBlocked + | ActionResult::SkippedByHost => html! {}, ActionResult::Seer(target, alignment) => { let all = Alignment::ALL .into_iter()