werewolves/werewolves-proto/src/game_test/mod.rs

680 lines
20 KiB
Rust
Raw Normal View History

mod night_order;
mod role;
use crate::{
character::{Character, CharacterId},
error::GameError,
2025-10-05 10:54:47 +01:00
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);
2025-10-06 20:55:12 +01:00
fn beholder(&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)
}
2025-10-06 20:55:12 +01:00
fn beholder(&self) {
assert_eq!(*self, ActionPromptTitle::Beholder)
}
}
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"),
}
}
}
2025-10-06 20:55:12 +01:00
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;
#[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_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| {
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(
2025-10-05 10:54:47 +01:00
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<u8>) -> 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();
2025-10-04 17:50:29 +01:00
settings.new_slot(RoleTitle::Shapeshifter);
settings.new_slot(RoleTitle::Protector);
for _ in 0..7 {
settings.new_slot(RoleTitle::Villager);
}
2025-10-04 17:50:29 +01:00
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()
2025-10-05 10:54:47 +01:00
.character_id();
match game
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
2025-10-05 10:54:47 +01:00
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);
}
2025-10-04 17:50:29 +01:00
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()
2025-10-05 10:54:47 +01:00
.character_id();
match game
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
2025-10-05 10:54:47 +01:00
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(
2025-10-05 10:54:47 +01:00
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);
}
}