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)
This commit is contained in:
emilis 2025-11-19 02:22:10 +00:00
parent bc7cb5b2cb
commit c2f33e670c
No known key found for this signature in database
14 changed files with 548 additions and 354 deletions

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Self> {
// let type_path = input.parse::<syn::TypePath>()?;
// let matching = input.parse::<syn::PatStruct>()?;
let name = input.parse::<syn::Ident>()?;
// panic!("{type_path:?}\n\n{matching:?}");
Ok(Self { name })
impl RefAndMut {
pub fn parse(input: syn::DeriveInput) -> syn::Result<Self> {
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::<Box<[_]>>();
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::<Box<[_]>>();
let fields_idents = fields
.named
.iter()
.map(|c| c.ident.as_ref().expect("this is a named field??"))
.collect::<Box<[_]>>();
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::<Box<[_]>>();
let fields_mut = fields
.unnamed
.iter()
.map(|field| {
let ty = &field.ty;
quote! {
pub &'a mut #ty
}
})
.collect::<Box<[_]>>();
let fields_names = fields
.unnamed
.iter()
.enumerate()
.map(|(idx, field)| Ident::new(format!("field{idx}").as_str(), field.span()))
.collect::<Box<[_]>>();
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,
}
}
}
});
}

View File

@ -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<Box<[ActionPrompt]>> {
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<KillingWolfOrder> {
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<Hunter<'a>> {
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<HunterMut<'a>> {
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<Shapeshifter<'a>> {
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<ShapeshifterMut<'a>> {
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<MasonLeader<'a>> {
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<MasonLeaderMut<'a>> {
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<Scapegoat<'a>> {
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<ScapegoatMut<'a>> {
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<Empath<'a>> {
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<EmpathMut<'a>> {
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<BlackKnight<'a>> {
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<BlackKnightKill<'a>> {
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<BlackKnightMut<'a>> {
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<Guardian<'a>> {
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<GuardianMut<'a>> {
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<Direwolf<'a>> {
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<DirewolfMut<'a>> {
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<Militia<'a>> {
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<MilitiaMut<'a>> {
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<MapleWolfMut<'a>> {
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<ProtectorMut<'a>> {
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<CharacterId>;
Shapeshifter, ShapeshifterMut: Option<CharacterId>;
Scapegoat, ScapegoatMut: bool;
Empath, EmpathMut: bool;
BlackKnight, BlackKnightMut: Option<DiedTo>;
Guardian, GuardianMut: Option<PreviousGuardianAction>;
Direwolf, DirewolfMut: Option<CharacterId>;
Militia, MilitiaMut: Option<CharacterId>;
MapleWolf, MapleWolfMut: u8;
Protector, ProtectorMut: Option<CharacterId>;
);
pub struct BlackKnightKill<'a> {
attacked: &'a Option<DiedTo>,
died_to: &'a mut Option<DiedTo>,
}
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);
}
}

View File

@ -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}");

View File

@ -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,

View File

@ -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));
};

View File

@ -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

View File

@ -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
);

View File

@ -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())
}
);
}

View File

@ -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()
]))
);
}

View File

@ -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
);

View File

@ -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())
);

View File

@ -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())
);

View File

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