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:
parent
bc7cb5b2cb
commit
c2f33e670c
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue