From 391e4ea6d707426aa27222d20d0a91c3200995ec Mon Sep 17 00:00:00 2001 From: emilis Date: Wed, 3 Dec 2025 23:14:12 +0000 Subject: [PATCH] bugfix: shapeshifted PRs no longer get prompted --- werewolves-proto/src/game/night.rs | 98 +++++++++++ werewolves-proto/src/game/night/next.rs | 79 +++++++-- werewolves-proto/src/game_test/mod.rs | 74 ++++---- werewolves-proto/src/game_test/previous.rs | 22 +-- werewolves-proto/src/game_test/role/elder.rs | 8 +- .../src/game_test/role/insomniac.rs | 12 +- .../src/game_test/role/shapeshifter.rs | 161 +++++++++++++++++- werewolves-proto/src/message/night.rs | 4 +- 8 files changed, 381 insertions(+), 77 deletions(-) diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 35c9ff3..677610b 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -494,6 +494,32 @@ impl Night { } pub fn previous_state(&mut self) -> Result<()> { + #[cfg(test)] + { + use colored::Colorize; + log::info!( + "{}({}): {}: [{}]; {}: [{}]", + "previous_state".bold(), + "initial".bold().yellow(), + "queue state".dimmed(), + self.action_queue + .iter() + .map(|a| a.title().to_string()) + .collect::>() + .join(", ") + .bold() + .green(), + "used actions state".dimmed(), + self.used_actions + .iter() + .map(|(a, _, _)| a.title().to_string()) + .collect::>() + .join(", ") + .bold() + .yellow() + ); + } + let all_current_changes = self.current_changes(); let (current_prompt, current_result, current_changes) = match &mut self.night_state { NightState::Active { current_prompt, @@ -534,6 +560,15 @@ impl Night { _ => None, }); + #[cfg(test)] + { + use colored::Colorize; + log::info!( + "{} [{}]", + "setting current prompt to".dimmed(), + prompt.title().to_string().bold().red(), + ); + } core::mem::swap(&mut prompt, current_prompt); let last_prompt = prompt; let next_prompt_ss_related_role_change = match (&last_prompt, ss_target) { @@ -551,9 +586,60 @@ impl Night { // role change associated with the shapeshift self.action_queue.push_front(last_prompt); } + // Check if the prompt is from someone shifted. in that case, it goes back in. + if let Some(source) = current_prompt.character_id() + && !current_prompt.is_wolfy() + && !matches!( + current_prompt, + ActionPrompt::RoleChange { + new_role: RoleTitle::Werewolf, + .. + } + ) + && let Some(NightChange::Shapeshift { into, .. }) = + ChangesLookup::new(&all_current_changes).shapeshift_change() + && source == into + { + #[cfg(test)] + { + use colored::Colorize; + log::info!( + "{source} is shifted, got prompt [{}] but calling previous_state again", + current_prompt.title().to_string().bold().red() + ); + } + + return self.previous_state(); + } *current_result = CurrentResult::None; *current_changes = Vec::new(); + + #[cfg(test)] + { + use colored::Colorize; + log::info!( + "{}({}): {}: [{}]; {}: [{}]", + "previous_state".bold(), + "final".bold().green(), + "queue state".dimmed(), + self.action_queue + .iter() + .map(|a| a.title().to_string()) + .collect::>() + .join(", ") + .bold() + .green(), + "used actions state".dimmed(), + self.used_actions + .iter() + .map(|(a, _, _)| a.title().to_string()) + .collect::>() + .join(", ") + .bold() + .yellow() + ); + } Ok(()) } else { Err(GameError::NoPreviousState) @@ -902,9 +988,21 @@ impl Night { &self, outcome: ResponseOutcome, ) -> ResponseOutcome { + let ss_change = ChangesLookup::new(&self.current_changes()).shapeshift_change(); + let same_char = self .current_character_id() .and_then(|curr| { + let is_shifted = ss_change + .as_ref() + .and_then(|c| match c { + NightChange::Shapeshift { into, .. } => Some(*into == curr), + _ => None, + }) + .unwrap_or_default(); + if is_shifted { + return None; + } self.action_queue .iter() .next() diff --git a/werewolves-proto/src/game/night/next.rs b/werewolves-proto/src/game/night/next.rs index 381a9a6..9f02f5e 100644 --- a/werewolves-proto/src/game/night/next.rs +++ b/werewolves-proto/src/game/night/next.rs @@ -19,7 +19,10 @@ use crate::{ character::CharacterId, diedto::DiedTo, error::GameError, - game::night::{CurrentResult, Night, NightState, changes::NightChange}, + game::night::{ + CurrentResult, Night, NightState, + changes::{ChangesLookup, NightChange}, + }, message::night::{ActionPrompt, ActionResult, ActionType}, role::{RoleBlock, RoleTitle}, }; @@ -82,22 +85,66 @@ impl Night { } => return Err(GameError::AwaitingResponse), NightState::Complete => return Err(GameError::NightOver), } - if let Some(prompt) = self.pull_next_prompt_with_dead_ignore()? { - if let ActionPrompt::Insomniac { character_id } = &prompt - && self.get_visits_for(character_id.character_id).is_empty() - { - // skip! - self.used_actions.pop(); // it will be re-added - return self.next(); + loop { + if let Some(prompt) = self.pull_next_prompt_with_dead_ignore()? { + if let ActionPrompt::Insomniac { character_id } = &prompt + && self.get_visits_for(character_id.character_id).is_empty() + { + // skip! + self.used_actions.pop(); // it will be re-added + return self.next(); + } + let current_changes = self.current_changes(); + if let Some(NightChange::Shapeshift { into, .. }) = + ChangesLookup::new(¤t_changes).shapeshift_change() + && !prompt.is_wolfy() + && let Some(char) = prompt.character_id() + && char == into + { + #[cfg(test)] + { + use colored::Colorize; + log::info!( + "{char} is shifted, ignoring prompt [{}]", + prompt.title().to_string().bold().red() + ); + } + self.used_actions.push(( + prompt.clone(), + ActionResult::GoBackToSleep, + Vec::new(), + )); + continue; + } + #[cfg(test)] + { + use colored::Colorize; + log::info!( + "{}: {}: [{}]; {}: [{}]", + "next".bold(), + "setting prompt to".dimmed(), + prompt.title().to_string().bold().purple(), + "queue".dimmed(), + self.action_queue + .iter() + .map(|a| a.title().to_string()) + .collect::>() + .join(", ") + .bold() + .cyan() + ); + } + self.night_state = NightState::Active { + current_prompt: prompt, + current_result: CurrentResult::None, + current_changes: Vec::new(), + current_page: 0, + }; + break; + } else { + self.night_state = NightState::Complete; + break; } - self.night_state = NightState::Active { - current_prompt: prompt, - current_result: CurrentResult::None, - current_changes: Vec::new(), - current_page: 0, - }; - } else { - self.night_state = NightState::Complete; } Ok(()) diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 04a9859..b5745d4 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -30,7 +30,7 @@ use crate::{ night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits}, }, player::PlayerId, - role::{Alignment, Killer, Powerful, RoleTitle}, + role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle}, }; use colored::Colorize; use core::{num::NonZeroU8, ops::Range}; @@ -182,7 +182,7 @@ pub trait ActionResultExt { fn r#continue(&self); fn seer(&self) -> Alignment; fn insomniac(&self) -> Visits; - fn arcanist(&self) -> bool; + fn arcanist(&self) -> AlignmentEq; fn role_blocked(&self); fn gravedigger(&self) -> Option; fn power_seer(&self) -> Powerful; @@ -248,9 +248,9 @@ impl ActionResultExt for ActionResult { } } - fn arcanist(&self) -> bool { + fn arcanist(&self) -> AlignmentEq { match self { - ActionResult::Arcanist(same) => same.same(), + ActionResult::Arcanist(same) => same.clone(), resp => panic!("expected an arcanist result, got {resp:?}"), } } @@ -949,6 +949,17 @@ fn big_game_test_based_on_story_test() { .shapeshift_failed(); game.r#continue().sleep(); + game.next().title().maple_wolf(); + game.mark( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(werewolf).character_id()); game.r#continue().seer(); @@ -985,17 +996,6 @@ fn big_game_test_based_on_story_test() { assert!(!game.r#continue().empath()); game.r#continue().sleep(); - game.next().title().maple_wolf(); - game.mark( - game.living_villager_excl(protect.player_id()) - .character_id(), - ); - game.r#continue().sleep(); - - game.next().title().hunter(); - game.mark(game.character_by_player_id(insomniac).character_id()); - game.r#continue().sleep(); - game.next().title().insomniac(); game.r#continue().insomniac(); game.r#continue().sleep(); @@ -1033,6 +1033,13 @@ fn big_game_test_based_on_story_test() { game.next().title().shapeshifter(); game.r#continue().sleep(); + game.next().title().maple_wolf(); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(werewolf).character_id()); game.r#continue().seer(); @@ -1069,13 +1076,6 @@ fn big_game_test_based_on_story_test() { assert!(game.r#continue().empath()); game.r#continue().sleep(); - game.next().title().maple_wolf(); - game.r#continue().sleep(); - - game.next().title().hunter(); - game.mark(game.character_by_player_id(insomniac).character_id()); - game.r#continue().sleep(); - game.next().title().insomniac(); game.r#continue().insomniac(); game.r#continue().sleep(); @@ -1114,6 +1114,13 @@ fn big_game_test_based_on_story_test() { ); game.r#continue().sleep(); + game.next().title().maple_wolf(); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(shapeshifter).character_id()); game.r#continue().seer(); @@ -1145,13 +1152,6 @@ fn big_game_test_based_on_story_test() { assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack); game.r#continue().sleep(); - game.next().title().maple_wolf(); - game.r#continue().sleep(); - - game.next().title().hunter(); - game.mark(game.character_by_player_id(insomniac).character_id()); - game.r#continue().sleep(); - game.next().title().insomniac(); game.r#continue().insomniac(); game.r#continue().sleep(); @@ -1166,6 +1166,14 @@ fn big_game_test_based_on_story_test() { game.mark(game.character_by_player_id(mortician).character_id()); game.r#continue().sleep(); + game.next().title().maple_wolf(); + game.mark(game.character_by_player_id(hunter).character_id()); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(insomniac).character_id()); game.r#continue().seer(); @@ -1200,14 +1208,6 @@ fn big_game_test_based_on_story_test() { ); game.r#continue().sleep(); - game.next().title().maple_wolf(); - game.mark(game.character_by_player_id(hunter).character_id()); - game.r#continue().sleep(); - - game.next().title().hunter(); - game.mark(game.character_by_player_id(empath).character_id()); - game.r#continue().sleep(); - game.next().title().insomniac(); game.r#continue().insomniac(); game.r#continue().sleep(); diff --git a/werewolves-proto/src/game_test/previous.rs b/werewolves-proto/src/game_test/previous.rs index 9fa8b9f..5268488 100644 --- a/werewolves-proto/src/game_test/previous.rs +++ b/werewolves-proto/src/game_test/previous.rs @@ -361,6 +361,17 @@ fn previous_prompt() { .shapeshift_failed(); game.r#continue().sleep(); + game.next().title().maple_wolf(); + game.mark( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(werewolf).character_id()); game.r#continue().seer(); @@ -397,17 +408,6 @@ fn previous_prompt() { assert!(!game.r#continue().empath()); game.r#continue().sleep(); - game.next().title().maple_wolf(); - game.mark( - game.living_villager_excl(protect.player_id()) - .character_id(), - ); - game.r#continue().sleep(); - - game.next().title().hunter(); - game.mark(game.character_by_player_id(insomniac).character_id()); - game.r#continue().sleep(); - game.next().title().insomniac(); game.r#continue().insomniac(); game.r#continue().sleep(); diff --git a/werewolves-proto/src/game_test/role/elder.rs b/werewolves-proto/src/game_test/role/elder.rs index 1319326..f18a3f1 100644 --- a/werewolves-proto/src/game_test/role/elder.rs +++ b/werewolves-proto/src/game_test/role/elder.rs @@ -238,15 +238,15 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() { game.mark(villagers.next().unwrap()); game.r#continue().sleep(); + game.next().title().hunter(); + game.mark(game.character_by_player_id(wolf_player_id).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.living_villager_excl(seer_player_id).character_id()); assert_eq!(game.r#continue().seer(), Alignment::Village); game.r#continue().sleep(); - game.next().title().hunter(); - game.mark(game.character_by_player_id(wolf_player_id).character_id()); - game.r#continue().sleep(); - game.next_expect_day(); assert_eq!( diff --git a/werewolves-proto/src/game_test/role/insomniac.rs b/werewolves-proto/src/game_test/role/insomniac.rs index 4a8aba0..5c2e577 100644 --- a/werewolves-proto/src/game_test/role/insomniac.rs +++ b/werewolves-proto/src/game_test/role/insomniac.rs @@ -19,7 +19,7 @@ use crate::{ game::{Game, GameSettings, SetupRole}, game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, message::night::{ActionPromptTitle, Visits}, - role::{Role, RoleTitle}, + role::{AlignmentEq, Role, RoleTitle}, }; #[test] @@ -54,7 +54,7 @@ fn sees_visits() { let mut villagers = game.villager_character_ids().into_iter(); game.mark(villagers.next().unwrap()); game.mark(villagers.next().unwrap()); - assert_eq!(game.r#continue().arcanist(), true); + assert_eq!(game.r#continue().arcanist(), AlignmentEq::Same); game.r#continue().sleep(); game.next_expect_day(); @@ -77,7 +77,7 @@ fn sees_visits() { game.character_by_player_id(insomniac_player_id) .character_id(), ); - assert_eq!(game.r#continue().arcanist(), true); + assert_eq!(game.r#continue().arcanist(), AlignmentEq::Same); game.r#continue().sleep(); game.next().title().insomniac(); @@ -124,7 +124,7 @@ fn direwolf_block_prevents_visits_so_they_are_not_seen() { let mut villagers = game.villager_character_ids().into_iter(); game.mark(villagers.next().unwrap()); game.mark(villagers.next().unwrap()); - assert_eq!(game.r#continue().arcanist(), true); + assert_eq!(game.r#continue().arcanist(), AlignmentEq::Same); game.r#continue().sleep(); game.next_expect_day(); @@ -185,7 +185,7 @@ fn dead_people_still_get_prompts_to_trigger_visits() { let mut villagers = game.villager_character_ids().into_iter(); game.mark(villagers.next().unwrap()); game.mark(villagers.next().unwrap()); - assert_eq!(game.r#continue().arcanist(), true); + assert_eq!(game.r#continue().arcanist(), AlignmentEq::Same); game.r#continue().sleep(); game.next_expect_day(); @@ -202,7 +202,7 @@ fn dead_people_still_get_prompts_to_trigger_visits() { game.next().title().arcanist(); game.mark(game.character_by_player_id(seer).character_id()); game.mark(game.character_by_player_id(insomniac).character_id()); - assert!(game.r#continue().arcanist()); + assert_eq!(game.r#continue().arcanist(), AlignmentEq::Same); game.r#continue().sleep(); game.next().title().insomniac(); diff --git a/werewolves-proto/src/game_test/role/shapeshifter.rs b/werewolves-proto/src/game_test/role/shapeshifter.rs index 0cc23c1..22dfb34 100644 --- a/werewolves-proto/src/game_test/role/shapeshifter.rs +++ b/werewolves-proto/src/game_test/role/shapeshifter.rs @@ -17,7 +17,7 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use crate::{ character::CharacterId, - game::{Game, GameSettings, SetupRole}, + game::{Game, GameSettings, GameState, SetupRole, night::changes::ChangesLookup}, game_test::{ ActionPromptTitleExt, ActionResultExt, GameExt, ServerToHostMessageExt, SettingsExt, gen_players, init_log, @@ -223,6 +223,7 @@ fn i_would_simply_refuse() { #[test] fn shapeshift_fail_can_continue() { + init_log(); let players = gen_players(1..21); let mut player_ids = players.iter().map(|p| p.player_id); let shapeshifter = player_ids.next().unwrap(); @@ -277,3 +278,161 @@ fn shapeshift_fail_can_continue() { game.next_expect_day(); } + +#[test] +fn shapeshift_removes_village_prompt() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let shapeshifter = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + let gravedigger = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Gravedigger, gravedigger); + 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.mark_for_execution(game.living_villager().character_id()); + game.execute().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(gravedigger).character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + + match game + .process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::Shapeshift, + ))) + .unwrap() + { + ServerToHostMessage::ActionResult(Some(ident), ActionResult::Continue) => { + assert_eq!(ident, game.character_by_player_id(shapeshifter).identity()); + } + other => panic!("expected shift, got {other:?}"), + }; + + assert_eq!( + game.next(), + ActionPrompt::RoleChange { + character_id: game.character_by_player_id(gravedigger).identity(), + new_role: RoleTitle::Werewolf + } + ); + game.r#continue().sleep(); + + game.next_expect_day(); +} + +#[test] +fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let shapeshifter = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + let gravedigger = player_ids.next().unwrap(); + let beholder = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Gravedigger, gravedigger); + settings.add_and_assign(SetupRole::Beholder, beholder); + 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(); + + let executed = game.living_villager(); + game.mark_for_execution(executed.character_id()); + game.execute().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(gravedigger).character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + + match game + .process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::Shapeshift, + ))) + .unwrap() + { + ServerToHostMessage::ActionResult(Some(ident), ActionResult::Continue) => { + assert_eq!(ident, game.character_by_player_id(shapeshifter).identity()); + } + other => panic!("expected shift, got {other:?}"), + }; + + assert_eq!( + game.next(), + ActionPrompt::RoleChange { + character_id: game.character_by_player_id(gravedigger).identity(), + new_role: RoleTitle::Werewolf + } + ); + game.r#continue().sleep(); + + game.next().title().beholder(); + + assert_eq!( + game.prev(), + ServerToHostMessage::ActionPrompt( + ActionPrompt::RoleChange { + character_id: game.character_by_player_id(gravedigger).identity(), + new_role: RoleTitle::Werewolf + }, + 0 + ) + ); + + assert_eq!( + game.prev(), + ServerToHostMessage::ActionPrompt( + ActionPrompt::Shapeshifter { + character_id: game.character_by_player_id(shapeshifter).identity() + }, + 0 + ) + ); + let current_changes = match game.game_state() { + GameState::Night { night } => night.current_changes(), + GameState::Day { .. } => unreachable!(), + }; + assert_eq!( + ChangesLookup::new(¤t_changes).shapeshift_change(), + None + ); + game.r#continue().sleep(); + + game.next().title().gravedigger(); + game.mark(executed.character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Villager)); + game.r#continue().sleep(); + + game.next().title().beholder(); + game.mark_villager(); + game.r#continue().sleep(); + + game.next_expect_day(); + assert_eq!( + game.character_by_player_id(gravedigger).role_title(), + RoleTitle::Gravedigger + ); + assert_eq!( + game.character_by_player_id(shapeshifter) + .shapeshifter_ref() + .unwrap() + .shifted_into + .clone(), + None + ); +} diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index db0e9ed..6115fac 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -95,7 +95,7 @@ pub enum ActionPrompt { dead_players: Box<[CharacterIdentity]>, marked: Option, }, - #[checks(ActionType::Other)] + #[checks(ActionType::VillageKill)] Hunter { character_id: CharacterIdentity, current_target: Option, @@ -108,7 +108,7 @@ pub enum ActionPrompt { living_players: Box<[CharacterIdentity]>, marked: Option, }, - #[checks(ActionType::Other)] + #[checks(ActionType::VillageKill)] MapleWolf { character_id: CharacterIdentity, nights_til_starvation: u8,