use core::num::NonZeroU8; use std::collections::VecDeque; use serde::{Deserialize, Serialize}; use werewolves_macros::Extract; use super::Result; use crate::{ character::{Character, CharacterId}, diedto::DiedTo, error::GameError, game::{ DateTime, Village, kill::{self, ChangesLookup}, }, message::night::{ActionPrompt, ActionResponse, ActionResult}, player::Protection, role::{PreviousGuardianAction, RoleBlock, RoleTitle}, }; #[derive(Debug, Clone, Serialize, Deserialize, Extract)] pub enum NightChange { RoleChange(CharacterId, RoleTitle), HunterTarget { source: CharacterId, target: CharacterId, }, Kill { target: CharacterId, died_to: DiedTo, }, RoleBlock { source: CharacterId, target: CharacterId, block_type: RoleBlock, }, Shapeshift { source: CharacterId, }, Protection { target: CharacterId, protection: Protection, }, ElderReveal { elder: CharacterId, }, MasonRecruit { mason_leader: CharacterId, recruiting: CharacterId, }, EmpathFoundScapegoat { empath: CharacterId, scapegoat: CharacterId, }, } enum BlockResolvedOutcome { PromptUpdate(ActionPrompt), ActionComplete(ActionResult, Option), } enum ResponseOutcome { PromptUpdate(ActionPrompt), ActionComplete(ActionComplete), } struct ActionComplete { pub result: ActionResult, pub change: Option, } impl From for ResponseOutcome { fn from(value: ActionComplete) -> Self { ResponseOutcome::ActionComplete(value) } } impl ActionPrompt { fn unless(&self) -> Option { match &self { ActionPrompt::MasonsWake { .. } | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } | ActionPrompt::Shapeshifter { .. } | ActionPrompt::ElderReveal { .. } | ActionPrompt::CoverOfDarkness => None, ActionPrompt::Arcanist { marked: (Some(marked1), Some(marked2)), .. } => Some(Unless::TargetsBlocked(*marked1, *marked2)), ActionPrompt::Seer { marked: Some(marked), .. } | ActionPrompt::Protector { marked: Some(marked), .. } | ActionPrompt::Gravedigger { marked: Some(marked), .. } | ActionPrompt::Hunter { marked: Some(marked), .. } | ActionPrompt::Militia { marked: Some(marked), .. } | ActionPrompt::MapleWolf { marked: Some(marked), .. } | ActionPrompt::Guardian { marked: Some(marked), .. } | ActionPrompt::Adjudicator { marked: Some(marked), .. } | ActionPrompt::PowerSeer { marked: Some(marked), .. } | ActionPrompt::Mortician { marked: Some(marked), .. } | ActionPrompt::Beholder { marked: Some(marked), .. } | ActionPrompt::MasonLeaderRecruit { marked: Some(marked), .. } | ActionPrompt::Empath { marked: Some(marked), .. } | ActionPrompt::Vindicator { marked: Some(marked), .. } | ActionPrompt::PyreMaster { marked: Some(marked), .. } | ActionPrompt::WolfPackKill { marked: Some(marked), .. } | ActionPrompt::AlphaWolf { marked: Some(marked), .. } | ActionPrompt::DireWolf { marked: Some(marked), .. } => Some(Unless::TargetBlocked(*marked)), ActionPrompt::Seer { marked: None, .. } | ActionPrompt::Protector { marked: None, .. } | ActionPrompt::Gravedigger { marked: None, .. } | ActionPrompt::Hunter { marked: None, .. } | ActionPrompt::Militia { marked: None, .. } | ActionPrompt::MapleWolf { marked: None, .. } | ActionPrompt::Guardian { marked: None, .. } | ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. } | ActionPrompt::Beholder { marked: None, .. } | ActionPrompt::MasonLeaderRecruit { marked: None, .. } | ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. } | ActionPrompt::PyreMaster { marked: None, .. } | ActionPrompt::WolfPackKill { marked: None, .. } | ActionPrompt::AlphaWolf { marked: None, .. } | ActionPrompt::DireWolf { marked: None, .. } | ActionPrompt::Arcanist { marked: (Some(_), None), .. } | ActionPrompt::Arcanist { marked: (None, Some(_)), .. } | ActionPrompt::Arcanist { marked: (None, None), .. } => None, } } } impl Default for ActionComplete { fn default() -> Self { Self { result: ActionResult::GoBackToSleep, change: None, } } } enum Unless { TargetBlocked(CharacterId), TargetsBlocked(CharacterId, CharacterId), } impl From for ActionResult { fn from(value: Unless) -> Self { match value { Unless::TargetBlocked(_) | Unless::TargetsBlocked(_, _) => ActionResult::RoleBlocked, } } } #[derive(Debug, Clone, Serialize, Deserialize)] #[allow(clippy::large_enum_variant)] enum NightState { Active { current_prompt: ActionPrompt, current_result: Option, }, Complete, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Night { village: Village, night: u8, action_queue: VecDeque, used_actions: Vec<(ActionPrompt, ActionResult)>, changes: Vec, night_state: NightState, } impl Night { pub fn new(village: Village) -> Result { let night = match village.date_time() { DateTime::Day { number: _ } => return Err(GameError::NotNight), DateTime::Night { number } => number, }; let filter = if village.executed_known_elder() { // there is a lynched elder, remove villager PRs from the prompts filter::no_village } else { filter::no_filter }; let mut action_queue = village .characters() .into_iter() .filter(filter) .map(|c| c.night_action_prompts(&village)) .collect::>>()? .into_iter() .flatten() .chain(village.wolf_pack_kill()) .collect::>(); action_queue.sort_by(|left_prompt, right_prompt| { left_prompt .partial_cmp(right_prompt) .unwrap_or(core::cmp::Ordering::Equal) }); let mut action_queue = VecDeque::from(action_queue); if night == 0 { action_queue.push_front(ActionPrompt::WolvesIntro { wolves: village .living_wolf_pack_players() .into_iter() .map(|w| (w.identity(), w.role_title())) .collect(), }); } // let current_prompt = action_queue.pop_front().ok_or(GameError::NoNightActions)?; let night_state = NightState::Active { current_prompt: ActionPrompt::CoverOfDarkness, current_result: None, }; let changes = Self::automatic_changes(&village, night); Ok(Self { night, changes, village, night_state, action_queue, used_actions: Vec::new(), }) } /// changes that require no input (such as hunter firing) fn automatic_changes(village: &Village, night: u8) -> Vec { let mut changes = Vec::new(); let night = match NonZeroU8::new(night) { Some(night) => night, None => return changes, }; if !village.executed_known_elder() { village .dead_characters() .into_iter() .filter_map(|c| c.died_to().map(|d| (c, d))) .filter_map(|(c, d)| c.hunter().ok().and_then(|h| *h).map(|t| (c, t, d))) .filter_map(|(c, t, d)| match d.date_time() { DateTime::Day { number } => (number.get() == night.get()).then_some((c, t)), DateTime::Night { number: _ } => None, }) .map(|(c, target)| NightChange::Kill { target, died_to: DiedTo::Hunter { night, killer: c.character_id(), }, }) .for_each(|c| changes.push(c)); } changes } pub fn previous_state(&mut self) -> Result<()> { return Err(GameError::NoPreviousState); let (prev_act, prev_result) = self.used_actions.pop().ok_or(GameError::NoPreviousState)?; log::info!("loading previous prompt: {prev_act:?}"); match &self.night_state { NightState::Active { current_prompt, current_result: Some(current_result), } => { log::info!("removing current result: {current_result:?}"); self.night_state = NightState::Active { current_prompt: current_prompt.clone(), current_result: None, }; } NightState::Active { current_prompt, current_result: None, } => { log::info!("pushing current prompt to front of action queue: {current_prompt:?}"); self.action_queue.push_front(current_prompt.clone()); self.night_state = NightState::Active { current_prompt: prev_act, current_result: None, } } NightState::Complete => { self.night_state = NightState::Active { current_prompt: prev_act, current_result: None, }; } } Ok(()) } #[cfg(test)] pub fn action_queue(&self) -> Box<[ActionPrompt]> { self.action_queue.iter().cloned().collect() } pub fn collect_completed(&self) -> Result { if !matches!(self.night_state, NightState::Complete) { return Err(GameError::NotEndOfNight); } let mut new_village = self.village.clone(); let mut changes = ChangesLookup::new(&self.changes); for change in self.changes.iter() { match change { NightChange::ElderReveal { elder } => { new_village.character_by_id_mut(*elder)?.elder_reveal() } NightChange::RoleChange(character_id, role_title) => new_village .character_by_id_mut(*character_id)? .role_change(*role_title, DateTime::Night { number: self.night })?, NightChange::HunterTarget { source, target } => { let hunter_character = new_village.character_by_id_mut(*source).unwrap(); hunter_character.hunter_mut()?.replace(*target); if changes.killed(*source).is_some() && changes.protected(source).is_none() && changes.protected(target).is_none() { new_village .character_by_id_mut(*target) .unwrap() .kill(DiedTo::Hunter { killer: *source, night: NonZeroU8::new(self.night).unwrap(), }) } } NightChange::Kill { target, died_to } => { if let Some(kill) = kill::resolve_kill( &mut changes, *target, died_to, self.night, &self.village, )? { kill.apply_to_village(&mut new_village)?; } } NightChange::Shapeshift { source } => { if let Some(target) = changes.wolf_pack_kill_target() && changes.protected(target).is_none() { let ss = new_village.character_by_id_mut(*source).unwrap(); ss.shapeshifter_mut().unwrap().replace(*target); ss.kill(DiedTo::Shapeshift { into: *target, night: NonZeroU8::new(self.night).unwrap(), }); // role change pushed in [apply_shapeshift] } } NightChange::RoleBlock { source: _, target: _, block_type: _, } | NightChange::Protection { target: _, protection: _, } => {} NightChange::MasonRecruit { mason_leader, recruiting, } => { if new_village.character_by_id(*recruiting)?.is_wolf() { new_village.character_by_id_mut(*mason_leader)?.kill( DiedTo::MasonLeaderRecruitFail { tried_recruiting: *recruiting, night: self.night, }, ); } else { new_village .character_by_id_mut(*mason_leader)? .mason_leader_mut()? .recruit(*recruiting); } } NightChange::EmpathFoundScapegoat { empath, scapegoat } => { new_village .character_by_id_mut(*scapegoat)? .role_change(RoleTitle::Villager, DateTime::Night { number: self.night })?; *new_village.character_by_id_mut(*empath)?.empath_mut()? = true; } } } for knight in new_village .characters_mut() .into_iter() .filter(|k| k.black_knight().ok().and_then(|t| (*t).clone()).is_some()) .filter(|k| changes.killed(k.character_id()).is_none()) { knight.black_knight_kill()?.kill(); } if new_village.is_game_over().is_none() { new_village.to_day()?; } Ok(new_village) } fn apply_mason_recruit( &mut self, mason_leader: CharacterId, recruiting: CharacterId, ) -> Result { if self.village.character_by_id(recruiting)?.is_village() { if let Some(masons) = self.action_queue.iter_mut().find_map(|a| match a { ActionPrompt::MasonsWake { character_id, masons, } => (character_id.character_id == mason_leader).then_some(masons), _ => None, }) { let mut ext_masons = masons.to_vec(); ext_masons.push(self.village.character_by_id(recruiting)?.identity()); *masons = ext_masons.into_boxed_slice(); } else { self.action_queue.push_front(ActionPrompt::MasonsWake { character_id: self.village.character_by_id(mason_leader)?.identity(), masons: Box::new([self.village.character_by_id(recruiting)?.identity()]), }); } Ok(ActionResult::Continue) } else { Ok(ActionResult::GoBackToSleep) } } fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> { if let Some(kill_target) = self.changes.iter().find_map(|c| match c { NightChange::Kill { target, died_to: DiedTo::Wolfpack { night: _, killing_wolf: _, }, } => Some(*target), _ => None, }) { if self.changes.iter().any(|c| match c { NightChange::Protection { target, protection: _, } => target == &kill_target, _ => false, }) { // there is protection, so the kill doesn't happen -> no shapeshift return Ok(()); } if self.changes.iter_mut().any(|c| { matches!( c, NightChange::Kill { target: _, died_to: DiedTo::Wolfpack { night: _, killing_wolf: _ } } ) }) { self.changes.push(NightChange::Kill { target: *source, died_to: DiedTo::Shapeshift { into: kill_target, night: NonZeroU8::new(self.night).unwrap(), }, }); } self.changes .push(NightChange::Shapeshift { source: *source }); self.action_queue.push_front(ActionPrompt::RoleChange { new_role: RoleTitle::Werewolf, character_id: self.village.character_by_id(kill_target)?.identity(), }); } // Remove any further shapeshift prompts from the queue let mut new_queue = VecDeque::new(); while let Some(prompt) = self.action_queue.pop_back() { match &prompt { ActionPrompt::Shapeshifter { character_id: _ } => {} _ => new_queue.push_front(prompt), } } self.action_queue = new_queue; Ok(()) } pub fn received_response(&mut self, resp: ActionResponse) -> Result { match self.received_response_with_role_blocks(resp)? { BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state { NightState::Active { current_result: Some(_), .. } => Err(GameError::AwaitingResponse), NightState::Active { current_prompt, .. } => { *current_prompt = prompt.clone(); Ok(ServerAction::Prompt(prompt)) } NightState::Complete => Err(GameError::NightOver), }, BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => { match &mut self.night_state { NightState::Active { current_prompt: _, current_result, } => current_result.replace(result.clone()), NightState::Complete => return Err(GameError::NightOver), }; if let NightChange::Shapeshift { source } = &change { // needs to be resolved _now_ so that the target can be woken // for the role change with the wolves self.apply_shapeshift(source)?; return Ok(ServerAction::Result( self.action_queue .iter() .next() .and_then(|a| a.is_wolfy().then_some(ActionResult::Continue)) .unwrap_or(ActionResult::GoBackToSleep), )); } if let NightChange::MasonRecruit { mason_leader, recruiting, } = &change { result = self.apply_mason_recruit(*mason_leader, *recruiting)?; } self.changes.push(change); Ok(ServerAction::Result(result)) } BlockResolvedOutcome::ActionComplete(result, None) => { match &mut self.night_state { NightState::Active { current_prompt: _, current_result, } => { current_result.replace(result.clone()); } NightState::Complete => return Err(GameError::NightOver), }; Ok(ServerAction::Result(result)) } } } fn received_response_consecutive_wolves_dont_sleep( &self, resp: ActionResponse, ) -> Result { let (current_cover, current_wolfy) = self .current_prompt() .map(|current_prompt| { ( *current_prompt == ActionPrompt::CoverOfDarkness, current_prompt.is_wolfy(), ) }) .unwrap_or_default(); let next_wolfy = self .action_queue .iter() .next() .map(|a| a.is_wolfy()) .unwrap_or_default(); if current_cover && let ActionResponse::Continue = &resp { return Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, change: None, })); } match ( self.received_response_inner(resp)?, current_wolfy, next_wolfy, ) { (ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)), ( ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source }), }), true, _, ) => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, change: Some(NightChange::Shapeshift { source }), })), ( ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change, }), true, true, ) => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, change, })), (outcome, _, _) => Ok(outcome), } } fn received_response_with_role_blocks( &self, resp: ActionResponse, ) -> Result { match self.received_response_consecutive_wolves_dont_sleep(resp)? { ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)), ResponseOutcome::ActionComplete(ActionComplete { result, change }) => { match self.current_prompt().ok_or(GameError::NightOver)?.unless() { Some(Unless::TargetBlocked(unless_blocked)) => { if self.changes.iter().any(|c| match c { NightChange::RoleBlock { source: _, target, block_type: _, } => target == &unless_blocked, _ => false, }) { Ok(BlockResolvedOutcome::ActionComplete( ActionResult::RoleBlocked, None, )) } else { Ok(BlockResolvedOutcome::ActionComplete(result, change)) } } Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => { if self.changes.iter().any(|c| match c { NightChange::RoleBlock { source: _, target, block_type: _, } => target == &unless_blocked1 || target == &unless_blocked2, _ => false, }) { Ok(BlockResolvedOutcome::ActionComplete( ActionResult::RoleBlocked, None, )) } else { Ok(BlockResolvedOutcome::ActionComplete(result, change)) } } None => Ok(BlockResolvedOutcome::ActionComplete(result, change)), } } } } fn received_response_inner(&self, resp: ActionResponse) -> Result { let current_prompt = match &self.night_state { NightState::Active { current_prompt: _, current_result: Some(_), } => return Err(GameError::NightNeedsNext), NightState::Active { current_prompt, current_result: None, } => current_prompt, NightState::Complete => return Err(GameError::NightOver), }; match resp { ActionResponse::MarkTarget(mark) => { return Ok(ResponseOutcome::PromptUpdate( current_prompt.with_mark(mark)?, )); } ActionResponse::Shapeshift => { return match current_prompt { ActionPrompt::Shapeshifter { character_id: source, } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source: source.character_id, }), })), _ => Err(GameError::InvalidMessageForGameState), }; } ActionResponse::Continue => { if let ActionPrompt::RoleChange { character_id, new_role, } = current_prompt { return Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::RoleChange( character_id.character_id, *new_role, )), })); } } }; match current_prompt { ActionPrompt::RoleChange { .. } | ActionPrompt::WolvesIntro { .. } | ActionPrompt::CoverOfDarkness => { Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: None, })) } ActionPrompt::ElderReveal { character_id } => { Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::ElderReveal { elder: character_id.character_id, }), })) } ActionPrompt::Seer { marked: Some(marked), .. } => { let alignment = self.village.character_by_id(*marked)?.alignment(); Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Seer(alignment), change: None, })) } ActionPrompt::Protector { marked: Some(marked), character_id, .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { target: *marked, protection: Protection::Protector { source: character_id.character_id, }, }), })), ActionPrompt::Arcanist { marked: (Some(marked1), Some(marked2)), .. } => { let same = self.village.character_by_id(*marked1)?.alignment() == self.village.character_by_id(*marked2)?.alignment(); Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Arcanist { same }, change: None, })) } ActionPrompt::Gravedigger { marked: Some(marked), .. } => { let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig(); Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GraveDigger(dig_role), change: None, })) } ActionPrompt::Hunter { character_id, marked: Some(marked), .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::HunterTarget { source: character_id.character_id, target: *marked, }), })), ActionPrompt::Militia { character_id, marked: Some(marked), .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: *marked, died_to: DiedTo::Militia { killer: character_id.character_id, night: NonZeroU8::new(self.night) .ok_or(GameError::InvalidMessageForGameState)?, }, }), })), ActionPrompt::Militia { marked: None, .. } => { Ok(ResponseOutcome::ActionComplete(Default::default())) } ActionPrompt::MapleWolf { character_id, kill_or_die, marked: Some(marked), .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: *marked, died_to: DiedTo::MapleWolf { source: character_id.character_id, night: NonZeroU8::new(self.night) .ok_or(GameError::InvalidMessageForGameState)?, starves_if_fails: *kill_or_die, }, }), })), ActionPrompt::MapleWolf { marked: None, .. } => { Ok(ResponseOutcome::ActionComplete(Default::default())) } ActionPrompt::Guardian { character_id, previous: None, marked: Some(marked), .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { target: *marked, protection: Protection::Guardian { source: character_id.character_id, guarding: false, }, }), })), ActionPrompt::Guardian { character_id, previous: Some(PreviousGuardianAction::Guard(prev_target)), marked: Some(marked), .. } => { if prev_target.character_id == *marked { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { target: *marked, protection: Protection::Guardian { source: character_id.character_id, guarding: false, }, }), })) } ActionPrompt::Guardian { character_id, previous: Some(PreviousGuardianAction::Protect(prev_protect)), marked: Some(marked), .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { target: *marked, protection: Protection::Guardian { source: character_id.character_id, guarding: prev_protect.character_id == *marked, }, }), })), ActionPrompt::WolfPackKill { marked: Some(marked), .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: *marked, died_to: DiedTo::Wolfpack { killing_wolf: self .village .killing_wolf() .ok_or(GameError::NoWolves)? .character_id(), night: NonZeroU8::new(self.night) .ok_or(GameError::InvalidMessageForGameState)?, }, }), })), ActionPrompt::Shapeshifter { character_id } => { Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source: character_id.character_id, }), })) } ActionPrompt::AlphaWolf { character_id, marked: Some(marked), .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: *marked, died_to: DiedTo::AlphaWolf { killer: character_id.character_id, night: NonZeroU8::new(self.night) .ok_or(GameError::InvalidMessageForGameState)?, }, }), })), ActionPrompt::AlphaWolf { marked: None, .. } => { Ok(ResponseOutcome::ActionComplete(Default::default())) } ActionPrompt::DireWolf { character_id, marked: Some(marked), .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::RoleBlock { source: character_id.character_id, target: *marked, block_type: RoleBlock::Direwolf, }), })), ActionPrompt::Adjudicator { marked: Some(marked), .. } => Ok(ActionComplete { result: ActionResult::Adjudicator { killer: self.village.character_by_id(*marked)?.killer(), }, change: None, } .into()), ActionPrompt::PowerSeer { marked: Some(marked), .. } => Ok(ActionComplete { result: ActionResult::PowerSeer { powerful: self.village.character_by_id(*marked)?.powerful(), }, change: None, } .into()), ActionPrompt::Mortician { marked: Some(marked), .. } => Ok(ActionComplete { result: ActionResult::Mortician( self.village .character_by_id(*marked)? .died_to() .ok_or(GameError::InvalidTarget)? .title(), ), change: None, } .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) }) { Ok(ActionComplete { result: result.clone(), change: None, } .into()) } else { Ok(ActionComplete { result: ActionResult::GoBackToSleep, change: None, } .into()) } } ActionPrompt::MasonsWake { .. } => Ok(ActionComplete { result: ActionResult::GoBackToSleep, change: None, } .into()), ActionPrompt::MasonLeaderRecruit { character_id, marked: Some(marked), .. } => Ok(ActionComplete { result: ActionResult::Continue, change: Some(NightChange::MasonRecruit { mason_leader: character_id.character_id, recruiting: *marked, }), } .into()), ActionPrompt::Empath { character_id, marked: Some(marked), .. } => { let marked = self.village.character_by_id(*marked)?; let scapegoat = marked.role_title() == RoleTitle::Scapegoat; Ok(ActionComplete { result: ActionResult::Empath { scapegoat }, change: scapegoat.then(|| NightChange::EmpathFoundScapegoat { empath: character_id.character_id, scapegoat: marked.character_id(), }), } .into()) } ActionPrompt::Vindicator { character_id, marked: Some(marked), .. } => Ok(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { target: *marked, protection: Protection::Vindicator { source: character_id.character_id, }, }), } .into()), ActionPrompt::PyreMaster { character_id, marked: Some(marked), .. } => Ok(ActionComplete { result: ActionResult::GoBackToSleep, change: NonZeroU8::new(self.night).map(|night| NightChange::Kill { target: *marked, died_to: DiedTo::PyreMaster { killer: character_id.character_id, night, }, }), } .into()), ActionPrompt::PyreMaster { marked: None, .. } | ActionPrompt::MasonLeaderRecruit { marked: None, .. } => Ok(ActionComplete { result: ActionResult::GoBackToSleep, change: None, } .into()), ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. } | ActionPrompt::Beholder { marked: None, .. } | ActionPrompt::Empath { marked: None, .. } | ActionPrompt::Vindicator { marked: None, .. } | ActionPrompt::Protector { marked: None, .. } | ActionPrompt::Arcanist { marked: (None, None), .. } | ActionPrompt::Arcanist { marked: (None, Some(_)), .. } | ActionPrompt::Arcanist { marked: (Some(_), None), .. } | ActionPrompt::Gravedigger { marked: None, .. } | ActionPrompt::Hunter { marked: None, .. } | ActionPrompt::Guardian { marked: None, .. } | ActionPrompt::WolfPackKill { marked: None, .. } | ActionPrompt::DireWolf { marked: None, .. } | ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState), } } pub const fn village(&self) -> &Village { &self.village } pub const fn current_result(&self) -> Option<&ActionResult> { match &self.night_state { NightState::Active { current_prompt: _, current_result, } => current_result.as_ref(), NightState::Complete => None, } } pub const fn current_prompt(&self) -> Option<&ActionPrompt> { match &self.night_state { NightState::Active { current_prompt, current_result: _, } => Some(current_prompt), NightState::Complete => None, } } pub const fn current_character_id(&self) -> Option { match &self.night_state { NightState::Active { current_prompt, current_result: _, } => match current_prompt { ActionPrompt::ElderReveal { character_id } | ActionPrompt::RoleChange { character_id, .. } | ActionPrompt::Seer { character_id, .. } | ActionPrompt::Protector { character_id, .. } | ActionPrompt::Arcanist { character_id, .. } | ActionPrompt::Gravedigger { character_id, .. } | ActionPrompt::Hunter { character_id, .. } | ActionPrompt::Militia { character_id, .. } | ActionPrompt::MapleWolf { character_id, .. } | ActionPrompt::Guardian { character_id, .. } | ActionPrompt::Shapeshifter { character_id } | ActionPrompt::AlphaWolf { character_id, .. } | ActionPrompt::Adjudicator { character_id, .. } | ActionPrompt::PowerSeer { character_id, .. } | ActionPrompt::Mortician { character_id, .. } | ActionPrompt::Beholder { character_id, .. } | ActionPrompt::MasonsWake { character_id, .. } | ActionPrompt::MasonLeaderRecruit { character_id, .. } | ActionPrompt::Empath { character_id, .. } | ActionPrompt::Vindicator { character_id, .. } | ActionPrompt::PyreMaster { character_id, .. } | ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id), ActionPrompt::WolvesIntro { wolves: _ } | ActionPrompt::WolfPackKill { .. } | ActionPrompt::CoverOfDarkness => None, }, NightState::Complete => None, } } pub fn current_character(&self) -> Option<&Character> { self.current_character_id() .and_then(|id| self.village.character_by_id(id).ok()) } pub const fn complete(&self) -> bool { matches!(self.night_state, NightState::Complete) } pub fn next(&mut self) -> Result<()> { match &self.night_state { NightState::Active { current_prompt, current_result: Some(result), } => { self.used_actions .push((current_prompt.clone(), result.clone())); } NightState::Active { current_prompt: _, current_result: None, } => return Err(GameError::AwaitingResponse), NightState::Complete => return Err(GameError::NightOver), } if let Some(prompt) = self.action_queue.pop_front() { self.night_state = NightState::Active { current_prompt: prompt, current_result: None, }; } else { self.night_state = NightState::Complete; } Ok(()) } pub const fn changes(&self) -> &[NightChange] { self.changes.as_slice() } } pub enum ServerAction { Prompt(ActionPrompt), Result(ActionResult), } mod filter { use crate::character::Character; pub fn no_filter(_: &Character) -> bool { true } pub fn no_village(c: &Character) -> bool { !c.is_village() } }