diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index c2ff442..972cbed 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -589,11 +589,16 @@ impl Character { }); } } - Role::Beholder => prompts.push(ActionPrompt::Beholder { - character_id: self.identity(), - living_players: village.living_players_excluding(self.character_id()), - marked: None, - }), + Role::Beholder => { + prompts.push(ActionPrompt::BeholderChooses { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }); + prompts.push(ActionPrompt::BeholderWakes { + character_id: self.identity(), + }) + } Role::MasonLeader { .. } => { log::error!( "night_action_prompts got to MasonLeader, should be handled before the living check" diff --git a/werewolves-proto/src/error.rs b/werewolves-proto/src/error.rs index 9bd4d35..f7f28c4 100644 --- a/werewolves-proto/src/error.rs +++ b/werewolves-proto/src/error.rs @@ -99,8 +99,8 @@ pub enum GameError { NoPreviousDuringDay, #[error("militia already spent")] MilitiaSpent, - #[error("this role doesn't mark anyone")] - RoleDoesntMark, + #[error("this prompt doesn't mark anyone")] + PromptDoesntMark, #[error("cannot shapeshift on a non-shapeshifter prompt")] ShapeshiftingIsForShapeshifters, #[error("must select a target")] diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 677610b..28a9ae3 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -32,7 +32,9 @@ use crate::{ kill::{self, KillOutcome}, night::changes::{ChangesLookup, NightChange}, }, - message::night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits}, + message::night::{ + ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, ActionType, Visits, + }, role::RoleTitle, }; @@ -62,7 +64,8 @@ impl From for ResponseOutcome { impl ActionPrompt { fn unless(&self) -> Option { match &self { - ActionPrompt::TraitorIntro { .. } + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::TraitorIntro { .. } | ActionPrompt::Insomniac { .. } | ActionPrompt::MasonsWake { .. } | ActionPrompt::WolvesIntro { .. } @@ -124,7 +127,7 @@ impl ActionPrompt { marked: Some(marked), .. } - | ActionPrompt::Beholder { + | ActionPrompt::BeholderChooses { marked: Some(marked), .. } @@ -169,7 +172,7 @@ impl ActionPrompt { | ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. } - | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::BeholderChooses { marked: None, .. } | ActionPrompt::MasonLeaderRecruit { marked: None, .. } | ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. } @@ -1003,10 +1006,13 @@ impl Night { if is_shifted { return None; } - self.action_queue - .iter() - .next() - .map(|n| n.character_id() == Some(curr)) + self.action_queue.iter().next().map(|n| match n { + ActionPrompt::BeholderWakes { character_id } => !matches!( + self.get_what_beholder_saw(character_id.character_id), + None | Some(ActionResult::GoBackToSleep) + ), + _ => n.character_id() == Some(curr), + }) }) .unwrap_or_default(); @@ -1175,6 +1181,25 @@ impl Night { } } + fn get_what_beholder_saw(&self, beholder_id: CharacterId) -> Option { + self.get_actions_for(beholder_id) + .into_iter() + .find_map(|(p, _)| match p { + ActionPrompt::BeholderChooses { marked, .. } => marked, + _ => None, + }) + .and_then(|beholder_target| { + self.died_to_tonight(beholder_target) + .ok() + .flatten() + .and_then(|_| { + self.get_actions_for(beholder_target) + .into_iter() + .find_map(|(prompt, result)| prompt.is_beholdable().then_some(result)) + }) + }) + } + pub const fn village(&self) -> &Village { &self.village } @@ -1265,6 +1290,43 @@ impl Night { ChangesLookup::new(&self.current_changes()).died_to(character_id, self.night, &self.village) } + fn get_actions_for(&self, char_id: CharacterId) -> Box<[(ActionPrompt, ActionResult)]> { + self.used_actions() + .into_iter() + .chain(match &self.night_state { + NightState::Active { + current_prompt, + current_result, + .. + } => match current_result { + CurrentResult::None => None, + CurrentResult::GoBackToSleepAfterShown { + result_with_data: action_result, + } + | CurrentResult::Result(action_result) => { + Some((current_prompt.clone(), action_result.clone())) + } + }, + NightState::Complete => None, + }) + .filter(|(p, _)| { + p.character_id() + .map(|cid| cid == char_id) + .unwrap_or_default() + }) + .collect() + } + + fn beholder_picked(&self) -> Box<[CharacterId]> { + self.used_actions + .iter() + .filter_map(|(p, _, _)| match p { + ActionPrompt::BeholderChooses { marked, .. } => *marked, + _ => None, + }) + .collect() + } + fn currently_dying(&self) -> Box<[DiedTo]> { let ch = self.current_changes(); let changes: ChangesLookup<'_> = ChangesLookup::new(&ch); @@ -1390,7 +1452,7 @@ impl Night { marked: Some(marked), .. } - | ActionPrompt::Beholder { + | ActionPrompt::BeholderChooses { character_id, marked: Some(marked), .. @@ -1431,7 +1493,8 @@ impl Night { .. } => (*marked == visit_char).then(|| character_id.clone()), - ActionPrompt::TraitorIntro { .. } + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::TraitorIntro { .. } | ActionPrompt::Bloodletter { .. } | ActionPrompt::WolfPackKill { marked: None, .. } | ActionPrompt::Arcanist { marked: _, .. } @@ -1446,7 +1509,7 @@ impl Night { | ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. } - | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::BeholderChooses { marked: None, .. } | ActionPrompt::MasonLeaderRecruit { marked: None, .. } | ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. } diff --git a/werewolves-proto/src/game/night/next.rs b/werewolves-proto/src/game/night/next.rs index 9f02f5e..662e9a6 100644 --- a/werewolves-proto/src/game/night/next.rs +++ b/werewolves-proto/src/game/night/next.rs @@ -87,6 +87,24 @@ impl Night { } loop { if let Some(prompt) = self.pull_next_prompt_with_dead_ignore()? { + if let ActionPrompt::BeholderWakes { character_id } = &prompt + && let Some((target, _)) = self + .get_actions_for(character_id.character_id) + .into_iter() + .find_map(|(prompt, _)| prompt.marked()) + { + let has_beholdable_actions = self + .get_actions_for(target) + .into_iter() + .any(|(p, _)| p.is_beholdable()); + let died = self.died_to_tonight(target)?.is_some(); + if !died || !has_beholdable_actions { + log::debug!( + "skipping beholder wake as their target did not die or wasn't chosen" + ); + continue; + } + } if let ActionPrompt::Insomniac { character_id } = &prompt && self.get_visits_for(character_id.character_id).is_empty() { @@ -237,11 +255,12 @@ impl Night { } fn pull_next_prompt_with_dead_ignore(&mut self) -> Result> { - let has_living_beholder = self + let beholder_picked = self.beholder_picked(); + let has_insomniac = self .village .characters() .into_iter() - .any(|c| matches!(c.role_title(), RoleTitle::Beholder | RoleTitle::Insomniac)); + .any(|c| matches!(c.role_title(), RoleTitle::Insomniac)); while let Some(prompt) = self.action_queue.pop_front() { if !matches!( prompt.action_type(), @@ -252,7 +271,10 @@ impl Night { let Some(char_id) = prompt.character_id() else { return Ok(Some(prompt)); }; - match (self.died_to_tonight(char_id)?, has_living_beholder) { + match ( + self.died_to_tonight(char_id)?, + beholder_picked.contains(&char_id) || has_insomniac, + ) { (Some(_), false) => {} (Some(DiedTo::Shapeshift { .. }), _) | (Some(_), true) | (None, _) => { return Ok(Some(prompt)); diff --git a/werewolves-proto/src/game/night/process.rs b/werewolves-proto/src/game/night/process.rs index 45d60e1..3e7b965 100644 --- a/werewolves-proto/src/game/night/process.rs +++ b/werewolves-proto/src/game/night/process.rs @@ -22,7 +22,10 @@ use crate::{ game::night::{ ActionComplete, CurrentResult, Night, NightState, ResponseOutcome, changes::NightChange, }, - message::night::{ActionPrompt, ActionResponse, ActionResult}, + message::{ + CharacterIdentity, + night::{ActionPrompt, ActionResponse, ActionResult, ActionType}, + }, player::Protection, role::{ Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleBlock, RoleTitle, @@ -116,6 +119,21 @@ impl Night { }; match current_prompt { + ActionPrompt::BeholderWakes { character_id } => Ok(ActionComplete { + result: self + .get_what_beholder_saw(character_id.character_id) + .map(|saw| { + if matches!(saw, ActionResult::RoleBlocked | ActionResult::Drunk) { + ActionResult::BeholderSawNothing + } else { + saw + } + }) + .unwrap_or(ActionResult::BeholderSawNothing), + change: None, + secondary_changes: vec![], + } + .into()), ActionPrompt::TraitorIntro { .. } => Ok(ActionComplete { result: ActionResult::GoBackToSleep, change: None, @@ -437,34 +455,14 @@ impl Night { secondary_changes: vec![], } .into()), - ActionPrompt::Beholder { - marked: Some(marked), - .. - } => { - if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| { - prompt.matches_beholding(*marked).then_some(result) - }) && self.died_to_tonight(*marked)?.is_some() - { - Ok(ActionComplete { - result: if matches!(result, ActionResult::RoleBlocked | ActionResult::Drunk) - { - ActionResult::BeholderSawNothing - } else { - result.clone() - }, - change: None, - secondary_changes: vec![], - } - .into()) - } else { - Ok(ActionComplete { - result: ActionResult::GoBackToSleep, - change: None, - secondary_changes: vec![], - } - .into()) - } + ActionPrompt::BeholderChooses { + marked: Some(_), .. + } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + secondary_changes: vec![], } + .into()), ActionPrompt::MasonsWake { .. } => Ok(ActionComplete { result: ActionResult::GoBackToSleep, change: None, @@ -582,7 +580,7 @@ impl Night { | ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. } - | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::BeholderChooses { marked: None, .. } | ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. } | ActionPrompt::Protector { marked: None, .. } diff --git a/werewolves-proto/src/game/story.rs b/werewolves-proto/src/game/story.rs index 46e4880..beaba05 100644 --- a/werewolves-proto/src/game/story.rs +++ b/werewolves-proto/src/game/story.rs @@ -214,6 +214,7 @@ pub enum StoryActionPrompt { impl StoryActionPrompt { pub fn new(prompt: ActionPrompt) -> Option { Some(match prompt { + ActionPrompt::BeholderWakes { .. } => return None, // TODO: rework story anyway ActionPrompt::Bloodletter { character_id, marked: Some(marked), @@ -317,7 +318,7 @@ impl StoryActionPrompt { character_id: character_id.character_id, chosen: marked, }, - ActionPrompt::Beholder { + ActionPrompt::BeholderChooses { character_id, marked: Some(marked), .. @@ -407,7 +408,7 @@ impl StoryActionPrompt { | ActionPrompt::Adjudicator { .. } | ActionPrompt::PowerSeer { .. } | ActionPrompt::Mortician { .. } - | ActionPrompt::Beholder { .. } + | ActionPrompt::BeholderChooses { .. } | ActionPrompt::MasonLeaderRecruit { .. } | ActionPrompt::Empath { .. } | ActionPrompt::Vindicator { .. } diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index b5745d4..cfe10b8 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -77,7 +77,8 @@ pub trait ActionPromptTitleExt { fn direwolf(&self); fn masons_wake(&self); fn masons_leader_recruit(&self); - fn beholder(&self); + fn beholder_chooses(&self); + fn beholder_wakes(&self); fn vindicator(&self); fn pyremaster(&self); fn empath(&self); @@ -151,8 +152,11 @@ impl ActionPromptTitleExt for ActionPromptTitle { fn masons_leader_recruit(&self) { assert_eq!(*self, ActionPromptTitle::MasonLeaderRecruit) } - fn beholder(&self) { - assert_eq!(*self, ActionPromptTitle::Beholder) + fn beholder_chooses(&self) { + assert_eq!(*self, ActionPromptTitle::BeholderChooses) + } + fn beholder_wakes(&self) { + assert_eq!(*self, ActionPromptTitle::BeholderWakes) } fn vindicator(&self) { assert_eq!(*self, ActionPromptTitle::Vindicator) @@ -414,7 +418,8 @@ impl GameExt for Game { fn mark_and_check(&mut self, mark: CharacterId) { let prompt = self.mark(mark); match prompt { - ActionPrompt::TraitorIntro { .. } + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::TraitorIntro { .. } | ActionPrompt::Insomniac { .. } | ActionPrompt::MasonsWake { .. } | ActionPrompt::ElderReveal { .. } @@ -448,7 +453,7 @@ impl GameExt for Game { marked: Some(marked), .. } - | ActionPrompt::Beholder { + | ActionPrompt::BeholderChooses { marked: Some(marked), .. } @@ -509,7 +514,7 @@ impl GameExt for Game { | ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. } - | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::BeholderChooses { marked: None, .. } | ActionPrompt::MasonLeaderRecruit { marked: None, .. } | ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. } @@ -960,6 +965,10 @@ fn big_game_test_based_on_story_test() { game.mark(game.character_by_player_id(insomniac).character_id()); game.r#continue().sleep(); + game.next().title().beholder_chooses(); + game.mark(game.character_by_player_id(power_seer).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(werewolf).character_id()); game.r#continue().seer(); @@ -1000,10 +1009,6 @@ fn big_game_test_based_on_story_test() { game.r#continue().insomniac(); game.r#continue().sleep(); - game.next().title().beholder(); - game.mark(game.character_by_player_id(power_seer).character_id()); - game.r#continue().sleep(); - game.next_expect_day(); assert_eq!( @@ -1040,6 +1045,10 @@ fn big_game_test_based_on_story_test() { game.mark(game.character_by_player_id(insomniac).character_id()); game.r#continue().sleep(); + game.next().title().beholder_chooses(); + game.mark(game.character_by_player_id(power_seer).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(werewolf).character_id()); game.r#continue().seer(); @@ -1080,10 +1089,6 @@ fn big_game_test_based_on_story_test() { game.r#continue().insomniac(); game.r#continue().sleep(); - game.next().title().beholder(); - game.mark(game.character_by_player_id(power_seer).character_id()); - game.r#continue().sleep(); - game.next_expect_day(); game.mark_for_execution( game.living_villager_excl(protect.player_id()) @@ -1121,6 +1126,10 @@ fn big_game_test_based_on_story_test() { game.mark(game.character_by_player_id(insomniac).character_id()); game.r#continue().sleep(); + game.next().title().beholder_chooses(); + game.mark(game.character_by_player_id(gravedigger).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(shapeshifter).character_id()); game.r#continue().seer(); @@ -1156,10 +1165,6 @@ fn big_game_test_based_on_story_test() { game.r#continue().insomniac(); game.r#continue().sleep(); - game.next().title().beholder(); - game.mark(game.character_by_player_id(gravedigger).character_id()); - game.r#continue().sleep(); - game.next_expect_day(); game.mark_for_execution(game.character_by_player_id(vindicator).character_id()); game.execute().title().wolf_pack_kill(); @@ -1174,6 +1179,10 @@ fn big_game_test_based_on_story_test() { game.mark(game.character_by_player_id(empath).character_id()); game.r#continue().sleep(); + game.next().title().beholder_chooses(); + game.mark(game.character_by_player_id(mortician).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(insomniac).character_id()); game.r#continue().seer(); @@ -1212,8 +1221,7 @@ fn big_game_test_based_on_story_test() { game.r#continue().insomniac(); game.r#continue().sleep(); - game.next().title().beholder(); - game.mark(game.character_by_player_id(mortician).character_id()); + game.next().title().beholder_wakes(); assert_eq!( game.r#continue().mortician(), DiedToTitle::GuardianProtecting diff --git a/werewolves-proto/src/game_test/previous.rs b/werewolves-proto/src/game_test/previous.rs index 5268488..3aee3af 100644 --- a/werewolves-proto/src/game_test/previous.rs +++ b/werewolves-proto/src/game_test/previous.rs @@ -372,6 +372,10 @@ fn previous_prompt() { game.mark(game.character_by_player_id(insomniac).character_id()); game.r#continue().sleep(); + game.next().title().beholder_chooses(); + game.mark(game.character_by_player_id(power_seer).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(werewolf).character_id()); game.r#continue().seer(); @@ -412,9 +416,5 @@ fn previous_prompt() { game.r#continue().insomniac(); game.r#continue().sleep(); - game.next().title().beholder(); - game.mark(game.character_by_player_id(power_seer).character_id()); - game.r#continue().sleep(); - game.next_expect_day(); } diff --git a/werewolves-proto/src/game_test/role/apprentice.rs b/werewolves-proto/src/game_test/role/apprentice.rs index 4421e7c..d4ac424 100644 --- a/werewolves-proto/src/game_test/role/apprentice.rs +++ b/werewolves-proto/src/game_test/role/apprentice.rs @@ -50,7 +50,7 @@ fn beholder_appropriate_prompt_position() { game.mark(game.character_by_player_id(beholder).character_id()); game.r#continue().sleep(); - game.next().title().beholder(); + game.next().title().beholder_chooses(); game.mark(game.character_by_player_id(wolf_player_id).character_id()); game.r#continue().sleep(); @@ -67,5 +67,5 @@ fn beholder_appropriate_prompt_position() { } ); game.r#continue().r#continue(); - game.next().title().beholder(); + game.next().title().beholder_chooses(); } diff --git a/werewolves-proto/src/game_test/role/beholder.rs b/werewolves-proto/src/game_test/role/beholder.rs index 2d1ce9f..f49e339 100644 --- a/werewolves-proto/src/game_test/role/beholder.rs +++ b/werewolves-proto/src/game_test/role/beholder.rs @@ -51,33 +51,85 @@ fn beholding_seer() { game.mark(game.living_villager().character_id()); game.r#continue().sleep(); + game.next().title().beholder_chooses(); + game.mark(game.character_by_player_id(seer_player_id).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(wolf_player_id).character_id()); game.r#continue().seer().wolves(); game.r#continue().sleep(); - game.next().title().beholder(); - game.mark(game.character_by_player_id(seer_player_id).character_id()); - game.r#continue().sleep(); - game.next_expect_day(); game.execute().title().wolf_pack_kill(); game.mark(game.character_by_player_id(seer_player_id).character_id()); game.r#continue().sleep(); + game.next().title().beholder_chooses(); + game.mark(game.character_by_player_id(seer_player_id).character_id()); + game.r#continue().sleep(); + game.next().title().seer(); game.mark(game.character_by_player_id(wolf_player_id).character_id()); assert_eq!(game.r#continue().seer(), Alignment::Wolves); game.r#continue().sleep(); - game.next().title().beholder(); - game.mark(game.character_by_player_id(seer_player_id).character_id()); + game.next().title().beholder_wakes(); assert_eq!(game.r#continue().seer(), Alignment::Wolves); game.r#continue().sleep(); game.next_expect_day(); } +#[test] +#[allow(non_snake_case)] +fn beholding_seer_but_arcanist_dies__arcanist_shouldnt_wake() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let seer = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + let beholder = player_ids.next().unwrap(); + let arcanist = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Seer, seer); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Beholder, beholder); + settings.add_and_assign(SetupRole::Arcanist, arcanist); + 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().seer().wolves(); + game.r#continue().sleep(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(seer).character_id()); + game.mark_villager(); + game.r#continue().arcanist(); + game.r#continue().sleep(); + + game.next_expect_day(); + game.execute().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(arcanist).character_id()); + game.r#continue().sleep(); + + game.next().title().beholder_chooses(); + game.mark(game.character_by_player_id(seer).character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(wolf).character_id()); + game.r#continue().seer().wolves(); + game.r#continue().sleep(); + + game.next_expect_day(); +} + #[test] fn beholding_wolf() { let players = gen_players(1..10); @@ -97,9 +149,36 @@ fn beholding_wolf() { game.mark(game.living_villager_excl(beholder_player_id).character_id()); game.r#continue().sleep(); - game.next().title().beholder(); + game.next().title().beholder_chooses(); game.mark(game.character_by_player_id(wolf_player_id).character_id()); game.r#continue().sleep(); game.next_expect_day(); } + +#[test] +fn beholding_villager() { + let players = gen_players(1..10); + let wolf_player_id = players[1].player_id; + let beholder_player_id = players[2].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.add_and_assign(SetupRole::Beholder, beholder_player_id); + settings.fill_remaining_slots_with_villagers(9); + 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(); + let target = game.living_villager(); + game.mark(target.character_id()); + game.r#continue().sleep(); + + game.next().title().beholder_chooses(); + game.mark(target.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); +} diff --git a/werewolves-proto/src/game_test/role/mason.rs b/werewolves-proto/src/game_test/role/mason.rs index 4df4b3b..1bfc0e1 100644 --- a/werewolves-proto/src/game_test/role/mason.rs +++ b/werewolves-proto/src/game_test/role/mason.rs @@ -20,7 +20,7 @@ use crate::{ diedto::DiedTo, game::{Game, GameSettings, OrRandom, SetupRole}, game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, - message::night::{ActionPrompt, ActionPromptTitle}, + message::night::{ActionPrompt, ActionPromptTitle, ActionResult}, }; #[test] @@ -278,6 +278,10 @@ fn masons_get_go_back_to_sleep() { game.mark_villager(); game.r#continue().sleep(); + game.next().title().beholder_chooses(); + game.mark_villager(); + game.r#continue().sleep(); + game.next().title().masons_leader_recruit(); game.mark(game.character_by_player_id(scapegoat).character_id()); game.r#continue().r#continue(); @@ -285,21 +289,17 @@ fn masons_get_go_back_to_sleep() { game.next().title().masons_wake(); game.r#continue().sleep(); - game.next().title().beholder(); + game.next_expect_day(); + game.execute().title().wolf_pack_kill(); game.mark_villager(); game.r#continue().sleep(); - game.next_expect_day(); - game.execute().title().wolf_pack_kill(); + game.next().title().beholder_chooses(); game.mark_villager(); game.r#continue().sleep(); game.next().title().masons_wake(); game.r#continue().sleep(); - game.next().title().beholder(); - game.mark_villager(); - game.r#continue().sleep(); - game.next_expect_day(); } diff --git a/werewolves-proto/src/game_test/role/shapeshifter.rs b/werewolves-proto/src/game_test/role/shapeshifter.rs index 22dfb34..e715977 100644 --- a/werewolves-proto/src/game_test/role/shapeshifter.rs +++ b/werewolves-proto/src/game_test/role/shapeshifter.rs @@ -381,7 +381,7 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() { ); game.r#continue().sleep(); - game.next().title().beholder(); + game.next().title().beholder_chooses(); assert_eq!( game.prev(), @@ -411,6 +411,18 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() { ChangesLookup::new(¤t_changes).shapeshift_change(), None ); + assert!(matches!( + game.prev(), + ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill { .. }, _) + )); + game.mark_villager(); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.r#continue().sleep(); + + game.next().title().beholder_chooses(); + game.mark_villager(); game.r#continue().sleep(); game.next().title().gravedigger(); @@ -418,10 +430,6 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() { 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(), diff --git a/werewolves-proto/src/message/host.rs b/werewolves-proto/src/message/host.rs index c3eee2c..2384eab 100644 --- a/werewolves-proto/src/message/host.rs +++ b/werewolves-proto/src/message/host.rs @@ -80,7 +80,6 @@ pub enum HostLobbyMessage { ManufacturePlayer(PublicIdentity), Kick(PlayerId), SetPlayerNumber(PlayerId, NonZeroU8), - GetGameSettings, SetGameSettings(GameSettings), SetQrMode(bool), Start, @@ -98,9 +97,11 @@ pub enum ServerToHostMessage { PlayerStates(Box<[CharacterState]>), ActionPrompt(ActionPrompt, usize), ActionResult(Option, ActionResult), - Lobby(Box<[PlayerState]>), + Lobby { + players: Box<[PlayerState]>, + settings: GameSettings, + }, QrMode(bool), - GameSettings(GameSettings), Error(GameError), GameOver(GameOver), WaitingForRoleRevealAcks { diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index 6115fac..5277da9 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -48,12 +48,12 @@ pub enum ActionType { LoneWolfKill, Block, VillageKill, + BeholderChooses, Intel, - Other, MasonRecruit, MasonsWake, Insomniac, - Beholder, + BeholderWakes, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles, Extract)] @@ -140,8 +140,8 @@ pub enum ActionPrompt { dead_players: Box<[CharacterIdentity]>, marked: Option, }, - #[checks(ActionType::Beholder)] - Beholder { + #[checks(ActionType::BeholderChooses)] + BeholderChooses { character_id: CharacterIdentity, living_players: Box<[CharacterIdentity]>, marked: Option, @@ -212,9 +212,46 @@ pub enum ActionPrompt { }, #[checks(ActionType::TraitorIntro)] TraitorIntro { character_id: CharacterIdentity }, + #[checks(ActionType::BeholderWakes)] + BeholderWakes { character_id: CharacterIdentity }, } impl ActionPrompt { + pub const fn is_beholdable(&self) -> bool { + match self { + ActionPrompt::BeholderChooses { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::ElderReveal { .. } + | ActionPrompt::Protector { .. } + | ActionPrompt::Hunter { .. } + | ActionPrompt::Militia { .. } + | ActionPrompt::MapleWolf { .. } + | ActionPrompt::Guardian { .. } + | ActionPrompt::WolvesIntro { .. } + | ActionPrompt::MasonsWake { .. } + | ActionPrompt::MasonLeaderRecruit { .. } + | ActionPrompt::Vindicator { .. } + | ActionPrompt::PyreMaster { .. } + | ActionPrompt::WolfPackKill { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::AlphaWolf { .. } + | ActionPrompt::DireWolf { .. } + | ActionPrompt::LoneWolfKill { .. } + | ActionPrompt::Bloodletter { .. } + | ActionPrompt::TraitorIntro { .. } + | ActionPrompt::CoverOfDarkness => false, + + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::Seer { .. } + | ActionPrompt::Arcanist { .. } + | ActionPrompt::Gravedigger { .. } + | ActionPrompt::Adjudicator { .. } + | ActionPrompt::PowerSeer { .. } + | ActionPrompt::Mortician { .. } + | ActionPrompt::Empath { .. } + | ActionPrompt::Insomniac { .. } => true, + } + } pub(crate) const fn marked(&self) -> Option<(CharacterId, Option)> { match self { ActionPrompt::Seer { marked, .. } @@ -227,7 +264,7 @@ impl ActionPrompt { | ActionPrompt::Adjudicator { marked, .. } | ActionPrompt::PowerSeer { marked, .. } | ActionPrompt::Mortician { marked, .. } - | ActionPrompt::Beholder { marked, .. } + | ActionPrompt::BeholderChooses { marked, .. } | ActionPrompt::MasonLeaderRecruit { marked, .. } | ActionPrompt::Empath { marked, .. } | ActionPrompt::Vindicator { marked, .. } @@ -257,6 +294,7 @@ impl ActionPrompt { marked: (None, None), .. } + | ActionPrompt::BeholderWakes { .. } | ActionPrompt::CoverOfDarkness | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } @@ -269,7 +307,8 @@ impl ActionPrompt { } pub(crate) const fn character_id(&self) -> Option { match self { - ActionPrompt::TraitorIntro { character_id } + ActionPrompt::BeholderWakes { character_id } + | ActionPrompt::TraitorIntro { character_id } | ActionPrompt::Insomniac { character_id, .. } | ActionPrompt::LoneWolfKill { character_id, .. } | ActionPrompt::ElderReveal { character_id } @@ -287,7 +326,7 @@ impl ActionPrompt { | ActionPrompt::Adjudicator { character_id, .. } | ActionPrompt::PowerSeer { character_id, .. } | ActionPrompt::Mortician { character_id, .. } - | ActionPrompt::Beholder { character_id, .. } + | ActionPrompt::BeholderChooses { character_id, .. } | ActionPrompt::MasonLeaderRecruit { character_id, .. } | ActionPrompt::Empath { character_id, .. } | ActionPrompt::Vindicator { character_id, .. } @@ -313,8 +352,9 @@ impl ActionPrompt { | ActionPrompt::Mortician { character_id, .. } | ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target, - ActionPrompt::TraitorIntro { .. } - | ActionPrompt::Beholder { .. } + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::TraitorIntro { .. } + | ActionPrompt::BeholderChooses { .. } | ActionPrompt::CoverOfDarkness | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } @@ -341,7 +381,7 @@ impl ActionPrompt { Self::Shapeshifter { .. } => true, _ => !matches!( self.with_mark(CharacterId::new()), - Err(GameError::RoleDoesntMark) + Err(GameError::PromptDoesntMark) ), } } @@ -349,14 +389,15 @@ impl ActionPrompt { pub(crate) fn with_mark(&self, mark: CharacterId) -> Result { let mut prompt = self.clone(); match &mut prompt { - ActionPrompt::TraitorIntro { .. } + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::TraitorIntro { .. } | ActionPrompt::Insomniac { .. } | ActionPrompt::MasonsWake { .. } | ActionPrompt::ElderReveal { .. } | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } | ActionPrompt::Shapeshifter { .. } - | ActionPrompt::CoverOfDarkness => Err(GameError::RoleDoesntMark), + | ActionPrompt::CoverOfDarkness => Err(GameError::PromptDoesntMark), ActionPrompt::Guardian { previous, @@ -446,7 +487,7 @@ impl ActionPrompt { marked, .. } - | ActionPrompt::Beholder { + | ActionPrompt::BeholderChooses { living_players: targets, marked, .. diff --git a/werewolves-server/src/game.rs b/werewolves-server/src/game.rs index b7672a5..f6b9a94 100644 --- a/werewolves-server/src/game.rs +++ b/werewolves-server/src/game.rs @@ -423,11 +423,13 @@ impl GameEnd { return self.process(Message::Host(HostMessage::GetState)); } Message::Host(HostMessage::PostGame(PostGameMessage::NewLobby)) => { - self.game() - .ok()? - .comms + let game = self.game().ok()?; + game.comms .host() - .send(ServerToHostMessage::Lobby(Box::new([]))) + .send(ServerToHostMessage::Lobby { + players: Box::new([]), + settings: game.game.village().settings(), + }) .log_debug(); let lobby = self.game.take()?.into_lobby(); return Some(ProcessOutcome::Lobby(lobby)); diff --git a/werewolves-server/src/lobby.rs b/werewolves-server/src/lobby.rs index f399f05..dad1b12 100644 --- a/werewolves-server/src/lobby.rs +++ b/werewolves-server/src/lobby.rs @@ -57,12 +57,6 @@ impl Lobby { pub fn set_settings(&mut self, settings: GameSettings) { self.settings = settings.clone(); - if let Ok(comms) = self.comms() { - comms - .host() - .send(ServerToHostMessage::GameSettings(settings)) - .log_debug(); - } } pub fn set_players_in_lobby(&mut self, players_in_lobby: LobbyPlayers) { @@ -94,7 +88,7 @@ impl Lobby { .await; } - async fn get_lobby_player_list(&self) -> Box<[PlayerState]> { + async fn get_lobby_info(&self) -> Box<[PlayerState]> { let mut players = Vec::new(); for (player, _) in self.players_in_lobby.iter() { players.push(PlayerState { @@ -107,10 +101,11 @@ impl Lobby { } pub async fn send_lobby_info_to_host(&mut self) -> Result<(), GameError> { - let players = self.get_lobby_player_list().await; + let players = self.get_lobby_info().await; let qr_mode = self.qr_mode; + let settings = self.settings.clone(); let host = self.comms()?.host(); - host.send(ServerToHostMessage::Lobby(players))?; + host.send(ServerToHostMessage::Lobby { players, settings })?; host.send(ServerToHostMessage::QrMode(qr_mode)) } @@ -212,18 +207,10 @@ impl Lobby { Message::Host(HostMessage::Lobby(HostLobbyMessage::GetState)) | Message::Host(HostMessage::GetState) => { self.send_lobby_info_to_host().await?; - let settings = self.settings.clone(); - self.comms()? - .host() - .send(ServerToHostMessage::GameSettings(settings)) - .log_warn(); - } - Message::Host(HostMessage::Lobby(HostLobbyMessage::GetGameSettings)) => { - let msg = ServerToHostMessage::GameSettings(self.settings.clone()); - let _ = self.comms().unwrap().host().send(msg); } Message::Host(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(settings))) => { self.settings = settings; + self.send_lobby_info_to_host().await?; } Message::Host(HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber(pid, num))) => { self.joined_players diff --git a/werewolves/src/clients/host/host.rs b/werewolves/src/clients/host/host.rs index 09d64aa..bfb46e8 100644 --- a/werewolves/src/clients/host/host.rs +++ b/werewolves/src/clients/host/host.rs @@ -194,9 +194,11 @@ pub enum HostEvent { SetBigScreenState(bool), SetState(HostState), Continue, - PlayerList(Box<[PlayerState]>), + Lobby { + players: Box<[PlayerState]>, + settings: GameSettings, + }, CharacterList(Box<[CharacterState]>), - Settings(GameSettings), Error(GameError), QrMode(bool), ToOverrideView, @@ -253,8 +255,9 @@ impl From for HostEvent { marked_for_execution, settings, }), - ServerToHostMessage::Lobby(players) => HostEvent::PlayerList(players), - ServerToHostMessage::GameSettings(settings) => HostEvent::Settings(settings), + ServerToHostMessage::Lobby { players, settings } => { + HostEvent::Lobby { players, settings } + } ServerToHostMessage::Error(err) => HostEvent::Error(err), ServerToHostMessage::GameOver(game_over) => { HostEvent::SetState(HostState::GameOver { result: game_over }) @@ -695,7 +698,10 @@ impl Host { (Some(HostState::CharacterStates(char)), true) } HostEvent::QrMode(mode) => (None, true), - HostEvent::PlayerList(mut players) => { + HostEvent::Lobby { + mut players, + settings, + } => { const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap(); players.sort_by(|l, r| { l.identification @@ -704,69 +710,16 @@ impl Host { .unwrap_or(LAST) .cmp(&r.identification.public.number.unwrap_or(LAST)) }); - match &self.state { - HostState::Lobby { settings, .. } => ( - Some(HostState::Lobby { - players: players.into_iter().collect(), - settings: settings.clone(), - }), - true, - ), - HostState::Story { .. } - | HostState::Disconnected - | HostState::GameOver { .. } => { - let mut send = self.send.clone(); - let on_err = self.error_callback.clone(); - - let state = Some(HostState::Lobby { - players: players.into_iter().collect(), - settings: Default::default(), - }); - yew::platform::spawn_local(async move { - if let Err(err) = send - .send(HostMessage::Lobby(HostLobbyMessage::GetGameSettings)) - .await - { - on_err.emit(Some(err.into())) - } - }); - (state, true) - } - - HostState::ScreenOverrides { .. } - | HostState::CharacterStates(_) - | HostState::Prompt(_, _) - | HostState::Result(_, _) - | HostState::RoleReveal { .. } - | HostState::Day { .. } => (None, false), - } + ( + Some(HostState::Lobby { + settings, + players: players.into_iter().collect(), + }), + true, + ) } HostEvent::SetErrorCallback(callback) => (None, false), HostEvent::SetState(state) => (Some(state), true), - HostEvent::Settings(settings) => match &self.state { - HostState::Lobby { players, .. } => ( - Some(HostState::Lobby { - settings, - players: players.clone(), - }), - true, - ), - HostState::ScreenOverrides { .. } - | HostState::CharacterStates(_) - | HostState::Story { .. } - | HostState::Prompt(_, _) - | HostState::Result(_, _) - | HostState::Disconnected - | HostState::RoleReveal { - ackd: _, - waiting: _, - } - | HostState::GameOver { result: _ } - | HostState::Day { .. } => { - log::info!("ignoring settings get"); - (None, false) - } - }, HostEvent::Error(_) => (None, false), HostEvent::SetBigScreenState(_) => (None, true), HostEvent::Continue => (None, false), @@ -825,12 +778,6 @@ impl Host { { log::error!("sending game settings update: {err}"); } - if let Err(err) = send - .send(HostMessage::Lobby(HostLobbyMessage::GetGameSettings)) - .await - { - log::error!("sending game settings get: {err}"); - } }); }); let send = self.send.clone(); diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index b533fa4..0dd8134 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -119,6 +119,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { } }); let (character_id, targets, marked, role_info) = match &props.prompt { + ActionPrompt::BeholderWakes { .. } => return html! {}, ActionPrompt::TraitorIntro { character_id } => { return html! {
@@ -221,7 +222,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { marked.iter().cloned().collect::>(), html! {{"adjudicator"}}, ), - ActionPrompt::Beholder { + ActionPrompt::BeholderChooses { character_id, living_players, marked, diff --git a/werewolves/src/pages/role_page.rs b/werewolves/src/pages/role_page.rs index 0de0bdd..bea4ee0 100644 --- a/werewolves/src/pages/role_page.rs +++ b/werewolves/src/pages/role_page.rs @@ -41,7 +41,7 @@ impl RolePage for ActionPrompt { }) }; match self { - ActionPrompt::Beholder { character_id, .. } => Rc::new([html! { + ActionPrompt::BeholderChooses { character_id, .. } => Rc::new([html! { <> {ident(character_id)} @@ -247,6 +247,12 @@ impl RolePage for ActionPrompt { }]), + ActionPrompt::BeholderWakes { character_id } => Rc::new([html! { + <> + {ident(character_id)} + + + }]), _ => Rc::new([]), } } diff --git a/werewolves/src/pages/role_page/beholder.rs b/werewolves/src/pages/role_page/beholder.rs index 4375165..0312c51 100644 --- a/werewolves/src/pages/role_page/beholder.rs +++ b/werewolves/src/pages/role_page/beholder.rs @@ -14,7 +14,7 @@ // along with this program. If not, see . use yew::prelude::*; -use crate::components::{Icon, IconSource, IconType}; +use crate::components::{Icon, IconSource}; #[function_component] pub fn BeholderPage1() -> Html { @@ -30,6 +30,22 @@ pub fn BeholderPage1() -> Html { } } +#[function_component] +pub fn BeholderWakePage1() -> Html { + html! { +
+

{"BEHOLDER"}

+
+

{"YOUR TARGET HAS DIED"}

+
+ +
+

{"THIS IS THE LAST PIECE OF INFORMATION THEY SAW"}

+
+
+ } +} + #[function_component] pub fn BeholderSawNothing() -> Html { html! { diff --git a/werewolves/src/test_util/mod.rs b/werewolves/src/test_util/mod.rs index b94b3e1..5823201 100644 --- a/werewolves/src/test_util/mod.rs +++ b/werewolves/src/test_util/mod.rs @@ -249,6 +249,9 @@ impl From for TestScreen { impl From for TestScreen { fn from(value: ActionPromptTitle) -> Self { Self::Prompt(match value { + ActionPromptTitle::BeholderWakes => ActionPrompt::BeholderWakes { + character_id: identity(), + }, ActionPromptTitle::CoverOfDarkness => ActionPrompt::CoverOfDarkness, ActionPromptTitle::WolvesIntro => ActionPrompt::WolvesIntro { wolves: identities(5) @@ -327,7 +330,7 @@ impl From for TestScreen { dead_players: identities(20), marked: None, }, - ActionPromptTitle::Beholder => ActionPrompt::Beholder { + ActionPromptTitle::BeholderChooses => ActionPrompt::BeholderChooses { character_id: identities(1).into_iter().next().unwrap(), living_players: identities(20), marked: None, @@ -417,13 +420,14 @@ fn prompt_class(prompt: &ActionPrompt) -> Option<&'static str> { ActionPrompt::ElderReveal { .. } | ActionPrompt::RoleChange { .. } | ActionPrompt::CoverOfDarkness => None, - ActionPrompt::Seer { .. } + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::Seer { .. } | ActionPrompt::Arcanist { .. } | ActionPrompt::Gravedigger { .. } | ActionPrompt::Adjudicator { .. } | ActionPrompt::PowerSeer { .. } | ActionPrompt::Mortician { .. } - | ActionPrompt::Beholder { .. } + | ActionPrompt::BeholderChooses { .. } | ActionPrompt::MasonsWake { .. } | ActionPrompt::MasonLeaderRecruit { .. } | ActionPrompt::Empath { .. } => Some("intel"), diff --git a/werewolves/src/test_util/prompt.rs b/werewolves/src/test_util/prompt.rs index 689585b..6a41b61 100644 --- a/werewolves/src/test_util/prompt.rs +++ b/werewolves/src/test_util/prompt.rs @@ -304,14 +304,15 @@ pub fn PromptScreenTest( } } - ActionPrompt::Protector { .. } + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::Protector { .. } | ActionPrompt::Arcanist { .. } | ActionPrompt::Gravedigger { .. } | ActionPrompt::Militia { .. } | ActionPrompt::Adjudicator { .. } | ActionPrompt::PowerSeer { .. } | ActionPrompt::Mortician { .. } - | ActionPrompt::Beholder { .. } + | ActionPrompt::BeholderChooses { .. } | ActionPrompt::Empath { .. } | ActionPrompt::Vindicator { .. } | ActionPrompt::PyreMaster { .. }