mod night_order; use crate::{ error::GameError, game::{Game, GameSettings}, message::{ CharacterState, Identification, PublicIdentity, host::{ HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage, ServerToHostMessageTitle, }, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, }, player::{CharacterId, PlayerId}, role::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; trait ServerToHostMessageExt { fn prompt(self) -> ActionPrompt; fn result(self) -> ActionResult; } impl ServerToHostMessageExt for ServerToHostMessage { 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"), } } } trait GameExt { fn next(&mut self) -> ActionPrompt; fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); 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); } impl GameExt for Game { 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 { self.process(HostGameMessage::Day(HostDayMessage::Execute)) .unwrap() .prompt() } } 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(); } 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 settings = GameSettings::default(); 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.add(RoleTitle::Shapeshifter).unwrap(); settings.sub(RoleTitle::Werewolf); settings.add(RoleTitle::Protector).unwrap(); let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::ClearCoverOfDarkness ))) .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::WolvesIntroAck ))) .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 settings = GameSettings::default(); let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::ClearCoverOfDarkness ))) .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::WolvesIntroAck ))) .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() .clone(); match game .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( execution_target.clone(), ))) .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::ClearCoverOfDarkness ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) ); assert!(matches!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap(), ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill { living_villagers: _ }) )); } #[test] fn protect_stops_shapeshift() { init_log(); let players = gen_players(1..10); let mut settings = GameSettings::default(); settings.add(RoleTitle::Shapeshifter).unwrap(); settings.sub(RoleTitle::Werewolf); settings.add(RoleTitle::Protector).unwrap(); let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::ClearCoverOfDarkness ))) .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::WolvesIntroAck ))) .unwrap(), ServerToHostMessage::ActionResult(None, 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() && !matches!(v.role().title(), RoleTitle::Protector)) .unwrap() .character_id() .clone(); match game .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( execution_target.clone(), ))) .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() .prompt() .title(), ActionPromptTitle::CoverOfDarkness ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::ClearCoverOfDarkness ))) .unwrap() .result(), ActionResult::Continue ); let (prot_and_wolf_target, prot_char_id) = match game .process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() { ServerToHostMessage::ActionPrompt(ActionPrompt::Protector { character_id: prot_char_id, targets, }) => ( targets .into_iter() .map(|c| game.village().character_by_id(&c.character_id).unwrap()) .find(|c| c.is_village()) .unwrap() .character_id() .clone(), prot_char_id, ), _ => panic!("first n2 prompt isn't protector"), }; let target = game .village() .character_by_id(&prot_and_wolf_target) .unwrap() .clone(); log::info!("target: {target:#?}"); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::Protector(prot_and_wolf_target.clone()) ))) .unwrap() .result(), ActionResult::GoBackToSleep, ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() .prompt() .title(), ActionPromptTitle::WolfPackKill ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::WolfPackKillVote(prot_and_wolf_target.clone()) ))) .unwrap() .result(), ActionResult::Continue, ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() .prompt() .title(), ActionPromptTitle::Shapeshifter, ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::Shapeshifter(true) ))) .unwrap() .result(), ActionResult::GoBackToSleep, ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() .title(), ServerToHostMessageTitle::Daytime, ); let target = game .village() .character_by_id(target.character_id()) .unwrap(); assert!(target.is_village()); assert!(target.alive()); let prot = game .village() .character_by_id(&prot_char_id.character_id) .unwrap(); assert!(prot.is_village()); assert!(prot.alive()); assert_eq!(prot.role().title(), RoleTitle::Protector); } #[test] fn wolfpack_kill_all_targets_valid() { init_log(); let players = gen_players(1..10); let mut settings = GameSettings::default(); settings.add(RoleTitle::Shapeshifter).unwrap(); settings.sub(RoleTitle::Werewolf); let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::ClearCoverOfDarkness ))) .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::WolvesIntroAck ))) .unwrap(), ServerToHostMessage::ActionResult(None, 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() && !matches!(v.role().title(), RoleTitle::Protector)) .unwrap() .character_id() .clone(); match game .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( execution_target.clone(), ))) .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() .prompt() .title(), ActionPromptTitle::CoverOfDarkness ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::ClearCoverOfDarkness ))) .unwrap() .result(), ActionResult::Continue ); let living_villagers = match game .process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() .prompt() { ActionPrompt::WolfPackKill { living_villagers } => 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::WolfPackKillVote(target.character_id.clone()), ))) .unwrap() { panic!("invalid target {target:?} at index [{idx}]"); } } } #[test] fn only_1_shapeshift_prompt_if_first_shifts() { let players = gen_players(1..10); let mut settings = GameSettings::default(); settings.add(RoleTitle::Shapeshifter).unwrap(); settings.add(RoleTitle::Shapeshifter).unwrap(); settings.sub(RoleTitle::Werewolf); let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.response(ActionResponse::ClearCoverOfDarkness), ActionResult::Continue ); assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); assert_eq!( game.response(ActionResponse::WolvesIntroAck), ActionResult::GoBackToSleep ); game.next_expect_day(); let target = game .village() .characters() .into_iter() .find_map(|c| c.is_village().then_some(c.character_id().clone())) .unwrap(); let (_, marked, _) = game.mark_for_execution(target.clone()); let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]); assert_eq!(target_list, marked); assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); assert_eq!( game.response(ActionResponse::ClearCoverOfDarkness), ActionResult::Continue ); assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); let target = game .village() .characters() .into_iter() .find_map(|c| (c.is_village() && c.alive()).then_some(c.character_id().clone())) .unwrap(); assert_eq!( game.response(ActionResponse::WolfPackKillVote(target)), ActionResult::Continue, ); assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter); assert_eq!( game.response(ActionResponse::Shapeshifter(true)), ActionResult::Continue, ); assert_eq!(game.next().title(), ActionPromptTitle::RoleChange); assert_eq!( game.response(ActionResponse::RoleChangeAck), ActionResult::GoBackToSleep ); game.next_expect_day(); }