Compare commits
3 Commits
b053912d9c
...
3388894504
| Author | SHA1 | Date |
|---|---|---|
|
|
3388894504 | |
|
|
c2f33e670c | |
|
|
bc7cb5b2cb |
|
|
@ -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(),
|
||||
|
|
@ -552,7 +556,7 @@ impl Character {
|
|||
&& village
|
||||
.executions_on_day(last_day)
|
||||
.iter()
|
||||
.any(|c| c.is_village())
|
||||
.any(|c| c.alignment().village())
|
||||
{
|
||||
prompts.push(ActionPrompt::Vindicator {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -934,6 +934,10 @@ fn big_game_test_based_on_story_test() {
|
|||
game.mark(protect.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().vindicator();
|
||||
game.mark_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
game.mark(protect.character_id());
|
||||
game.r#continue().r#continue();
|
||||
|
|
|
|||
|
|
@ -348,6 +348,10 @@ fn previous_prompt() {
|
|||
game.mark(protect.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().vindicator();
|
||||
game.mark_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
game.mark(protect.character_id());
|
||||
game.r#continue().r#continue();
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,4 +32,5 @@ mod protector;
|
|||
mod pyremaster;
|
||||
mod scapegoat;
|
||||
mod shapeshifter;
|
||||
mod vindicator;
|
||||
mod weightlifter;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
// Copyright (C) 2025 Emilis Bliūdžius
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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 core::num::NonZeroU8;
|
||||
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||
},
|
||||
message::night::ActionPromptTitle,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn direwolf_kill_activates() {
|
||||
init_log();
|
||||
let players = gen_players(1..21);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let vindicator = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let direwolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.add_and_assign(SetupRole::DireWolf, direwolf);
|
||||
settings.add_and_assign(SetupRole::Vindicator, vindicator);
|
||||
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_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
game.mark_for_execution(game.character_by_player_id(direwolf).character_id());
|
||||
game.execute().title().vindicator();
|
||||
let prot = game.living_villager();
|
||||
game.mark(prot.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
game.mark(prot.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(
|
||||
game.character_by_player_id(prot.player_id())
|
||||
.died_to()
|
||||
.cloned(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maplewolf_kill_does_not_activate() {
|
||||
init_log();
|
||||
let players = gen_players(1..21);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let vindicator = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let maplewolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.add_and_assign(SetupRole::MapleWolf, maplewolf);
|
||||
settings.add_and_assign(SetupRole::Vindicator, vindicator);
|
||||
settings.fill_remaining_slots_with_villagers(players.len());
|
||||
let mut game = Game::new(&players, settings).unwrap();
|
||||
game.r#continue().r#continue();
|
||||
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
game.mark_for_execution(game.character_by_player_id(maplewolf).character_id());
|
||||
|
||||
game.execute().title().wolf_pack_kill();
|
||||
let target = game.living_villager();
|
||||
game.mark(target.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(
|
||||
game.character_by_player_id(target.player_id())
|
||||
.died_to()
|
||||
.cloned(),
|
||||
Some(DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ $village_border: color.change($village_color, $alpha: 1.0);
|
|||
$wolves_border: color.change($wolves_color, $alpha: 1.0);
|
||||
$intel_color: color.adjust($village_color, $hue: -30deg);
|
||||
$intel_border: color.change($intel_color, $alpha: 1.0);
|
||||
$defensive_color: color.adjust($intel_color, $hue: -30deg);
|
||||
$defensive_color: color.adjust($village_color, $hue: -60deg);
|
||||
$defensive_border: color.change($defensive_color, $alpha: 1.0);
|
||||
$offensive_color: color.adjust($village_color, $hue: 30deg);
|
||||
$offensive_border: color.change($offensive_color, $alpha: 1.0);
|
||||
$starts_as_villager_color: color.adjust($offensive_color, $hue: 30deg);
|
||||
$starts_as_villager_color: color.adjust($village_color, $hue: 60deg);
|
||||
$starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0);
|
||||
$traitor_color: color.adjust($village_color, $hue: 45deg);
|
||||
$traitor_border: color.change($traitor_color, $alpha: 1.0);
|
||||
|
|
@ -1433,6 +1433,7 @@ input {
|
|||
}
|
||||
|
||||
& .title {
|
||||
text-shadow: black 3px 2px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
|
@ -1468,6 +1469,8 @@ input {
|
|||
}
|
||||
|
||||
.role {
|
||||
text-shadow: black 3px 2px;
|
||||
|
||||
width: 100%;
|
||||
filter: saturate(40%);
|
||||
padding-left: 10px;
|
||||
|
|
@ -2414,3 +2417,58 @@ li.choice {
|
|||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.top-of-day-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
padding: 10px;
|
||||
|
||||
gap: 10vw;
|
||||
|
||||
.info-tidbit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
label {
|
||||
font-size: 1.5em;
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
.parity {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.parity-pct {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.last-nights-kills {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 2cm;
|
||||
|
||||
.identity {
|
||||
.number {
|
||||
color: red;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
text-align: center;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.current-day {
|
||||
color: red;
|
||||
font-size: 3em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ impl Component for Host {
|
|||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
gloo::utils::document().set_title("Werewolves Host");
|
||||
gloo::utils::document().set_title(format!("{} — host", crate::TITLE).as_str());
|
||||
if let Some(clients) = gloo::utils::document()
|
||||
.query_selector("clients")
|
||||
.ok()
|
||||
|
|
@ -759,8 +759,8 @@ impl Host {
|
|||
(None, false)
|
||||
}
|
||||
},
|
||||
HostEvent::Error(err) => (None, false),
|
||||
HostEvent::SetBigScreenState(state) => (None, true),
|
||||
HostEvent::Error(_) => (None, false),
|
||||
HostEvent::SetBigScreenState(_) => (None, true),
|
||||
HostEvent::Continue => (None, false),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,43 @@ pub fn DaytimePlayerList(
|
|||
) -> Html {
|
||||
let on_select = big_screen.not().then(|| on_mark.clone());
|
||||
let mut characters = characters.clone();
|
||||
let last_nights_kills = {
|
||||
let kills = characters
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
c.died_to
|
||||
.as_ref()
|
||||
.and_then(|died_to| match died_to.date_time() {
|
||||
GameTime::Day { .. } => None,
|
||||
GameTime::Night { number } => {
|
||||
if let Some(day) = day.as_ref()
|
||||
&& number == day.get() - 1
|
||||
{
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.map(|killed| {
|
||||
let ident = killed.identity.clone().into_public();
|
||||
html! {
|
||||
<span>
|
||||
<Identity ident={ident} />
|
||||
</span>
|
||||
}
|
||||
})
|
||||
.collect::<Box<[_]>>();
|
||||
kills.is_empty().not().then_some(html! {
|
||||
<div class="info-tidbit">
|
||||
<label>{"died last night"}</label>
|
||||
<div class="last-nights-kills">
|
||||
{kills.into_iter().collect::<Html>()}
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
};
|
||||
characters.sort_by(|l, r| l.identity.number.cmp(&r.identity.number));
|
||||
let chars = characters
|
||||
.iter()
|
||||
|
|
@ -84,6 +121,29 @@ pub fn DaytimePlayerList(
|
|||
} else {
|
||||
"execute"
|
||||
};
|
||||
let parity = {
|
||||
let wolves = characters
|
||||
.iter()
|
||||
.filter(|c| c.died_to.is_none() && c.role.wolf())
|
||||
.count();
|
||||
let total = characters.iter().filter(|c| c.died_to.is_none()).count();
|
||||
let pct_parity = (((wolves as f64) * 100.0) / (total as f64)).round();
|
||||
html! {
|
||||
<div class="info-tidbit">
|
||||
<label>{"parity"}</label>
|
||||
<span class="parity">
|
||||
<span class="red">{wolves}</span>
|
||||
{"/"}
|
||||
<span class="total">{total}</span>
|
||||
</span>
|
||||
<span class="parity-pct">
|
||||
{"("}
|
||||
{pct_parity}
|
||||
{"%)"}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
let button = big_screen
|
||||
.not()
|
||||
.then_some(())
|
||||
|
|
@ -97,12 +157,19 @@ pub fn DaytimePlayerList(
|
|||
});
|
||||
let day = day.as_ref().map(|day| {
|
||||
html! {
|
||||
<h2>{"day "}{day.get()}</h2>
|
||||
<div class="info-tidbit">
|
||||
<label>{"day"}</label>
|
||||
<span class="current-day">{day.get()}</span>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div class="character-picker">
|
||||
{day}
|
||||
<div class="top-of-day-info">
|
||||
{day}
|
||||
{parity}
|
||||
{last_nights_kills}
|
||||
</div>
|
||||
<div class="player-list">
|
||||
{chars}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -131,20 +131,21 @@ pub fn SetupCategory(
|
|||
.map(|(r, count)| {
|
||||
let as_role = r.into_role();
|
||||
let wakes = as_role.wakes_night_zero().then_some("wakes");
|
||||
let count = matches!(mode, CategoryMode::ShowExactRoleCount).then(|| {
|
||||
html! {
|
||||
<span class="count">{count}</span>
|
||||
}
|
||||
});
|
||||
let count =
|
||||
(matches!(mode, CategoryMode::ShowExactRoleCount) && count > 0).then(|| {
|
||||
html! {
|
||||
<span class="count">{count}</span>
|
||||
}
|
||||
});
|
||||
let killer_inactive = as_role.killer().killer().not().then_some("inactive");
|
||||
let powerful_inactive = as_role.powerful().powerful().not().then_some("inactive");
|
||||
let alignment = as_role.alignment().icon();
|
||||
html! {
|
||||
<div class={classes!("slot")}>
|
||||
{count}
|
||||
<div class={classes!("role", wakes, r.category().class())}>
|
||||
<span class={classes!("role", wakes, r.category().class())}>
|
||||
{r.to_string().to_case(Case::Title)}
|
||||
</div>
|
||||
</span>
|
||||
<div class="attributes">
|
||||
<div class="alignment">
|
||||
<Icon source={alignment} icon_type={IconType::Small}/>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ const BUILD_ID: &str = werewolves_macros::build_id!();
|
|||
const BUILD_ID_LONG: &str = werewolves_macros::build_id_long!();
|
||||
const BUILD_DIRTY: bool = werewolves_macros::build_dirty!();
|
||||
const BUILD_TIME: &str = werewolves_macros::build_time!();
|
||||
const TITLE: &str = match option_env!("LOCAL") {
|
||||
Some(_) => "LOCAL werewolves",
|
||||
None => "werewolves",
|
||||
};
|
||||
|
||||
use crate::clients::{
|
||||
client::{Client2, ClientContext},
|
||||
|
|
@ -55,6 +59,7 @@ fn main() {
|
|||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
log::debug!("starting werewolves build {BUILD_ID}");
|
||||
let document = gloo::utils::document();
|
||||
document.set_title(crate::TITLE);
|
||||
let url = document.document_uri().expect("get uri");
|
||||
let url_obj = Url::new(&url).unwrap();
|
||||
let path = url_obj.pathname();
|
||||
|
|
|
|||
Loading…
Reference in New Issue