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

616 lines
19 KiB
Rust
Raw Normal View History

mod night_order;
mod role;
use crate::{
diedto::DiedTo,
error::GameError,
game::{Game, GameSettings, OrRandom, SetupRole, SetupSlot, night::NightChange},
message::{
CharacterState, Identification, PublicIdentity,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
},
player::{Character, 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;
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));
}
}
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);
}
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);
}
}
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 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(), Role::Villager) && c.player_id() != excl)
.unwrap()
}
fn villager_character_ids(&self) -> Box<[CharacterId]> {
self.village()
.characters()
.into_iter()
.filter_map(|c| matches!(c.role(), Role::Villager).then_some(c.character_id().clone()))
.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.clone()),
)))
.unwrap()
.prompt()
}
fn mark_and_check(&mut self, mark: CharacterId) {
let prompt = self.mark(mark);
match prompt {
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::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::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 {
self.process(HostGameMessage::Day(HostDayMessage::Execute))
.unwrap()
.prompt()
}
}
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()
.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 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()
.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);
}
}