beholder: now wakes twice in the night

once to pick their target, another time to see what they saw (if any)
also fixes host screen (including /big) not responding to some lobby
changes
This commit is contained in:
emilis 2025-12-11 18:14:35 +00:00
parent 6dd78aa1b5
commit 01c61c143e
No known key found for this signature in database
22 changed files with 405 additions and 215 deletions

View File

@ -589,11 +589,16 @@ impl Character {
}); });
} }
} }
Role::Beholder => prompts.push(ActionPrompt::Beholder { Role::Beholder => {
prompts.push(ActionPrompt::BeholderChooses {
character_id: self.identity(), character_id: self.identity(),
living_players: village.living_players_excluding(self.character_id()), living_players: village.living_players_excluding(self.character_id()),
marked: None, marked: None,
}), });
prompts.push(ActionPrompt::BeholderWakes {
character_id: self.identity(),
})
}
Role::MasonLeader { .. } => { Role::MasonLeader { .. } => {
log::error!( log::error!(
"night_action_prompts got to MasonLeader, should be handled before the living check" "night_action_prompts got to MasonLeader, should be handled before the living check"

View File

@ -99,8 +99,8 @@ pub enum GameError {
NoPreviousDuringDay, NoPreviousDuringDay,
#[error("militia already spent")] #[error("militia already spent")]
MilitiaSpent, MilitiaSpent,
#[error("this role doesn't mark anyone")] #[error("this prompt doesn't mark anyone")]
RoleDoesntMark, PromptDoesntMark,
#[error("cannot shapeshift on a non-shapeshifter prompt")] #[error("cannot shapeshift on a non-shapeshifter prompt")]
ShapeshiftingIsForShapeshifters, ShapeshiftingIsForShapeshifters,
#[error("must select a target")] #[error("must select a target")]

View File

@ -32,7 +32,9 @@ use crate::{
kill::{self, KillOutcome}, kill::{self, KillOutcome},
night::changes::{ChangesLookup, NightChange}, night::changes::{ChangesLookup, NightChange},
}, },
message::night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits}, message::night::{
ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, ActionType, Visits,
},
role::RoleTitle, role::RoleTitle,
}; };
@ -62,7 +64,8 @@ impl From<ActionComplete> for ResponseOutcome {
impl ActionPrompt { impl ActionPrompt {
fn unless(&self) -> Option<Unless> { fn unless(&self) -> Option<Unless> {
match &self { match &self {
ActionPrompt::TraitorIntro { .. } ActionPrompt::BeholderWakes { .. }
| ActionPrompt::TraitorIntro { .. }
| ActionPrompt::Insomniac { .. } | ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. } | ActionPrompt::MasonsWake { .. }
| ActionPrompt::WolvesIntro { .. } | ActionPrompt::WolvesIntro { .. }
@ -124,7 +127,7 @@ impl ActionPrompt {
marked: Some(marked), marked: Some(marked),
.. ..
} }
| ActionPrompt::Beholder { | ActionPrompt::BeholderChooses {
marked: Some(marked), marked: Some(marked),
.. ..
} }
@ -169,7 +172,7 @@ impl ActionPrompt {
| ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::Adjudicator { marked: None, .. }
| ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. }
| ActionPrompt::Mortician { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. }
| ActionPrompt::Beholder { marked: None, .. } | ActionPrompt::BeholderChooses { marked: None, .. }
| ActionPrompt::MasonLeaderRecruit { marked: None, .. } | ActionPrompt::MasonLeaderRecruit { marked: None, .. }
| ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Empath { marked: None, .. }
| ActionPrompt::Vindicator { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. }
@ -1003,10 +1006,13 @@ impl Night {
if is_shifted { if is_shifted {
return None; return None;
} }
self.action_queue self.action_queue.iter().next().map(|n| match n {
.iter() ActionPrompt::BeholderWakes { character_id } => !matches!(
.next() self.get_what_beholder_saw(character_id.character_id),
.map(|n| n.character_id() == Some(curr)) None | Some(ActionResult::GoBackToSleep)
),
_ => n.character_id() == Some(curr),
})
}) })
.unwrap_or_default(); .unwrap_or_default();
@ -1175,6 +1181,25 @@ impl Night {
} }
} }
fn get_what_beholder_saw(&self, beholder_id: CharacterId) -> Option<ActionResult> {
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 { pub const fn village(&self) -> &Village {
&self.village &self.village
} }
@ -1265,6 +1290,43 @@ impl Night {
ChangesLookup::new(&self.current_changes()).died_to(character_id, self.night, &self.village) 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]> { fn currently_dying(&self) -> Box<[DiedTo]> {
let ch = self.current_changes(); let ch = self.current_changes();
let changes: ChangesLookup<'_> = ChangesLookup::new(&ch); let changes: ChangesLookup<'_> = ChangesLookup::new(&ch);
@ -1390,7 +1452,7 @@ impl Night {
marked: Some(marked), marked: Some(marked),
.. ..
} }
| ActionPrompt::Beholder { | ActionPrompt::BeholderChooses {
character_id, character_id,
marked: Some(marked), marked: Some(marked),
.. ..
@ -1431,7 +1493,8 @@ impl Night {
.. ..
} => (*marked == visit_char).then(|| character_id.clone()), } => (*marked == visit_char).then(|| character_id.clone()),
ActionPrompt::TraitorIntro { .. } ActionPrompt::BeholderWakes { .. }
| ActionPrompt::TraitorIntro { .. }
| ActionPrompt::Bloodletter { .. } | ActionPrompt::Bloodletter { .. }
| ActionPrompt::WolfPackKill { marked: None, .. } | ActionPrompt::WolfPackKill { marked: None, .. }
| ActionPrompt::Arcanist { marked: _, .. } | ActionPrompt::Arcanist { marked: _, .. }
@ -1446,7 +1509,7 @@ impl Night {
| ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::Adjudicator { marked: None, .. }
| ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. }
| ActionPrompt::Mortician { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. }
| ActionPrompt::Beholder { marked: None, .. } | ActionPrompt::BeholderChooses { marked: None, .. }
| ActionPrompt::MasonLeaderRecruit { marked: None, .. } | ActionPrompt::MasonLeaderRecruit { marked: None, .. }
| ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Empath { marked: None, .. }
| ActionPrompt::Vindicator { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. }

View File

@ -87,6 +87,24 @@ impl Night {
} }
loop { loop {
if let Some(prompt) = self.pull_next_prompt_with_dead_ignore()? { 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 if let ActionPrompt::Insomniac { character_id } = &prompt
&& self.get_visits_for(character_id.character_id).is_empty() && 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<Option<ActionPrompt>> { fn pull_next_prompt_with_dead_ignore(&mut self) -> Result<Option<ActionPrompt>> {
let has_living_beholder = self let beholder_picked = self.beholder_picked();
let has_insomniac = self
.village .village
.characters() .characters()
.into_iter() .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() { while let Some(prompt) = self.action_queue.pop_front() {
if !matches!( if !matches!(
prompt.action_type(), prompt.action_type(),
@ -252,7 +271,10 @@ impl Night {
let Some(char_id) = prompt.character_id() else { let Some(char_id) = prompt.character_id() else {
return Ok(Some(prompt)); 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(_), false) => {}
(Some(DiedTo::Shapeshift { .. }), _) | (Some(_), true) | (None, _) => { (Some(DiedTo::Shapeshift { .. }), _) | (Some(_), true) | (None, _) => {
return Ok(Some(prompt)); return Ok(Some(prompt));

View File

@ -22,7 +22,10 @@ use crate::{
game::night::{ game::night::{
ActionComplete, CurrentResult, Night, NightState, ResponseOutcome, changes::NightChange, ActionComplete, CurrentResult, Night, NightState, ResponseOutcome, changes::NightChange,
}, },
message::night::{ActionPrompt, ActionResponse, ActionResult}, message::{
CharacterIdentity,
night::{ActionPrompt, ActionResponse, ActionResult, ActionType},
},
player::Protection, player::Protection,
role::{ role::{
Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleBlock, RoleTitle, Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleBlock, RoleTitle,
@ -116,6 +119,21 @@ impl Night {
}; };
match current_prompt { 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 { ActionPrompt::TraitorIntro { .. } => Ok(ActionComplete {
result: ActionResult::GoBackToSleep, result: ActionResult::GoBackToSleep,
change: None, change: None,
@ -437,34 +455,14 @@ impl Night {
secondary_changes: vec![], secondary_changes: vec![],
} }
.into()), .into()),
ActionPrompt::Beholder { ActionPrompt::BeholderChooses {
marked: Some(marked), marked: Some(_), ..
.. } => Ok(ActionComplete {
} => {
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, result: ActionResult::GoBackToSleep,
change: None, change: None,
secondary_changes: vec![], secondary_changes: vec![],
} }
.into()) .into()),
}
}
ActionPrompt::MasonsWake { .. } => Ok(ActionComplete { ActionPrompt::MasonsWake { .. } => Ok(ActionComplete {
result: ActionResult::GoBackToSleep, result: ActionResult::GoBackToSleep,
change: None, change: None,
@ -582,7 +580,7 @@ impl Night {
| ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::Adjudicator { marked: None, .. }
| ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. }
| ActionPrompt::Mortician { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. }
| ActionPrompt::Beholder { marked: None, .. } | ActionPrompt::BeholderChooses { marked: None, .. }
| ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Empath { marked: None, .. }
| ActionPrompt::Vindicator { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. }
| ActionPrompt::Protector { marked: None, .. } | ActionPrompt::Protector { marked: None, .. }

View File

@ -214,6 +214,7 @@ pub enum StoryActionPrompt {
impl StoryActionPrompt { impl StoryActionPrompt {
pub fn new(prompt: ActionPrompt) -> Option<Self> { pub fn new(prompt: ActionPrompt) -> Option<Self> {
Some(match prompt { Some(match prompt {
ActionPrompt::BeholderWakes { .. } => return None, // TODO: rework story anyway
ActionPrompt::Bloodletter { ActionPrompt::Bloodletter {
character_id, character_id,
marked: Some(marked), marked: Some(marked),
@ -317,7 +318,7 @@ impl StoryActionPrompt {
character_id: character_id.character_id, character_id: character_id.character_id,
chosen: marked, chosen: marked,
}, },
ActionPrompt::Beholder { ActionPrompt::BeholderChooses {
character_id, character_id,
marked: Some(marked), marked: Some(marked),
.. ..
@ -407,7 +408,7 @@ impl StoryActionPrompt {
| ActionPrompt::Adjudicator { .. } | ActionPrompt::Adjudicator { .. }
| ActionPrompt::PowerSeer { .. } | ActionPrompt::PowerSeer { .. }
| ActionPrompt::Mortician { .. } | ActionPrompt::Mortician { .. }
| ActionPrompt::Beholder { .. } | ActionPrompt::BeholderChooses { .. }
| ActionPrompt::MasonLeaderRecruit { .. } | ActionPrompt::MasonLeaderRecruit { .. }
| ActionPrompt::Empath { .. } | ActionPrompt::Empath { .. }
| ActionPrompt::Vindicator { .. } | ActionPrompt::Vindicator { .. }

View File

@ -77,7 +77,8 @@ pub trait ActionPromptTitleExt {
fn direwolf(&self); fn direwolf(&self);
fn masons_wake(&self); fn masons_wake(&self);
fn masons_leader_recruit(&self); fn masons_leader_recruit(&self);
fn beholder(&self); fn beholder_chooses(&self);
fn beholder_wakes(&self);
fn vindicator(&self); fn vindicator(&self);
fn pyremaster(&self); fn pyremaster(&self);
fn empath(&self); fn empath(&self);
@ -151,8 +152,11 @@ impl ActionPromptTitleExt for ActionPromptTitle {
fn masons_leader_recruit(&self) { fn masons_leader_recruit(&self) {
assert_eq!(*self, ActionPromptTitle::MasonLeaderRecruit) assert_eq!(*self, ActionPromptTitle::MasonLeaderRecruit)
} }
fn beholder(&self) { fn beholder_chooses(&self) {
assert_eq!(*self, ActionPromptTitle::Beholder) assert_eq!(*self, ActionPromptTitle::BeholderChooses)
}
fn beholder_wakes(&self) {
assert_eq!(*self, ActionPromptTitle::BeholderWakes)
} }
fn vindicator(&self) { fn vindicator(&self) {
assert_eq!(*self, ActionPromptTitle::Vindicator) assert_eq!(*self, ActionPromptTitle::Vindicator)
@ -414,7 +418,8 @@ impl GameExt for Game {
fn mark_and_check(&mut self, mark: CharacterId) { fn mark_and_check(&mut self, mark: CharacterId) {
let prompt = self.mark(mark); let prompt = self.mark(mark);
match prompt { match prompt {
ActionPrompt::TraitorIntro { .. } ActionPrompt::BeholderWakes { .. }
| ActionPrompt::TraitorIntro { .. }
| ActionPrompt::Insomniac { .. } | ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. } | ActionPrompt::MasonsWake { .. }
| ActionPrompt::ElderReveal { .. } | ActionPrompt::ElderReveal { .. }
@ -448,7 +453,7 @@ impl GameExt for Game {
marked: Some(marked), marked: Some(marked),
.. ..
} }
| ActionPrompt::Beholder { | ActionPrompt::BeholderChooses {
marked: Some(marked), marked: Some(marked),
.. ..
} }
@ -509,7 +514,7 @@ impl GameExt for Game {
| ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::Adjudicator { marked: None, .. }
| ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. }
| ActionPrompt::Mortician { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. }
| ActionPrompt::Beholder { marked: None, .. } | ActionPrompt::BeholderChooses { marked: None, .. }
| ActionPrompt::MasonLeaderRecruit { marked: None, .. } | ActionPrompt::MasonLeaderRecruit { marked: None, .. }
| ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Empath { marked: None, .. }
| ActionPrompt::Vindicator { 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.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().sleep(); 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.next().title().seer();
game.mark(game.character_by_player_id(werewolf).character_id()); game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().seer(); game.r#continue().seer();
@ -1000,10 +1009,6 @@ fn big_game_test_based_on_story_test() {
game.r#continue().insomniac(); game.r#continue().insomniac();
game.r#continue().sleep(); 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.next_expect_day();
assert_eq!( 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.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().sleep(); 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.next().title().seer();
game.mark(game.character_by_player_id(werewolf).character_id()); game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().seer(); game.r#continue().seer();
@ -1080,10 +1089,6 @@ fn big_game_test_based_on_story_test() {
game.r#continue().insomniac(); game.r#continue().insomniac();
game.r#continue().sleep(); 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.next_expect_day();
game.mark_for_execution( game.mark_for_execution(
game.living_villager_excl(protect.player_id()) 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.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().sleep(); 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.next().title().seer();
game.mark(game.character_by_player_id(shapeshifter).character_id()); game.mark(game.character_by_player_id(shapeshifter).character_id());
game.r#continue().seer(); game.r#continue().seer();
@ -1156,10 +1165,6 @@ fn big_game_test_based_on_story_test() {
game.r#continue().insomniac(); game.r#continue().insomniac();
game.r#continue().sleep(); 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.next_expect_day();
game.mark_for_execution(game.character_by_player_id(vindicator).character_id()); game.mark_for_execution(game.character_by_player_id(vindicator).character_id());
game.execute().title().wolf_pack_kill(); 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.mark(game.character_by_player_id(empath).character_id());
game.r#continue().sleep(); 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.next().title().seer();
game.mark(game.character_by_player_id(insomniac).character_id()); game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().seer(); game.r#continue().seer();
@ -1212,8 +1221,7 @@ fn big_game_test_based_on_story_test() {
game.r#continue().insomniac(); game.r#continue().insomniac();
game.r#continue().sleep(); game.r#continue().sleep();
game.next().title().beholder(); game.next().title().beholder_wakes();
game.mark(game.character_by_player_id(mortician).character_id());
assert_eq!( assert_eq!(
game.r#continue().mortician(), game.r#continue().mortician(),
DiedToTitle::GuardianProtecting DiedToTitle::GuardianProtecting

View File

@ -372,6 +372,10 @@ fn previous_prompt() {
game.mark(game.character_by_player_id(insomniac).character_id()); game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().sleep(); 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.next().title().seer();
game.mark(game.character_by_player_id(werewolf).character_id()); game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().seer(); game.r#continue().seer();
@ -412,9 +416,5 @@ fn previous_prompt() {
game.r#continue().insomniac(); game.r#continue().insomniac();
game.r#continue().sleep(); 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.next_expect_day();
} }

View File

@ -50,7 +50,7 @@ fn beholder_appropriate_prompt_position() {
game.mark(game.character_by_player_id(beholder).character_id()); game.mark(game.character_by_player_id(beholder).character_id());
game.r#continue().sleep(); 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.mark(game.character_by_player_id(wolf_player_id).character_id());
game.r#continue().sleep(); game.r#continue().sleep();
@ -67,5 +67,5 @@ fn beholder_appropriate_prompt_position() {
} }
); );
game.r#continue().r#continue(); game.r#continue().r#continue();
game.next().title().beholder(); game.next().title().beholder_chooses();
} }

View File

@ -51,33 +51,85 @@ fn beholding_seer() {
game.mark(game.living_villager().character_id()); game.mark(game.living_villager().character_id());
game.r#continue().sleep(); 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.next().title().seer();
game.mark(game.character_by_player_id(wolf_player_id).character_id()); game.mark(game.character_by_player_id(wolf_player_id).character_id());
game.r#continue().seer().wolves(); game.r#continue().seer().wolves();
game.r#continue().sleep(); 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.next_expect_day();
game.execute().title().wolf_pack_kill(); game.execute().title().wolf_pack_kill();
game.mark(game.character_by_player_id(seer_player_id).character_id()); game.mark(game.character_by_player_id(seer_player_id).character_id());
game.r#continue().sleep(); 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.next().title().seer();
game.mark(game.character_by_player_id(wolf_player_id).character_id()); game.mark(game.character_by_player_id(wolf_player_id).character_id());
assert_eq!(game.r#continue().seer(), Alignment::Wolves); assert_eq!(game.r#continue().seer(), Alignment::Wolves);
game.r#continue().sleep(); game.r#continue().sleep();
game.next().title().beholder(); game.next().title().beholder_wakes();
game.mark(game.character_by_player_id(seer_player_id).character_id());
assert_eq!(game.r#continue().seer(), Alignment::Wolves); assert_eq!(game.r#continue().seer(), Alignment::Wolves);
game.r#continue().sleep(); game.r#continue().sleep();
game.next_expect_day(); 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] #[test]
fn beholding_wolf() { fn beholding_wolf() {
let players = gen_players(1..10); 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.mark(game.living_villager_excl(beholder_player_id).character_id());
game.r#continue().sleep(); 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.mark(game.character_by_player_id(wolf_player_id).character_id());
game.r#continue().sleep(); game.r#continue().sleep();
game.next_expect_day(); 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();
}

View File

@ -20,7 +20,7 @@ use crate::{
diedto::DiedTo, diedto::DiedTo,
game::{Game, GameSettings, OrRandom, SetupRole}, game::{Game, GameSettings, OrRandom, SetupRole},
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
message::night::{ActionPrompt, ActionPromptTitle}, message::night::{ActionPrompt, ActionPromptTitle, ActionResult},
}; };
#[test] #[test]
@ -278,6 +278,10 @@ fn masons_get_go_back_to_sleep() {
game.mark_villager(); game.mark_villager();
game.r#continue().sleep(); game.r#continue().sleep();
game.next().title().beholder_chooses();
game.mark_villager();
game.r#continue().sleep();
game.next().title().masons_leader_recruit(); game.next().title().masons_leader_recruit();
game.mark(game.character_by_player_id(scapegoat).character_id()); game.mark(game.character_by_player_id(scapegoat).character_id());
game.r#continue().r#continue(); game.r#continue().r#continue();
@ -285,21 +289,17 @@ fn masons_get_go_back_to_sleep() {
game.next().title().masons_wake(); game.next().title().masons_wake();
game.r#continue().sleep(); game.r#continue().sleep();
game.next().title().beholder(); game.next_expect_day();
game.execute().title().wolf_pack_kill();
game.mark_villager(); game.mark_villager();
game.r#continue().sleep(); game.r#continue().sleep();
game.next_expect_day(); game.next().title().beholder_chooses();
game.execute().title().wolf_pack_kill();
game.mark_villager(); game.mark_villager();
game.r#continue().sleep(); game.r#continue().sleep();
game.next().title().masons_wake(); game.next().title().masons_wake();
game.r#continue().sleep(); game.r#continue().sleep();
game.next().title().beholder();
game.mark_villager();
game.r#continue().sleep();
game.next_expect_day(); game.next_expect_day();
} }

View File

@ -381,7 +381,7 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() {
); );
game.r#continue().sleep(); game.r#continue().sleep();
game.next().title().beholder(); game.next().title().beholder_chooses();
assert_eq!( assert_eq!(
game.prev(), game.prev(),
@ -411,6 +411,18 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() {
ChangesLookup::new(&current_changes).shapeshift_change(), ChangesLookup::new(&current_changes).shapeshift_change(),
None 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.r#continue().sleep();
game.next().title().gravedigger(); 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)); assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Villager));
game.r#continue().sleep(); game.r#continue().sleep();
game.next().title().beholder();
game.mark_villager();
game.r#continue().sleep();
game.next_expect_day(); game.next_expect_day();
assert_eq!( assert_eq!(
game.character_by_player_id(gravedigger).role_title(), game.character_by_player_id(gravedigger).role_title(),

View File

@ -80,7 +80,6 @@ pub enum HostLobbyMessage {
ManufacturePlayer(PublicIdentity), ManufacturePlayer(PublicIdentity),
Kick(PlayerId), Kick(PlayerId),
SetPlayerNumber(PlayerId, NonZeroU8), SetPlayerNumber(PlayerId, NonZeroU8),
GetGameSettings,
SetGameSettings(GameSettings), SetGameSettings(GameSettings),
SetQrMode(bool), SetQrMode(bool),
Start, Start,
@ -98,9 +97,11 @@ pub enum ServerToHostMessage {
PlayerStates(Box<[CharacterState]>), PlayerStates(Box<[CharacterState]>),
ActionPrompt(ActionPrompt, usize), ActionPrompt(ActionPrompt, usize),
ActionResult(Option<CharacterIdentity>, ActionResult), ActionResult(Option<CharacterIdentity>, ActionResult),
Lobby(Box<[PlayerState]>), Lobby {
players: Box<[PlayerState]>,
settings: GameSettings,
},
QrMode(bool), QrMode(bool),
GameSettings(GameSettings),
Error(GameError), Error(GameError),
GameOver(GameOver), GameOver(GameOver),
WaitingForRoleRevealAcks { WaitingForRoleRevealAcks {

View File

@ -48,12 +48,12 @@ pub enum ActionType {
LoneWolfKill, LoneWolfKill,
Block, Block,
VillageKill, VillageKill,
BeholderChooses,
Intel, Intel,
Other,
MasonRecruit, MasonRecruit,
MasonsWake, MasonsWake,
Insomniac, Insomniac,
Beholder, BeholderWakes,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles, Extract)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles, Extract)]
@ -140,8 +140,8 @@ pub enum ActionPrompt {
dead_players: Box<[CharacterIdentity]>, dead_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>, marked: Option<CharacterId>,
}, },
#[checks(ActionType::Beholder)] #[checks(ActionType::BeholderChooses)]
Beholder { BeholderChooses {
character_id: CharacterIdentity, character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>, living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>, marked: Option<CharacterId>,
@ -212,9 +212,46 @@ pub enum ActionPrompt {
}, },
#[checks(ActionType::TraitorIntro)] #[checks(ActionType::TraitorIntro)]
TraitorIntro { character_id: CharacterIdentity }, TraitorIntro { character_id: CharacterIdentity },
#[checks(ActionType::BeholderWakes)]
BeholderWakes { character_id: CharacterIdentity },
} }
impl ActionPrompt { 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<CharacterId>)> { pub(crate) const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
match self { match self {
ActionPrompt::Seer { marked, .. } ActionPrompt::Seer { marked, .. }
@ -227,7 +264,7 @@ impl ActionPrompt {
| ActionPrompt::Adjudicator { marked, .. } | ActionPrompt::Adjudicator { marked, .. }
| ActionPrompt::PowerSeer { marked, .. } | ActionPrompt::PowerSeer { marked, .. }
| ActionPrompt::Mortician { marked, .. } | ActionPrompt::Mortician { marked, .. }
| ActionPrompt::Beholder { marked, .. } | ActionPrompt::BeholderChooses { marked, .. }
| ActionPrompt::MasonLeaderRecruit { marked, .. } | ActionPrompt::MasonLeaderRecruit { marked, .. }
| ActionPrompt::Empath { marked, .. } | ActionPrompt::Empath { marked, .. }
| ActionPrompt::Vindicator { marked, .. } | ActionPrompt::Vindicator { marked, .. }
@ -257,6 +294,7 @@ impl ActionPrompt {
marked: (None, None), marked: (None, None),
.. ..
} }
| ActionPrompt::BeholderWakes { .. }
| ActionPrompt::CoverOfDarkness | ActionPrompt::CoverOfDarkness
| ActionPrompt::WolvesIntro { .. } | ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. } | ActionPrompt::RoleChange { .. }
@ -269,7 +307,8 @@ impl ActionPrompt {
} }
pub(crate) const fn character_id(&self) -> Option<CharacterId> { pub(crate) const fn character_id(&self) -> Option<CharacterId> {
match self { match self {
ActionPrompt::TraitorIntro { character_id } ActionPrompt::BeholderWakes { character_id }
| ActionPrompt::TraitorIntro { character_id }
| ActionPrompt::Insomniac { character_id, .. } | ActionPrompt::Insomniac { character_id, .. }
| ActionPrompt::LoneWolfKill { character_id, .. } | ActionPrompt::LoneWolfKill { character_id, .. }
| ActionPrompt::ElderReveal { character_id } | ActionPrompt::ElderReveal { character_id }
@ -287,7 +326,7 @@ impl ActionPrompt {
| ActionPrompt::Adjudicator { character_id, .. } | ActionPrompt::Adjudicator { character_id, .. }
| ActionPrompt::PowerSeer { character_id, .. } | ActionPrompt::PowerSeer { character_id, .. }
| ActionPrompt::Mortician { character_id, .. } | ActionPrompt::Mortician { character_id, .. }
| ActionPrompt::Beholder { character_id, .. } | ActionPrompt::BeholderChooses { character_id, .. }
| ActionPrompt::MasonLeaderRecruit { character_id, .. } | ActionPrompt::MasonLeaderRecruit { character_id, .. }
| ActionPrompt::Empath { character_id, .. } | ActionPrompt::Empath { character_id, .. }
| ActionPrompt::Vindicator { character_id, .. } | ActionPrompt::Vindicator { character_id, .. }
@ -313,8 +352,9 @@ impl ActionPrompt {
| ActionPrompt::Mortician { character_id, .. } | ActionPrompt::Mortician { character_id, .. }
| ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target, | ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target,
ActionPrompt::TraitorIntro { .. } ActionPrompt::BeholderWakes { .. }
| ActionPrompt::Beholder { .. } | ActionPrompt::TraitorIntro { .. }
| ActionPrompt::BeholderChooses { .. }
| ActionPrompt::CoverOfDarkness | ActionPrompt::CoverOfDarkness
| ActionPrompt::WolvesIntro { .. } | ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. } | ActionPrompt::RoleChange { .. }
@ -341,7 +381,7 @@ impl ActionPrompt {
Self::Shapeshifter { .. } => true, Self::Shapeshifter { .. } => true,
_ => !matches!( _ => !matches!(
self.with_mark(CharacterId::new()), 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<ActionPrompt> { pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
let mut prompt = self.clone(); let mut prompt = self.clone();
match &mut prompt { match &mut prompt {
ActionPrompt::TraitorIntro { .. } ActionPrompt::BeholderWakes { .. }
| ActionPrompt::TraitorIntro { .. }
| ActionPrompt::Insomniac { .. } | ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. } | ActionPrompt::MasonsWake { .. }
| ActionPrompt::ElderReveal { .. } | ActionPrompt::ElderReveal { .. }
| ActionPrompt::WolvesIntro { .. } | ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. } | ActionPrompt::RoleChange { .. }
| ActionPrompt::Shapeshifter { .. } | ActionPrompt::Shapeshifter { .. }
| ActionPrompt::CoverOfDarkness => Err(GameError::RoleDoesntMark), | ActionPrompt::CoverOfDarkness => Err(GameError::PromptDoesntMark),
ActionPrompt::Guardian { ActionPrompt::Guardian {
previous, previous,
@ -446,7 +487,7 @@ impl ActionPrompt {
marked, marked,
.. ..
} }
| ActionPrompt::Beholder { | ActionPrompt::BeholderChooses {
living_players: targets, living_players: targets,
marked, marked,
.. ..

View File

@ -423,11 +423,13 @@ impl GameEnd {
return self.process(Message::Host(HostMessage::GetState)); return self.process(Message::Host(HostMessage::GetState));
} }
Message::Host(HostMessage::PostGame(PostGameMessage::NewLobby)) => { Message::Host(HostMessage::PostGame(PostGameMessage::NewLobby)) => {
self.game() let game = self.game().ok()?;
.ok()? game.comms
.comms
.host() .host()
.send(ServerToHostMessage::Lobby(Box::new([]))) .send(ServerToHostMessage::Lobby {
players: Box::new([]),
settings: game.game.village().settings(),
})
.log_debug(); .log_debug();
let lobby = self.game.take()?.into_lobby(); let lobby = self.game.take()?.into_lobby();
return Some(ProcessOutcome::Lobby(lobby)); return Some(ProcessOutcome::Lobby(lobby));

View File

@ -57,12 +57,6 @@ impl Lobby {
pub fn set_settings(&mut self, settings: GameSettings) { pub fn set_settings(&mut self, settings: GameSettings) {
self.settings = settings.clone(); 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) { pub fn set_players_in_lobby(&mut self, players_in_lobby: LobbyPlayers) {
@ -94,7 +88,7 @@ impl Lobby {
.await; .await;
} }
async fn get_lobby_player_list(&self) -> Box<[PlayerState]> { async fn get_lobby_info(&self) -> Box<[PlayerState]> {
let mut players = Vec::new(); let mut players = Vec::new();
for (player, _) in self.players_in_lobby.iter() { for (player, _) in self.players_in_lobby.iter() {
players.push(PlayerState { players.push(PlayerState {
@ -107,10 +101,11 @@ impl Lobby {
} }
pub async fn send_lobby_info_to_host(&mut self) -> Result<(), GameError> { 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 qr_mode = self.qr_mode;
let settings = self.settings.clone();
let host = self.comms()?.host(); let host = self.comms()?.host();
host.send(ServerToHostMessage::Lobby(players))?; host.send(ServerToHostMessage::Lobby { players, settings })?;
host.send(ServerToHostMessage::QrMode(qr_mode)) host.send(ServerToHostMessage::QrMode(qr_mode))
} }
@ -212,18 +207,10 @@ impl Lobby {
Message::Host(HostMessage::Lobby(HostLobbyMessage::GetState)) Message::Host(HostMessage::Lobby(HostLobbyMessage::GetState))
| Message::Host(HostMessage::GetState) => { | Message::Host(HostMessage::GetState) => {
self.send_lobby_info_to_host().await?; 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))) => { Message::Host(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(settings))) => {
self.settings = settings; self.settings = settings;
self.send_lobby_info_to_host().await?;
} }
Message::Host(HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber(pid, num))) => { Message::Host(HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber(pid, num))) => {
self.joined_players self.joined_players

View File

@ -194,9 +194,11 @@ pub enum HostEvent {
SetBigScreenState(bool), SetBigScreenState(bool),
SetState(HostState), SetState(HostState),
Continue, Continue,
PlayerList(Box<[PlayerState]>), Lobby {
players: Box<[PlayerState]>,
settings: GameSettings,
},
CharacterList(Box<[CharacterState]>), CharacterList(Box<[CharacterState]>),
Settings(GameSettings),
Error(GameError), Error(GameError),
QrMode(bool), QrMode(bool),
ToOverrideView, ToOverrideView,
@ -253,8 +255,9 @@ impl From<ServerToHostMessage> for HostEvent {
marked_for_execution, marked_for_execution,
settings, settings,
}), }),
ServerToHostMessage::Lobby(players) => HostEvent::PlayerList(players), ServerToHostMessage::Lobby { players, settings } => {
ServerToHostMessage::GameSettings(settings) => HostEvent::Settings(settings), HostEvent::Lobby { players, settings }
}
ServerToHostMessage::Error(err) => HostEvent::Error(err), ServerToHostMessage::Error(err) => HostEvent::Error(err),
ServerToHostMessage::GameOver(game_over) => { ServerToHostMessage::GameOver(game_over) => {
HostEvent::SetState(HostState::GameOver { result: game_over }) HostEvent::SetState(HostState::GameOver { result: game_over })
@ -695,7 +698,10 @@ impl Host {
(Some(HostState::CharacterStates(char)), true) (Some(HostState::CharacterStates(char)), true)
} }
HostEvent::QrMode(mode) => (None, true), HostEvent::QrMode(mode) => (None, true),
HostEvent::PlayerList(mut players) => { HostEvent::Lobby {
mut players,
settings,
} => {
const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap(); const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap();
players.sort_by(|l, r| { players.sort_by(|l, r| {
l.identification l.identification
@ -704,69 +710,16 @@ impl Host {
.unwrap_or(LAST) .unwrap_or(LAST)
.cmp(&r.identification.public.number.unwrap_or(LAST)) .cmp(&r.identification.public.number.unwrap_or(LAST))
}); });
match &self.state { (
HostState::Lobby { settings, .. } => (
Some(HostState::Lobby { Some(HostState::Lobby {
settings,
players: players.into_iter().collect(), players: players.into_iter().collect(),
settings: settings.clone(),
}), }),
true, 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),
}
} }
HostEvent::SetErrorCallback(callback) => (None, false), HostEvent::SetErrorCallback(callback) => (None, false),
HostEvent::SetState(state) => (Some(state), true), 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::Error(_) => (None, false),
HostEvent::SetBigScreenState(_) => (None, true), HostEvent::SetBigScreenState(_) => (None, true),
HostEvent::Continue => (None, false), HostEvent::Continue => (None, false),
@ -825,12 +778,6 @@ impl Host {
{ {
log::error!("sending game settings update: {err}"); 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(); let send = self.send.clone();

View File

@ -119,6 +119,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
} }
}); });
let (character_id, targets, marked, role_info) = match &props.prompt { let (character_id, targets, marked, role_info) = match &props.prompt {
ActionPrompt::BeholderWakes { .. } => return html! {},
ActionPrompt::TraitorIntro { character_id } => { ActionPrompt::TraitorIntro { character_id } => {
return html! { return html! {
<div class="prompt"> <div class="prompt">
@ -221,7 +222,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
marked.iter().cloned().collect::<Box<[CharacterId]>>(), marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {{"adjudicator"}}, html! {{"adjudicator"}},
), ),
ActionPrompt::Beholder { ActionPrompt::BeholderChooses {
character_id, character_id,
living_players, living_players,
marked, marked,

View File

@ -41,7 +41,7 @@ impl RolePage for ActionPrompt {
}) })
}; };
match self { match self {
ActionPrompt::Beholder { character_id, .. } => Rc::new([html! { ActionPrompt::BeholderChooses { character_id, .. } => Rc::new([html! {
<> <>
{ident(character_id)} {ident(character_id)}
<BeholderPage1 /> <BeholderPage1 />
@ -247,6 +247,12 @@ impl RolePage for ActionPrompt {
<BloodletterPage1 /> <BloodletterPage1 />
</> </>
}]), }]),
ActionPrompt::BeholderWakes { character_id } => Rc::new([html! {
<>
{ident(character_id)}
<BeholderWakePage1 />
</>
}]),
_ => Rc::new([]), _ => Rc::new([]),
} }
} }

View File

@ -14,7 +14,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Icon, IconSource, IconType}; use crate::components::{Icon, IconSource};
#[function_component] #[function_component]
pub fn BeholderPage1() -> Html { pub fn BeholderPage1() -> Html {
@ -30,6 +30,22 @@ pub fn BeholderPage1() -> Html {
} }
} }
#[function_component]
pub fn BeholderWakePage1() -> Html {
html! {
<div class="role-page">
<h1 class="intel">{"BEHOLDER"}</h1>
<div class="information intel faint">
<h2>{"YOUR TARGET HAS DIED"}</h2>
<div class="info-icon-grow">
<Icon source={IconSource::Beholder}/>
</div>
<h2 class="yellow">{"THIS IS THE LAST PIECE OF INFORMATION THEY SAW"}</h2>
</div>
</div>
}
}
#[function_component] #[function_component]
pub fn BeholderSawNothing() -> Html { pub fn BeholderSawNothing() -> Html {
html! { html! {

View File

@ -249,6 +249,9 @@ impl From<ActionResultTitle> for TestScreen {
impl From<ActionPromptTitle> for TestScreen { impl From<ActionPromptTitle> for TestScreen {
fn from(value: ActionPromptTitle) -> Self { fn from(value: ActionPromptTitle) -> Self {
Self::Prompt(match value { Self::Prompt(match value {
ActionPromptTitle::BeholderWakes => ActionPrompt::BeholderWakes {
character_id: identity(),
},
ActionPromptTitle::CoverOfDarkness => ActionPrompt::CoverOfDarkness, ActionPromptTitle::CoverOfDarkness => ActionPrompt::CoverOfDarkness,
ActionPromptTitle::WolvesIntro => ActionPrompt::WolvesIntro { ActionPromptTitle::WolvesIntro => ActionPrompt::WolvesIntro {
wolves: identities(5) wolves: identities(5)
@ -327,7 +330,7 @@ impl From<ActionPromptTitle> for TestScreen {
dead_players: identities(20), dead_players: identities(20),
marked: None, marked: None,
}, },
ActionPromptTitle::Beholder => ActionPrompt::Beholder { ActionPromptTitle::BeholderChooses => ActionPrompt::BeholderChooses {
character_id: identities(1).into_iter().next().unwrap(), character_id: identities(1).into_iter().next().unwrap(),
living_players: identities(20), living_players: identities(20),
marked: None, marked: None,
@ -417,13 +420,14 @@ fn prompt_class(prompt: &ActionPrompt) -> Option<&'static str> {
ActionPrompt::ElderReveal { .. } ActionPrompt::ElderReveal { .. }
| ActionPrompt::RoleChange { .. } | ActionPrompt::RoleChange { .. }
| ActionPrompt::CoverOfDarkness => None, | ActionPrompt::CoverOfDarkness => None,
ActionPrompt::Seer { .. } ActionPrompt::BeholderWakes { .. }
| ActionPrompt::Seer { .. }
| ActionPrompt::Arcanist { .. } | ActionPrompt::Arcanist { .. }
| ActionPrompt::Gravedigger { .. } | ActionPrompt::Gravedigger { .. }
| ActionPrompt::Adjudicator { .. } | ActionPrompt::Adjudicator { .. }
| ActionPrompt::PowerSeer { .. } | ActionPrompt::PowerSeer { .. }
| ActionPrompt::Mortician { .. } | ActionPrompt::Mortician { .. }
| ActionPrompt::Beholder { .. } | ActionPrompt::BeholderChooses { .. }
| ActionPrompt::MasonsWake { .. } | ActionPrompt::MasonsWake { .. }
| ActionPrompt::MasonLeaderRecruit { .. } | ActionPrompt::MasonLeaderRecruit { .. }
| ActionPrompt::Empath { .. } => Some("intel"), | ActionPrompt::Empath { .. } => Some("intel"),

View File

@ -304,14 +304,15 @@ pub fn PromptScreenTest(
} }
} }
ActionPrompt::Protector { .. } ActionPrompt::BeholderWakes { .. }
| ActionPrompt::Protector { .. }
| ActionPrompt::Arcanist { .. } | ActionPrompt::Arcanist { .. }
| ActionPrompt::Gravedigger { .. } | ActionPrompt::Gravedigger { .. }
| ActionPrompt::Militia { .. } | ActionPrompt::Militia { .. }
| ActionPrompt::Adjudicator { .. } | ActionPrompt::Adjudicator { .. }
| ActionPrompt::PowerSeer { .. } | ActionPrompt::PowerSeer { .. }
| ActionPrompt::Mortician { .. } | ActionPrompt::Mortician { .. }
| ActionPrompt::Beholder { .. } | ActionPrompt::BeholderChooses { .. }
| ActionPrompt::Empath { .. } | ActionPrompt::Empath { .. }
| ActionPrompt::Vindicator { .. } | ActionPrompt::Vindicator { .. }
| ActionPrompt::PyreMaster { .. } | ActionPrompt::PyreMaster { .. }