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)]