From 88665302f6b7b27c0402b1f090eb1a6d2cb7a10f Mon Sep 17 00:00:00 2001 From: emilis Date: Sun, 5 Oct 2025 10:52:37 +0100 Subject: [PATCH] elder death protection & lynching also made PlayerId and CharacterId Copy --- werewolves-proto/src/game/kill.rs | 10 +- werewolves-proto/src/game/night.rs | 124 ++-- werewolves-proto/src/game/settings.rs | 20 + .../src/game/settings/settings_role.rs | 3 +- werewolves-proto/src/game/village.rs | 37 +- werewolves-proto/src/game_test/mod.rs | 576 ++++++------------ werewolves-proto/src/game_test/role/elder.rs | 298 +++++++++ werewolves-proto/src/game_test/role/mod.rs | 3 + .../src/game_test/role/scapegoat.rs | 158 +++++ .../src/game_test/role/shapeshifter.rs | 209 +++++++ werewolves-proto/src/message/night.rs | 5 +- werewolves-proto/src/player.rs | 65 +- werewolves-proto/src/role.rs | 20 +- werewolves-server/src/connection.rs | 12 +- werewolves-server/src/game.rs | 24 +- werewolves-server/src/lobby.rs | 18 +- werewolves/src/components/action/prompt.rs | 16 + 17 files changed, 1108 insertions(+), 490 deletions(-) create mode 100644 werewolves-proto/src/game_test/role/elder.rs create mode 100644 werewolves-proto/src/game_test/role/mod.rs create mode 100644 werewolves-proto/src/game_test/role/scapegoat.rs create mode 100644 werewolves-proto/src/game_test/role/shapeshifter.rs diff --git a/werewolves-proto/src/game/kill.rs b/werewolves-proto/src/game/kill.rs index 608c40d..aa11b16 100644 --- a/werewolves-proto/src/game/kill.rs +++ b/werewolves-proto/src/game/kill.rs @@ -25,7 +25,7 @@ impl KillOutcome { match self { KillOutcome::Single(character_id, died_to) => { village - .character_by_id_mut(&character_id) + .character_by_id_mut(character_id) .ok_or(GameError::InvalidTarget)? .kill(died_to); Ok(()) @@ -40,10 +40,10 @@ impl KillOutcome { // check if guardian exists before we mutably borrow killer, which would // prevent us from borrowing village to check after. village - .character_by_id(&guardian) + .character_by_id(guardian) .ok_or(GameError::InvalidTarget)?; village - .character_by_id_mut(&original_killer) + .character_by_id_mut(original_killer) .ok_or(GameError::InvalidTarget)? .kill(DiedTo::GuardianProtecting { night, @@ -53,7 +53,7 @@ impl KillOutcome { protecting_from_cause: Box::new(original_kill.clone()), }); village - .character_by_id_mut(&guardian) + .character_by_id_mut(guardian) .ok_or(GameError::InvalidTarget)? .kill(original_kill); Ok(()) @@ -115,7 +115,7 @@ pub fn resolve_kill( && let Some(ss_source) = changes.shapeshifter() { let killing_wolf = village - .character_by_id(killing_wolf) + .character_by_id(*killing_wolf) .ok_or(GameError::InvalidTarget)?; match changes.protected_take(target) { diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 690aad0..3a388b6 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -1,4 +1,4 @@ -use core::num::NonZeroU8; +use core::{num::NonZeroU8, ops::Not}; use std::collections::VecDeque; use serde::{Deserialize, Serialize}; @@ -40,6 +40,9 @@ pub enum NightChange { target: CharacterId, protection: Protection, }, + ElderReveal { + elder: CharacterId, + }, } enum BlockResolvedOutcome { @@ -108,9 +111,17 @@ impl Night { DateTime::Night { number } => number, }; + let filter = if village.executed_known_elder() { + // there is a lynched elder, remove villager PRs from the prompts + filter::no_village + } else { + filter::no_filter + }; + let mut action_queue = village .characters() .into_iter() + .filter(filter) .map(|c| c.night_action_prompt(&village)) .collect::>>()? .into_iter() @@ -141,30 +152,8 @@ impl Night { current_prompt: ActionPrompt::CoverOfDarkness, current_result: None, }; - let mut changes = Vec::new(); - if let Some(night_nz) = NonZeroU8::new(night) { - // TODO: prob should be an end-of-night thing - changes = village - .dead_characters() - .into_iter() - .filter_map(|c| c.died_to().map(|d| (c, d))) - .filter_map(|(c, d)| match c.role() { - Role::Hunter { target } => target.clone().map(|t| (c, t, d)), - _ => None, - }) - .filter_map(|(c, t, d)| match d.date_time() { - DateTime::Day { number } => (number.get() == night).then_some((c, t)), - DateTime::Night { number: _ } => None, - }) - .map(|(c, target)| NightChange::Kill { - target, - died_to: DiedTo::Hunter { - killer: c.character_id().clone(), - night: night_nz, - }, - }) - .collect(); - } + + let changes = Self::automatic_changes(&village, night); Ok(Self { night, @@ -176,6 +165,39 @@ impl Night { }) } + /// changes that require no input (such as hunter firing) + fn automatic_changes(village: &Village, night: u8) -> Vec { + let mut changes = Vec::new(); + let night = match NonZeroU8::new(night) { + Some(night) => night, + None => return changes, + }; + if !village.executed_known_elder() { + village + .dead_characters() + .into_iter() + .filter_map(|c| c.died_to().map(|d| (c, d))) + .filter_map(|(c, d)| match c.role() { + Role::Hunter { target } => target.clone().map(|t| (c, t, d)), + _ => None, + }) + .filter_map(|(c, t, d)| match d.date_time() { + DateTime::Day { number } => (number.get() == night.get()).then_some((c, t)), + DateTime::Night { number: _ } => None, + }) + .map(|(c, target)| NightChange::Kill { + target, + died_to: DiedTo::Hunter { + night, + killer: c.character_id().clone(), + }, + }) + .for_each(|c| changes.push(c)); + } + + changes + } + pub fn previous_state(&mut self) -> Result<()> { let prev_act = self.used_actions.pop().ok_or(GameError::NoPreviousState)?; log::info!("loading previous prompt: {prev_act:?}"); @@ -224,13 +246,17 @@ impl Night { let mut changes = ChangesLookup::new(&self.changes); for change in self.changes.iter() { match change { + NightChange::ElderReveal { elder } => new_village + .character_by_id_mut(*elder) + .ok_or(GameError::InvalidTarget)? + .elder_reveal(), NightChange::RoleChange(character_id, role_title) => new_village - .character_by_id_mut(character_id) - .unwrap() + .character_by_id_mut(*character_id) + .ok_or(GameError::InvalidTarget)? .role_change(*role_title, DateTime::Night { number: self.night })?, NightChange::HunterTarget { source, target } => { if let Role::Hunter { target: t } = - new_village.character_by_id_mut(source).unwrap().role_mut() + new_village.character_by_id_mut(*source).unwrap().role_mut() { t.replace(target.clone()); } @@ -239,7 +265,7 @@ impl Night { && changes.protected(target).is_none() { new_village - .character_by_id_mut(target) + .character_by_id_mut(*target) .unwrap() .kill(DiedTo::Hunter { killer: source.clone(), @@ -262,7 +288,7 @@ impl Night { if let Some(target) = changes.wolf_pack_kill_target() && changes.protected(target).is_none() { - let ss = new_village.character_by_id_mut(source).unwrap(); + let ss = new_village.character_by_id_mut(*source).unwrap(); match ss.role_mut() { Role::Shapeshifter { shifted_into } => { *shifted_into = Some(target.clone()) @@ -343,7 +369,7 @@ impl Night { new_role: RoleTitle::Werewolf, character_id: self .village - .character_by_id(&kill_target) + .character_by_id(kill_target) .ok_or(GameError::NoMatchingCharacterFound)? .identity(), }); @@ -593,13 +619,22 @@ impl Night { unless: None, })) } + ActionPrompt::ElderReveal { character_id } => { + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::ElderReveal { + elder: character_id.character_id.clone(), + }), + unless: None, + })) + } ActionPrompt::Seer { marked: Some(marked), .. } => { let alignment = self .village - .character_by_id(marked) + .character_by_id(*marked) .ok_or(GameError::InvalidTarget)? .alignment(); Ok(ResponseOutcome::ActionComplete(ActionComplete { @@ -628,12 +663,12 @@ impl Night { } => { let same = self .village - .character_by_id(marked1) + .character_by_id(*marked1) .ok_or(GameError::InvalidMessageForGameState)? .alignment() == self .village - .character_by_id(marked2) + .character_by_id(*marked2) .ok_or(GameError::InvalidMessageForGameState)? .alignment(); @@ -649,7 +684,7 @@ impl Night { } => { let dig_role = self .village - .character_by_id(marked) + .character_by_id(*marked) .ok_or(GameError::InvalidMessageForGameState)? .gravedigger_dig(); Ok(ResponseOutcome::ActionComplete(ActionComplete { @@ -871,13 +906,14 @@ impl Night { } } - pub const fn current_character_id(&self) -> Option<&CharacterId> { + pub const fn current_character_id(&self) -> Option { match &self.night_state { NightState::Active { current_prompt, current_result: _, } => match current_prompt { - ActionPrompt::RoleChange { character_id, .. } + ActionPrompt::ElderReveal { character_id } + | ActionPrompt::RoleChange { character_id, .. } | ActionPrompt::Seer { character_id, .. } | ActionPrompt::Protector { character_id, .. } | ActionPrompt::Arcanist { character_id, .. } @@ -888,7 +924,7 @@ impl Night { | ActionPrompt::Guardian { character_id, .. } | ActionPrompt::Shapeshifter { character_id } | ActionPrompt::AlphaWolf { character_id, .. } - | ActionPrompt::DireWolf { character_id, .. } => Some(&character_id.character_id), + | ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id), ActionPrompt::WolvesIntro { wolves: _ } | ActionPrompt::WolfPackKill { .. } | ActionPrompt::CoverOfDarkness => None, @@ -940,3 +976,15 @@ pub enum ServerAction { Prompt(ActionPrompt), Result(ActionResult), } + +mod filter { + use crate::player::Character; + + pub fn no_filter(_: &Character) -> bool { + true + } + + pub fn no_village(c: &Character) -> bool { + !c.is_village() + } +} diff --git a/werewolves-proto/src/game/settings.rs b/werewolves-proto/src/game/settings.rs index 0301930..a405903 100644 --- a/werewolves-proto/src/game/settings.rs +++ b/werewolves-proto/src/game/settings.rs @@ -27,6 +27,22 @@ impl Default for GameSettings { } impl GameSettings { + pub fn empty() -> Self { + Self { + roles: vec![], + next_order: 1, + } + } + + pub fn fill_remaining_slots_with_villagers(&mut self, player_count: usize) { + if self.roles.len() >= player_count { + return; + } + for _ in 0..(player_count - self.roles.len()) { + self.new_slot(RoleTitle::Villager); + } + } + pub fn wolves_count(&self) -> usize { self.roles .iter() @@ -38,6 +54,10 @@ impl GameSettings { &self.roles } + pub fn get_slot_by_id(&self, slot_id: SlotId) -> Option<&SetupSlot> { + self.roles.iter().find(|s| s.slot_id == slot_id) + } + pub fn village_roles_count(&self) -> usize { log::warn!( "wolves: {} total: {}", diff --git a/werewolves-proto/src/game/settings/settings_role.rs b/werewolves-proto/src/game/settings/settings_role.rs index 7974502..ba685b1 100644 --- a/werewolves-proto/src/game/settings/settings_role.rs +++ b/werewolves-proto/src/game/settings/settings_role.rs @@ -148,7 +148,8 @@ impl SetupRole { } SetupRole::Elder { knows_on_night } => Role::Elder { knows_on_night, - has_protection: true, + woken_for_reveal: false, + lost_protection_night: None, }, SetupRole::Werewolf => Role::Werewolf, SetupRole::AlphaWolf => Role::AlphaWolf { killed: None }, diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index 70689ba..f256cf2 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -1,10 +1,11 @@ use core::num::NonZeroU8; -use rand::{Rng, seq::SliceRandom}; +use rand::Rng; use serde::{Deserialize, Serialize}; use super::Result; use crate::{ + diedto::DiedTo, error::GameError, game::{DateTime, GameOver, GameSettings}, message::{CharacterIdentity, Identification}, @@ -62,7 +63,7 @@ impl Village { self.date_time } - pub fn find_by_character_id(&self, character_id: &CharacterId) -> Option<&Character> { + pub fn find_by_character_id(&self, character_id: CharacterId) -> Option<&Character> { self.characters .iter() .find(|c| c.character_id() == character_id) @@ -70,7 +71,7 @@ impl Village { pub fn find_by_character_id_mut( &mut self, - character_id: &CharacterId, + character_id: CharacterId, ) -> Option<&mut Character> { self.characters .iter_mut() @@ -115,7 +116,7 @@ impl Village { let targets = self .characters .iter_mut() - .filter(|c| characters.contains(c.character_id())) + .filter(|c| characters.contains(&c.character_id())) .collect::>(); for t in targets { t.execute(day)?; @@ -162,7 +163,7 @@ impl Village { .collect() } - pub fn target_by_id(&self, character_id: &CharacterId) -> Option { + pub fn target_by_id(&self, character_id: CharacterId) -> Option { self.character_by_id(character_id).map(Character::identity) } @@ -174,7 +175,7 @@ impl Village { .collect() } - pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[CharacterIdentity]> { + pub fn living_players_excluding(&self, exclude: CharacterId) -> Box<[CharacterIdentity]> { self.characters .iter() .filter(|c| c.alive() && c.character_id() != exclude) @@ -194,6 +195,21 @@ impl Village { self.characters.iter().filter(|c| !c.alive()).collect() } + pub fn executed_known_elder(&self) -> bool { + self.characters.iter().any(|d| { + matches!( + d.role(), + Role::Elder { + woken_for_reveal: true, + .. + } + ) && d + .died_to() + .map(|d| matches!(d, DiedTo::Execution { .. })) + .unwrap_or_default() + }) + } + pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> { self.characters .iter() @@ -206,19 +222,19 @@ impl Village { self.characters.iter().cloned().collect() } - pub fn character_by_id_mut(&mut self, character_id: &CharacterId) -> Option<&mut Character> { + pub fn character_by_id_mut(&mut self, character_id: CharacterId) -> Option<&mut Character> { self.characters .iter_mut() .find(|c| c.character_id() == character_id) } - pub fn character_by_id(&self, character_id: &CharacterId) -> Option<&Character> { + pub fn character_by_id(&self, character_id: CharacterId) -> Option<&Character> { self.characters .iter() .find(|c| c.character_id() == character_id) } - pub fn character_by_player_id(&self, player_id: &PlayerId) -> Option<&Character> { + pub fn character_by_player_id(&self, player_id: PlayerId) -> Option<&Character> { self.characters.iter().find(|c| c.player_id() == player_id) } } @@ -234,7 +250,8 @@ impl RoleTitle { RoleTitle::Arcanist => Role::Arcanist, RoleTitle::Elder => Role::Elder { knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(), - has_protection: true, + woken_for_reveal: false, + lost_protection_night: None, }, RoleTitle::Werewolf => Role::Werewolf, RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None }, diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 4731399..e3e00da 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -1,15 +1,16 @@ mod night_order; +mod role; use crate::{ diedto::DiedTo, error::GameError, - game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange}, + game::{Game, GameSettings, OrRandom, SetupRole, SetupSlot, night::NightChange}, message::{ CharacterState, Identification, PublicIdentity, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, }, - player::{CharacterId, PlayerId}, + player::{Character, CharacterId, PlayerId}, role::{Alignment, Role, RoleTitle}, }; use colored::Colorize; @@ -22,7 +23,92 @@ use core::{ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use std::io::Write; -trait ActionResultExt { +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; @@ -45,7 +131,7 @@ impl ActionResultExt for ActionResult { } } -trait ServerToHostMessageExt { +pub trait ServerToHostMessageExt { fn prompt(self) -> ActionPrompt; fn result(self) -> ActionResult; fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); @@ -83,21 +169,53 @@ impl ServerToHostMessageExt for ServerToHostMessage { } } -trait GameExt { +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, check: impl FnOnce(&ActionPrompt) -> bool); + 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, @@ -106,7 +224,7 @@ impl GameExt for Game { .result() } - fn mark(&mut self, mark: &CharacterId) -> ActionPrompt { + fn mark(&mut self, mark: CharacterId) -> ActionPrompt { self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( ActionResponse::MarkTarget(mark.clone()), ))) @@ -114,10 +232,66 @@ impl GameExt for Game { .prompt() } - fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool) { + fn mark_and_check(&mut self, mark: CharacterId) { let prompt = self.mark(mark); - if !check(&prompt) { - panic!("unexpected prompt: {prompt:?}"); + 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"), } } @@ -175,7 +349,7 @@ impl GameExt for Game { } } -fn init_log() { +pub fn init_log() { let _ = pretty_env_logger::formatted_builder() .filter_level(log::LevelFilter::Debug) .format(|f, record| match record.file() { @@ -206,7 +380,7 @@ fn init_log() { .try_init(); } -fn gen_players(range: Range) -> Box<[Identification]> { +pub fn gen_players(range: Range) -> Box<[Identification]> { range .into_iter() .map(|num| Identification { @@ -365,157 +539,6 @@ fn yes_wolf_kill_n2() { )); } -#[test] -fn protect_stops_shapeshift() { - init_log(); - let players = gen_players(1..10); - let mut settings = GameSettings::default(); - settings.new_slot(RoleTitle::Shapeshifter); - settings.new_slot(RoleTitle::Protector); - for _ in 0..7 { - settings.new_slot(RoleTitle::Villager); - } - 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: _, - } - )); - - 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 - ); - game.r#continue().r#continue(); - - 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, - marked: None, - }) => ( - 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:#?}"); - - match game - .process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::MarkTarget(prot_and_wolf_target.clone()), - ))) - .unwrap() - { - ServerToHostMessage::ActionPrompt(ActionPrompt::Protector { - marked: Some(mark), .. - }) => assert_eq!(mark, prot_and_wolf_target, "marked target"), - resp => panic!("unexpected response: {resp:?}"), - } - - game.r#continue().sleep(); - - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); - - 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(); - - assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,); - - game.response(ActionResponse::Shapeshift); - - game.next_expect_day(); - - 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(); @@ -590,228 +613,3 @@ fn wolfpack_kill_all_targets_valid() { assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter); } } - -#[test] -fn only_1_shapeshift_prompt_if_first_shifts() { - let players = gen_players(1..10); - let mut settings = GameSettings::default(); - settings.new_slot(RoleTitle::Shapeshifter); - settings.new_slot(RoleTitle::Shapeshifter); - for _ in 0..7 { - settings.new_slot(RoleTitle::Villager); - } - 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 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); - game.r#continue().r#continue(); - 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(); - - game.mark_and_check(&target, |p| match p { - ActionPrompt::WolfPackKill { - marked: Some(t), .. - } => *t == target, - _ => false, - }); - game.r#continue().r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter); - game.response(ActionResponse::Shapeshift).r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::RoleChange); - game.r#continue().sleep(); - - game.next_expect_day(); -} - -#[test] -fn redeemed_scapegoat_role_changes() { - let players = gen_players(1..10); - let scapegoat_player_id = players[0].player_id.clone(); - let seer_player_id = players[1].player_id.clone(); - let wolf_player_id = players[2].player_id.clone(); - let wolf_target_2_player_id = players[3].player_id.clone(); - let mut settings = GameSettings::default(); - { - let scapegoat_slot = settings.new_slot(RoleTitle::Scapegoat); - let mut scapegoat_slot = settings - .slots() - .iter() - .find(|s| s.slot_id == scapegoat_slot) - .unwrap() - .clone(); - scapegoat_slot.role = SetupRole::Scapegoat { - redeemed: OrRandom::Determined(true), - }; - scapegoat_slot.assign_to = Some(scapegoat_player_id.clone()); - settings.update_slot(scapegoat_slot); - } - { - let mut slot = settings - .slots() - .iter() - .find(|s| matches!(s.role, SetupRole::Werewolf)) - .unwrap() - .clone(); - slot.assign_to = Some(wolf_player_id.clone()); - settings.update_slot(slot); - } - { - let slot = settings.new_slot(RoleTitle::Seer); - let mut slot = settings - .slots() - .iter() - .find(|s| s.slot_id == slot) - .unwrap() - .clone(); - slot.assign_to = Some(seer_player_id.clone()); - settings.update_slot(slot); - } - for _ in 0..6 { - settings.new_slot(RoleTitle::Villager); - } - let mut game = Game::new(&players, settings).unwrap(); - game.r#continue().r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); - game.r#continue().sleep(); - assert_eq!(game.next().title(), ActionPromptTitle::Seer); - let wolf_char_id = game - .village() - .characters() - .into_iter() - .find(|c| c.player_id() == &wolf_player_id) - .unwrap() - .character_id() - .clone(); - game.mark_and_check(&wolf_char_id, |p| match p { - ActionPrompt::Seer { - marked: Some(marked), - .. - } => marked == &wolf_char_id, - _ => false, - }); - assert_eq!(game.r#continue().seer(), Alignment::Wolves); - game.next_expect_day(); - - assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); - game.r#continue().r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); - let seer = game - .village() - .characters() - .into_iter() - .find(|c| c.player_id() == &seer_player_id) - .unwrap() - .character_id() - .clone(); - - game.mark_and_check(&seer, |p| match p { - ActionPrompt::WolfPackKill { - marked: Some(t), .. - } => *t == seer, - _ => false, - }); - game.r#continue().sleep(); - - assert_eq!(game.next().title(), ActionPromptTitle::Seer); - game.mark_and_check(&wolf_char_id, |p| match p { - ActionPrompt::Seer { - marked: Some(marked), - .. - } => marked == &wolf_char_id, - _ => false, - }); - assert_eq!(game.r#continue().seer(), Alignment::Wolves); - game.next_expect_day(); - - assert_eq!( - *game - .village() - .character_by_id(&seer) - .unwrap() - .died_to() - .unwrap(), - DiedTo::Wolfpack { - killing_wolf: wolf_char_id.clone(), - night: NonZero::new(1).unwrap() - } - ); - assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); - game.r#continue().r#continue(); - - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); - let wolf_target_2 = game - .village() - .characters() - .iter() - .find(|c| c.player_id() == &wolf_target_2_player_id) - .unwrap() - .character_id() - .clone(); - game.mark_and_check(&wolf_target_2, |r| match r { - ActionPrompt::WolfPackKill { - marked: Some(marked), - .. - } => marked == &wolf_target_2, - _ => false, - }); - game.r#continue().sleep(); - let scapegoat = game - .village() - .characters() - .into_iter() - .find(|c| c.player_id() == &scapegoat_player_id) - .unwrap() - .clone(); - assert_eq!( - game.next(), - ActionPrompt::RoleChange { - character_id: scapegoat.identity(), - new_role: RoleTitle::Seer - } - ); - game.r#continue().sleep(); - - match game.game_state() { - crate::game::GameState::Night { night } => night - .changes() - .iter() - .find(|c| match c { - NightChange::RoleChange(char, role) => { - char == scapegoat.character_id() && role == &RoleTitle::Seer - } - _ => false, - }) - .expect("no role change"), - _ => unreachable!(), - }; - - game.next_expect_day(); - let day_scapegoat = game - .village() - .character_by_id(scapegoat.character_id()) - .unwrap(); - assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer); -} diff --git a/werewolves-proto/src/game_test/role/elder.rs b/werewolves-proto/src/game_test/role/elder.rs new file mode 100644 index 0000000..44d019e --- /dev/null +++ b/werewolves-proto/src/game_test/role/elder.rs @@ -0,0 +1,298 @@ +use core::num::NonZeroU8; + +use crate::{ + diedto::DiedTo, + game::{Game, GameSettings, SetupRole}, + game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, + message::night::{ActionPrompt, ActionPromptTitle}, + role::{Alignment, Role}, +}; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +#[test] +fn elder_doesnt_die_first_try_night_doesnt_know() { + let players = gen_players(1..10); + let elder_player_id = players[0].player_id.clone(); + let wolf_player_id = players[2].player_id.clone(); + let mut settings = GameSettings::empty(); + settings.add_role( + SetupRole::Elder { + knows_on_night: NonZeroU8::new(3).unwrap(), + }, + |slot| { + slot.assign_to = Some(elder_player_id.clone()); + }, + ); + settings.add_role(SetupRole::Werewolf, |slot| { + slot.assign_to = Some(wolf_player_id.clone()) + }); + settings.fill_remaining_slots_with_villagers(players.len()); + 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(); + + assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + let elder = game.character_by_player_id(elder_player_id); + + game.mark_and_check(elder.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + let elder = game.character_by_player_id(elder_player_id); + assert_eq!(elder.died_to().cloned(), None); + + assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness); + game.r#continue().r#continue(); + + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + game.mark_and_check(elder.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + assert_eq!( + game.character_by_player_id(elder_player_id) + .died_to() + .cloned(), + Some(DiedTo::Wolfpack { + killing_wolf: game + .character_by_player_id(wolf_player_id) + .character_id() + .clone(), + night: NonZeroU8::new(2).unwrap(), + }) + ); +} + +#[test] +fn elder_doesnt_die_first_try_night_knows() { + let players = gen_players(1..10); + let elder_player_id = players[0].player_id.clone(); + let wolf_player_id = players[2].player_id.clone(); + let mut settings = GameSettings::empty(); + settings.add_role( + SetupRole::Elder { + knows_on_night: NonZeroU8::new(1).unwrap(), + }, + |slot| { + slot.assign_to = Some(elder_player_id.clone()); + }, + ); + settings.add_role(SetupRole::Werewolf, |slot| { + slot.assign_to = Some(wolf_player_id.clone()) + }); + settings.fill_remaining_slots_with_villagers(players.len()); + 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(); + + assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + let elder = game.character_by_player_id(elder_player_id); + + game.mark_and_check(elder.character_id()); + game.r#continue().sleep(); + + assert_eq!( + game.next(), + ActionPrompt::ElderReveal { + character_id: elder.identity() + }, + ); + game.r#continue().sleep(); + + game.next_expect_day(); + + let elder = game.character_by_player_id(elder_player_id); + assert_eq!(elder.died_to().cloned(), None); + + assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness); + game.r#continue().r#continue(); + + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + game.mark_and_check(elder.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + assert_eq!( + game.character_by_player_id(elder_player_id) + .died_to() + .cloned(), + Some(DiedTo::Wolfpack { + killing_wolf: game.character_by_player_id(wolf_player_id).character_id(), + night: NonZeroU8::new(2).unwrap(), + }) + ); +} + +#[test] +fn elder_executed_doesnt_know() { + let players = gen_players(1..10); + let elder_player_id = players[0].player_id; + let seer_player_id = players[1].player_id; + let wolf_player_id = players[2].player_id; + let mut settings = GameSettings::empty(); + settings.add_role( + SetupRole::Elder { + knows_on_night: NonZeroU8::new(3).unwrap(), + }, + |slot| { + slot.assign_to = Some(elder_player_id.clone()); + }, + ); + settings.add_and_assign(SetupRole::Seer, seer_player_id.clone()); + settings.add_role(SetupRole::Werewolf, |slot| { + slot.assign_to = Some(wolf_player_id.clone()) + }); + settings.fill_remaining_slots_with_villagers(players.len()); + let mut game = Game::new(&players, settings).unwrap(); + let mut villagers = game.villager_character_ids().into_iter(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(villagers.next().unwrap()); + assert_eq!(game.r#continue().seer(), Alignment::Village); + + game.next_expect_day(); + let elder = game.character_by_player_id(elder_player_id); + + game.mark_for_execution(elder.character_id()); + + game.execute().title().cover_of_darkness(); + game.r#continue().r#continue(); + + game.next().title().wolf_pack_kill(); + + assert_eq!( + game.character_by_player_id(elder_player_id) + .died_to() + .cloned(), + Some(DiedTo::Execution { + day: NonZeroU8::new(1).unwrap() + }) + ); + + assert_eq!( + game.character_by_player_id(elder_player_id).role().clone(), + Role::Elder { + knows_on_night: NonZeroU8::new(3).unwrap(), + woken_for_reveal: false, + lost_protection_night: None + } + ); + + game.mark(villagers.next().unwrap()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(villagers.next().unwrap()); + assert_eq!(game.r#continue().seer(), Alignment::Village); + + game.next_expect_day(); +} + +#[test] +fn elder_executed_knows_no_powers_incl_hunter_activation() { + let players = gen_players(1..10); + let elder_player_id = players[0].player_id; + let seer_player_id = players[1].player_id; + let wolf_player_id = players[2].player_id; + let hunter_player_id = players[3].player_id; + let mut settings = GameSettings::empty(); + settings.add_role( + SetupRole::Elder { + knows_on_night: NonZeroU8::new(1).unwrap(), + }, + |slot| { + slot.assign_to = Some(elder_player_id.clone()); + }, + ); + settings.add_and_assign(SetupRole::Seer, seer_player_id.clone()); + settings.add_role(SetupRole::Werewolf, |slot| { + slot.assign_to = Some(wolf_player_id.clone()) + }); + settings.add_and_assign(SetupRole::Hunter, hunter_player_id); + settings.fill_remaining_slots_with_villagers(players.len()); + let mut game = Game::new(&players, settings).unwrap(); + let mut villagers = game.villager_character_ids().into_iter(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.living_villager_excl(seer_player_id).character_id()); + assert_eq!(game.r#continue().seer(), Alignment::Village); + + game.next_expect_day(); + + game.execute().title().cover_of_darkness(); + game.r#continue().r#continue(); + + game.next().title().wolf_pack_kill(); + game.mark(villagers.next().unwrap()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.living_villager_excl(seer_player_id).character_id()); + assert_eq!(game.r#continue().seer(), Alignment::Village); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(wolf_player_id).character_id()); + game.r#continue().sleep(); + + assert_eq!( + game.next(), + ActionPrompt::ElderReveal { + character_id: game.character_by_player_id(elder_player_id).identity() + } + ); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(elder_player_id).role().clone(), + Role::Elder { + knows_on_night: NonZeroU8::new(1).unwrap(), + woken_for_reveal: true, + lost_protection_night: None + } + ); + + game.mark_for_execution(game.character_by_player_id(elder_player_id).character_id()); + game.execute().title().cover_of_darkness(); + game.r#continue().r#continue(); + + game.next().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(hunter_player_id).character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(hunter_player_id) + .died_to() + .cloned(), + Some(DiedTo::Wolfpack { + killing_wolf: game.character_by_player_id(wolf_player_id).character_id(), + night: NonZeroU8::new(2).unwrap() + }) + ); + + assert_eq!( + game.character_by_player_id(wolf_player_id) + .died_to() + .cloned(), + None + ); +} diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs new file mode 100644 index 0000000..42bb379 --- /dev/null +++ b/werewolves-proto/src/game_test/role/mod.rs @@ -0,0 +1,3 @@ +mod elder; +mod scapegoat; +mod shapeshifter; diff --git a/werewolves-proto/src/game_test/role/scapegoat.rs b/werewolves-proto/src/game_test/role/scapegoat.rs new file mode 100644 index 0000000..4dc17bf --- /dev/null +++ b/werewolves-proto/src/game_test/role/scapegoat.rs @@ -0,0 +1,158 @@ +use core::num::NonZero; + +use crate::{ + diedto::DiedTo, + game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange}, + game_test::{ActionResultExt, GameExt, gen_players}, + message::night::{ActionPrompt, ActionPromptTitle}, + role::{Alignment, RoleTitle}, +}; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +#[test] +fn redeemed_scapegoat_role_changes() { + let players = gen_players(1..10); + let scapegoat_player_id = players[0].player_id.clone(); + let seer_player_id = players[1].player_id.clone(); + let wolf_player_id = players[2].player_id.clone(); + let wolf_target_2_player_id = players[3].player_id.clone(); + let mut settings = GameSettings::default(); + { + let scapegoat_slot = settings.new_slot(RoleTitle::Scapegoat); + let mut scapegoat_slot = settings + .slots() + .iter() + .find(|s| s.slot_id == scapegoat_slot) + .unwrap() + .clone(); + scapegoat_slot.role = SetupRole::Scapegoat { + redeemed: OrRandom::Determined(true), + }; + scapegoat_slot.assign_to = Some(scapegoat_player_id.clone()); + settings.update_slot(scapegoat_slot); + } + { + let mut slot = settings + .slots() + .iter() + .find(|s| matches!(s.role, SetupRole::Werewolf)) + .unwrap() + .clone(); + slot.assign_to = Some(wolf_player_id.clone()); + settings.update_slot(slot); + } + { + let slot = settings.new_slot(RoleTitle::Seer); + let mut slot = settings + .slots() + .iter() + .find(|s| s.slot_id == slot) + .unwrap() + .clone(); + slot.assign_to = Some(seer_player_id.clone()); + settings.update_slot(slot); + } + for _ in 0..6 { + settings.new_slot(RoleTitle::Villager); + } + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + assert_eq!(game.next().title(), ActionPromptTitle::Seer); + let wolf_char_id = game + .village() + .characters() + .into_iter() + .find(|c| c.player_id() == wolf_player_id) + .unwrap() + .character_id() + .clone(); + game.mark_and_check(wolf_char_id); + assert_eq!(game.r#continue().seer(), Alignment::Wolves); + game.next_expect_day(); + + assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + let seer = game + .village() + .characters() + .into_iter() + .find(|c| c.player_id() == seer_player_id) + .unwrap() + .character_id() + .clone(); + + game.mark_and_check(seer); + game.r#continue().sleep(); + + assert_eq!(game.next().title(), ActionPromptTitle::Seer); + game.mark_and_check(wolf_char_id); + assert_eq!(game.r#continue().seer(), Alignment::Wolves); + game.next_expect_day(); + + assert_eq!( + *game + .village() + .character_by_id(seer) + .unwrap() + .died_to() + .unwrap(), + DiedTo::Wolfpack { + killing_wolf: wolf_char_id.clone(), + night: NonZero::new(1).unwrap() + } + ); + assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); + game.r#continue().r#continue(); + + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + let wolf_target_2 = game + .village() + .characters() + .iter() + .find(|c| c.player_id() == wolf_target_2_player_id) + .unwrap() + .character_id() + .clone(); + game.mark_and_check(wolf_target_2); + game.r#continue().sleep(); + let scapegoat = game + .village() + .characters() + .into_iter() + .find(|c| c.player_id() == scapegoat_player_id) + .unwrap() + .clone(); + assert_eq!( + game.next(), + ActionPrompt::RoleChange { + character_id: scapegoat.identity(), + new_role: RoleTitle::Seer + } + ); + game.r#continue().sleep(); + + match game.game_state() { + crate::game::GameState::Night { night } => night + .changes() + .iter() + .find(|c| match c { + NightChange::RoleChange(char, role) => { + char == &scapegoat.character_id() && role == &RoleTitle::Seer + } + _ => false, + }) + .expect("no role change"), + _ => unreachable!(), + }; + + game.next_expect_day(); + let day_scapegoat = game + .village() + .character_by_id(scapegoat.character_id()) + .unwrap(); + assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer); +} diff --git a/werewolves-proto/src/game_test/role/shapeshifter.rs b/werewolves-proto/src/game_test/role/shapeshifter.rs new file mode 100644 index 0000000..8e44c3e --- /dev/null +++ b/werewolves-proto/src/game_test/role/shapeshifter.rs @@ -0,0 +1,209 @@ +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + game::{Game, GameSettings, SetupRole}, + game_test::{ActionResultExt, GameExt, ServerToHostMessageExt, gen_players, init_log}, + message::{ + host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, + night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, + }, + player::CharacterId, + role::RoleTitle, +}; + +#[test] +fn protect_stops_shapeshift() { + init_log(); + let players = gen_players(1..10); + let mut settings = GameSettings::default(); + settings.new_slot(RoleTitle::Shapeshifter); + settings.new_slot(RoleTitle::Protector); + for _ in 0..7 { + settings.new_slot(RoleTitle::Villager); + } + 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: _, + } + )); + + 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 + ); + game.r#continue().r#continue(); + + 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, + marked: None, + }) => ( + 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:#?}"); + + match game + .process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::MarkTarget(prot_and_wolf_target.clone()), + ))) + .unwrap() + { + ServerToHostMessage::ActionPrompt(ActionPrompt::Protector { + marked: Some(mark), .. + }) => assert_eq!(mark, prot_and_wolf_target, "marked target"), + resp => panic!("unexpected response: {resp:?}"), + } + + game.r#continue().sleep(); + + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + + game.mark_and_check(prot_and_wolf_target); + game.r#continue().r#continue(); + + assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,); + + game.response(ActionResponse::Shapeshift); + + game.next_expect_day(); + + 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 only_1_shapeshift_prompt_if_first_shifts() { + let players = gen_players(1..10); + let mut settings = GameSettings::default(); + settings.new_slot(RoleTitle::Shapeshifter); + settings.new_slot(RoleTitle::Shapeshifter); + for _ in 0..7 { + settings.new_slot(RoleTitle::Villager); + } + 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 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); + game.r#continue().r#continue(); + 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(); + + game.mark_and_check(target); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter); + game.response(ActionResponse::Shapeshift).r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::RoleChange); + game.r#continue().sleep(); + + game.next_expect_day(); +} diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index d68fc12..9689d88 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -48,6 +48,8 @@ pub enum ActionPrompt { character_id: CharacterIdentity, new_role: RoleTitle, }, + #[checks(ActionType::RoleChange)] + ElderReveal { character_id: CharacterIdentity }, #[checks(ActionType::Other)] Seer { character_id: CharacterIdentity, @@ -124,7 +126,8 @@ impl ActionPrompt { pub(crate) fn with_mark(&self, mark: CharacterId) -> Result { let mut prompt = self.clone(); match &mut prompt { - ActionPrompt::WolvesIntro { .. } + ActionPrompt::ElderReveal { .. } + | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } | ActionPrompt::Shapeshifter { .. } | ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState), diff --git a/werewolves-proto/src/player.rs b/werewolves-proto/src/player.rs index a3acbea..c878d74 100644 --- a/werewolves-proto/src/player.rs +++ b/werewolves-proto/src/player.rs @@ -12,7 +12,7 @@ use crate::{ role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, }; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct PlayerId(uuid::Uuid); impl PlayerId { @@ -30,7 +30,7 @@ impl Display for PlayerId { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct CharacterId(uuid::Uuid); impl CharacterId { @@ -165,6 +165,29 @@ impl Character { } pub fn kill(&mut self, died_to: DiedTo) { + match (&mut self.role, died_to.date_time()) { + ( + Role::Elder { + lost_protection_night: Some(_), + .. + }, + _, + ) => {} + ( + Role::Elder { + lost_protection_night, + .. + }, + DateTime::Night { number: night }, + ) => { + *lost_protection_night = lost_protection_night + .is_none() + .then_some(night) + .and_then(NonZeroU8::new); + return; + } + _ => {} + } match &self.died_to { Some(_) => {} None => self.died_to = Some(died_to), @@ -183,12 +206,12 @@ impl Character { Ok(()) } - pub const fn character_id(&self) -> &CharacterId { - &self.identity.character_id + pub const fn character_id(&self) -> CharacterId { + self.identity.character_id } - pub const fn player_id(&self) -> &PlayerId { - &self.player_id + pub const fn player_id(&self) -> PlayerId { + self.player_id } pub const fn role(&self) -> &Role { @@ -212,6 +235,15 @@ impl Character { &mut self.role } + pub fn elder_reveal(&mut self) { + if let Role::Elder { + woken_for_reveal, .. + } = &mut self.role + { + *woken_for_reveal = true + } + } + pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<(), GameError> { let mut role = new_role.title_to_role_excl_apprentice(); core::mem::swap(&mut role, &mut self.role); @@ -253,6 +285,10 @@ impl Character { | Role::AlphaWolf { killed: Some(_) } | Role::Militia { targeted: Some(_) } | Role::Scapegoat { redeemed: false } + | Role::Elder { + woken_for_reveal: true, + .. + } | Role::Villager => return Ok(None), Role::Scapegoat { redeemed: true } => { let mut dead = village.dead_characters(); @@ -283,7 +319,7 @@ impl Character { last_protected: Some(last_protected), } => ActionPrompt::Protector { character_id: self.identity(), - targets: village.living_players_excluding(last_protected), + targets: village.living_players_excluding(*last_protected), marked: None, }, Role::Protector { @@ -312,15 +348,18 @@ impl Character { new_role: *role, })); } - Role::Elder { knows_on_night, .. } => { + Role::Elder { + knows_on_night, + woken_for_reveal: false, + .. + } => { let current_night = match village.date_time() { DateTime::Day { number: _ } => return Ok(None), DateTime::Night { number } => number, }; - return Ok((current_night == knows_on_night.get()).then_some({ - ActionPrompt::RoleChange { + return Ok((current_night >= knows_on_night.get()).then_some({ + ActionPrompt::ElderReveal { character_id: self.identity(), - new_role: RoleTitle::Elder, } })); } @@ -359,7 +398,7 @@ impl Character { } Role::Hunter { target } => ActionPrompt::Hunter { character_id: self.identity(), - current_target: target.as_ref().and_then(|t| village.target_by_id(t)), + current_target: target.as_ref().and_then(|t| village.target_by_id(*t)), living_players: village.living_players_excluding(self.character_id()), marked: None, }, @@ -374,7 +413,7 @@ impl Character { } => ActionPrompt::Guardian { character_id: self.identity(), previous: Some(PreviousGuardianAction::Guard(prev_target.clone())), - living_players: village.living_players_excluding(&prev_target.character_id), + living_players: village.living_players_excluding(prev_target.character_id), marked: None, }, Role::Guardian { diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index 0692db4..191ed62 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -9,7 +9,7 @@ use crate::{ player::CharacterId, }; -#[derive(Debug, Clone, Serialize, Deserialize, ChecksAs, Titles)] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)] pub enum Role { #[checks(Alignment::Village)] Villager, @@ -64,7 +64,8 @@ pub enum Role { #[checks("is_mentor")] Elder { knows_on_night: NonZeroU8, - has_protection: bool, + woken_for_reveal: bool, + lost_protection_night: Option, }, #[checks(Alignment::Wolves)] @@ -147,10 +148,17 @@ impl Role { .iter() .any(|c| c.role().title() == *title), - Role::Elder { knows_on_night, .. } => match village.date_time() { - DateTime::Night { number } => number == knows_on_night.get(), - _ => false, - }, + Role::Elder { + knows_on_night, + woken_for_reveal, + .. + } => { + !woken_for_reveal + && match village.date_time() { + DateTime::Night { number } => number == knows_on_night.get(), + _ => false, + } + } } } } diff --git a/werewolves-server/src/connection.rs b/werewolves-server/src/connection.rs index e4c9c86..7928097 100644 --- a/werewolves-server/src/connection.rs +++ b/werewolves-server/src/connection.rs @@ -94,8 +94,8 @@ impl JoinedPlayers { } } - pub async fn is_connected(&self, player_id: &PlayerId) -> bool { - self.players.lock().await.contains_key(player_id) + pub async fn is_connected(&self, player_id: PlayerId) -> bool { + self.players.lock().await.contains_key(&player_id) } pub async fn update(&self, player_id: &PlayerId, f: impl FnOnce(&mut JoinedPlayer)) { @@ -110,9 +110,9 @@ impl JoinedPlayers { } } - pub async fn get_player_identity(&self, player_id: &PlayerId) -> Option { + pub async fn get_player_identity(&self, player_id: PlayerId) -> Option { self.players.lock().await.iter().find_map(|(id, p)| { - (id == player_id).then(|| PublicIdentity { + (*id == player_id).then(|| PublicIdentity { name: p.name.clone(), pronouns: p.pronouns.clone(), number: p.number, @@ -139,11 +139,11 @@ impl JoinedPlayers { None } - pub async fn get_sender(&self, player_id: &PlayerId) -> Option> { + pub async fn get_sender(&self, player_id: PlayerId) -> Option> { self.players .lock() .await - .get(player_id) + .get(&player_id) .map(|c| c.sender.clone()) } diff --git a/werewolves-server/src/game.rs b/werewolves-server/src/game.rs index 6b5e399..f79a06b 100644 --- a/werewolves-server/src/game.rs +++ b/werewolves-server/src/game.rs @@ -104,7 +104,7 @@ impl GameRunner { .log_err(); }; (update_host)(&acks, &mut self.comms); - let notify_of_role = |player_id: &PlayerId, village: &Village, sender: &LobbyPlayers| { + let notify_of_role = |player_id: PlayerId, village: &Village, sender: &LobbyPlayers| { if let Some(char) = village.character_by_player_id(player_id) { sender .send_if_present( @@ -133,7 +133,7 @@ impl GameRunner { match msg { Message::Host(HostMessage::ForceRoleAckFor(char_id)) => { if let Some((c, ackd)) = - acks.iter_mut().find(|(c, _)| c.character_id() == &char_id) + acks.iter_mut().find(|(c, _)| c.character_id() == char_id) { *ackd = true; (notify_of_role)(c.player_id(), self.game.village(), &self.player_sender); @@ -152,12 +152,12 @@ impl GameRunner { message: ClientMessage::GetState, }) => { let sender = - if let Some(sender) = self.joined_players.get_sender(&player_id).await { + if let Some(sender) = self.joined_players.get_sender(player_id).await { sender } else { continue; }; - if acks.iter().any(|(c, d)| c.player_id() == &player_id && *d) { + if acks.iter().any(|(c, d)| c.player_id() == player_id && *d) { // already ack'd just sleep sender.send(ServerMessage::Sleep).log_debug(); continue; @@ -167,14 +167,14 @@ impl GameRunner { .village() .characters() .iter() - .find(|c| c.player_id() == &player_id) + .find(|c| c.player_id() == player_id) { sender .send(ServerMessage::GameStart { role: char.role().initial_shown_role(), }) .log_debug(); - } else if let Some(sender) = self.joined_players.get_sender(&player_id).await { + } else if let Some(sender) = self.joined_players.get_sender(player_id).await { sender.send(ServerMessage::GameInProgress).log_debug(); } } @@ -187,15 +187,15 @@ impl GameRunner { message: ClientMessage::RoleAck, }) => { if let Some((_, ackd)) = - acks.iter_mut().find(|(t, _)| t.player_id() == &player_id) + acks.iter_mut().find(|(t, _)| t.player_id() == player_id) { *ackd = true; self.player_sender - .send_if_present(&player_id, ServerMessage::Sleep) + .send_if_present(player_id, ServerMessage::Sleep) .log_debug(); } (update_host)(&acks, &mut self.comms); - if let Some(sender) = self.joined_players.get_sender(&player_id).await { + if let Some(sender) = self.joined_players.get_sender(player_id).await { sender.send(ServerMessage::Sleep).log_debug(); } } @@ -206,11 +206,11 @@ impl GameRunner { public: _, }, message: _, - }) => (notify_of_role)(&player_id, self.game.village(), &self.player_sender), + }) => (notify_of_role)(player_id, self.game.village(), &self.player_sender), Message::ConnectedList(c) => { let newly_connected = c.iter().filter(|c| connect_list.contains(*c)); for connected in newly_connected { - (notify_of_role)(connected, self.game.village(), &self.player_sender) + (notify_of_role)(*connected, self.game.village(), &self.player_sender) } connect_list = c; } @@ -332,7 +332,7 @@ impl GameEnd { self.game() .unwrap() .player_sender - .send_if_present(&identity.player_id, ServerMessage::GameOver(result)) + .send_if_present(identity.player_id, ServerMessage::GameOver(result)) .log_debug(); } Message::ConnectedList(_) => {} diff --git a/werewolves-server/src/lobby.rs b/werewolves-server/src/lobby.rs index be981b4..f3f31dd 100644 --- a/werewolves-server/src/lobby.rs +++ b/werewolves-server/src/lobby.rs @@ -70,7 +70,7 @@ impl Lobby { for (player, _) in self.players_in_lobby.iter() { players.push(PlayerState { identification: player.clone(), - connected: self.joined_players.is_connected(&player.player_id).await, + connected: self.joined_players.is_connected(player.player_id).await, }); } @@ -128,7 +128,7 @@ impl Lobby { )) => { let _ = self .players_in_lobby - .send_if_present(&player_id, ServerMessage::InvalidMessageForGameState); + .send_if_present(player_id, ServerMessage::InvalidMessageForGameState); } Err(( Message::Client(IdentifiedClientMessage { @@ -140,7 +140,7 @@ impl Lobby { log::error!("processing message from {public} [{player_id}]: {err}"); let _ = self .players_in_lobby - .send_if_present(&player_id, ServerMessage::Reset); + .send_if_present(player_id, ServerMessage::Reset); } Err((Message::ConnectedList(_), _)) => {} } @@ -178,7 +178,7 @@ impl Lobby { .players_in_lobby .iter_mut() .find_map(|(c, _)| (c.player_id == pid).then_some(c)) - && let Some(joined_id) = self.joined_players.get_player_identity(&pid).await + && let Some(joined_id) = self.joined_players.get_player_identity(pid).await { p.public = joined_id; } @@ -217,7 +217,7 @@ impl Lobby { // Already have the player return Ok(None); } - if let Some(sender) = self.joined_players.get_sender(&identity.player_id).await { + if let Some(sender) = self.joined_players.get_sender(identity.player_id).await { self.players_in_lobby.push((identity, sender.clone())); self.send_lobby_info_to_clients().await; self.send_lobby_info_to_host().await?; @@ -264,7 +264,7 @@ impl Lobby { .map(|(id, _)| id.public.clone()) .collect(), }; - if let Some(sender) = self.joined_players.get_sender(&player_id).await { + if let Some(sender) = self.joined_players.get_sender(player_id).await { sender.send(msg).log_debug(); } } @@ -321,14 +321,14 @@ impl DerefMut for LobbyPlayers { } impl LobbyPlayers { - pub fn find(&self, player_id: &PlayerId) -> Option<&Sender> { + pub fn find(&self, player_id: PlayerId) -> Option<&Sender> { self.iter() - .find_map(|(id, s)| (&id.player_id == player_id).then_some(s)) + .find_map(|(id, s)| (id.player_id == player_id).then_some(s)) } pub fn send_if_present( &self, - player_id: &PlayerId, + player_id: PlayerId, message: ServerMessage, ) -> Result<(), GameError> { if let Some(sender) = self.find(player_id) { diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index 0f8a698..e00fd0c 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -71,6 +71,22 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { /> }; } + ActionPrompt::ElderReveal { character_id } => { + let cont = continue_callback.map(|continue_callback| { + html! { + + } + }); + return html! { +
+ {identity_html(props, Some(character_id))} +

{"you are the elder"}

+ {cont} +
+ }; + } ActionPrompt::RoleChange { character_id, new_role,