2025-09-30 13:07:59 +01:00
|
|
|
mod night_order;
|
|
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
error::GameError,
|
2025-10-04 17:50:29 +01:00
|
|
|
game::{Game, GameSettings, SetupRole},
|
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},
|
|
|
|
|
},
|
|
|
|
|
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;
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
trait ActionResultExt {
|
|
|
|
|
fn sleep(&self);
|
|
|
|
|
fn r#continue(&self);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ActionResultExt for ActionResult {
|
|
|
|
|
fn sleep(&self) {
|
|
|
|
|
assert_eq!(*self, ActionResult::GoBackToSleep)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn r#continue(&self) {
|
|
|
|
|
assert_eq!(*self, ActionResult::Continue)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
trait ServerToHostMessageExt {
|
|
|
|
|
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"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trait GameExt {
|
|
|
|
|
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-03 22:47:38 +01:00
|
|
|
fn mark(&mut self, mark: &CharacterId) -> ActionPrompt;
|
|
|
|
|
fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool);
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GameExt for Game {
|
2025-10-03 22:47:38 +01:00
|
|
|
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:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
|
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<u8>) -> Box<[Identification]> {
|
|
|
|
|
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);
|
|
|
|
|
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();
|
2025-10-04 17:50:29 +01:00
|
|
|
settings.new_slot(RoleTitle::Shapeshifter);
|
|
|
|
|
settings.new_slot(RoleTitle::Protector);
|
|
|
|
|
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);
|
|
|
|
|
let settings = GameSettings::default();
|
|
|
|
|
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()
|
|
|
|
|
.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(
|
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 protect_stops_shapeshift() {
|
|
|
|
|
init_log();
|
|
|
|
|
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);
|
|
|
|
|
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: _,
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
);
|
2025-10-03 22:47:38 +01:00
|
|
|
game.r#continue().r#continue();
|
2025-09-30 13:07:59 +01:00
|
|
|
|
|
|
|
|
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,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: None,
|
2025-09-30 13:07:59 +01:00
|
|
|
}) => (
|
|
|
|
|
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:#?}");
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
match game
|
|
|
|
|
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
|
|
|
|
ActionResponse::MarkTarget(prot_and_wolf_target.clone()),
|
2025-09-30 13:07:59 +01:00
|
|
|
)))
|
|
|
|
|
.unwrap()
|
2025-10-03 22:47:38 +01:00
|
|
|
{
|
|
|
|
|
ServerToHostMessage::ActionPrompt(ActionPrompt::Protector {
|
|
|
|
|
marked: Some(mark), ..
|
|
|
|
|
}) => assert_eq!(mark, prot_and_wolf_target, "marked target"),
|
|
|
|
|
resp => panic!("unexpected response: {resp:?}"),
|
|
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
game.r#continue().sleep();
|
2025-09-30 13:07:59 +01:00
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
2025-09-30 13:07:59 +01:00
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
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();
|
2025-09-30 13:07:59 +01:00
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,);
|
2025-09-30 13:07:59 +01:00
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
game.response(ActionResponse::Shapeshift);
|
|
|
|
|
|
|
|
|
|
game.next_expect_day();
|
2025-09-30 13:07:59 +01:00
|
|
|
|
|
|
|
|
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();
|
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()
|
|
|
|
|
.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:#?}"),
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
|
|
|
|
game.r#continue().r#continue();
|
2025-09-30 13:07:59 +01:00
|
|
|
|
|
|
|
|
let living_villagers = match game
|
|
|
|
|
.process(HostGameMessage::Night(HostNightMessage::Next))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.prompt()
|
|
|
|
|
{
|
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-03 22:47:38 +01:00
|
|
|
ActionResponse::MarkTarget(target.character_id.clone()),
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn only_1_shapeshift_prompt_if_first_shifts() {
|
|
|
|
|
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::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();
|
2025-09-30 13:07:59 +01:00
|
|
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
2025-10-03 22:47:38 +01:00
|
|
|
game.r#continue().sleep();
|
2025-09-30 13:07:59 +01:00
|
|
|
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);
|
2025-10-03 22:47:38 +01:00
|
|
|
game.r#continue().r#continue();
|
2025-09-30 13:07:59 +01:00
|
|
|
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();
|
2025-10-03 22:47:38 +01:00
|
|
|
|
|
|
|
|
game.mark_and_check(&target, |p| match p {
|
|
|
|
|
ActionPrompt::WolfPackKill {
|
|
|
|
|
marked: Some(t), ..
|
|
|
|
|
} => *t == target,
|
|
|
|
|
_ => false,
|
|
|
|
|
});
|
|
|
|
|
game.r#continue().r#continue();
|
2025-09-30 13:07:59 +01:00
|
|
|
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter);
|
2025-10-03 22:47:38 +01:00
|
|
|
game.response(ActionResponse::Shapeshift).r#continue();
|
2025-09-30 13:07:59 +01:00
|
|
|
assert_eq!(game.next().title(), ActionPromptTitle::RoleChange);
|
2025-10-03 22:47:38 +01:00
|
|
|
game.r#continue().sleep();
|
2025-09-30 13:07:59 +01:00
|
|
|
|
|
|
|
|
game.next_expect_day();
|
|
|
|
|
}
|