2025-09-30 13:07:59 +01:00
|
|
|
mod night_order;
|
2025-10-05 10:52:37 +01:00
|
|
|
mod role;
|
2025-09-30 13:07:59 +01:00
|
|
|
|
|
|
|
|
use crate::{
|
2025-10-06 20:45:15 +01:00
|
|
|
character::{Character, CharacterId},
|
2025-09-30 13:07:59 +01:00
|
|
|
error::GameError,
|
2025-10-05 10:54:47 +01:00
|
|
|
game::{Game, GameSettings, SetupRole, SetupSlot},
|
2025-09-30 13:07:59 +01:00
|
|
|
message::{
|
|
|
|
|
CharacterState, Identification, PublicIdentity,
|
2025-10-03 22:47:38 +01:00
|
|
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
2025-09-30 13:07:59 +01:00
|
|
|
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
|
|
|
|
},
|
2025-10-06 20:45:15 +01:00
|
|
|
player::PlayerId,
|
|
|
|
|
role::{Alignment, RoleTitle},
|
2025-09-30 13:07:59 +01:00
|
|
|
};
|
|
|
|
|
use colored::Colorize;
|
2025-10-06 20:45:15 +01:00
|
|
|
use core::{num::NonZeroU8, ops::Range};
|
2025-09-30 13:07:59 +01:00
|
|
|
#[allow(unused)]
|
|
|
|
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
|
|
|
|
use std::io::Write;
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
#[allow(unused)]
|
2025-10-05 10:52:37 +01:00
|
|
|
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);
|
2025-10-06 20:45:15 +01:00
|
|
|
fn masons_wake(&self);
|
|
|
|
|
fn masons_leader_recruit(&self);
|
2025-10-06 20:55:12 +01:00
|
|
|
fn beholder(&self);
|
2025-10-06 21:12:12 +01:00
|
|
|
fn vindicator(&self);
|
|
|
|
|
fn pyremaster(&self);
|
|
|
|
|
fn empath(&self);
|
|
|
|
|
fn adjudicator(&self);
|
2025-10-05 10:52:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2025-10-06 20:45:15 +01:00
|
|
|
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)
|
|
|
|
|
}
|
2025-10-06 21:12:12 +01:00
|
|
|
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)
|
|
|
|
|
}
|
2025-10-05 10:52:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub trait ActionResultExt {
|
2025-10-03 22:47:38 +01:00
|
|
|
fn sleep(&self);
|
|
|
|
|
fn r#continue(&self);
|
2025-10-05 01:22:55 +01:00
|
|
|
fn seer(&self) -> Alignment;
|
2025-10-03 22:47:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ActionResultExt for ActionResult {
|
|
|
|
|
fn sleep(&self) {
|
|
|
|
|
assert_eq!(*self, ActionResult::GoBackToSleep)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn r#continue(&self) {
|
|
|
|
|
assert_eq!(*self, ActionResult::Continue)
|
|
|
|
|
}
|
2025-10-05 01:22:55 +01:00
|
|
|
|
|
|
|
|
fn seer(&self) -> Alignment {
|
|
|
|
|
match self {
|
|
|
|
|
ActionResult::Seer(a) => a.clone(),
|
|
|
|
|
_ => panic!("expected a seer result"),
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-03 22:47:38 +01:00
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
pub trait ServerToHostMessageExt {
|
2025-09-30 13:07:59 +01:00
|
|
|
fn prompt(self) -> ActionPrompt;
|
|
|
|
|
fn result(self) -> ActionResult;
|
2025-10-03 22:47:38 +01:00
|
|
|
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
2025-09-30 13:07:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ServerToHostMessageExt for ServerToHostMessage {
|
2025-10-03 22:47:38 +01:00
|
|
|
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Daytime {
|
|
|
|
|
characters,
|
|
|
|
|
marked,
|
|
|
|
|
day,
|
|
|
|
|
} => (characters, marked, day),
|
|
|
|
|
resp => panic!("expected daytime, got {resp:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
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"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
pub trait GameExt {
|
|
|
|
|
fn villager_character_ids(&self) -> Box<[CharacterId]>;
|
|
|
|
|
fn character_by_player_id(&self, player_id: PlayerId) -> Character;
|
2025-09-30 13:07:59 +01:00
|
|
|
fn next(&mut self) -> ActionPrompt;
|
2025-10-03 22:47:38 +01:00
|
|
|
fn r#continue(&mut self) -> ActionResult;
|
2025-09-30 13:07:59 +01:00
|
|
|
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
2025-10-05 10:52:37 +01:00
|
|
|
fn mark(&mut self, mark: CharacterId) -> ActionPrompt;
|
|
|
|
|
fn mark_and_check(&mut self, mark: CharacterId);
|
2025-09-30 13:07:59 +01:00
|
|
|
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);
|
2025-10-05 10:52:37 +01:00
|
|
|
fn living_villager_excl(&self, excl: PlayerId) -> Character;
|
|
|
|
|
#[allow(unused)]
|
|
|
|
|
fn get_state(&mut self) -> ServerToHostMessage;
|
2025-09-30 13:07:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GameExt for Game {
|
2025-10-05 10:52:37 +01:00
|
|
|
fn get_state(&mut self) -> ServerToHostMessage {
|
|
|
|
|
self.process(HostGameMessage::GetState).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn living_villager_excl(&self, excl: PlayerId) -> Character {
|
|
|
|
|
self.village()
|
|
|
|
|
.characters()
|
|
|
|
|
.into_iter()
|
2025-10-06 20:45:15 +01:00
|
|
|
.find(|c| {
|
|
|
|
|
c.alive() && matches!(c.role_title(), RoleTitle::Villager) && c.player_id() != excl
|
|
|
|
|
})
|
2025-10-05 10:52:37 +01:00
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn villager_character_ids(&self) -> Box<[CharacterId]> {
|
|
|
|
|
self.village()
|
|
|
|
|
.characters()
|
|
|
|
|
.into_iter()
|
2025-10-06 20:45:15 +01:00
|
|
|
.filter_map(|c| {
|
|
|
|
|
matches!(c.role_title(), RoleTitle::Villager).then_some(c.character_id())
|
|
|
|
|
})
|
2025-10-05 10:52:37 +01:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn character_by_player_id(&self, player_id: PlayerId) -> Character {
|
|
|
|
|
self.village()
|
|
|
|
|
.character_by_player_id(player_id)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
fn r#continue(&mut self) -> ActionResult {
|
|
|
|
|
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
|
|
|
|
ActionResponse::Continue,
|
|
|
|
|
)))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
fn mark(&mut self, mark: CharacterId) -> ActionPrompt {
|
2025-10-03 22:47:38 +01:00
|
|
|
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
2025-10-05 10:54:47 +01:00
|
|
|
ActionResponse::MarkTarget(mark),
|
2025-10-03 22:47:38 +01:00
|
|
|
)))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.prompt()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
fn mark_and_check(&mut self, mark: CharacterId) {
|
2025-10-03 22:47:38 +01:00
|
|
|
let prompt = self.mark(mark);
|
2025-10-05 10:52:37 +01:00
|
|
|
match prompt {
|
2025-10-06 20:45:15 +01:00
|
|
|
ActionPrompt::MasonsWake { .. }
|
|
|
|
|
| ActionPrompt::ElderReveal { .. }
|
2025-10-05 10:52:37 +01:00
|
|
|
| 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),
|
|
|
|
|
..
|
|
|
|
|
}
|
2025-10-06 20:45:15 +01:00
|
|
|
| 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),
|
|
|
|
|
..
|
|
|
|
|
}
|
2025-10-05 10:52:37 +01:00
|
|
|
| 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, .. }
|
2025-10-06 20:45:15 +01:00
|
|
|
| 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, .. }
|
2025-10-05 10:52:37 +01:00
|
|
|
| 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"),
|
2025-10-03 22:47:38 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
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 {
|
2025-10-06 20:45:15 +01:00
|
|
|
assert_eq!(
|
|
|
|
|
self.process(HostGameMessage::Day(HostDayMessage::Execute))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.prompt(),
|
|
|
|
|
ActionPrompt::CoverOfDarkness
|
|
|
|
|
);
|
|
|
|
|
self.r#continue().r#continue();
|
|
|
|
|
self.next()
|
2025-09-30 13:07:59 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
pub fn init_log() {
|
2025-09-30 13:07:59 +01:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
pub fn gen_players(range: Range<u8>) -> Box<[Identification]> {
|
2025-09-30 13:07:59 +01:00
|
|
|
range
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|num| Identification {
|
|
|
|
|
player_id: PlayerId::from_u128(num as _),
|
|
|
|
|
public: PublicIdentity {
|
|
|
|
|
name: format!("player {num}"),
|
|
|
|
|
pronouns: None,
|
2025-10-02 17:52:12 +01:00
|
|
|
number: NonZeroU8::new(num),
|
2025-09-30 13:07:59 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn starts_with_wolf_intro() {
|
|
|
|
|
let players = gen_players(1..10);
|
2025-10-05 01:22:55 +01:00
|
|
|
let mut settings = GameSettings::default();
|
|
|
|
|
for _ in 0..8 {
|
|
|
|
|
settings.new_slot(RoleTitle::Villager);
|
|
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
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);
|
2025-10-05 01:22:55 +01:00
|
|
|
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);
|
|
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
let mut game = Game::new(&players, settings).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
2025-10-03 22:47:38 +01:00
|
|
|
ActionResponse::Continue
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.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(
|
2025-10-03 22:47:38 +01:00
|
|
|
ActionResponse::Continue
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.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);
|
2025-10-05 01:22:55 +01:00
|
|
|
let mut settings = GameSettings::default();
|
|
|
|
|
for _ in 0..8 {
|
|
|
|
|
settings.new_slot(RoleTitle::Villager);
|
|
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
let mut game = Game::new(&players, settings).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
2025-10-03 22:47:38 +01:00
|
|
|
ActionResponse::Continue
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.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(
|
2025-10-03 22:47:38 +01:00
|
|
|
ActionResponse::Continue
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.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();
|
2025-09-30 13:07:59 +01:00
|
|
|
match game
|
|
|
|
|
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
|
2025-10-05 10:54:47 +01:00
|
|
|
execution_target,
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.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(
|
2025-10-03 22:47:38 +01:00
|
|
|
ActionResponse::Continue
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.unwrap(),
|
|
|
|
|
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
game.process(HostGameMessage::Night(HostNightMessage::Next))
|
|
|
|
|
.unwrap(),
|
|
|
|
|
ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill {
|
2025-10-03 22:47:38 +01:00
|
|
|
living_villagers: _,
|
|
|
|
|
marked: _,
|
2025-09-30 13:07:59 +01:00
|
|
|
})
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wolfpack_kill_all_targets_valid() {
|
|
|
|
|
init_log();
|
|
|
|
|
let players = gen_players(1..10);
|
|
|
|
|
let mut settings = GameSettings::default();
|
2025-10-05 01:22:55 +01:00
|
|
|
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);
|
|
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
let mut game = Game::new(&players, settings).unwrap();
|
2025-10-03 22:47:38 +01:00
|
|
|
game.r#continue().r#continue();
|
|
|
|
|
|
|
|
|
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
|
|
|
|
game.r#continue().sleep();
|
|
|
|
|
game.next_expect_day();
|
2025-09-30 13:07:59 +01:00
|
|
|
|
|
|
|
|
let execution_target = game
|
|
|
|
|
.village()
|
|
|
|
|
.characters()
|
|
|
|
|
.into_iter()
|
2025-10-06 20:45:15 +01:00
|
|
|
.find(|v| v.is_village() && !matches!(v.role_title(), RoleTitle::Protector))
|
2025-09-30 13:07:59 +01:00
|
|
|
.unwrap()
|
2025-10-05 10:54:47 +01:00
|
|
|
.character_id();
|
2025-09-30 13:07:59 +01:00
|
|
|
match game
|
|
|
|
|
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
|
2025-10-05 10:54:47 +01:00
|
|
|
execution_target,
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.unwrap()
|
|
|
|
|
{
|
|
|
|
|
ServerToHostMessage::Daytime {
|
|
|
|
|
characters: _,
|
|
|
|
|
marked,
|
|
|
|
|
day: _,
|
|
|
|
|
} => assert_eq!(marked.to_vec(), vec![execution_target]),
|
|
|
|
|
resp => panic!("unexpected server message: {resp:#?}"),
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
let living_villagers = match game.execute() {
|
2025-10-03 22:47:38 +01:00
|
|
|
ActionPrompt::WolfPackKill {
|
|
|
|
|
living_villagers,
|
|
|
|
|
marked: _,
|
|
|
|
|
} => living_villagers,
|
2025-09-30 13:07:59 +01:00
|
|
|
_ => 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),
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.unwrap()
|
|
|
|
|
{
|
|
|
|
|
panic!("invalid target {target:?} at index [{idx}]");
|
|
|
|
|
}
|
2025-10-03 22:47:38 +01:00
|
|
|
attempt.r#continue().r#continue();
|
|
|
|
|
assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter);
|
2025-09-30 13:07:59 +01:00
|
|
|
}
|
|
|
|
|
}
|