mod night_order; use crate::{ diedto::DiedTo, error::GameError, game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange}, message::{ CharacterState, Identification, PublicIdentity, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, }, player::{CharacterId, PlayerId}, role::{Alignment, Role, RoleTitle}, }; use colored::Colorize; use core::{ char, num::{NonZero, NonZeroU8}, ops::Range, }; #[allow(unused)] use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use std::io::Write; 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"), } } } 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"), } } } trait GameExt { 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, check: impl FnOnce(&ActionPrompt) -> bool); 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 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.clone()), ))) .unwrap() .prompt() } fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool) { let prompt = self.mark(mark); if !check(&prompt) { panic!("unexpected prompt: {prompt:?}"); } } 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 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() .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::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 protect_stops_shapeshift() { init_log(); 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: _, } )); 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 ); game.r#continue().r#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, marked: None, }) => ( 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:#?}"); match game .process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::MarkTarget(prot_and_wolf_target.clone()), ))) .unwrap() { ServerToHostMessage::ActionPrompt(ActionPrompt::Protector { marked: Some(mark), .. }) => assert_eq!(mark, prot_and_wolf_target, "marked target"), resp => panic!("unexpected response: {resp:?}"), } game.r#continue().sleep(); assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); game.mark_and_check(&prot_and_wolf_target, |c| match c { ActionPrompt::WolfPackKill { marked: Some(mark), .. } => prot_and_wolf_target == *mark, _ => false, }); game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,); game.response(ActionResponse::Shapeshift); game.next_expect_day(); 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(); 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() .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.execute().title(), ActionPromptTitle::CoverOfDarkness); game.r#continue().r#continue(); let living_villagers = match game .process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() .prompt() { 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.clone()), ))) .unwrap() { panic!("invalid target {target:?} at index [{idx}]"); } attempt.r#continue().r#continue(); assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter); } } #[test] fn only_1_shapeshift_prompt_if_first_shifts() { let players = gen_players(1..10); let mut settings = GameSettings::default(); settings.new_slot(RoleTitle::Shapeshifter); settings.new_slot(RoleTitle::Shapeshifter); 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(); game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); game.r#continue().sleep(); 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); game.r#continue().r#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(); game.mark_and_check(&target, |p| match p { ActionPrompt::WolfPackKill { marked: Some(t), .. } => *t == target, _ => false, }); game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter); game.response(ActionResponse::Shapeshift).r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::RoleChange); game.r#continue().sleep(); game.next_expect_day(); } #[test] fn redeemed_scapegoat_role_changes() { let players = gen_players(1..10); let scapegoat_player_id = players[0].player_id.clone(); let seer_player_id = players[1].player_id.clone(); let wolf_player_id = players[2].player_id.clone(); let wolf_target_2_player_id = players[3].player_id.clone(); let mut settings = GameSettings::default(); { let scapegoat_slot = settings.new_slot(RoleTitle::Scapegoat); let mut scapegoat_slot = settings .slots() .iter() .find(|s| s.slot_id == scapegoat_slot) .unwrap() .clone(); scapegoat_slot.role = SetupRole::Scapegoat { redeemed: OrRandom::Determined(true), }; scapegoat_slot.assign_to = Some(scapegoat_player_id.clone()); settings.update_slot(scapegoat_slot); } { let mut slot = settings .slots() .iter() .find(|s| matches!(s.role, SetupRole::Werewolf)) .unwrap() .clone(); slot.assign_to = Some(wolf_player_id.clone()); settings.update_slot(slot); } { let slot = settings.new_slot(RoleTitle::Seer); let mut slot = settings .slots() .iter() .find(|s| s.slot_id == slot) .unwrap() .clone(); slot.assign_to = Some(seer_player_id.clone()); settings.update_slot(slot); } for _ in 0..6 { settings.new_slot(RoleTitle::Villager); } let mut game = Game::new(&players, settings).unwrap(); game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); game.r#continue().sleep(); assert_eq!(game.next().title(), ActionPromptTitle::Seer); let wolf_char_id = game .village() .characters() .into_iter() .find(|c| c.player_id() == &wolf_player_id) .unwrap() .character_id() .clone(); game.mark_and_check(&wolf_char_id, |p| match p { ActionPrompt::Seer { marked: Some(marked), .. } => marked == &wolf_char_id, _ => false, }); assert_eq!(game.r#continue().seer(), Alignment::Wolves); game.next_expect_day(); assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); let seer = game .village() .characters() .into_iter() .find(|c| c.player_id() == &seer_player_id) .unwrap() .character_id() .clone(); game.mark_and_check(&seer, |p| match p { ActionPrompt::WolfPackKill { marked: Some(t), .. } => *t == seer, _ => false, }); game.r#continue().sleep(); assert_eq!(game.next().title(), ActionPromptTitle::Seer); game.mark_and_check(&wolf_char_id, |p| match p { ActionPrompt::Seer { marked: Some(marked), .. } => marked == &wolf_char_id, _ => false, }); assert_eq!(game.r#continue().seer(), Alignment::Wolves); game.next_expect_day(); assert_eq!( *game .village() .character_by_id(&seer) .unwrap() .died_to() .unwrap(), DiedTo::Wolfpack { killing_wolf: wolf_char_id.clone(), night: NonZero::new(1).unwrap() } ); assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); let wolf_target_2 = game .village() .characters() .iter() .find(|c| c.player_id() == &wolf_target_2_player_id) .unwrap() .character_id() .clone(); game.mark_and_check(&wolf_target_2, |r| match r { ActionPrompt::WolfPackKill { marked: Some(marked), .. } => marked == &wolf_target_2, _ => false, }); game.r#continue().sleep(); let scapegoat = game .village() .characters() .into_iter() .find(|c| c.player_id() == &scapegoat_player_id) .unwrap() .clone(); assert_eq!( game.next(), ActionPrompt::RoleChange { character_id: scapegoat.identity(), new_role: RoleTitle::Seer } ); game.r#continue().sleep(); match game.game_state() { crate::game::GameState::Night { night } => night .changes() .iter() .find(|c| match c { NightChange::RoleChange(char, role) => { char == scapegoat.character_id() && role == &RoleTitle::Seer } _ => false, }) .expect("no role change"), _ => unreachable!(), }; game.next_expect_day(); let day_scapegoat = game .village() .character_by_id(scapegoat.character_id()) .unwrap(); assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer); }