mod night_order; mod role; use crate::{ character::{Character, CharacterId}, error::GameError, game::{Game, GameSettings, SetupRole, SetupSlot}, message::{ CharacterState, Identification, PublicIdentity, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, }, player::PlayerId, role::{Alignment, RoleTitle}, }; use colored::Colorize; use core::{num::NonZeroU8, ops::Range}; #[allow(unused)] use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use std::io::Write; pub trait SettingsExt { fn add_role(&mut self, role: SetupRole, modify: impl FnOnce(&mut SetupSlot)); fn add_and_assign(&mut self, role: SetupRole, assignee: PlayerId); } impl SettingsExt for GameSettings { fn add_role(&mut self, role: SetupRole, modify: impl FnOnce(&mut SetupSlot)) { let slot_id = self.new_slot(role.clone().into()); let mut slot = self.get_slot_by_id(slot_id).unwrap().clone(); slot.role = role; modify(&mut slot); self.update_slot(slot); } fn add_and_assign(&mut self, role: SetupRole, assignee: PlayerId) { self.add_role(role, |slot| slot.assign_to = Some(assignee)); } } #[allow(unused)] pub trait ActionPromptTitleExt { fn wolf_pack_kill(&self); fn cover_of_darkness(&self); fn wolves_intro(&self); fn role_change(&self); fn seer(&self); fn protector(&self); fn arcanist(&self); fn gravedigger(&self); fn hunter(&self); fn militia(&self); fn maplewolf(&self); fn guardian(&self); fn shapeshifter(&self); fn alphawolf(&self); fn direwolf(&self); fn masons_wake(&self); fn masons_leader_recruit(&self); fn beholder(&self); fn vindicator(&self); fn pyremaster(&self); fn empath(&self); fn adjudicator(&self); } impl ActionPromptTitleExt for ActionPromptTitle { fn cover_of_darkness(&self) { assert_eq!(*self, ActionPromptTitle::CoverOfDarkness); } fn wolves_intro(&self) { assert_eq!(*self, ActionPromptTitle::WolvesIntro); } fn role_change(&self) { assert_eq!(*self, ActionPromptTitle::RoleChange); } fn seer(&self) { assert_eq!(*self, ActionPromptTitle::Seer); } fn protector(&self) { assert_eq!(*self, ActionPromptTitle::Protector); } fn arcanist(&self) { assert_eq!(*self, ActionPromptTitle::Arcanist); } fn gravedigger(&self) { assert_eq!(*self, ActionPromptTitle::Gravedigger); } fn hunter(&self) { assert_eq!(*self, ActionPromptTitle::Hunter); } fn militia(&self) { assert_eq!(*self, ActionPromptTitle::Militia); } fn maplewolf(&self) { assert_eq!(*self, ActionPromptTitle::MapleWolf); } fn guardian(&self) { assert_eq!(*self, ActionPromptTitle::Guardian); } fn shapeshifter(&self) { assert_eq!(*self, ActionPromptTitle::Shapeshifter); } fn alphawolf(&self) { assert_eq!(*self, ActionPromptTitle::AlphaWolf); } fn direwolf(&self) { assert_eq!(*self, ActionPromptTitle::DireWolf); } fn wolf_pack_kill(&self) { assert_eq!(*self, ActionPromptTitle::WolfPackKill); } fn masons_wake(&self) { assert_eq!(*self, ActionPromptTitle::MasonsWake) } fn masons_leader_recruit(&self) { assert_eq!(*self, ActionPromptTitle::MasonLeaderRecruit) } fn beholder(&self) { assert_eq!(*self, ActionPromptTitle::Beholder) } fn vindicator(&self) { assert_eq!(*self, ActionPromptTitle::Vindicator) } fn pyremaster(&self) { assert_eq!(*self, ActionPromptTitle::PyreMaster) } fn adjudicator(&self) { assert_eq!(*self, ActionPromptTitle::Adjudicator) } fn empath(&self) { assert_eq!(*self, ActionPromptTitle::Empath) } } pub trait ActionResultExt { fn sleep(&self); fn r#continue(&self); fn seer(&self) -> Alignment; } impl ActionResultExt for ActionResult { fn sleep(&self) { assert_eq!(*self, ActionResult::GoBackToSleep) } fn r#continue(&self) { assert_eq!(*self, ActionResult::Continue) } fn seer(&self) -> Alignment { match self { ActionResult::Seer(a) => a.clone(), _ => panic!("expected a seer result"), } } } pub trait AlignmentExt { fn village(&self); fn wolves(&self); } impl AlignmentExt for Alignment { fn village(&self) { assert_eq!(*self, Alignment::Village) } fn wolves(&self) { assert_eq!(*self, Alignment::Wolves) } } pub trait ServerToHostMessageExt { fn prompt(self) -> ActionPrompt; fn result(self) -> ActionResult; fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); } impl ServerToHostMessageExt for ServerToHostMessage { fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { match self { Self::Daytime { characters, marked, day, } => (characters, marked, day), resp => panic!("expected daytime, got {resp:?}"), } } fn prompt(self) -> ActionPrompt { match self { Self::ActionPrompt(prompt) => prompt, Self::Daytime { characters: _, marked: _, day: _, } => panic!("{}", "[got daytime]".bold().red()), msg => panic!("expected server message <<{msg:?}>> to be an ActionPrompt"), } } fn result(self) -> ActionResult { match self { Self::ActionResult(_, res) => res, msg => panic!("expected server message <<{msg:?}>> to be an ActionResult"), } } } pub trait GameExt { fn villager_character_ids(&self) -> Box<[CharacterId]>; fn character_by_player_id(&self, player_id: PlayerId) -> Character; fn next(&mut self) -> ActionPrompt; fn r#continue(&mut self) -> ActionResult; fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); fn mark(&mut self, mark: CharacterId) -> ActionPrompt; fn mark_and_check(&mut self, mark: CharacterId); fn response(&mut self, resp: ActionResponse) -> ActionResult; fn execute(&mut self) -> ActionPrompt; fn mark_for_execution( &mut self, target: CharacterId, ) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); fn living_villager_excl(&self, excl: PlayerId) -> Character; fn living_villager(&self) -> Character; #[allow(unused)] fn get_state(&mut self) -> ServerToHostMessage; } impl GameExt for Game { fn get_state(&mut self) -> ServerToHostMessage { self.process(HostGameMessage::GetState).unwrap() } fn living_villager(&self) -> Character { self.village() .characters() .into_iter() .find(|c| c.alive() && matches!(c.role_title(), RoleTitle::Villager)) .unwrap() } fn living_villager_excl(&self, excl: PlayerId) -> Character { self.village() .characters() .into_iter() .find(|c| { c.alive() && matches!(c.role_title(), RoleTitle::Villager) && c.player_id() != excl }) .unwrap() } fn villager_character_ids(&self) -> Box<[CharacterId]> { self.village() .characters() .into_iter() .filter_map(|c| { (c.alive() && matches!(c.role_title(), RoleTitle::Villager)) .then_some(c.character_id()) }) .collect() } fn character_by_player_id(&self, player_id: PlayerId) -> Character { self.village() .character_by_player_id(player_id) .unwrap() .clone() } fn r#continue(&mut self) -> ActionResult { self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::Continue, ))) .unwrap() .result() } fn mark(&mut self, mark: CharacterId) -> ActionPrompt { self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::MarkTarget(mark), ))) .unwrap() .prompt() } fn mark_and_check(&mut self, mark: CharacterId) { let prompt = self.mark(mark); match prompt { ActionPrompt::MasonsWake { .. } | ActionPrompt::ElderReveal { .. } | ActionPrompt::CoverOfDarkness | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } | ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"), ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"), ActionPrompt::Seer { 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::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::WolfPackKill { marked: Some(marked), .. } | ActionPrompt::AlphaWolf { marked: Some(marked), .. } | ActionPrompt::DireWolf { marked: Some(marked), .. } => assert_eq!(marked, mark, "marked character"), ActionPrompt::Seer { 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::Protector { marked: None, .. } | ActionPrompt::Gravedigger { marked: None, .. } | ActionPrompt::Hunter { marked: None, .. } | ActionPrompt::Militia { marked: None, .. } | ActionPrompt::MapleWolf { marked: None, .. } | ActionPrompt::Guardian { marked: None, .. } | ActionPrompt::WolfPackKill { marked: None, .. } | ActionPrompt::AlphaWolf { marked: None, .. } | ActionPrompt::DireWolf { marked: None, .. } => panic!("no mark"), } } fn next(&mut self) -> ActionPrompt { self.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() .prompt() } fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { match self .process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() { ServerToHostMessage::Daytime { characters, marked, day, } => (characters, marked, day), res => panic!("unexpected response to next_expect_day: {res:?}"), } } fn response(&mut self, resp: ActionResponse) -> ActionResult { self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( resp, ))) .unwrap() .result() } fn mark_for_execution( &mut self, target: CharacterId, ) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { match self .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( target, ))) .unwrap() { ServerToHostMessage::Daytime { characters, marked, day, } => (characters, marked, day), res => panic!("unexpected response to mark_for_execution: {res:?}"), } } fn execute(&mut self) -> ActionPrompt { assert_eq!( self.process(HostGameMessage::Day(HostDayMessage::Execute)) .unwrap() .prompt(), ActionPrompt::CoverOfDarkness ); self.r#continue().r#continue(); self.next() } } pub fn init_log() { let _ = pretty_env_logger::formatted_builder() .filter_level(log::LevelFilter::Debug) .format(|f, record| match record.file() { Some(file) => { let file = format!( "[{file}{}]", record .line() .map(|l| format!(":{l}")) .unwrap_or_else(String::new), ) .dimmed(); let level = match record.level() { log::Level::Error => "[err]".red().bold(), log::Level::Warn => "[warn]".yellow().bold(), log::Level::Info => "[info]".white().bold(), log::Level::Debug => "[debug]".dimmed().bold(), log::Level::Trace => "[trace]".dimmed(), }; let args = record.args(); let arrow = "➢".bold().magenta(); writeln!(f, "{file}\n{level} {arrow} {args}") } _ => writeln!(f, "[{}] {}", record.level(), record.args()), }) .is_test(true) .try_init(); } pub fn gen_players(range: Range) -> Box<[Identification]> { range .into_iter() .map(|num| Identification { player_id: PlayerId::from_u128(num as _), public: PublicIdentity { name: format!("player {num}"), pronouns: None, number: NonZeroU8::new(num), }, }) .collect() } #[test] fn starts_with_wolf_intro() { let players = gen_players(1..10); let mut settings = GameSettings::default(); for _ in 0..8 { settings.new_slot(RoleTitle::Villager); } let mut game = Game::new(&players, settings).unwrap(); let resp = game.process(HostGameMessage::GetState).unwrap(); assert_eq!( resp, ServerToHostMessage::ActionPrompt(ActionPrompt::CoverOfDarkness) ) } #[test] fn no_wolf_kill_n1() { let players = gen_players(1..10); let mut settings = GameSettings::default(); settings.new_slot(RoleTitle::Shapeshifter); settings.new_slot(RoleTitle::Protector); for _ in 0..7 { settings.new_slot(RoleTitle::Villager); } if let Some(slot) = settings .slots() .iter() .find(|s| matches!(s.role, SetupRole::Werewolf)) { settings.remove_slot(slot.slot_id); } let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) ); assert!(matches!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap(), ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ }) )); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep), ); assert!(matches!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap(), ServerToHostMessage::Daytime { characters: _, marked: _, day: _, } )); } #[test] fn yes_wolf_kill_n2() { let players = gen_players(1..10); let mut settings = GameSettings::default(); for _ in 0..8 { settings.new_slot(RoleTitle::Villager); } let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) ); assert!(matches!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap(), ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ }) )); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::Continue ))) .unwrap() .result(), ActionResult::GoBackToSleep, ); assert!(matches!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap(), ServerToHostMessage::Daytime { characters: _, marked: _, day: _, } )); let execution_target = game .village() .characters() .into_iter() .find(|v| v.is_village()) .unwrap() .character_id(); match game .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( execution_target, ))) .unwrap() { ServerToHostMessage::Daytime { characters: _, marked, day: _, } => assert_eq!(marked.to_vec(), vec![execution_target]), resp => panic!("unexpected server message: {resp:#?}"), } assert_eq!( game.process(HostGameMessage::Day(HostDayMessage::Execute)) .unwrap(), ServerToHostMessage::ActionPrompt(ActionPrompt::CoverOfDarkness) ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) ); assert!(matches!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap(), ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill { living_villagers: _, marked: _, }) )); } #[test] fn wolfpack_kill_all_targets_valid() { init_log(); let players = gen_players(1..10); let mut settings = GameSettings::default(); for _ in 0..8 { settings.new_slot(RoleTitle::Villager); } settings.new_slot(RoleTitle::Shapeshifter); if let Some(slot) = settings .slots() .iter() .find(|s| matches!(s.role, SetupRole::Werewolf)) { settings.remove_slot(slot.slot_id); } 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(); let execution_target = game .village() .characters() .into_iter() .find(|v| v.is_village() && !matches!(v.role_title(), RoleTitle::Protector)) .unwrap() .character_id(); match game .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( execution_target, ))) .unwrap() { ServerToHostMessage::Daytime { characters: _, marked, day: _, } => assert_eq!(marked.to_vec(), vec![execution_target]), resp => panic!("unexpected server message: {resp:#?}"), } let living_villagers = match game.execute() { ActionPrompt::WolfPackKill { living_villagers, marked: _, } => living_villagers, _ => panic!("not wolf pack kill"), }; for (idx, target) in living_villagers.into_iter().enumerate() { let mut attempt = game.clone(); if let ServerToHostMessage::Error(GameError::InvalidTarget) = attempt .process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::MarkTarget(target.character_id), ))) .unwrap() { panic!("invalid target {target:?} at index [{idx}]"); } attempt.r#continue().r#continue(); assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter); } }