From c2f33e670c083504ea4644a546f7ded662828115 Mon Sep 17 00:00:00 2001 From: emilis Date: Wed, 19 Nov 2025 02:22:10 +0000 Subject: [PATCH] small fixes, see commit message killing roles no longer get skipped if they die actually, no roles except intel + mason recruit get skipped if their source dies that night scapegoats can no longer redeem into apprentices limited killers (militia/alpha wolf): protection causes them to expend their shot (shapeshifter still safe) insomniac: test for dire wolf block (blocks don't count as visits) --- werewolves-macros/src/lib.rs | 4 +- werewolves-macros/src/ref_and_mut.rs | 230 +++++++++++- werewolves-proto/src/character.rs | 351 ++---------------- werewolves-proto/src/game/kill.rs | 1 + werewolves-proto/src/game/night.rs | 7 +- werewolves-proto/src/game/night/next.rs | 10 +- werewolves-proto/src/game/village/apply.rs | 44 ++- .../src/game_test/role/direwolf.rs | 3 +- .../src/game_test/role/guardian.rs | 4 +- .../src/game_test/role/insomniac.rs | 125 +++++++ .../src/game_test/role/maple_wolf.rs | 60 ++- .../src/game_test/role/militia.rs | 58 ++- .../src/game_test/role/protector.rs | 1 + werewolves-proto/src/role.rs | 4 +- 14 files changed, 548 insertions(+), 354 deletions(-) diff --git a/werewolves-macros/src/lib.rs b/werewolves-macros/src/lib.rs index 274ebc8..64361fd 100644 --- a/werewolves-macros/src/lib.rs +++ b/werewolves-macros/src/lib.rs @@ -570,9 +570,9 @@ pub fn all(input: proc_macro::TokenStream) -> proc_macro::TokenStream { quote! {#all}.into() } -#[proc_macro] +#[proc_macro_derive(RefAndMut)] pub fn ref_and_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let ref_and_mut = parse_macro_input!(input as RefAndMut); + let ref_and_mut = ref_and_mut::RefAndMut::parse(parse_macro_input!(input)).unwrap(); quote! {#ref_and_mut}.into() } #[proc_macro] diff --git a/werewolves-macros/src/ref_and_mut.rs b/werewolves-macros/src/ref_and_mut.rs index 911ef83..fdd353a 100644 --- a/werewolves-macros/src/ref_and_mut.rs +++ b/werewolves-macros/src/ref_and_mut.rs @@ -1,3 +1,4 @@ +use convert_case::Casing; // Copyright (C) 2025 Emilis Bliūdžius // // This program is free software: you can redistribute it and/or modify @@ -13,24 +14,237 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use quote::{ToTokens, quote}; -use syn::parse::Parse; +use syn::{Fields, FieldsNamed, FieldsUnnamed, Ident, spanned::Spanned}; #[allow(unused)] pub struct RefAndMut { name: syn::Ident, + variants: Vec<(Ident, Fields)>, } -impl Parse for RefAndMut { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - // let type_path = input.parse::()?; - // let matching = input.parse::()?; - let name = input.parse::()?; - // panic!("{type_path:?}\n\n{matching:?}"); - Ok(Self { name }) +impl RefAndMut { + pub fn parse(input: syn::DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(data) => data.variants, + syn::Data::Struct(_) | syn::Data::Union(_) => { + return Err(syn::Error::new( + input.span(), + "RefAndMut can only be used on enums", + )); + } + }; + + Ok(Self { + name: input.ident, + variants: variants.into_iter().map(|v| (v.ident, v.fields)).collect(), + }) } } impl ToTokens for RefAndMut { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + for (field, value) in self.variants.iter().cloned() { + match value { + Fields::Named(named) => named_fields(self.name.clone(), field, named, tokens), + Fields::Unnamed(unnamed) => { + unnamed_fields(self.name.clone(), field, unnamed, tokens) + } + Fields::Unit => continue, + } + } tokens.extend(quote! {}); } } + +fn named_fields( + struct_name: Ident, + name: Ident, + fields: FieldsNamed, + tokens: &mut proc_macro2::TokenStream, +) { + let base_name = name.to_string().to_case(convert_case::Case::Pascal); + let name_ref = Ident::new(format!("{base_name}Ref").as_str(), name.span()); + let fn_name_ref = Ident::new( + format!( + "{}_ref", + name.to_string().to_case(convert_case::Case::Snake) + ) + .as_str(), + name.span(), + ); + let fn_name_mut = Ident::new( + format!( + "{}_mut", + name.to_string().to_case(convert_case::Case::Snake) + ) + .as_str(), + name.span(), + ); + let name_mut = Ident::new(format!("{base_name}Mut").as_str(), name.span()); + let struct_fields_ref = fields + .named + .iter() + .map(|c| { + let name = c.ident.as_ref().expect("this is a named field??"); + let ty = &c.ty; + quote! { + pub #name: &'a #ty, + } + }) + .collect::>(); + let struct_fields_mut = fields + .named + .iter() + .map(|c| { + let name = c.ident.as_ref().expect("this is a named field??"); + let ty = &c.ty; + quote! { + pub #name: &'a mut #ty, + } + }) + .collect::>(); + let fields_idents = fields + .named + .iter() + .map(|c| c.ident.as_ref().expect("this is a named field??")) + .collect::>(); + + let for_character = (struct_name == "Role").then_some(quote!{ + impl crate::character::Character { + pub fn #fn_name_ref<'a>(&'a self) -> core::result::Result<#name_ref<'a>, crate::error::GameError> { + let got = self.role_title(); + self.role().#fn_name_ref().ok_or(crate::error::GameError::InvalidRole { + got, + expected: RoleTitle::#name, + }) + } + pub fn #fn_name_mut<'a>(&'a mut self) -> core::result::Result<#name_mut<'a>, crate::error::GameError> { + let got = self.role_title(); + self.role_mut().#fn_name_mut().ok_or(crate::error::GameError::InvalidRole { + got, + expected: RoleTitle::#name, + }) + } + } + }); + + tokens.extend(quote! { + pub struct #name_ref<'a> { + #(#struct_fields_ref)* + } + + pub struct #name_mut<'a> { + #(#struct_fields_mut)* + } + + #for_character + + impl #struct_name { + pub fn #fn_name_ref<'a>(&'a self) -> Option<#name_ref<'a>> { + match self { + Self::#name{ #(#fields_idents),* } => Some(#name_ref {#(#fields_idents),* }), + _ => None, + } + } + pub fn #fn_name_mut<'a>(&'a mut self) -> Option<#name_mut<'a>> { + match self { + Self::#name{ #(#fields_idents),* } => Some(#name_mut {#(#fields_idents),* }), + _ => None, + } + } + } + }); +} + +fn unnamed_fields( + struct_name: Ident, + name: Ident, + fields: FieldsUnnamed, + tokens: &mut proc_macro2::TokenStream, +) { + let base_name = name.to_string().to_case(convert_case::Case::Pascal); + let name_ref = Ident::new(format!("{base_name}Ref").as_str(), name.span()); + let fn_name_ref = Ident::new( + format!( + "{}_ref", + name.to_string().to_case(convert_case::Case::Snake) + ) + .as_str(), + name.span(), + ); + let fn_name_mut = Ident::new( + format!( + "{}_mut", + name.to_string().to_case(convert_case::Case::Snake) + ) + .as_str(), + name.span(), + ); + let name_mut = Ident::new(format!("{base_name}Mut").as_str(), name.span()); + + let fields_ref = fields + .unnamed + .iter() + .map(|field| { + let ty = &field.ty; + quote! { + pub &'a #ty + } + }) + .collect::>(); + let fields_mut = fields + .unnamed + .iter() + .map(|field| { + let ty = &field.ty; + quote! { + pub &'a mut #ty + } + }) + .collect::>(); + let fields_names = fields + .unnamed + .iter() + .enumerate() + .map(|(idx, field)| Ident::new(format!("field{idx}").as_str(), field.span())) + .collect::>(); + let for_character = (struct_name == "Role").then_some(quote! { + impl crate::character::Character { + pub fn #fn_name_ref<'a>(&'a self) -> core::result::Result<#name_ref<'a>, crate::error::GameError> { + let got = self.role_title(); + self.role().#fn_name_ref().ok_or(crate::error::GameError::InvalidRole { + got, + expected: RoleTitle::#name, + }) + } + pub fn #fn_name_mut<'a>(&'a mut self) -> core::result::Result<#name_mut<'a>, crate::error::GameError> { + let got = self.role_title(); + self.role_mut().#fn_name_mut().ok_or(crate::error::GameError::InvalidRole { + got, + expected: RoleTitle::#name, + }) + } + } + }); + tokens.extend(quote! { + pub struct #name_ref<'a>(#(#fields_ref),*); + + pub struct #name_mut<'a>(#(#fields_mut),*); + + #for_character + + impl #struct_name { + pub fn #fn_name_ref<'a>(&'a self) -> Option<#name_ref<'a>> { + match self { + Self::#name(#(#fields_names),*) => Some(#name_ref(#(#fields_names),*)), + _ => None, + } + } + pub fn #fn_name_mut<'a>(&'a mut self) -> Option<#name_mut<'a>> { + match self { + Self::#name(#(#fields_names),*) => Some(#name_mut(#(#fields_names),*)), + _ => None, + } + } + } + }); +} diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index a35365b..2d54a8e 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -29,8 +29,8 @@ use crate::{ message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, player::{PlayerId, RoleChange}, role::{ - Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful, - PreviousGuardianAction, Role, RoleTitle, + Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, + Powerful, PreviousGuardianAction, Role, RoleTitle, }, }; @@ -93,8 +93,12 @@ impl Character { }) } - pub const fn is_power_role(&self) -> bool { - !matches!(&self.role, Role::Scapegoat { .. } | Role::Villager) + pub const fn scapegoat_can_redeem_into(&self) -> bool { + !self.role.wolf() + && !matches!( + &self.role, + Role::Scapegoat { .. } | Role::Villager | Role::Apprentice(_) + ) } pub fn identity(&self) -> CharacterIdentity { @@ -314,7 +318,7 @@ impl Character { pub fn night_action_prompts(&self, village: &Village) -> Result> { let mut prompts = Vec::new(); - if self.mason_leader().is_ok() { + if let Role::MasonLeader { .. } = &self.role { // add them here so masons wake up even with a dead leader prompts.append(&mut self.mason_prompts(village)?); } @@ -358,7 +362,7 @@ impl Character { dead.shuffle(&mut rand::rng()); if let Some(pr) = dead .into_iter() - .find_map(|d| (d.is_village() && d.is_power_role()).then_some(d.role_title())) + .find_map(|d| d.scapegoat_can_redeem_into().then_some(d.role_title())) { prompts.push(ActionPrompt::RoleChange { character_id: self.identity(), @@ -576,15 +580,21 @@ impl Character { Ok(prompts.into_boxed_slice()) } - #[cfg(test)] - pub const fn role(&self) -> &Role { - &self.role - } - pub const fn killing_wolf_order(&self) -> Option { self.role.killing_wolf_order() } + pub fn black_knight_kill(&mut self) -> Result<()> { + let attacked = self.black_knight_ref()?.attacked; + if let Some(attacked) = attacked.as_ref() + && let Some(next) = attacked.next_night() + && self.died_to.is_none() + { + self.died_to = Some(next); + } + Ok(()) + } + pub fn alignment(&self) -> Alignment { if let Some(alignment) = self.auras.overrides_alignment() { return alignment; @@ -615,323 +625,28 @@ impl Character { self.role.powerful() } - pub const fn hunter<'a>(&'a self) -> Result> { - match &self.role { - Role::Hunter { target } => Ok(Hunter(target)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Hunter, - got: self.role_title(), - }), - } - } - - pub const fn hunter_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::Hunter { target } => Ok(HunterMut(target)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Hunter, - got: title, - }), - } - } - - pub const fn shapeshifter<'a>(&'a self) -> Result> { - match &self.role { - Role::Shapeshifter { shifted_into } => Ok(Shapeshifter(shifted_into)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Shapeshifter, - got: self.role_title(), - }), - } - } - - pub const fn shapeshifter_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::Shapeshifter { shifted_into } => Ok(ShapeshifterMut(shifted_into)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Shapeshifter, - got: title, - }), - } - } - - pub const fn mason_leader<'a>(&'a self) -> Result> { - match &self.role { - Role::MasonLeader { - recruits_available, - recruits, - } => Ok(MasonLeader(recruits_available, recruits)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::MasonLeader, - got: self.role_title(), - }), - } - } - - pub const fn mason_leader_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::MasonLeader { - recruits_available, - recruits, - } => Ok(MasonLeaderMut(recruits_available, recruits)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::MasonLeader, - got: title, - }), - } - } - - pub const fn scapegoat<'a>(&'a self) -> Result> { - match &self.role { - Role::Scapegoat { redeemed } => Ok(Scapegoat(redeemed)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Scapegoat, - got: self.role_title(), - }), - } - } - - pub const fn scapegoat_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::Scapegoat { redeemed } => Ok(ScapegoatMut(redeemed)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Scapegoat, - got: title, - }), - } - } - - pub const fn empath<'a>(&'a self) -> Result> { - match &self.role { - Role::Empath { cursed } => Ok(Empath(cursed)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Empath, - got: self.role_title(), - }), - } - } - - pub const fn empath_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::Empath { cursed } => Ok(EmpathMut(cursed)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Empath, - got: title, - }), - } - } - - pub const fn black_knight<'a>(&'a self) -> Result> { - match &self.role { - Role::BlackKnight { attacked } => Ok(BlackKnight(attacked)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::BlackKnight, - got: self.role_title(), - }), - } - } - - pub const fn black_knight_kill<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &self.role { - Role::BlackKnight { attacked } => Ok(BlackKnightKill { - attacked, - died_to: &mut self.died_to, - }), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::BlackKnight, - got: title, - }), - } - } - pub const fn black_knight_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::BlackKnight { attacked } => Ok(BlackKnightMut(attacked)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::BlackKnight, - got: title, - }), - } - } - - pub const fn guardian<'a>(&'a self) -> Result> { - let title = self.role.title(); - match &self.role { - Role::Guardian { last_protected } => Ok(Guardian(last_protected)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Guardian, - got: title, - }), - } - } - - pub const fn guardian_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::Guardian { last_protected } => Ok(GuardianMut(last_protected)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Guardian, - got: title, - }), - } - } - - pub const fn direwolf<'a>(&'a self) -> Result> { - let title = self.role.title(); - match &self.role { - Role::DireWolf { last_blocked } => Ok(Direwolf(last_blocked)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::DireWolf, - got: title, - }), - } - } - - pub const fn direwolf_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::DireWolf { last_blocked } => Ok(DirewolfMut(last_blocked)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::DireWolf, - got: title, - }), - } - } - - pub const fn militia<'a>(&'a self) -> Result> { - let title = self.role.title(); - match &self.role { - Role::Militia { targeted } => Ok(Militia(targeted)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Militia, - got: title, - }), - } - } - - pub const fn militia_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::Militia { targeted } => Ok(MilitiaMut(targeted)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Militia, - got: title, - }), - } - } - - pub const fn maple_wolf_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::MapleWolf { last_kill_on_night } => Ok(MapleWolfMut(last_kill_on_night)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::MapleWolf, - got: title, - }), - } - } - - pub const fn protector_mut<'a>(&'a mut self) -> Result> { - let title = self.role.title(); - match &mut self.role { - Role::Protector { last_protected } => Ok(ProtectorMut(last_protected)), - _ => Err(GameError::InvalidRole { - expected: RoleTitle::Protector, - got: title, - }), - } - } - pub const fn initial_shown_role(&self) -> RoleTitle { self.role.initial_shown_role() } -} -macro_rules! decl_ref_and_mut { - ($($name:ident, $name_mut:ident: $contains:ty;)*) => { - $( - pub struct $name<'a>(&'a $contains); - impl core::ops::Deref for $name<'_> { - type Target = $contains; + #[doc(hidden)] + pub fn role(&self) -> &Role { + &self.role + } - fn deref(&self) -> &Self::Target { - self.0 - } - } - pub struct $name_mut<'a>(&'a mut $contains); - impl core::ops::Deref for $name_mut<'_> { - type Target = $contains; - - fn deref(&self) -> &Self::Target { - self.0 - } - } - impl core::ops::DerefMut for $name_mut<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.0 - } - } - )* - }; -} - -decl_ref_and_mut!( - Hunter, HunterMut: Option; - Shapeshifter, ShapeshifterMut: Option; - Scapegoat, ScapegoatMut: bool; - Empath, EmpathMut: bool; - BlackKnight, BlackKnightMut: Option; - Guardian, GuardianMut: Option; - Direwolf, DirewolfMut: Option; - Militia, MilitiaMut: Option; - MapleWolf, MapleWolfMut: u8; - Protector, ProtectorMut: Option; -); - -pub struct BlackKnightKill<'a> { - attacked: &'a Option, - died_to: &'a mut Option, -} -impl BlackKnightKill<'_> { - pub fn kill(self) { - if let Some(attacked) = self.attacked.as_ref().and_then(|a| a.next_night()) - && self.died_to.is_none() - { - self.died_to.replace(attacked.clone()); - } + #[doc(hidden)] + pub fn role_mut(&mut self) -> &mut Role { + &mut self.role } } -pub struct MasonLeader<'a>(&'a u8, &'a [CharacterId]); -impl MasonLeader<'_> { - pub const fn remaining_recruits(&self) -> u8 { - *self.0 - } - - pub const fn recruits(&self) -> usize { - self.1.len() - } -} - -pub struct MasonLeaderMut<'a>(&'a mut u8, &'a mut Box<[CharacterId]>); -impl MasonLeaderMut<'_> { - pub const fn remaining_recruits(&self) -> u8 { - *self.0 - } - +// pub struct MasonLeaderMut<'a>(&'a mut u8, &'a mut Box<[CharacterId]>); +impl crate::role::MasonLeaderMut<'_> { pub fn recruit(self, target: CharacterId) { - let mut recruits = self.1.to_vec(); + let mut recruits = self.recruits.to_vec(); recruits.push(target); - *self.1 = recruits.into_boxed_slice(); - if let Some(new) = self.0.checked_sub(1) { - *self.0 = new; - } + *self.recruits = recruits.into_boxed_slice(); + *self.recruits_available = self.recruits_available.saturating_sub(1); } } diff --git a/werewolves-proto/src/game/kill.rs b/werewolves-proto/src/game/kill.rs index 28f5f96..50a4fcc 100644 --- a/werewolves-proto/src/game/kill.rs +++ b/werewolves-proto/src/game/kill.rs @@ -53,6 +53,7 @@ impl KillOutcome { && let Some(existing) = village .character_by_id_mut(killer)? .militia_mut()? + .targeted .replace(character_id) { log::error!("militia kill after already recording a kill on {existing}"); diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 3982a4d..6da4299 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -458,7 +458,12 @@ impl Night { .dead_characters() .into_iter() .filter_map(|c| c.died_to().map(|d| (c, d))) - .filter_map(|(c, d)| c.hunter().ok().and_then(|h| *h).map(|t| (c, t, d))) + .filter_map(|(c, d)| { + c.hunter_ref() + .ok() + .and_then(|h| *h.target) + .map(|t| (c, t, d)) + }) .filter_map(|(c, t, d)| match d.date_time() { GameTime::Day { number } => (number.get() == night.get()).then_some((c, t)), GameTime::Night { number: _ } => None, diff --git a/werewolves-proto/src/game/night/next.rs b/werewolves-proto/src/game/night/next.rs index 76d6a03..381a9a6 100644 --- a/werewolves-proto/src/game/night/next.rs +++ b/werewolves-proto/src/game/night/next.rs @@ -20,7 +20,7 @@ use crate::{ diedto::DiedTo, error::GameError, game::night::{CurrentResult, Night, NightState, changes::NightChange}, - message::night::{ActionPrompt, ActionResult}, + message::night::{ActionPrompt, ActionResult, ActionType}, role::{RoleBlock, RoleTitle}, }; @@ -194,8 +194,14 @@ impl Night { .village .characters() .into_iter() - .any(|c| matches!(c.role_title(), RoleTitle::Beholder)); + .any(|c| matches!(c.role_title(), RoleTitle::Beholder | RoleTitle::Insomniac)); while let Some(prompt) = self.action_queue.pop_front() { + if !matches!( + prompt.action_type(), + ActionType::Intel | ActionType::Insomniac + ) { + return Ok(Some(prompt)); + } let Some(char_id) = prompt.character_id() else { return Ok(Some(prompt)); }; diff --git a/werewolves-proto/src/game/village/apply.rs b/werewolves-proto/src/game/village/apply.rs index 3087181..46bab86 100644 --- a/werewolves-proto/src/game/village/apply.rs +++ b/werewolves-proto/src/game/village/apply.rs @@ -88,7 +88,7 @@ impl Village { .role_change(*role_title, GameTime::Night { number: night })?, NightChange::HunterTarget { source, target } => { let hunter_character = new_village.character_by_id_mut(*source).unwrap(); - hunter_character.hunter_mut()?.replace(*target); + hunter_character.hunter_mut()?.target.replace(*target); if changes .died_to(hunter_character.character_id(), night, self)? .is_some() @@ -126,11 +126,28 @@ impl Village { if let DiedTo::MapleWolf { source, .. } = died_to && let Ok(maple) = new_village.character_by_id_mut(*source) { - *maple.maple_wolf_mut()? = night; + *maple.maple_wolf_mut()?.last_kill_on_night = night; } } else { recorded_changes.retain(|c| c != change); } + match died_to { + DiedTo::Militia { killer, .. } => { + new_village + .character_by_id_mut(*killer)? + .militia_mut()? + .targeted + .replace(*target); + } + DiedTo::AlphaWolf { killer, .. } => { + new_village + .character_by_id_mut(*killer)? + .alpha_wolf_mut()? + .killed + .replace(*target); + } + _ => {} + }; } NightChange::Shapeshift { source, into } => { if let Some(target) = changes.wolf_pack_kill_target() @@ -141,7 +158,7 @@ impl Village { continue; } let ss = new_village.character_by_id_mut(*source).unwrap(); - ss.shapeshifter_mut().unwrap().replace(*target); + ss.shapeshifter_mut().unwrap().shifted_into.replace(*target); ss.kill(DiedTo::Shapeshift { into: *target, night: NonZeroU8::new(night).unwrap(), @@ -160,6 +177,7 @@ impl Village { new_village .character_by_id_mut(*source)? .guardian_mut()? + .last_protected .replace(if *guarding { PreviousGuardianAction::Guard(target) } else { @@ -174,7 +192,8 @@ impl Village { } => { new_village .character_by_id_mut(*source)? - .direwolf_mut()? + .dire_wolf_mut()? + .last_blocked .replace(*target); recorded_changes.retain(|c| { @@ -206,7 +225,10 @@ impl Village { new_village .character_by_id_mut(*scapegoat)? .role_change(RoleTitle::Villager, GameTime::Night { number: night })?; - *new_village.character_by_id_mut(*empath)?.empath_mut()? = true; + *new_village + .character_by_id_mut(*empath)? + .empath_mut()? + .cursed = true; } NightChange::LostAura { character, aura } => { new_village @@ -223,6 +245,7 @@ impl Village { new_village .character_by_id_mut(*source)? .guardian_mut()? + .last_protected .replace(PreviousGuardianAction::Guard(target_ident)); } Protection::Guardian { @@ -232,12 +255,14 @@ impl Village { new_village .character_by_id_mut(*source)? .guardian_mut()? + .last_protected .replace(PreviousGuardianAction::Protect(target_ident)); } Protection::Protector { source } => { new_village .character_by_id_mut(*source)? .protector_mut()? + .last_protected .replace(*target); } Protection::Vindicator { .. } => {} @@ -250,10 +275,15 @@ impl Village { .characters_mut() .into_iter() .filter(|k| k.alive()) - .filter(|k| k.black_knight().ok().and_then(|t| (*t).clone()).is_some()) + .filter(|k| { + k.black_knight_ref() + .ok() + .and_then(|t| (*t.attacked).clone()) + .is_some() + }) .filter(|k| changes.killed(k.character_id()).is_none()) { - knight.black_knight_kill()?.kill(); + knight.black_knight_kill()?; } // pyre masters death diff --git a/werewolves-proto/src/game_test/role/direwolf.rs b/werewolves-proto/src/game_test/role/direwolf.rs index 1ee6803..f86dc68 100644 --- a/werewolves-proto/src/game_test/role/direwolf.rs +++ b/werewolves-proto/src/game_test/role/direwolf.rs @@ -115,8 +115,9 @@ fn block_on_guardian_target_prevents_the_visit() { assert_eq!( game.character_by_player_id(guardian) - .guardian() + .guardian_ref() .unwrap() + .last_protected .clone(), None ); diff --git a/werewolves-proto/src/game_test/role/guardian.rs b/werewolves-proto/src/game_test/role/guardian.rs index bc2d079..62aa612 100644 --- a/werewolves-proto/src/game_test/role/guardian.rs +++ b/werewolves-proto/src/game_test/role/guardian.rs @@ -266,6 +266,8 @@ fn protects_from_militia() { ); assert_eq!( game.character_by_player_id(militia).role().clone(), - Role::Militia { targeted: None } + Role::Militia { + targeted: Some(protected.character_id()) + } ); } diff --git a/werewolves-proto/src/game_test/role/insomniac.rs b/werewolves-proto/src/game_test/role/insomniac.rs index 7a112e6..4a8aba0 100644 --- a/werewolves-proto/src/game_test/role/insomniac.rs +++ b/werewolves-proto/src/game_test/role/insomniac.rs @@ -89,3 +89,128 @@ fn sees_visits() { ])) ); } + +#[test] +fn direwolf_block_prevents_visits_so_they_are_not_seen() { + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let insomniac = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + let seer = player_ids.next().unwrap(); + let arcanist = player_ids.next().unwrap(); + let direwolf = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Insomniac, insomniac); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Seer, seer); + settings.add_and_assign(SetupRole::Arcanist, arcanist); + settings.add_and_assign(SetupRole::DireWolf, direwolf); + 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().r#continue(); + + game.next().title().direwolf(); + game.mark(game.character_by_player_id(seer).character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().seer().village(); + game.r#continue().sleep(); + + game.next().title().arcanist(); + let mut villagers = game.villager_character_ids().into_iter(); + game.mark(villagers.next().unwrap()); + game.mark(villagers.next().unwrap()); + assert_eq!(game.r#continue().arcanist(), true); + game.r#continue().sleep(); + + game.next_expect_day(); + + game.execute().title().wolf_pack_kill(); + game.mark(game.living_villager().character_id()); + game.r#continue().r#continue(); + + game.next().title().direwolf(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().role_blocked(); + game.r#continue().sleep(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(seer).character_id()); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().role_blocked(); + game.r#continue().sleep(); + + game.next().title().insomniac(); + assert_eq!( + game.r#continue().insomniac(), + Visits::new(Box::new( + [game.character_by_player_id(direwolf).identity(),] + )) + ); +} + +#[test] +fn dead_people_still_get_prompts_to_trigger_visits() { + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let insomniac = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + let seer = player_ids.next().unwrap(); + let arcanist = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Insomniac, insomniac); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Seer, seer); + settings.add_and_assign(SetupRole::Arcanist, arcanist); + 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().title().seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().seer().village(); + game.r#continue().sleep(); + + game.next().title().arcanist(); + let mut villagers = game.villager_character_ids().into_iter(); + game.mark(villagers.next().unwrap()); + game.mark(villagers.next().unwrap()); + assert_eq!(game.r#continue().arcanist(), true); + game.r#continue().sleep(); + + game.next_expect_day(); + + game.execute().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(seer).character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().seer().village(); + game.r#continue().sleep(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(seer).character_id()); + game.mark(game.character_by_player_id(insomniac).character_id()); + assert!(game.r#continue().arcanist()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + assert_eq!( + game.r#continue().insomniac(), + Visits::new(Box::new([ + game.character_by_player_id(seer).identity(), + game.character_by_player_id(arcanist).identity() + ])) + ); +} diff --git a/werewolves-proto/src/game_test/role/maple_wolf.rs b/werewolves-proto/src/game_test/role/maple_wolf.rs index a019123..e25f2a5 100644 --- a/werewolves-proto/src/game_test/role/maple_wolf.rs +++ b/werewolves-proto/src/game_test/role/maple_wolf.rs @@ -56,7 +56,11 @@ fn maple_starves() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 0 ); @@ -111,7 +115,11 @@ fn maple_last_eat_counter_increments() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 0 ); @@ -134,7 +142,11 @@ fn maple_last_eat_counter_increments() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 1 ); @@ -157,7 +169,11 @@ fn maple_last_eat_counter_increments() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 2 ); @@ -180,7 +196,11 @@ fn maple_last_eat_counter_increments() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 3 ); let (maple_kill, wolf_kill) = { @@ -202,7 +222,11 @@ fn maple_last_eat_counter_increments() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 4 ); @@ -234,7 +258,11 @@ fn drunk_maple_doesnt_eat() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 0 ); @@ -258,7 +286,11 @@ fn drunk_maple_doesnt_eat() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 0 ); assert_eq!( @@ -289,7 +321,11 @@ fn drunk_maple_doesnt_eat() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 0 ); assert_eq!( @@ -320,7 +356,11 @@ fn drunk_maple_doesnt_eat() { game.next_expect_day(); assert_eq!( - *game.character_by_player_id(maple).maple_wolf_mut().unwrap(), + *game + .character_by_player_id(maple) + .maple_wolf_mut() + .unwrap() + .last_kill_on_night, 0 ); diff --git a/werewolves-proto/src/game_test/role/militia.rs b/werewolves-proto/src/game_test/role/militia.rs index c2bd59a..1c1060f 100644 --- a/werewolves-proto/src/game_test/role/militia.rs +++ b/werewolves-proto/src/game_test/role/militia.rs @@ -54,9 +54,63 @@ fn spent_shot() { assert_eq!( game.character_by_player_id(militia) - .militia() + .militia_ref() .unwrap() - .deref() + .targeted + .clone(), + Some(game.character_by_player_id(target_wolf).character_id()) + ); + + assert_eq!( + game.character_by_player_id(target_wolf).died_to().cloned(), + Some(DiedTo::Militia { + killer: game.character_by_player_id(militia).character_id(), + night: NonZeroU8::new(1).unwrap() + }) + ); + + game.execute().title().wolf_pack_kill(); + game.mark(game.living_villager().character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); +} + +#[test] +fn being_killed_doesnt_stop_them() { + let players = gen_players(1..10); + let mut player_ids = players.iter().map(|p| p.player_id); + let militia = player_ids.next().unwrap(); + let target_wolf = player_ids.next().unwrap(); + let other_wolf = player_ids.next().unwrap(); + + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Militia, militia); + settings.add_and_assign(SetupRole::Werewolf, target_wolf); + settings.add_and_assign(SetupRole::Werewolf, other_wolf); + + settings.fill_remaining_slots_with_villagers(9); + 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(); + game.execute().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(militia).character_id()); + game.r#continue().sleep(); + + game.next().title().militia(); + game.mark(game.character_by_player_id(target_wolf).character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(militia) + .militia_ref() + .unwrap() + .targeted .clone(), Some(game.character_by_player_id(target_wolf).character_id()) ); diff --git a/werewolves-proto/src/game_test/role/protector.rs b/werewolves-proto/src/game_test/role/protector.rs index 46e8608..6e4e074 100644 --- a/werewolves-proto/src/game_test/role/protector.rs +++ b/werewolves-proto/src/game_test/role/protector.rs @@ -55,6 +55,7 @@ fn cannot_protect_same_target() { game.character_by_player_id(protector) .protector_mut() .unwrap() + .last_protected .clone(), Some(prot.character_id()) ); diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index bdfe77e..f974854 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -15,7 +15,7 @@ use core::{fmt::Display, num::NonZeroU8, ops::Not}; use serde::{Deserialize, Serialize}; -use werewolves_macros::{All, ChecksAs, Titles}; +use werewolves_macros::{All, ChecksAs, RefAndMut, Titles}; use crate::{ character::CharacterId, @@ -117,7 +117,7 @@ impl AlignmentEq { } } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles, RefAndMut)] pub enum Role { #[checks(Alignment::Village)] #[checks(Killer::NotKiller)]