use core::{num::NonZeroU8, ops::Not}; use std::collections::VecDeque; use serde::{Deserialize, Serialize}; use werewolves_macros::Extract; use super::Result; use crate::{ diedto::DiedTo, error::GameError, game::{ DateTime, Village, kill::{self, ChangesLookup}, }, message::night::{ActionPrompt, ActionResponse, ActionResult}, player::{Character, CharacterId, Protection}, role::{PreviousGuardianAction, Role, 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, }, } struct ResponseOutcome { pub result: ActionResult, pub change: Option, pub unless: Option, } 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, 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 mut action_queue = village .characters() .into_iter() .map(|c| c.night_action_prompt(&village)) .collect::>>()? .into_iter() .flatten() .chain((night > 0).then(|| ActionPrompt::WolfPackKill { living_villagers: village.living_villagers(), })) .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 mut changes = Vec::new(); if let Some(night_nz) = NonZeroU8::new(night) { // TODO: prob should be an end-of-night thing changes = village .dead_characters() .into_iter() .filter_map(|c| c.died_to().map(|d| (c, d))) .filter_map(|(c, d)| match c.role() { Role::Hunter { target } => target.clone().map(|t| (c, t, d)), _ => None, }) .filter_map(|(c, t, d)| match d.date_time() { DateTime::Day { number } => (number.get() == night).then_some((c, t)), DateTime::Night { number: _ } => None, }) .map(|(c, target)| NightChange::Kill { target, died_to: DiedTo::Hunter { killer: c.character_id().clone(), night: night_nz, }, }) .collect(); } Ok(Self { night, changes, village, night_state, action_queue, used_actions: Vec::new(), }) } pub fn previous_state(&mut self) -> Result<()> { let prev_act = 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::RoleChange(character_id, role_title) => new_village .character_by_id_mut(character_id) .unwrap() .role_change(*role_title, DateTime::Night { number: self.night })?, NightChange::HunterTarget { source, target } => { if let Role::Hunter { target: t } = new_village.character_by_id_mut(source).unwrap().role_mut() { t.replace(target.clone()); } 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.clone(), 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(); match ss.role_mut() { Role::Shapeshifter { shifted_into } => { *shifted_into = Some(target.clone()) } _ => unreachable!(), } ss.kill(DiedTo::Shapeshift { into: target.clone(), night: NonZeroU8::new(self.night).unwrap(), }); // role change pushed in [apply_shapeshift] } } NightChange::RoleBlock { source: _, target: _, block_type: _, } | NightChange::Protection { target: _, protection: _, } => {} } } if new_village.is_game_over().is_none() { new_village.to_day()?; } Ok(new_village) } 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.clone()), _ => 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.clone(), died_to: DiedTo::Shapeshift { into: kill_target.clone(), night: NonZeroU8::new(self.night).unwrap(), }, }); } self.changes.push(NightChange::Shapeshift { source: source.clone(), }); self.action_queue.push_front(ActionPrompt::RoleChange { new_role: RoleTitle::Werewolf, character_id: self .village .character_by_id(&kill_target) .ok_or(GameError::NoMatchingCharacterFound)? .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 { if let NightState::Active { current_prompt: ActionPrompt::CoverOfDarkness, current_result: None, } = &mut self.night_state && let ActionResponse::ClearCoverOfDarkness = &resp { self.night_state = NightState::Active { current_prompt: ActionPrompt::CoverOfDarkness, current_result: Some(ActionResult::Continue), }; return Ok(ActionResult::Continue); } match self.received_response_with_role_blocks(resp)? { (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(self .action_queue .iter() .next() .and_then(|a| a.is_wolfy().then_some(ActionResult::Continue)) .unwrap_or(ActionResult::GoBackToSleep)); } self.changes.push(change); Ok(result) } (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(result) } } } fn received_response_consecutive_wolves_dont_sleep( &self, resp: ActionResponse, ) -> Result { let current_wolfy = self.current_prompt().unwrap().is_wolfy(); let next_wolfy = self .action_queue .iter() .next() .map(|a| a.is_wolfy()) .unwrap_or_default(); match ( self.received_response_inner(resp)?, current_wolfy, next_wolfy, ) { ( ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source }), unless, }, true, _, ) => Ok(ResponseOutcome { result: ActionResult::Continue, change: Some(NightChange::Shapeshift { source }), unless, }), ( ResponseOutcome { result: ActionResult::GoBackToSleep, change, unless, }, true, true, ) => Ok(ResponseOutcome { result: ActionResult::Continue, change, unless, }), (outcome, _, _) => Ok(outcome), } } fn received_response_with_role_blocks( &self, resp: ActionResponse, ) -> Result<(ActionResult, Option)> { match self.received_response_consecutive_wolves_dont_sleep(resp)? { ResponseOutcome { result, change, 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((ActionResult::RoleBlocked, None)) } else { Ok((result, change)) } } ResponseOutcome { result, change, unless: 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((ActionResult::RoleBlocked, None)) } else { Ok((result, change)) } } ResponseOutcome { result, change, unless: None, } => Ok((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 (current_prompt, resp) { ( ActionPrompt::RoleChange { character_id: current_char, new_role, }, ActionResponse::RoleChangeAck, ) => Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::RoleChange( current_char.character_id.clone(), *new_role, )), unless: None, }), (ActionPrompt::WolvesIntro { wolves: _ }, ActionResponse::WolvesIntroAck) => { Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }) } ( ActionPrompt::WolfPackKill { living_villagers }, ActionResponse::WolfPackKillVote(target), ) => { let night = match NonZeroU8::new(self.night) { Some(night) => night, None => { return Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }); } }; if !living_villagers.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: target.clone(), died_to: DiedTo::Wolfpack { night, killing_wolf: self.village.killing_wolf_id(), }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::Seer { character_id: _, living_players, }, ActionResponse::Seer(target), ) => { if !living_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome { result: ActionResult::Seer( self.village .character_by_id(&target) .unwrap() .role() .alignment(), ), change: None, unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::Arcanist { character_id: _, living_players, }, ActionResponse::Arcanist(target1, target2), ) => { if !(living_players.iter().any(|p| p.character_id == target1) && living_players.iter().any(|p| p.character_id == target2)) { return Err(GameError::InvalidTarget); } let target1_align = self .village .character_by_id(&target1) .unwrap() .role() .alignment(); let target2_align = self .village .character_by_id(&target2) .unwrap() .role() .alignment(); Ok(ResponseOutcome { result: ActionResult::Arcanist { same: target1_align == target2_align, }, change: None, unless: Some(Unless::TargetsBlocked(target1, target2)), }) } ( ActionPrompt::Gravedigger { character_id: _, dead_players, }, ActionResponse::Gravedigger(target), ) => { if !dead_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } let target_role = self.village.character_by_id(&target).unwrap().role(); Ok(ResponseOutcome { result: ActionResult::GraveDigger( matches!( target_role, Role::Shapeshifter { shifted_into: Some(_) } ) .not() .then(|| target_role.title()), ), change: None, unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::Hunter { character_id: current_char, current_target: _, living_players, }, ActionResponse::Hunter(target), ) => { if !living_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::HunterTarget { target: target.clone(), source: current_char.character_id.clone(), }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::Militia { character_id: current_char, living_players, }, ActionResponse::Militia(Some(target)), ) => { if !living_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } let night = if let Some(night) = NonZeroU8::new(self.night) { night } else { return Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }); }; Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: target.clone(), died_to: DiedTo::Militia { night, killer: current_char.character_id.clone(), }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::Militia { character_id: _, living_players: _, }, ActionResponse::Militia(None), ) => Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }), ( ActionPrompt::MapleWolf { character_id: current_char, kill_or_die, living_players, }, ActionResponse::MapleWolf(Some(target)), ) => { if !living_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } let night = if let Some(night) = NonZeroU8::new(self.night) { night } else { return Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }); }; Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: target.clone(), died_to: DiedTo::MapleWolf { night, source: current_char.character_id.clone(), starves_if_fails: *kill_or_die, }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::MapleWolf { character_id: current_char, kill_or_die: true, living_players: _, }, ActionResponse::MapleWolf(None), ) => { let night = if let Some(night) = NonZeroU8::new(self.night) { night } else { return Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }); }; Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: current_char.character_id.clone(), died_to: DiedTo::MapleWolfStarved { night }, }), unless: None, }) } ( ActionPrompt::MapleWolf { character_id: _, kill_or_die: false, living_players: _, }, ActionResponse::MapleWolf(None), ) => Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }), ( ActionPrompt::Guardian { character_id: current_char, previous: Some(previous), living_players, }, ActionResponse::Guardian(target), ) => { if !living_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } let guarding = match previous { PreviousGuardianAction::Protect(prev_target) => { prev_target.character_id == target } PreviousGuardianAction::Guard(prev_target) => { if prev_target.character_id == target { return Err(GameError::InvalidTarget); } else { false } } }; Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { target: target.clone(), protection: Protection::Guardian { source: current_char.character_id.clone(), guarding, }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::Guardian { character_id: current_char, previous: None, living_players, }, ActionResponse::Guardian(target), ) => { if !living_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { target: target.clone(), protection: Protection::Guardian { source: current_char.character_id.clone(), guarding: false, }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::Shapeshifter { character_id: current_char, }, ActionResponse::Shapeshifter(true), ) => Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source: current_char.character_id.clone(), }), unless: None, }), ( ActionPrompt::AlphaWolf { character_id: _, living_villagers: _, }, ActionResponse::AlphaWolf(None), ) | ( ActionPrompt::Shapeshifter { character_id: _ }, ActionResponse::Shapeshifter(false), ) => Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }), ( ActionPrompt::AlphaWolf { character_id: current_char, living_villagers, }, ActionResponse::AlphaWolf(Some(target)), ) => { let night = match NonZeroU8::new(self.night) { Some(night) => night, None => { return Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }); } }; if !living_villagers.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: target.clone(), died_to: DiedTo::AlphaWolf { killer: current_char.character_id.clone(), night, }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::DireWolf { character_id: current_char, living_players, }, ActionResponse::Direwolf(target), ) => { if !living_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::RoleBlock { source: current_char.character_id.clone(), target, block_type: RoleBlock::Direwolf, }), unless: None, }) } ( ActionPrompt::Protector { character_id: current_char, targets, }, ActionResponse::Protector(target), ) => { if !targets.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { target: target.clone(), protection: Protection::Protector { source: current_char.character_id.clone(), }, }), unless: Some(Unless::TargetBlocked(target)), }) } // For other responses that are invalid -- this allows the match to error // if a new prompt is added ( ActionPrompt::WolfPackKill { living_villagers: _, }, _, ) | (ActionPrompt::WolvesIntro { wolves: _ }, _) | (ActionPrompt::CoverOfDarkness, _) | ( ActionPrompt::RoleChange { character_id: _, new_role: _, }, _, ) | ( ActionPrompt::Seer { character_id: _, living_players: _, }, _, ) | ( ActionPrompt::Protector { character_id: _, targets: _, }, _, ) | ( ActionPrompt::Arcanist { character_id: _, living_players: _, }, _, ) | ( ActionPrompt::Gravedigger { character_id: _, dead_players: _, }, _, ) | ( ActionPrompt::Hunter { character_id: _, current_target: _, living_players: _, }, _, ) | ( ActionPrompt::Militia { character_id: _, living_players: _, }, _, ) | ( ActionPrompt::MapleWolf { character_id: _, kill_or_die: _, living_players: _, }, _, ) | ( ActionPrompt::Guardian { character_id: _, previous: _, living_players: _, }, _, ) | (ActionPrompt::Shapeshifter { character_id: _ }, _) | ( ActionPrompt::AlphaWolf { character_id: _, living_villagers: _, }, _, ) | ( ActionPrompt::DireWolf { character_id: _, living_players: _, }, _, ) => 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<&CharacterId> { match &self.night_state { NightState::Active { current_prompt, current_result: _, } => match current_prompt { ActionPrompt::RoleChange { character_id, new_role: _, } | ActionPrompt::Seer { character_id, living_players: _, } | ActionPrompt::Protector { character_id, targets: _, } | ActionPrompt::Arcanist { character_id, living_players: _, } | ActionPrompt::Gravedigger { character_id, dead_players: _, } | ActionPrompt::Hunter { character_id, current_target: _, living_players: _, } | ActionPrompt::Militia { character_id, living_players: _, } | ActionPrompt::MapleWolf { character_id, kill_or_die: _, living_players: _, } | ActionPrompt::Guardian { character_id, previous: _, living_players: _, } | ActionPrompt::Shapeshifter { character_id } | ActionPrompt::AlphaWolf { character_id, living_villagers: _, } | ActionPrompt::DireWolf { character_id, living_players: _, } => Some(&character_id.character_id), ActionPrompt::WolvesIntro { wolves: _ } | ActionPrompt::WolfPackKill { living_villagers: _, } | 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)) } 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(_), } => {} 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.used_actions.push(prompt.clone()); 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() } }