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}, message::night::{ActionPrompt, ActionResponse, ActionResult}, player::{Character, CharacterId, Protection}, role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle}, }; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Night { village: Village, night: u8, action_queue: VecDeque<(ActionPrompt, Character)>, changes: Vec, night_state: NightState, } #[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_char: CharacterId, current_result: Option, }, Complete, } 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).map(|prompt| (prompt, c))) .collect::>>()? .into_iter() .filter_map(|(prompt, char)| prompt.map(|p| (p, char))) .collect::>(); action_queue.sort_by(|(left_prompt, _), (right_prompt, _)| { left_prompt .partial_cmp(right_prompt) .unwrap_or(core::cmp::Ordering::Equal) }); let action_queue = VecDeque::from(action_queue); let (current_prompt, current_char) = if night == 0 { ( ActionPrompt::WolvesIntro { wolves: village .living_wolf_pack_players() .into_iter() .map(|w| (w.target(), w.role().title())) .collect(), }, village .living_wolf_pack_players() .into_iter() .next() .unwrap() .character_id() .clone(), ) } else { ( ActionPrompt::WolfPackKill { living_villagers: village.living_villagers(), }, village .living_wolf_pack_players() .into_iter() .next() .unwrap() .character_id() .clone(), ) }; let night_state = NightState::Active { current_char, current_prompt, 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, }) } 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 changes = ChangesLookup(&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 DiedTo::MapleWolf { source, night, starves_if_fails: true, } = died_to && changes.protected(target).is_some() { // kill maple first, then act as if they get their kill attempt new_village .character_by_id_mut(source) .unwrap() .kill(DiedTo::MapleWolfStarved { night: *night }); } if let Some(prot) = changes.protected(target) { match prot { Protection::Guardian { source, guarding: true, } => { let kill_source = match died_to { DiedTo::MapleWolfStarved { night } => { new_village .character_by_id_mut(target) .unwrap() .kill(DiedTo::MapleWolfStarved { night: *night }); continue; } DiedTo::Execution { day: _ } => unreachable!(), DiedTo::MapleWolf { source, night: _, starves_if_fails: _, } | DiedTo::Militia { killer: source, night: _, } | DiedTo::AlphaWolf { killer: source, night: _, } | DiedTo::Hunter { killer: source, night: _, } => source.clone(), DiedTo::Wolfpack { night: _ } => { if let Some(wolf_to_kill) = new_village .living_wolf_pack_players() .into_iter() .find(|w| matches!(w.role(), Role::Werewolf)) .map(|w| w.character_id().clone()) .or_else(|| { new_village .living_wolf_pack_players() .into_iter() .next() .map(|w| w.character_id().clone()) }) { wolf_to_kill } else { // No wolves? Game over? continue; } } DiedTo::Shapeshift { into: _, night: _ } => target.clone(), DiedTo::Guardian { killer: _, night: _, } => continue, }; new_village.character_by_id_mut(&kill_source).unwrap().kill( DiedTo::Guardian { killer: source.clone(), night: NonZeroU8::new(self.night).unwrap(), }, ); new_village.character_by_id_mut(source).unwrap().kill( DiedTo::Wolfpack { night: NonZeroU8::new(self.night).unwrap(), }, ); continue; } Protection::Guardian { source: _, guarding: false, } => continue, Protection::Protector { source: _ } => continue, } } new_village .character_by_id_mut(target) .unwrap() .kill(DiedTo::Wolfpack { night: NonZeroU8::new(self.night).unwrap(), }); } NightChange::Shapeshift { source } => { // TODO: shapeshift should probably notify immediately after it happens 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(), }); let target = new_village.find_by_character_id_mut(target).unwrap(); target .role_change( RoleTitle::Werewolf, DateTime::Night { number: self.night }, ) .unwrap(); } } 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) } pub fn received_response(&mut self, resp: ActionResponse) -> Result { if let ( NightState::Active { current_prompt: ActionPrompt::WolvesIntro { wolves: _ }, current_char: _, current_result, }, ActionResponse::WolvesIntroAck, ) = (&mut self.night_state, &resp) { *current_result = Some(ActionResult::WolvesIntroDone); return Ok(ActionResult::WolvesIntroDone); } match self.received_response_with_role_blocks(resp) { Ok((result, Some(change))) => { match &mut self.night_state { NightState::Active { current_prompt: _, current_char: _, current_result, } => current_result.replace(result.clone()), NightState::Complete => return Err(GameError::NightOver), }; self.changes.push(change); Ok(result) } Ok((result, None)) => { match &mut self.night_state { NightState::Active { current_prompt: _, current_char: _, current_result, } => { current_result.replace(result.clone()); } NightState::Complete => return Err(GameError::NightOver), }; Ok(result) } Err(err) => Err(err), } } fn received_response_with_role_blocks( &self, resp: ActionResponse, ) -> Result<(ActionResult, Option)> { match self.received_response_inner(resp) { Ok(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)) } } Ok(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)) } } Ok(ResponseOutcome { result, change, unless: None, }) => Ok((result, change)), Err(err) => Err(err), } } fn received_response_inner(&self, resp: ActionResponse) -> Result { let (current_prompt, current_char) = match &self.night_state { NightState::Active { current_prompt: _, current_char: _, current_result: Some(_), } => return Err(GameError::NightNeedsNext), NightState::Active { current_prompt, current_char, current_result: None, } => (current_prompt, current_char), NightState::Complete => return Err(GameError::NightOver), }; match (current_prompt, resp) { (ActionPrompt::RoleChange { new_role }, ActionResponse::RoleChangeAck) => { Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::RoleChange(current_char.clone(), *new_role)), unless: None, }) } (ActionPrompt::Seer { 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 { 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 { 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 { 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.clone(), }), unless: Some(Unless::TargetBlocked(target)), }) } (ActionPrompt::Militia { 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.clone(), }, }), unless: Some(Unless::TargetBlocked(target)), }) } (ActionPrompt::Militia { living_players: _ }, ActionResponse::Militia(None)) => { Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }) } ( ActionPrompt::MapleWolf { 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.clone(), starves_if_fails: *kill_or_die, }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::MapleWolf { 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.clone(), died_to: DiedTo::MapleWolfStarved { night }, }), unless: None, }) } ( ActionPrompt::MapleWolf { kill_or_die: false, living_players: _, }, ActionResponse::MapleWolf(None), ) => Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }), ( ActionPrompt::Guardian { 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.clone(), guarding, }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( ActionPrompt::Guardian { 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.clone(), guarding: false, }, }), unless: Some(Unless::TargetBlocked(target)), }) } ( 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 }, }), unless: Some(Unless::TargetBlocked(target)), }) } (ActionPrompt::Shapeshifter, ActionResponse::Shapeshifter(true)) => { Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source: current_char.clone(), }), unless: None, }) } ( ActionPrompt::AlphaWolf { living_villagers: _, }, ActionResponse::AlphaWolf(None), ) | (ActionPrompt::Shapeshifter, ActionResponse::Shapeshifter(false)) => { Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: None, unless: None, }) } ( ActionPrompt::AlphaWolf { 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.clone(), night, }, }), unless: Some(Unless::TargetBlocked(target)), }) } (ActionPrompt::DireWolf { 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.clone(), target, block_type: RoleBlock::Direwolf, }), unless: None, }) } (ActionPrompt::Protector { 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.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::RoleChange { new_role: _ }, _) | (ActionPrompt::Seer { living_players: _ }, _) | (ActionPrompt::Protector { targets: _ }, _) | (ActionPrompt::Arcanist { living_players: _ }, _) | (ActionPrompt::Gravedigger { dead_players: _ }, _) | ( ActionPrompt::Hunter { current_target: _, living_players: _, }, _, ) | (ActionPrompt::Militia { living_players: _ }, _) | ( ActionPrompt::MapleWolf { kill_or_die: _, living_players: _, }, _, ) | ( ActionPrompt::Guardian { previous: _, living_players: _, }, _, ) | ( ActionPrompt::WolfPackKill { living_villagers: _, }, _, ) | (ActionPrompt::Shapeshifter, _) | ( ActionPrompt::AlphaWolf { living_villagers: _, }, _, ) | (ActionPrompt::DireWolf { living_players: _ }, _) => { Err(GameError::InvalidMessageForGameState) } (ActionPrompt::WolvesIntro { wolves: _ }, _) => { 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_char: _, 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_char: _, 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_char, current_result: _, } => Some(current_char), 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_char: _, current_result: Some(_), } => {} NightState::Active { current_prompt: _, current_char: _, current_result: None, } => return Err(GameError::AwaitingResponse), NightState::Complete => return Err(GameError::NightOver), } if let Some((prompt, character)) = self.action_queue.pop_front() { self.night_state = NightState::Active { current_prompt: prompt, current_char: character.character_id().clone(), current_result: None, }; } else { self.night_state = NightState::Complete; } Ok(()) } pub const fn changes(&self) -> &[NightChange] { self.changes.as_slice() } } struct ChangesLookup<'a>(&'a [NightChange]); impl<'a> ChangesLookup<'a> { pub fn killed(&self, target: &CharacterId) -> Option<&'a DiedTo> { self.0.iter().find_map(|c| match c { NightChange::Kill { target: t, died_to } => (t == target).then_some(died_to), _ => None, }) } pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> { self.0.iter().find_map(|c| match c { NightChange::Protection { target: t, protection, } => (t == target).then_some(protection), _ => None, }) } pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> { self.0.iter().find_map(|c| match c { NightChange::Kill { target, died_to: DiedTo::Wolfpack { night: _ }, } => Some(target), _ => None, }) } }