From da580a459e44800d78617cd5f30b856e60eb27bc Mon Sep 17 00:00:00 2001 From: emilis Date: Mon, 6 Oct 2025 22:30:01 +0100 Subject: [PATCH] pyremaster role and tests --- werewolves-proto/src/diedto.rs | 7 +++ werewolves-proto/src/game/night.rs | 31 +++++++++- werewolves-proto/src/game_test/mod.rs | 11 +++- werewolves-proto/src/game_test/role/mod.rs | 1 + .../src/game_test/role/pyremaster.rs | 61 +++++++++++++++++++ werewolves-proto/src/role.rs | 1 + 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 werewolves-proto/src/game_test/role/pyremaster.rs diff --git a/werewolves-proto/src/diedto.rs b/werewolves-proto/src/diedto.rs index 7b784d3..3f516a8 100644 --- a/werewolves-proto/src/diedto.rs +++ b/werewolves-proto/src/diedto.rs @@ -49,6 +49,10 @@ pub enum DiedTo { killer: CharacterId, night: NonZeroU8, }, + PyreMasterLynchMob { + source: CharacterId, + night: NonZeroU8, + }, MasonLeaderRecruitFail { tried_recruiting: CharacterId, night: u8, @@ -68,6 +72,7 @@ impl DiedTo { | DiedTo::Shapeshift { night, .. } | DiedTo::Hunter { night, .. } | DiedTo::GuardianProtecting { night, .. } + | DiedTo::PyreMasterLynchMob { night, .. } | DiedTo::PyreMaster { night, .. } => *night = NonZeroU8::new(night.get() + 1)?, DiedTo::MasonLeaderRecruitFail { night, .. } => *night = *night + 1, } @@ -95,6 +100,7 @@ impl DiedTo { tried_recruiting: killer, .. } + | DiedTo::PyreMasterLynchMob { source: killer, .. } | DiedTo::PyreMaster { killer, .. } => Some(*killer), } } @@ -121,6 +127,7 @@ impl DiedTo { } | DiedTo::AlphaWolf { killer: _, night } | DiedTo::Shapeshift { into: _, night } + | DiedTo::PyreMasterLynchMob { night, .. } | DiedTo::PyreMaster { night, .. } | DiedTo::Hunter { killer: _, night } => DateTime::Night { number: night.get(), diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index ad1b85f..92960c9 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -15,7 +15,7 @@ use crate::{ }, message::night::{ActionPrompt, ActionResponse, ActionResult}, player::Protection, - role::{PreviousGuardianAction, RoleBlock, RoleTitle}, + role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle}, }; #[derive(Debug, Clone, Serialize, Deserialize, Extract)] @@ -457,15 +457,44 @@ impl Night { } } } + // black knights death for knight in new_village .characters_mut() .into_iter() + .filter(|k| k.alive()) .filter(|k| k.black_knight().ok().and_then(|t| (*t).clone()).is_some()) .filter(|k| changes.killed(k.character_id()).is_none()) { knight.black_knight_kill()?.kill(); } + // pyre masters death + let village_dead = new_village + .characters() + .into_iter() + .filter(|c| c.is_village()) + .filter_map(|c| c.died_to().cloned()) + .filter_map(|c| c.killer().map(|k| (k, c))) + .collect::>(); + for pyremaster in new_village + .living_characters_by_role_mut(RoleTitle::PyreMaster) + .into_iter() + .filter(|p| { + village_dead + .iter() + .filter(|(k, _)| *k == p.character_id()) + .count() + >= PYREMASTER_VILLAGER_KILLS_TO_DIE.get() as usize + }) + { + if let Some(night) = NonZeroU8::new(self.night) { + pyremaster.kill(DiedTo::PyreMasterLynchMob { + night, + source: pyremaster.character_id(), + }); + } + } + if new_village.is_game_over().is_none() { new_village.to_day()?; } diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index fde52e1..2751ca0 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -224,6 +224,7 @@ pub trait GameExt { target: CharacterId, ) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); fn living_villager_excl(&self, excl: PlayerId) -> Character; + fn living_villager(&self) -> Character; #[allow(unused)] fn get_state(&mut self) -> ServerToHostMessage; } @@ -232,6 +233,13 @@ impl GameExt for Game { fn get_state(&mut self) -> ServerToHostMessage { self.process(HostGameMessage::GetState).unwrap() } + fn living_villager(&self) -> Character { + self.village() + .characters() + .into_iter() + .find(|c| c.alive() && matches!(c.role_title(), RoleTitle::Villager)) + .unwrap() + } fn living_villager_excl(&self, excl: PlayerId) -> Character { self.village() @@ -248,7 +256,8 @@ impl GameExt for Game { .characters() .into_iter() .filter_map(|c| { - matches!(c.role_title(), RoleTitle::Villager).then_some(c.character_id()) + (c.alive() && matches!(c.role_title(), RoleTitle::Villager)) + .then_some(c.character_id()) }) .collect() } diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs index fc9920d..ab0a9bf 100644 --- a/werewolves-proto/src/game_test/role/mod.rs +++ b/werewolves-proto/src/game_test/role/mod.rs @@ -4,5 +4,6 @@ mod diseased; mod elder; mod empath; mod mason; +mod pyremaster; mod scapegoat; mod shapeshifter; diff --git a/werewolves-proto/src/game_test/role/pyremaster.rs b/werewolves-proto/src/game_test/role/pyremaster.rs new file mode 100644 index 0000000..1aec801 --- /dev/null +++ b/werewolves-proto/src/game_test/role/pyremaster.rs @@ -0,0 +1,61 @@ +use core::num::NonZeroU8; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + diedto::DiedTo, + game::{Game, GameSettings, SetupRole}, + game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, + message::night::ActionPromptTitle, +}; + +#[test] +fn burnout() { + let players = gen_players(1..21); + let pyremaster_player_id = players[0].player_id; + let wolf_player_id = players[1].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::PyreMaster, pyremaster_player_id); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.fill_remaining_slots_with_villagers(20); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + let mut villagers = game.villager_character_ids().into_iter(); + game.next_expect_day(); + game.execute().title().wolf_pack_kill(); + game.mark(villagers.next().unwrap()); + game.r#continue().sleep(); + + game.next().title().pyremaster(); + game.mark(villagers.next().unwrap()); + game.r#continue().sleep(); + + game.next_expect_day(); + + villagers = game.villager_character_ids().into_iter(); + + game.execute().title().wolf_pack_kill(); + game.mark(villagers.next().unwrap()); + game.r#continue().sleep(); + + game.next().title().pyremaster(); + game.mark(villagers.next().unwrap()); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(pyremaster_player_id) + .died_to() + .cloned(), + Some(DiedTo::PyreMasterLynchMob { + source: game + .character_by_player_id(pyremaster_player_id) + .character_id(), + night: NonZeroU8::new(2).unwrap() + }) + ); +} diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index 3263992..f9e3856 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -254,6 +254,7 @@ pub enum ArcanistCheck { } pub const MAPLE_WOLF_ABSTAIN_LIMIT: NonZeroU8 = NonZeroU8::new(3).unwrap(); +pub const PYREMASTER_VILLAGER_KILLS_TO_DIE: NonZeroU8 = NonZeroU8::new(2).unwrap(); #[derive(Debug, Clone, Serialize, Deserialize)] pub enum RoleBlock {