From a8506c5881b88088549f67cb11bb41e4a8c68302 Mon Sep 17 00:00:00 2001 From: emilis Date: Mon, 6 Oct 2025 20:45:15 +0100 Subject: [PATCH] more roles, no longer exposing role through char --- werewolves-macros/src/lib.rs | 9 + werewolves-macros/src/ref_and_mut.rs | 22 + werewolves-proto/src/character.rs | 674 ++++++++++++++++++ werewolves-proto/src/diedto.rs | 47 +- werewolves-proto/src/game/kill.rs | 33 +- werewolves-proto/src/game/mod.rs | 4 +- werewolves-proto/src/game/night.rs | 538 ++++++++++---- werewolves-proto/src/game/settings.rs | 6 +- .../src/game/settings/settings_role.rs | 95 ++- werewolves-proto/src/game/village.rs | 77 +- werewolves-proto/src/game_test/mod.rs | 93 ++- werewolves-proto/src/game_test/night_order.rs | 2 +- werewolves-proto/src/game_test/role/elder.rs | 37 +- werewolves-proto/src/game_test/role/mason.rs | 69 ++ werewolves-proto/src/game_test/role/mod.rs | 1 + .../src/game_test/role/scapegoat.rs | 50 +- .../src/game_test/role/shapeshifter.rs | 7 +- werewolves-proto/src/lib.rs | 1 + werewolves-proto/src/message.rs | 2 +- werewolves-proto/src/message/host.rs | 3 +- werewolves-proto/src/message/ident.rs | 6 +- werewolves-proto/src/message/night.rs | 160 ++++- werewolves-proto/src/player.rs | 390 +--------- werewolves-proto/src/role.rs | 79 +- werewolves-server/src/game.rs | 9 +- werewolves/src/clients/host/host.rs | 3 +- werewolves/src/components/action/picker.rs | 2 +- werewolves/src/components/action/prompt.rs | 135 +++- werewolves/src/components/action/result.rs | 78 +- werewolves/src/components/action/target.rs | 632 ++++++++-------- werewolves/src/components/host/daytime.rs | 2 +- werewolves/src/components/host/setup.rs | 7 +- werewolves/src/components/settings.rs | 53 ++ 33 files changed, 2289 insertions(+), 1037 deletions(-) create mode 100644 werewolves-macros/src/ref_and_mut.rs create mode 100644 werewolves-proto/src/character.rs create mode 100644 werewolves-proto/src/game_test/role/mason.rs diff --git a/werewolves-macros/src/lib.rs b/werewolves-macros/src/lib.rs index 174562a..8b5d010 100644 --- a/werewolves-macros/src/lib.rs +++ b/werewolves-macros/src/lib.rs @@ -9,9 +9,12 @@ use proc_macro2::Span; use quote::{ToTokens, quote}; use syn::{parse::Parse, parse_macro_input}; +use crate::ref_and_mut::RefAndMut; + mod all; mod checks; pub(crate) mod hashlist; +mod ref_and_mut; mod targets; struct IncludePath { @@ -551,3 +554,9 @@ pub fn all(input: proc_macro::TokenStream) -> proc_macro::TokenStream { quote! {#all}.into() } + +#[proc_macro] +pub fn ref_and_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ref_and_mut = parse_macro_input!(input as RefAndMut); + quote! {#ref_and_mut}.into() +} diff --git a/werewolves-macros/src/ref_and_mut.rs b/werewolves-macros/src/ref_and_mut.rs new file mode 100644 index 0000000..08b766f --- /dev/null +++ b/werewolves-macros/src/ref_and_mut.rs @@ -0,0 +1,22 @@ +use quote::{ToTokens, quote}; +use syn::{parse::Parse, spanned::Spanned}; + +pub struct RefAndMut { + name: syn::Ident, +} + +impl Parse for RefAndMut { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + // let type_path = input.parse::()?; + // let matching = input.parse::()?; + let name = input.parse::()?; + // panic!("{type_path:?}\n\n{matching:?}"); + Ok(Self { name }) + } +} + +impl ToTokens for RefAndMut { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(quote! {}); + } +} diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs new file mode 100644 index 0000000..a42603f --- /dev/null +++ b/werewolves-proto/src/character.rs @@ -0,0 +1,674 @@ +use core::{fmt::Display, num::NonZeroU8, ops::Not}; + +use rand::seq::SliceRandom; +use serde::{Deserialize, Serialize}; + +use crate::{ + diedto::DiedTo, + error::GameError, + game::{DateTime, Village}, + message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, + modifier::Modifier, + player::{PlayerId, RoleChange}, + role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, +}; + +type Result = core::result::Result; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct CharacterId(uuid::Uuid); + +impl CharacterId { + pub fn new() -> Self { + Self(uuid::Uuid::new_v4()) + } + pub const fn from_u128(v: u128) -> Self { + Self(uuid::Uuid::from_u128(v)) + } +} + +impl Display for CharacterId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Character { + player_id: PlayerId, + identity: CharacterIdentity, + role: Role, + modifier: Option, + died_to: Option, + role_changes: Vec, +} + +impl Character { + pub fn new( + Identification { + player_id, + public: + PublicIdentity { + name, + pronouns, + number, + }, + }: Identification, + role: Role, + ) -> Option { + Some(Self { + role, + identity: CharacterIdentity { + character_id: CharacterId::new(), + name, + pronouns, + number: number?, + }, + player_id, + modifier: None, + died_to: None, + role_changes: Vec::new(), + }) + } + + pub const fn is_power_role(&self) -> bool { + match &self.role { + Role::Scapegoat { .. } | Role::Villager => false, + + _ => true, + } + } + + pub fn identity(&self) -> CharacterIdentity { + self.identity.clone() + } + + pub fn name(&self) -> &str { + self.identity.name.as_str() + } + + pub const fn number(&self) -> NonZeroU8 { + self.identity.number + } + + pub const fn pronouns(&self) -> Option<&str> { + match self.identity.pronouns.as_ref() { + Some(p) => Some(p.as_str()), + None => None, + } + } + + pub fn died_to(&self) -> Option<&DiedTo> { + self.died_to.as_ref() + } + + pub fn kill(&mut self, died_to: DiedTo) { + match (&mut self.role, died_to.date_time()) { + ( + Role::Elder { + lost_protection_night: Some(_), + .. + }, + _, + ) => {} + ( + Role::Elder { + lost_protection_night, + .. + }, + DateTime::Night { number: night }, + ) => { + *lost_protection_night = lost_protection_night + .is_none() + .then_some(night) + .and_then(NonZeroU8::new); + return; + } + _ => {} + } + match &self.died_to { + Some(_) => {} + None => self.died_to = Some(died_to), + } + } + + pub const fn alive(&self) -> bool { + self.died_to.is_none() + } + + pub fn execute(&mut self, day: NonZeroU8) -> Result<()> { + if self.died_to.is_some() { + return Err(GameError::CharacterAlreadyDead); + } + self.died_to = Some(DiedTo::Execution { day }); + Ok(()) + } + + pub const fn character_id(&self) -> CharacterId { + self.identity.character_id + } + + pub const fn player_id(&self) -> PlayerId { + self.player_id + } + + pub const fn role_title(&self) -> RoleTitle { + self.role.title() + } + + pub const fn gravedigger_dig(&self) -> Option { + match &self.role { + Role::Shapeshifter { + shifted_into: Some(_), + } => None, + _ => Some(self.role.title()), + } + } + + pub const fn alignment(&self) -> Alignment { + if let Role::Empath { cursed: true } = &self.role { + return Alignment::Wolves; + } + self.role.alignment() + } + + pub fn elder_reveal(&mut self) { + if let Role::Elder { + woken_for_reveal, .. + } = &mut self.role + { + *woken_for_reveal = true + } + } + + pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<()> { + let mut role = new_role.title_to_role_excl_apprentice(); + core::mem::swap(&mut role, &mut self.role); + self.role_changes.push(RoleChange { + role, + new_role, + changed_on_night: match at { + DateTime::Day { number: _ } => return Err(GameError::NotNight), + DateTime::Night { number } => number, + }, + }); + + Ok(()) + } + + pub const fn is_wolf(&self) -> bool { + self.role.wolf() + } + + pub const fn is_village(&self) -> bool { + !self.is_wolf() + } + + pub const fn known_elder(&self) -> bool { + matches!( + self.role, + Role::Elder { + woken_for_reveal: true, + .. + } + ) + } + + pub fn night_action_prompts(&self, village: &Village) -> Result> { + if !self.alive() || !self.role.wakes(village) { + return Ok(Box::new([])); + } + let night = match village.date_time() { + DateTime::Day { number: _ } => return Err(GameError::NotNight), + DateTime::Night { number } => number, + }; + Ok(Box::new([match &self.role { + Role::Empath { cursed: true } + | Role::Diseased + | Role::Weightlifter + | Role::BlackKnight { .. } + | Role::Shapeshifter { + shifted_into: Some(_), + } + | Role::AlphaWolf { killed: Some(_) } + | Role::Militia { targeted: Some(_) } + | Role::Scapegoat { redeemed: false } + | Role::Elder { + woken_for_reveal: true, + .. + } + | Role::Villager => return Ok(Box::new([])), + Role::Scapegoat { redeemed: true } => { + let mut dead = village.dead_characters(); + 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())) + { + ActionPrompt::RoleChange { + character_id: self.identity(), + new_role: pr, + } + } else { + return Ok(Box::new([])); + } + } + Role::Seer => ActionPrompt::Seer { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::Arcanist => ActionPrompt::Arcanist { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: (None, None), + }, + Role::Protector { + last_protected: Some(last_protected), + } => ActionPrompt::Protector { + character_id: self.identity(), + targets: village.living_players_excluding(*last_protected), + marked: None, + }, + Role::Protector { + last_protected: None, + } => ActionPrompt::Protector { + character_id: self.identity(), + targets: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::Apprentice(role) => { + let current_night = match village.date_time() { + DateTime::Day { number: _ } => return Ok(Box::new([])), + DateTime::Night { number } => number, + }; + return Ok(village + .characters() + .into_iter() + .filter(|c| c.role_title() == *role) + .filter_map(|char| char.died_to) + .any(|died_to| match died_to.date_time() { + DateTime::Day { number } => number.get() + 1 >= current_night, + DateTime::Night { number } => number + 1 >= current_night, + }) + .then(|| ActionPrompt::RoleChange { + character_id: self.identity(), + new_role: *role, + }) + .into_iter() + .collect()); + } + Role::Elder { + knows_on_night, + woken_for_reveal: false, + .. + } => { + let current_night = match village.date_time() { + DateTime::Day { number: _ } => return Ok(Box::new([])), + DateTime::Night { number } => number, + }; + return Ok((current_night >= knows_on_night.get()) + .then_some({ + ActionPrompt::ElderReveal { + character_id: self.identity(), + } + }) + .into_iter() + .collect()); + } + Role::Militia { targeted: None } => ActionPrompt::Militia { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::Werewolf => ActionPrompt::WolfPackKill { + living_villagers: village.living_players(), + marked: None, + }, + Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf { + character_id: self.identity(), + living_villagers: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::DireWolf => ActionPrompt::DireWolf { + character_id: self.identity(), + living_players: village.living_players(), + marked: None, + }, + Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter { + character_id: self.identity(), + }, + Role::Gravedigger => { + let dead = village.dead_targets(); + if dead.is_empty() { + return Ok(Box::new([])); + } + ActionPrompt::Gravedigger { + character_id: self.identity(), + dead_players: village.dead_targets(), + marked: None, + } + } + Role::Hunter { target } => ActionPrompt::Hunter { + character_id: self.identity(), + current_target: target.as_ref().and_then(|t| village.target_by_id(*t).ok()), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf { + character_id: self.identity(), + kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night, + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::Guardian { + last_protected: Some(PreviousGuardianAction::Guard(prev_target)), + } => ActionPrompt::Guardian { + character_id: self.identity(), + previous: Some(PreviousGuardianAction::Guard(prev_target.clone())), + living_players: village.living_players_excluding(prev_target.character_id), + marked: None, + }, + Role::Guardian { + last_protected: Some(PreviousGuardianAction::Protect(prev_target)), + } => ActionPrompt::Guardian { + character_id: self.identity(), + previous: Some(PreviousGuardianAction::Protect(prev_target.clone())), + living_players: village.living_players(), + marked: None, + }, + Role::Guardian { + last_protected: None, + } => ActionPrompt::Guardian { + character_id: self.identity(), + previous: None, + living_players: village.living_players(), + marked: None, + }, + Role::Adjudicator => ActionPrompt::Adjudicator { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::PowerSeer => ActionPrompt::PowerSeer { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::Mortician => ActionPrompt::Mortician { + character_id: self.identity(), + dead_players: village.dead_targets(), + marked: None, + }, + Role::Beholder => ActionPrompt::Beholder { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::MasonLeader { + recruits_available, + recruits, + } => { + return Ok(recruits + .is_empty() + .not() + .then_some(ActionPrompt::MasonsWake { + character_id: self.identity(), + masons: recruits + .iter() + .map(|r| village.character_by_id(*r).map(|c| c.identity())) + .collect::>>()?, + }) + .into_iter() + .chain( + NonZeroU8::new(*recruits_available).map(|recruits_available| { + ActionPrompt::MasonLeaderRecruit { + character_id: self.identity(), + recruits_left: recruits_available, + potential_recruits: village + .living_players_excluding(self.character_id()) + .into_iter() + .filter(|c| !recruits.contains(&c.character_id)) + .collect(), + marked: None, + } + }), + ) + .collect()); + } + Role::Empath { cursed: false } => ActionPrompt::Empath { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + Role::Vindicator => { + let last_day = match village.date_time() { + DateTime::Day { .. } => { + log::error!( + "vindicator trying to get a prompt during the day? village state: {village:?}" + ); + return Ok(Box::new([])); + } + DateTime::Night { number } => { + if number == 0 { + return Ok(Box::new([])); + } + NonZeroU8::new(number).unwrap() + } + }; + return Ok(village + .executions_on_day(last_day) + .iter() + .any(|c| c.is_village()) + .then(|| ActionPrompt::Vindicator { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }) + .into_iter() + .collect()); + } + Role::PyreMaster { .. } => ActionPrompt::PyreMaster { + character_id: self.identity(), + living_players: village.living_players_excluding(self.character_id()), + marked: None, + }, + }])) + } + + #[cfg(test)] + pub const fn role(&self) -> &Role { + &self.role + } + + pub const fn killer(&self) -> bool { + if let Role::Empath { cursed: true } = &self.role { + return true; + } + self.role.killer() + } + + pub const fn powerful(&self) -> bool { + if let Role::Empath { cursed: true } = &self.role { + return true; + } + self.role.powerful() + } + + pub const fn hunter<'a>(&'a self) -> Result> { + match &self.role { + Role::Hunter { target } => Ok(Hunter(target)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Hunter, + got: self.role_title(), + }), + } + } + + pub const fn hunter_mut<'a>(&'a mut self) -> Result> { + let title = self.role.title(); + match &mut self.role { + Role::Hunter { target } => Ok(HunterMut(target)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Hunter, + got: title, + }), + } + } + + pub const fn shapeshifter<'a>(&'a self) -> Result> { + match &self.role { + Role::Shapeshifter { shifted_into } => Ok(Shapeshifter(shifted_into)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Shapeshifter, + got: self.role_title(), + }), + } + } + + pub const fn shapeshifter_mut<'a>(&'a mut self) -> Result> { + let title = self.role.title(); + match &mut self.role { + Role::Shapeshifter { shifted_into } => Ok(ShapeshifterMut(shifted_into)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Shapeshifter, + got: title, + }), + } + } + + pub const fn mason_leader<'a>(&'a self) -> Result> { + match &self.role { + Role::MasonLeader { + recruits_available, + recruits, + } => Ok(MasonLeader(recruits_available, recruits)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::MasonLeader, + got: self.role_title(), + }), + } + } + + pub const fn mason_leader_mut<'a>(&'a mut self) -> Result> { + let title = self.role.title(); + match &mut self.role { + Role::MasonLeader { + recruits_available, + recruits, + } => Ok(MasonLeaderMut(recruits_available, recruits)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::MasonLeader, + got: title, + }), + } + } + + pub const fn scapegoat<'a>(&'a self) -> Result> { + match &self.role { + Role::Scapegoat { redeemed } => Ok(Scapegoat(redeemed)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Scapegoat, + got: self.role_title(), + }), + } + } + + pub const fn scapegoat_mut<'a>(&'a mut self) -> Result> { + let title = self.role.title(); + match &mut self.role { + Role::Scapegoat { redeemed } => Ok(ScapegoatMut(redeemed)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Scapegoat, + got: title, + }), + } + } + + pub const fn empath<'a>(&'a self) -> Result> { + match &self.role { + Role::Empath { cursed } => Ok(Empath(cursed)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Empath, + got: self.role_title(), + }), + } + } + + pub const fn empath_mut<'a>(&'a mut self) -> Result> { + let title = self.role.title(); + match &mut self.role { + Role::Empath { cursed } => Ok(EmpathMut(cursed)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Empath, + got: title, + }), + } + } + + pub const fn 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; + + fn deref(&self) -> &Self::Target { + self.0 + } + } + pub struct $name_mut<'a>(&'a mut $contains); + impl core::ops::Deref for $name_mut<'_> { + type Target = $contains; + + fn deref(&self) -> &Self::Target { + self.0 + } + } + impl core::ops::DerefMut for $name_mut<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0 + } + } + )* + }; +} + +decl_ref_and_mut!( + Hunter, HunterMut: Option; + Shapeshifter, ShapeshifterMut: Option; + Scapegoat, ScapegoatMut: bool; + Empath, EmpathMut: bool; +); + +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 fn recruit(self, target: CharacterId) { + let mut recruits = self.1.to_vec(); + recruits.push(target); + *self.1 = recruits.into_boxed_slice(); + if let Some(new) = self.0.checked_sub(1) { + *self.0 = new; + } + } +} diff --git a/werewolves-proto/src/diedto.rs b/werewolves-proto/src/diedto.rs index 3f96e21..2779880 100644 --- a/werewolves-proto/src/diedto.rs +++ b/werewolves-proto/src/diedto.rs @@ -1,16 +1,15 @@ use core::{fmt::Debug, num::NonZeroU8}; use serde::{Deserialize, Serialize}; -use werewolves_macros::Extract; +use werewolves_macros::Titles; -use crate::{game::DateTime, player::CharacterId}; +use crate::{character::CharacterId, game::DateTime}; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Extract)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)] pub enum DiedTo { Execution { day: NonZeroU8, }, - #[extract(source as killer)] MapleWolf { source: CharacterId, night: NonZeroU8, @@ -19,17 +18,14 @@ pub enum DiedTo { MapleWolfStarved { night: NonZeroU8, }, - #[extract(killer as killer)] Militia { killer: CharacterId, night: NonZeroU8, }, - #[extract(killing_wolf as killer)] Wolfpack { killing_wolf: CharacterId, night: NonZeroU8, }, - #[extract(killer as killer)] AlphaWolf { killer: CharacterId, night: NonZeroU8, @@ -38,12 +34,10 @@ pub enum DiedTo { into: CharacterId, night: NonZeroU8, }, - #[extract(killer as killer)] Hunter { killer: CharacterId, night: NonZeroU8, }, - #[extract(source as killer)] GuardianProtecting { source: CharacterId, protecting: CharacterId, @@ -51,13 +45,44 @@ pub enum DiedTo { protecting_from_cause: Box, night: NonZeroU8, }, + PyreMaster { + killer: CharacterId, + night: NonZeroU8, + }, + MasonLeaderRecruitFail { + tried_recruiting: CharacterId, + night: u8, + }, } impl DiedTo { + pub const fn killer(&self) -> Option { + match self { + DiedTo::Execution { .. } + | DiedTo::MapleWolfStarved { .. } + | DiedTo::Shapeshift { .. } => None, + DiedTo::MapleWolf { source: killer, .. } + | DiedTo::Militia { killer, .. } + | DiedTo::Wolfpack { + killing_wolf: killer, + .. + } + | DiedTo::AlphaWolf { killer, .. } + | DiedTo::Hunter { killer, .. } + | DiedTo::GuardianProtecting { + protecting_from: killer, + .. + } + | DiedTo::MasonLeaderRecruitFail { + tried_recruiting: killer, + .. + } + | DiedTo::PyreMaster { killer, .. } => Some(*killer), + } + } pub const fn date_time(&self) -> DateTime { match self { DiedTo::Execution { day } => DateTime::Day { number: *day }, - DiedTo::GuardianProtecting { source: _, protecting: _, @@ -78,9 +103,11 @@ impl DiedTo { } | DiedTo::AlphaWolf { killer: _, night } | DiedTo::Shapeshift { into: _, night } + | DiedTo::PyreMaster { night, .. } | DiedTo::Hunter { killer: _, night } => DateTime::Night { number: night.get(), }, + DiedTo::MasonLeaderRecruitFail { night, .. } => DateTime::Night { number: *night }, } } } diff --git a/werewolves-proto/src/game/kill.rs b/werewolves-proto/src/game/kill.rs index 1e83b23..538d1c9 100644 --- a/werewolves-proto/src/game/kill.rs +++ b/werewolves-proto/src/game/kill.rs @@ -2,10 +2,11 @@ use core::{num::NonZeroU8, ops::Not}; use super::Result; use crate::{ + character::CharacterId, diedto::DiedTo, error::GameError, game::{Village, night::NightChange}, - player::{CharacterId, Protection}, + player::Protection, }; #[derive(Debug, PartialEq)] @@ -24,10 +25,7 @@ impl KillOutcome { pub fn apply_to_village(self, village: &mut Village) -> Result<()> { match self { KillOutcome::Single(character_id, died_to) => { - village - .character_by_id_mut(character_id) - .ok_or(GameError::InvalidTarget)? - .kill(died_to); + village.character_by_id_mut(character_id)?.kill(died_to); Ok(()) } KillOutcome::Guarding { @@ -39,12 +37,9 @@ impl KillOutcome { } => { // check if guardian exists before we mutably borrow killer, which would // prevent us from borrowing village to check after. + village.character_by_id(guardian)?; village - .character_by_id(guardian) - .ok_or(GameError::InvalidTarget)?; - village - .character_by_id_mut(original_killer) - .ok_or(GameError::InvalidTarget)? + .character_by_id_mut(original_killer)? .kill(DiedTo::GuardianProtecting { night, source: guardian, @@ -52,10 +47,7 @@ impl KillOutcome { protecting_from: original_killer, protecting_from_cause: Box::new(original_kill.clone()), }); - village - .character_by_id_mut(guardian) - .ok_or(GameError::InvalidTarget)? - .kill(original_kill); + village.character_by_id_mut(guardian)?.kill(original_kill); Ok(()) } } @@ -84,6 +76,7 @@ fn resolve_protection( source: _, guarding: false, } + | Protection::Vindicator { .. } | Protection::Protector { source: _ } => None, } } @@ -114,9 +107,7 @@ pub fn resolve_kill( } = died_to && let Some(ss_source) = changes.shapeshifter() { - let killing_wolf = village - .character_by_id(*killing_wolf) - .ok_or(GameError::InvalidTarget)?; + let killing_wolf = village.character_by_id(*killing_wolf)?; match changes.protected_take(target) { Some(protection) => { @@ -151,7 +142,7 @@ pub fn resolve_kill( source, guarding: true, } => Ok(Some(KillOutcome::Guarding { - original_killer: *died_to + original_killer: died_to .killer() .ok_or(GameError::GuardianInvalidOriginalKill)?, original_target: *target, @@ -160,10 +151,10 @@ pub fn resolve_kill( night: NonZeroU8::new(night).unwrap(), })), Protection::Guardian { - source: _, - guarding: false, + guarding: false, .. } - | Protection::Protector { source: _ } => Ok(None), + | Protection::Vindicator { .. } + | Protection::Protector { .. } => Ok(None), } } diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index 3e953b1..18439dd 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -13,13 +13,13 @@ use rand::{Rng, seq::SliceRandom}; use serde::{Deserialize, Serialize}; use crate::{ + character::CharacterId, error::GameError, game::night::{Night, ServerAction}, message::{ CharacterState, Identification, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, }, - player::CharacterId, }; pub use { @@ -98,7 +98,7 @@ impl Game { .map(|c| CharacterState { player_id: c.player_id(), identity: c.identity(), - role: c.role().title(), + role: c.role_title(), died_to: c.died_to().cloned(), }) .collect(), diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 472de50..352ffd3 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -6,6 +6,7 @@ use werewolves_macros::Extract; use super::Result; use crate::{ + character::{Character, CharacterId}, diedto::DiedTo, error::GameError, game::{ @@ -13,8 +14,8 @@ use crate::{ kill::{self, ChangesLookup}, }, message::night::{ActionPrompt, ActionResponse, ActionResult}, - player::{Character, CharacterId, Protection}, - role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle}, + player::Protection, + role::{PreviousGuardianAction, RoleBlock, RoleTitle}, }; #[derive(Debug, Clone, Serialize, Deserialize, Extract)] @@ -43,6 +44,14 @@ pub enum NightChange { ElderReveal { elder: CharacterId, }, + MasonRecruit { + mason_leader: CharacterId, + recruiting: CharacterId, + }, + EmpathFoundScapegoat { + empath: CharacterId, + scapegoat: CharacterId, + }, } enum BlockResolvedOutcome { @@ -58,7 +67,134 @@ enum ResponseOutcome { struct ActionComplete { pub result: ActionResult, pub change: Option, - pub unless: Option, +} + +impl From for ResponseOutcome { + fn from(value: ActionComplete) -> Self { + ResponseOutcome::ActionComplete(value) + } +} + +impl ActionPrompt { + fn unless(&self) -> Option { + match &self { + ActionPrompt::MasonsWake { .. } + | ActionPrompt::WolvesIntro { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::ElderReveal { .. } + | ActionPrompt::CoverOfDarkness => None, + + ActionPrompt::Arcanist { + marked: (Some(marked1), Some(marked2)), + .. + } => Some(Unless::TargetsBlocked(*marked1, *marked2)), + + ActionPrompt::Seer { + marked: Some(marked), + .. + } + | ActionPrompt::Protector { + marked: Some(marked), + .. + } + | ActionPrompt::Gravedigger { + marked: Some(marked), + .. + } + | ActionPrompt::Hunter { + marked: Some(marked), + .. + } + | ActionPrompt::Militia { + marked: Some(marked), + .. + } + | ActionPrompt::MapleWolf { + marked: Some(marked), + .. + } + | ActionPrompt::Guardian { + marked: Some(marked), + .. + } + | ActionPrompt::Adjudicator { + marked: Some(marked), + .. + } + | ActionPrompt::PowerSeer { + marked: Some(marked), + .. + } + | ActionPrompt::Mortician { + marked: Some(marked), + .. + } + | ActionPrompt::Beholder { + marked: Some(marked), + .. + } + | ActionPrompt::MasonLeaderRecruit { + marked: Some(marked), + .. + } + | ActionPrompt::Empath { + marked: Some(marked), + .. + } + | ActionPrompt::Vindicator { + marked: Some(marked), + .. + } + | ActionPrompt::PyreMaster { + marked: Some(marked), + .. + } + | ActionPrompt::WolfPackKill { + marked: Some(marked), + .. + } + | ActionPrompt::AlphaWolf { + marked: Some(marked), + .. + } + | ActionPrompt::DireWolf { + marked: Some(marked), + .. + } => Some(Unless::TargetBlocked(*marked)), + + ActionPrompt::Seer { marked: None, .. } + | ActionPrompt::Protector { marked: None, .. } + | ActionPrompt::Gravedigger { marked: None, .. } + | ActionPrompt::Hunter { marked: None, .. } + | ActionPrompt::Militia { marked: None, .. } + | ActionPrompt::MapleWolf { marked: None, .. } + | ActionPrompt::Guardian { marked: None, .. } + | ActionPrompt::Adjudicator { marked: None, .. } + | ActionPrompt::PowerSeer { marked: None, .. } + | ActionPrompt::Mortician { marked: None, .. } + | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::MasonLeaderRecruit { marked: None, .. } + | ActionPrompt::Empath { marked: None, .. } + | ActionPrompt::Vindicator { marked: None, .. } + | ActionPrompt::PyreMaster { marked: None, .. } + | ActionPrompt::WolfPackKill { marked: None, .. } + | ActionPrompt::AlphaWolf { marked: None, .. } + | ActionPrompt::DireWolf { marked: None, .. } + | ActionPrompt::Arcanist { + marked: (Some(_), None), + .. + } + | ActionPrompt::Arcanist { + marked: (None, Some(_)), + .. + } + | ActionPrompt::Arcanist { + marked: (None, None), + .. + } => None, + } + } } impl Default for ActionComplete { @@ -66,7 +202,6 @@ impl Default for ActionComplete { Self { result: ActionResult::GoBackToSleep, change: None, - unless: None, } } } @@ -99,7 +234,7 @@ pub struct Night { village: Village, night: u8, action_queue: VecDeque, - used_actions: Vec, + used_actions: Vec<(ActionPrompt, ActionResult)>, changes: Vec, night_state: NightState, } @@ -122,7 +257,7 @@ impl Night { .characters() .into_iter() .filter(filter) - .map(|c| c.night_action_prompt(&village)) + .map(|c| c.night_action_prompts(&village)) .collect::>>()? .into_iter() .flatten() @@ -143,7 +278,7 @@ impl Night { wolves: village .living_wolf_pack_players() .into_iter() - .map(|w| (w.identity(), w.role().title())) + .map(|w| (w.identity(), w.role_title())) .collect(), }); } @@ -177,10 +312,7 @@ impl Night { .dead_characters() .into_iter() .filter_map(|c| c.died_to().map(|d| (c, d))) - .filter_map(|(c, d)| match c.role() { - Role::Hunter { target } => (*target).map(|t| (c, t, d)), - _ => None, - }) + .filter_map(|(c, d)| c.hunter().ok().and_then(|h| *h).map(|t| (c, t, d))) .filter_map(|(c, t, d)| match d.date_time() { DateTime::Day { number } => (number.get() == night.get()).then_some((c, t)), DateTime::Night { number: _ } => None, @@ -199,7 +331,8 @@ impl Night { } pub fn previous_state(&mut self) -> Result<()> { - let prev_act = self.used_actions.pop().ok_or(GameError::NoPreviousState)?; + return Err(GameError::NoPreviousState); + let (prev_act, prev_result) = self.used_actions.pop().ok_or(GameError::NoPreviousState)?; log::info!("loading previous prompt: {prev_act:?}"); match &self.night_state { NightState::Active { @@ -246,20 +379,15 @@ impl Night { let mut changes = ChangesLookup::new(&self.changes); for change in self.changes.iter() { match change { - NightChange::ElderReveal { elder } => new_village - .character_by_id_mut(*elder) - .ok_or(GameError::InvalidTarget)? - .elder_reveal(), + NightChange::ElderReveal { elder } => { + new_village.character_by_id_mut(*elder)?.elder_reveal() + } NightChange::RoleChange(character_id, role_title) => new_village - .character_by_id_mut(*character_id) - .ok_or(GameError::InvalidTarget)? + .character_by_id_mut(*character_id)? .role_change(*role_title, DateTime::Night { number: self.night })?, NightChange::HunterTarget { source, target } => { - if let Role::Hunter { target: t } = - new_village.character_by_id_mut(*source).unwrap().role_mut() - { - t.replace(*target); - } + let hunter_character = new_village.character_by_id_mut(*source).unwrap(); + hunter_character.hunter_mut()?.replace(*target); if changes.killed(source).is_some() && changes.protected(source).is_none() && changes.protected(target).is_none() @@ -289,12 +417,7 @@ impl Night { && changes.protected(target).is_none() { let ss = new_village.character_by_id_mut(*source).unwrap(); - match ss.role_mut() { - Role::Shapeshifter { shifted_into } => { - *shifted_into = Some(*target) - } - _ => unreachable!(), - } + ss.shapeshifter_mut().unwrap().replace(*target); ss.kill(DiedTo::Shapeshift { into: *target, night: NonZeroU8::new(self.night).unwrap(), @@ -311,6 +434,30 @@ impl Night { target: _, protection: _, } => {} + NightChange::MasonRecruit { + mason_leader, + recruiting, + } => { + if new_village.character_by_id(*recruiting)?.is_wolf() { + new_village.character_by_id_mut(*mason_leader)?.kill( + DiedTo::MasonLeaderRecruitFail { + tried_recruiting: *recruiting, + night: self.night, + }, + ); + } else { + new_village + .character_by_id_mut(*mason_leader)? + .mason_leader_mut()? + .recruit(*recruiting); + } + } + NightChange::EmpathFoundScapegoat { empath, scapegoat } => { + new_village + .character_by_id_mut(*scapegoat)? + .role_change(RoleTitle::Villager, DateTime::Night { number: self.night })?; + *new_village.character_by_id_mut(*empath)?.empath_mut()? = true; + } } } if new_village.is_game_over().is_none() { @@ -319,6 +466,34 @@ impl Night { Ok(new_village) } + fn apply_mason_recruit( + &mut self, + mason_leader: CharacterId, + recruiting: CharacterId, + ) -> Result { + if self.village.character_by_id(recruiting)?.is_village() { + if let Some(masons) = self.action_queue.iter_mut().find_map(|a| match a { + ActionPrompt::MasonsWake { + character_id, + masons, + } => (character_id.character_id == mason_leader).then_some(masons), + _ => None, + }) { + let mut ext_masons = masons.to_vec(); + ext_masons.push(self.village.character_by_id(recruiting)?.identity()); + *masons = ext_masons.into_boxed_slice(); + } else { + self.action_queue.push_front(ActionPrompt::MasonsWake { + character_id: self.village.character_by_id(mason_leader)?.identity(), + masons: Box::new([self.village.character_by_id(recruiting)?.identity()]), + }); + } + Ok(ActionResult::Continue) + } else { + Ok(ActionResult::GoBackToSleep) + } + } + fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> { if let Some(kill_target) = self.changes.iter().find_map(|c| match c { NightChange::Kill { @@ -362,16 +537,11 @@ impl Night { }, }); } - self.changes.push(NightChange::Shapeshift { - source: *source, - }); + self.changes + .push(NightChange::Shapeshift { source: *source }); self.action_queue.push_front(ActionPrompt::RoleChange { new_role: RoleTitle::Werewolf, - character_id: self - .village - .character_by_id(kill_target) - .ok_or(GameError::NoMatchingCharacterFound)? - .identity(), + character_id: self.village.character_by_id(kill_target)?.identity(), }); } // Remove any further shapeshift prompts from the queue @@ -399,7 +569,7 @@ impl Night { } NightState::Complete => Err(GameError::NightOver), }, - BlockResolvedOutcome::ActionComplete(result, Some(change)) => { + BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => { match &mut self.night_state { NightState::Active { current_prompt: _, @@ -419,6 +589,13 @@ impl Night { .unwrap_or(ActionResult::GoBackToSleep), )); } + if let NightChange::MasonRecruit { + mason_leader, + recruiting, + } = &change + { + result = self.apply_mason_recruit(*mason_leader, *recruiting)?; + } self.changes.push(change); Ok(ServerAction::Result(result)) } @@ -461,7 +638,6 @@ impl Night { return Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, change: None, - unless: None, })); } @@ -475,27 +651,23 @@ impl Night { ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source }), - unless, }), true, _, ) => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, change: Some(NightChange::Shapeshift { source }), - unless, })), ( ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change, - unless, }), true, true, ) => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, change, - unless, })), (outcome, _, _) => Ok(outcome), } @@ -507,54 +679,45 @@ impl Night { ) -> Result { match self.received_response_consecutive_wolves_dont_sleep(resp)? { ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)), - ResponseOutcome::ActionComplete(ActionComplete { - result, - change, - unless: Some(Unless::TargetBlocked(unless_blocked)), - }) => { - if self.changes.iter().any(|c| match c { - NightChange::RoleBlock { - source: _, - target, - block_type: _, - } => target == &unless_blocked, - _ => false, - }) { - Ok(BlockResolvedOutcome::ActionComplete( - ActionResult::RoleBlocked, - None, - )) - } else { - Ok(BlockResolvedOutcome::ActionComplete(result, change)) + ResponseOutcome::ActionComplete(ActionComplete { result, change }) => { + match self.current_prompt().ok_or(GameError::NightOver)?.unless() { + Some(Unless::TargetBlocked(unless_blocked)) => { + if self.changes.iter().any(|c| match c { + NightChange::RoleBlock { + source: _, + target, + block_type: _, + } => target == &unless_blocked, + _ => false, + }) { + Ok(BlockResolvedOutcome::ActionComplete( + ActionResult::RoleBlocked, + None, + )) + } else { + Ok(BlockResolvedOutcome::ActionComplete(result, change)) + } + } + Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => { + if self.changes.iter().any(|c| match c { + NightChange::RoleBlock { + source: _, + target, + block_type: _, + } => target == &unless_blocked1 || target == &unless_blocked2, + _ => false, + }) { + Ok(BlockResolvedOutcome::ActionComplete( + ActionResult::RoleBlocked, + None, + )) + } else { + Ok(BlockResolvedOutcome::ActionComplete(result, change)) + } + } + None => Ok(BlockResolvedOutcome::ActionComplete(result, change)), } } - ResponseOutcome::ActionComplete(ActionComplete { - result, - change, - unless: Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)), - }) => { - if self.changes.iter().any(|c| match c { - NightChange::RoleBlock { - source: _, - target, - block_type: _, - } => target == &unless_blocked1 || target == &unless_blocked2, - _ => false, - }) { - Ok(BlockResolvedOutcome::ActionComplete( - ActionResult::RoleBlocked, - None, - )) - } else { - Ok(BlockResolvedOutcome::ActionComplete(result, change)) - } - } - - ResponseOutcome::ActionComplete(ActionComplete { - result, - change, - unless: None, - }) => Ok(BlockResolvedOutcome::ActionComplete(result, change)), } } @@ -586,7 +749,6 @@ impl Night { change: Some(NightChange::Shapeshift { source: source.character_id, }), - unless: None, })), _ => Err(GameError::InvalidMessageForGameState), }; @@ -603,7 +765,6 @@ impl Night { character_id.character_id, *new_role, )), - unless: None, })); } } @@ -616,7 +777,6 @@ impl Night { Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: None, - unless: None, })) } ActionPrompt::ElderReveal { character_id } => { @@ -625,22 +785,16 @@ impl Night { change: Some(NightChange::ElderReveal { elder: character_id.character_id, }), - unless: None, })) } ActionPrompt::Seer { marked: Some(marked), .. } => { - let alignment = self - .village - .character_by_id(*marked) - .ok_or(GameError::InvalidTarget)? - .alignment(); + let alignment = self.village.character_by_id(*marked)?.alignment(); Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Seer(alignment), change: None, - unless: Some(Unless::TargetBlocked(*marked)), })) } ActionPrompt::Protector { @@ -655,42 +809,27 @@ impl Night { source: character_id.character_id, }, }), - unless: Some(Unless::TargetBlocked(*marked)), })), ActionPrompt::Arcanist { marked: (Some(marked1), Some(marked2)), .. } => { - let same = self - .village - .character_by_id(*marked1) - .ok_or(GameError::InvalidMessageForGameState)? - .alignment() - == self - .village - .character_by_id(*marked2) - .ok_or(GameError::InvalidMessageForGameState)? - .alignment(); + let same = self.village.character_by_id(*marked1)?.alignment() + == self.village.character_by_id(*marked2)?.alignment(); Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Arcanist { same }, change: None, - unless: Some(Unless::TargetsBlocked(*marked1, *marked2)), })) } ActionPrompt::Gravedigger { marked: Some(marked), .. } => { - let dig_role = self - .village - .character_by_id(*marked) - .ok_or(GameError::InvalidMessageForGameState)? - .gravedigger_dig(); + let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig(); Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GraveDigger(dig_role), change: None, - unless: Some(Unless::TargetBlocked(*marked)), })) } ActionPrompt::Hunter { @@ -703,7 +842,6 @@ impl Night { source: character_id.character_id, target: *marked, }), - unless: Some(Unless::TargetBlocked(*marked)), })), ActionPrompt::Militia { character_id, @@ -719,7 +857,6 @@ impl Night { .ok_or(GameError::InvalidMessageForGameState)?, }, }), - unless: Some(Unless::TargetBlocked(*marked)), })), ActionPrompt::Militia { marked: None, .. } => { Ok(ResponseOutcome::ActionComplete(Default::default())) @@ -740,7 +877,6 @@ impl Night { starves_if_fails: *kill_or_die, }, }), - unless: Some(Unless::TargetBlocked(*marked)), })), ActionPrompt::MapleWolf { marked: None, .. } => { Ok(ResponseOutcome::ActionComplete(Default::default())) @@ -759,7 +895,6 @@ impl Night { guarding: false, }, }), - unless: Some(Unless::TargetBlocked(*marked)), })), ActionPrompt::Guardian { character_id, @@ -779,7 +914,6 @@ impl Night { guarding: false, }, }), - unless: Some(Unless::TargetBlocked(*marked)), })) } ActionPrompt::Guardian { @@ -796,7 +930,6 @@ impl Night { guarding: prev_protect.character_id == *marked, }, }), - unless: Some(Unless::TargetBlocked(*marked)), })), ActionPrompt::WolfPackKill { marked: Some(marked), @@ -815,7 +948,6 @@ impl Night { .ok_or(GameError::InvalidMessageForGameState)?, }, }), - unless: Some(Unless::TargetBlocked(*marked)), })), ActionPrompt::Shapeshifter { character_id } => { Ok(ResponseOutcome::ActionComplete(ActionComplete { @@ -823,7 +955,6 @@ impl Night { change: Some(NightChange::Shapeshift { source: character_id.character_id, }), - unless: None, })) } ActionPrompt::AlphaWolf { @@ -840,7 +971,6 @@ impl Night { .ok_or(GameError::InvalidMessageForGameState)?, }, }), - unless: Some(Unless::TargetBlocked(*marked)), })), ActionPrompt::AlphaWolf { marked: None, .. } => { Ok(ResponseOutcome::ActionComplete(Default::default())) @@ -856,10 +986,139 @@ impl Night { target: *marked, block_type: RoleBlock::Direwolf, }), - unless: Some(Unless::TargetBlocked(*marked)), })), + ActionPrompt::Adjudicator { + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::Adjudicator { + killer: self.village.character_by_id(*marked)?.killer(), + }, + change: None, + } + .into()), + ActionPrompt::PowerSeer { + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::PowerSeer { + powerful: self.village.character_by_id(*marked)?.powerful(), + }, + change: None, + } + .into()), + ActionPrompt::Mortician { + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::Mortician( + self.village + .character_by_id(*marked)? + .died_to() + .ok_or(GameError::InvalidTarget)? + .title(), + ), + change: None, + } + .into()), + ActionPrompt::Beholder { + marked: Some(marked), + .. + } => { + if let Some(result) = self.used_actions.iter().find_map(|(prompt, result)| { + prompt.matches_beholding(*marked).then_some(result) + }) { + Ok(ActionComplete { + result: result.clone(), + change: None, + } + .into()) + } else { + Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + } + .into()) + } + } + ActionPrompt::MasonsWake { .. } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + } + .into()), + ActionPrompt::MasonLeaderRecruit { + character_id, + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::Continue, + change: Some(NightChange::MasonRecruit { + mason_leader: character_id.character_id, + recruiting: *marked, + }), + } + .into()), + ActionPrompt::Empath { + character_id, + marked: Some(marked), + .. + } => { + let marked = self.village.character_by_id(*marked)?; + let scapegoat = marked.role_title() != RoleTitle::Scapegoat; - ActionPrompt::Protector { marked: None, .. } + Ok(ActionComplete { + result: ActionResult::Empath { scapegoat }, + change: scapegoat.then(|| NightChange::EmpathFoundScapegoat { + empath: character_id.character_id, + scapegoat: marked.character_id(), + }), + } + .into()) + } + ActionPrompt::Vindicator { + character_id, + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: *marked, + protection: Protection::Vindicator { + source: character_id.character_id, + }, + }), + } + .into()), + ActionPrompt::PyreMaster { + character_id, + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: NonZeroU8::new(self.night).map(|night| NightChange::Kill { + target: *marked, + died_to: DiedTo::PyreMaster { + killer: character_id.character_id, + night, + }, + }), + } + .into()), + + ActionPrompt::PyreMaster { marked: None, .. } + | ActionPrompt::MasonLeaderRecruit { marked: None, .. } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + } + .into()), + + ActionPrompt::Adjudicator { marked: None, .. } + | ActionPrompt::PowerSeer { marked: None, .. } + | ActionPrompt::Mortician { marked: None, .. } + | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::Empath { marked: None, .. } + | ActionPrompt::Vindicator { marked: None, .. } + | ActionPrompt::Protector { marked: None, .. } | ActionPrompt::Arcanist { marked: (None, None), .. @@ -923,6 +1182,15 @@ impl Night { | ActionPrompt::Guardian { character_id, .. } | ActionPrompt::Shapeshifter { character_id } | ActionPrompt::AlphaWolf { character_id, .. } + | ActionPrompt::Adjudicator { character_id, .. } + | ActionPrompt::PowerSeer { character_id, .. } + | ActionPrompt::Mortician { character_id, .. } + | ActionPrompt::Beholder { character_id, .. } + | ActionPrompt::MasonsWake { character_id, .. } + | ActionPrompt::MasonLeaderRecruit { character_id, .. } + | ActionPrompt::Empath { character_id, .. } + | ActionPrompt::Vindicator { character_id, .. } + | ActionPrompt::PyreMaster { character_id, .. } | ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id), ActionPrompt::WolvesIntro { wolves: _ } | ActionPrompt::WolfPackKill { .. } @@ -934,7 +1202,7 @@ impl Night { pub fn current_character(&self) -> Option<&Character> { self.current_character_id() - .and_then(|id| self.village.character_by_id(id)) + .and_then(|id| self.village.character_by_id(id).ok()) } pub const fn complete(&self) -> bool { @@ -944,9 +1212,12 @@ impl Night { pub fn next(&mut self) -> Result<()> { match &self.night_state { NightState::Active { - current_prompt: _, - current_result: Some(_), - } => {} + current_prompt, + current_result: Some(result), + } => { + self.used_actions + .push((current_prompt.clone(), result.clone())); + } NightState::Active { current_prompt: _, current_result: None, @@ -954,7 +1225,6 @@ impl Night { NightState::Complete => return Err(GameError::NightOver), } if let Some(prompt) = self.action_queue.pop_front() { - self.used_actions.push(prompt.clone()); self.night_state = NightState::Active { current_prompt: prompt, current_result: None, @@ -977,7 +1247,7 @@ pub enum ServerAction { } mod filter { - use crate::player::Character; + use crate::character::Character; pub fn no_filter(_: &Character) -> bool { true diff --git a/werewolves-proto/src/game/settings.rs b/werewolves-proto/src/game/settings.rs index 417097e..6de59d3 100644 --- a/werewolves-proto/src/game/settings.rs +++ b/werewolves-proto/src/game/settings.rs @@ -9,7 +9,7 @@ use super::Result; use serde::{Deserialize, Serialize}; -use crate::{error::GameError, message::Identification, player::Character, role::RoleTitle}; +use crate::{character::Character, error::GameError, message::Identification, role::RoleTitle}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GameSettings { @@ -212,9 +212,7 @@ impl GameSettings { SetupRole::Apprentice { to: None } => (mentor_count > 0) .then_some(()) .ok_or(GameError::NoApprenticeMentor), - SetupRole::Apprentice { - to: Some(role), - } => role + SetupRole::Apprentice { to: Some(role) } => role .is_mentor() .then_some(()) .ok_or(GameError::NotAMentor(*role)), diff --git a/werewolves-proto/src/game/settings/settings_role.rs b/werewolves-proto/src/game/settings/settings_role.rs index 3af1b47..29a5ea1 100644 --- a/werewolves-proto/src/game/settings/settings_role.rs +++ b/werewolves-proto/src/game/settings/settings_role.rs @@ -9,10 +9,11 @@ use uuid::Uuid; use werewolves_macros::{All, ChecksAs, Titles}; use crate::{ + character::Character, error::GameError, message::Identification, modifier::Modifier, - player::{Character, PlayerId}, + player::PlayerId, role::{Role, RoleTitle}, }; @@ -110,10 +111,33 @@ pub enum SetupRole { DireWolf, #[checks(Category::Wolves)] Shapeshifter, + + #[checks(Category::Intel)] + Adjudicator, + #[checks(Category::Intel)] + PowerSeer, + #[checks(Category::Intel)] + Mortician, + #[checks(Category::Intel)] + Beholder, + #[checks(Category::Intel)] + MasonLeader { recruits_available: NonZeroU8 }, + #[checks(Category::Intel)] + Empath, + #[checks(Category::Defensive)] + Vindicator, + #[checks(Category::Defensive)] + Diseased, + #[checks(Category::Defensive)] + BlackKnight, + #[checks(Category::Offensive)] + Weightlifter, + #[checks(Category::Offensive)] + PyreMaster, } impl SetupRoleTitle { - pub const fn into_role(self) -> Role { + pub fn into_role(self) -> Role { match self { SetupRoleTitle::Villager => Role::Villager, SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false }, @@ -141,6 +165,22 @@ impl SetupRoleTitle { SetupRoleTitle::AlphaWolf => Role::AlphaWolf { killed: None }, SetupRoleTitle::DireWolf => Role::DireWolf, SetupRoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None }, + SetupRoleTitle::Adjudicator => Role::Adjudicator, + SetupRoleTitle::PowerSeer => Role::PowerSeer, + SetupRoleTitle::Mortician => Role::Mortician, + SetupRoleTitle::Beholder => Role::Beholder, + SetupRoleTitle::MasonLeader => Role::MasonLeader { + recruits_available: 1, + recruits: Box::new([]), + }, + SetupRoleTitle::Empath => Role::Empath { cursed: false }, + SetupRoleTitle::Vindicator => Role::Vindicator, + SetupRoleTitle::Diseased => Role::Diseased, + SetupRoleTitle::BlackKnight => Role::BlackKnight { attacked: false }, + SetupRoleTitle::Weightlifter => Role::Weightlifter, + SetupRoleTitle::PyreMaster => Role::PyreMaster { + villagers_killed: 0, + }, } } } @@ -164,6 +204,17 @@ impl Display for SetupRole { SetupRole::AlphaWolf => "AlphaWolf", SetupRole::DireWolf => "DireWolf", SetupRole::Shapeshifter => "Shapeshifter", + SetupRole::Adjudicator => "Adjudicator", + SetupRole::PowerSeer => "PowerSeer", + SetupRole::Mortician => "Mortician", + SetupRole::Beholder => "Beholder", + SetupRole::MasonLeader { .. } => "Mason Leader", + SetupRole::Empath => "Empath", + SetupRole::Vindicator => "Vindicator", + SetupRole::Diseased => "Diseased", + SetupRole::BlackKnight => "Black Knight", + SetupRole::Weightlifter => "Weightlifter", + SetupRole::PyreMaster => "Pyremaster", }) } } @@ -210,6 +261,22 @@ impl SetupRole { SetupRole::AlphaWolf => Role::AlphaWolf { killed: None }, SetupRole::DireWolf => Role::DireWolf, SetupRole::Shapeshifter => Role::Shapeshifter { shifted_into: None }, + SetupRole::MasonLeader { recruits_available } => Role::MasonLeader { + recruits_available: recruits_available.get(), + recruits: Box::new([]), + }, + SetupRole::Adjudicator => Role::Adjudicator, + SetupRole::PowerSeer => Role::PowerSeer, + SetupRole::Mortician => Role::Mortician, + SetupRole::Beholder => Role::Beholder, + SetupRole::Empath => Role::Empath { cursed: false }, + SetupRole::Vindicator => Role::Vindicator, + SetupRole::Diseased => Role::Diseased, + SetupRole::BlackKnight => Role::BlackKnight { attacked: false }, + SetupRole::Weightlifter => Role::Weightlifter, + SetupRole::PyreMaster => Role::PyreMaster { + villagers_killed: 0, + }, }) } } @@ -233,6 +300,17 @@ impl From for RoleTitle { SetupRole::AlphaWolf => RoleTitle::AlphaWolf, SetupRole::DireWolf => RoleTitle::DireWolf, SetupRole::Shapeshifter => RoleTitle::Shapeshifter, + SetupRole::Adjudicator => RoleTitle::Adjudicator, + SetupRole::PowerSeer => RoleTitle::PowerSeer, + SetupRole::Mortician => RoleTitle::Mortician, + SetupRole::Beholder => RoleTitle::Beholder, + SetupRole::MasonLeader { .. } => RoleTitle::MasonLeader, + SetupRole::Empath => RoleTitle::Empath, + SetupRole::Vindicator => RoleTitle::Vindicator, + SetupRole::Diseased => RoleTitle::Diseased, + SetupRole::BlackKnight => RoleTitle::BlackKnight, + SetupRole::Weightlifter => RoleTitle::Weightlifter, + SetupRole::PyreMaster => RoleTitle::PyreMaster, } } } @@ -260,6 +338,19 @@ impl From for SetupRole { RoleTitle::AlphaWolf => SetupRole::AlphaWolf, RoleTitle::DireWolf => SetupRole::DireWolf, RoleTitle::Shapeshifter => SetupRole::Shapeshifter, + RoleTitle::Adjudicator => SetupRole::Adjudicator, + RoleTitle::PowerSeer => SetupRole::PowerSeer, + RoleTitle::Mortician => SetupRole::Mortician, + RoleTitle::Beholder => SetupRole::Beholder, + RoleTitle::MasonLeader => SetupRole::MasonLeader { + recruits_available: NonZeroU8::new(1).unwrap(), + }, + RoleTitle::Empath => SetupRole::Empath, + RoleTitle::Vindicator => SetupRole::Vindicator, + RoleTitle::Diseased => SetupRole::Diseased, + RoleTitle::BlackKnight => SetupRole::BlackKnight, + RoleTitle::Weightlifter => SetupRole::Weightlifter, + RoleTitle::PyreMaster => SetupRole::PyreMaster, } } } diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index 2bc6078..da8e689 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -5,11 +5,12 @@ use serde::{Deserialize, Serialize}; use super::Result; use crate::{ + character::{Character, CharacterId}, diedto::DiedTo, error::GameError, game::{DateTime, GameOver, GameSettings}, message::{CharacterIdentity, Identification}, - player::{Character, CharacterId, PlayerId}, + player::PlayerId, role::{Role, RoleTitle}, }; @@ -43,7 +44,7 @@ impl Village { { let ww = wolves .clone() - .filter(|w| matches!(w.role().title(), RoleTitle::Werewolf)) + .filter(|w| matches!(w.role_title(), RoleTitle::Werewolf)) .collect::>(); if !ww.is_empty() { return Some(ww[rand::random_range(0..ww.len())]); @@ -63,12 +64,6 @@ impl Village { self.date_time } - pub fn find_by_character_id(&self, character_id: CharacterId) -> Option<&Character> { - self.characters - .iter() - .find(|c| c.character_id() == character_id) - } - pub fn find_by_character_id_mut( &mut self, character_id: CharacterId, @@ -137,18 +132,22 @@ impl Village { pub fn living_wolf_pack_players(&self) -> Box<[Character]> { self.characters .iter() - .filter(|c| c.role().wolf() && c.alive()) + .filter(|c| c.is_wolf() && c.alive()) .cloned() .collect() } pub fn killing_wolf_id(&self) -> CharacterId { let wolves = self.living_wolf_pack_players(); - if let Some(ww) = wolves.iter().find(|w| matches!(w.role(), Role::Werewolf)) { + if let Some(ww) = wolves + .iter() + .find(|w| matches!(w.role_title(), RoleTitle::Werewolf)) + { ww.character_id() - } else if let Some(non_ss_wolf) = wolves.iter().find(|w| { - w.role().wolf() && !matches!(w.role(), Role::Shapeshifter { shifted_into: _ }) - }) { + } else if let Some(non_ss_wolf) = wolves + .iter() + .find(|w| w.is_wolf() && !matches!(w.role_title(), RoleTitle::Shapeshifter)) + { non_ss_wolf.character_id() } else { wolves.into_iter().next().unwrap().character_id() @@ -163,7 +162,7 @@ impl Village { .collect() } - pub fn target_by_id(&self, character_id: CharacterId) -> Option { + pub fn target_by_id(&self, character_id: CharacterId) -> Result { self.character_by_id(character_id).map(Character::identity) } @@ -197,23 +196,28 @@ impl Village { pub fn executed_known_elder(&self) -> bool { self.characters.iter().any(|d| { - matches!( - d.role(), - Role::Elder { - woken_for_reveal: true, - .. - } - ) && d - .died_to() - .map(|d| matches!(d, DiedTo::Execution { .. })) - .unwrap_or_default() + d.known_elder() + && d.died_to() + .map(|d| matches!(d, DiedTo::Execution { .. })) + .unwrap_or_default() }) } + pub fn executions_on_day(&self, on_day: NonZeroU8) -> Box<[Character]> { + self.characters + .iter() + .filter(|c| match c.died_to() { + Some(DiedTo::Execution { day }) => day.get() == on_day.get(), + _ => false, + }) + .cloned() + .collect() + } + pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> { self.characters .iter() - .filter(|c| c.role().title() == role) + .filter(|c| c.role_title() == role) .cloned() .collect() } @@ -222,16 +226,18 @@ impl Village { self.characters.iter().cloned().collect() } - pub fn character_by_id_mut(&mut self, character_id: CharacterId) -> Option<&mut Character> { + pub fn character_by_id_mut(&mut self, character_id: CharacterId) -> Result<&mut Character> { self.characters .iter_mut() .find(|c| c.character_id() == character_id) + .ok_or(GameError::InvalidTarget) } - pub fn character_by_id(&self, character_id: CharacterId) -> Option<&Character> { + pub fn character_by_id(&self, character_id: CharacterId) -> Result<&Character> { self.characters .iter() .find(|c| c.character_id() == character_id) + .ok_or(GameError::InvalidTarget) } pub fn character_by_player_id(&self, player_id: PlayerId) -> Option<&Character> { @@ -269,8 +275,23 @@ impl RoleTitle { RoleTitle::Guardian => Role::Guardian { last_protected: None, }, - // fallback to villager RoleTitle::Apprentice => Role::Villager, + RoleTitle::Adjudicator => Role::Adjudicator, + RoleTitle::PowerSeer => Role::PowerSeer, + RoleTitle::Mortician => Role::Mortician, + RoleTitle::Beholder => Role::Beholder, + RoleTitle::MasonLeader => Role::MasonLeader { + recruits_available: 1, + recruits: Box::new([]), + }, + RoleTitle::Empath => Role::Empath { cursed: false }, + RoleTitle::Vindicator => Role::Vindicator, + RoleTitle::Diseased => Role::Diseased, + RoleTitle::BlackKnight => Role::BlackKnight { attacked: false }, + RoleTitle::Weightlifter => Role::Weightlifter, + RoleTitle::PyreMaster => Role::PyreMaster { + villagers_killed: 0, + }, } } } diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 6a62570..ef9cc06 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -2,6 +2,7 @@ mod night_order; mod role; use crate::{ + character::{Character, CharacterId}, error::GameError, game::{Game, GameSettings, SetupRole, SetupSlot}, message::{ @@ -9,14 +10,11 @@ use crate::{ host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, }, - player::{Character, CharacterId, PlayerId}, - role::{Alignment, Role, RoleTitle}, + player::PlayerId, + role::{Alignment, RoleTitle}, }; use colored::Colorize; -use core::{ - num::NonZeroU8, - ops::Range, -}; +use core::{num::NonZeroU8, ops::Range}; #[allow(unused)] use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use std::io::Write; @@ -40,6 +38,7 @@ impl SettingsExt for GameSettings { } } +#[allow(unused)] pub trait ActionPromptTitleExt { fn wolf_pack_kill(&self); fn cover_of_darkness(&self); @@ -56,6 +55,8 @@ pub trait ActionPromptTitleExt { fn shapeshifter(&self); fn alphawolf(&self); fn direwolf(&self); + fn masons_wake(&self); + fn masons_leader_recruit(&self); } impl ActionPromptTitleExt for ActionPromptTitle { @@ -104,6 +105,12 @@ impl ActionPromptTitleExt for ActionPromptTitle { fn wolf_pack_kill(&self) { assert_eq!(*self, ActionPromptTitle::WolfPackKill); } + fn masons_wake(&self) { + assert_eq!(*self, ActionPromptTitle::MasonsWake) + } + fn masons_leader_recruit(&self) { + assert_eq!(*self, ActionPromptTitle::MasonLeaderRecruit) + } } pub trait ActionResultExt { @@ -195,7 +202,9 @@ impl GameExt for Game { self.village() .characters() .into_iter() - .find(|c| c.alive() && matches!(c.role(), Role::Villager) && c.player_id() != excl) + .find(|c| { + c.alive() && matches!(c.role_title(), RoleTitle::Villager) && c.player_id() != excl + }) .unwrap() } @@ -203,7 +212,9 @@ impl GameExt for Game { self.village() .characters() .into_iter() - .filter_map(|c| matches!(c.role(), Role::Villager).then_some(c.character_id())) + .filter_map(|c| { + matches!(c.role_title(), RoleTitle::Villager).then_some(c.character_id()) + }) .collect() } @@ -233,7 +244,8 @@ impl GameExt for Game { fn mark_and_check(&mut self, mark: CharacterId) { let prompt = self.mark(mark); match prompt { - ActionPrompt::ElderReveal { .. } + ActionPrompt::MasonsWake { .. } + | ActionPrompt::ElderReveal { .. } | ActionPrompt::CoverOfDarkness | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } @@ -243,6 +255,38 @@ impl GameExt for Game { marked: Some(marked), .. } + | ActionPrompt::Adjudicator { + marked: Some(marked), + .. + } + | ActionPrompt::PowerSeer { + marked: Some(marked), + .. + } + | ActionPrompt::Mortician { + marked: Some(marked), + .. + } + | ActionPrompt::Beholder { + marked: Some(marked), + .. + } + | ActionPrompt::MasonLeaderRecruit { + marked: Some(marked), + .. + } + | ActionPrompt::Empath { + marked: Some(marked), + .. + } + | ActionPrompt::Vindicator { + marked: Some(marked), + .. + } + | ActionPrompt::PyreMaster { + marked: Some(marked), + .. + } | ActionPrompt::Protector { marked: Some(marked), .. @@ -279,8 +323,15 @@ impl GameExt for Game { marked: Some(marked), .. } => assert_eq!(marked, mark, "marked character"), - ActionPrompt::Seer { marked: None, .. } + | ActionPrompt::Adjudicator { marked: None, .. } + | ActionPrompt::PowerSeer { marked: None, .. } + | ActionPrompt::Mortician { marked: None, .. } + | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::MasonLeaderRecruit { marked: None, .. } + | ActionPrompt::Empath { marked: None, .. } + | ActionPrompt::Vindicator { marked: None, .. } + | ActionPrompt::PyreMaster { marked: None, .. } | ActionPrompt::Protector { marked: None, .. } | ActionPrompt::Gravedigger { marked: None, .. } | ActionPrompt::Hunter { marked: None, .. } @@ -341,9 +392,14 @@ impl GameExt for Game { } fn execute(&mut self) -> ActionPrompt { - self.process(HostGameMessage::Day(HostDayMessage::Execute)) - .unwrap() - .prompt() + assert_eq!( + self.process(HostGameMessage::Day(HostDayMessage::Execute)) + .unwrap() + .prompt(), + ActionPrompt::CoverOfDarkness + ); + self.r#continue().r#continue(); + self.next() } } @@ -563,7 +619,7 @@ fn wolfpack_kill_all_targets_valid() { .village() .characters() .into_iter() - .find(|v| v.is_village() && !matches!(v.role().title(), RoleTitle::Protector)) + .find(|v| v.is_village() && !matches!(v.role_title(), RoleTitle::Protector)) .unwrap() .character_id(); match game @@ -580,14 +636,7 @@ fn wolfpack_kill_all_targets_valid() { resp => panic!("unexpected server message: {resp:#?}"), } - assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); - game.r#continue().r#continue(); - - let living_villagers = match game - .process(HostGameMessage::Night(HostNightMessage::Next)) - .unwrap() - .prompt() - { + let living_villagers = match game.execute() { ActionPrompt::WolfPackKill { living_villagers, marked: _, diff --git a/werewolves-proto/src/game_test/night_order.rs b/werewolves-proto/src/game_test/night_order.rs index ae05e38..6283342 100644 --- a/werewolves-proto/src/game_test/night_order.rs +++ b/werewolves-proto/src/game_test/night_order.rs @@ -3,11 +3,11 @@ use core::num::NonZeroU8; use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use crate::{ + character::CharacterId, message::{ CharacterIdentity, night::{ActionPrompt, ActionPromptTitle}, }, - player::CharacterId, }; fn character_identity() -> CharacterIdentity { diff --git a/werewolves-proto/src/game_test/role/elder.rs b/werewolves-proto/src/game_test/role/elder.rs index d13fe4f..3c72f19 100644 --- a/werewolves-proto/src/game_test/role/elder.rs +++ b/werewolves-proto/src/game_test/role/elder.rs @@ -34,9 +34,7 @@ fn elder_doesnt_die_first_try_night_doesnt_know() { game.r#continue().sleep(); game.next_expect_day(); - assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); - game.r#continue().r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + game.execute().title().wolf_pack_kill(); let elder = game.character_by_player_id(elder_player_id); game.mark_and_check(elder.character_id()); @@ -47,10 +45,7 @@ fn elder_doesnt_die_first_try_night_doesnt_know() { let elder = game.character_by_player_id(elder_player_id); assert_eq!(elder.died_to().cloned(), None); - assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness); - game.r#continue().r#continue(); - - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + game.execute().title().wolf_pack_kill(); game.mark_and_check(elder.character_id()); game.r#continue().sleep(); @@ -60,9 +55,7 @@ fn elder_doesnt_die_first_try_night_doesnt_know() { .died_to() .cloned(), Some(DiedTo::Wolfpack { - killing_wolf: game - .character_by_player_id(wolf_player_id) - .character_id(), + killing_wolf: game.character_by_player_id(wolf_player_id).character_id(), night: NonZeroU8::new(2).unwrap(), }) ); @@ -92,9 +85,7 @@ fn elder_doesnt_die_first_try_night_knows() { game.r#continue().sleep(); game.next_expect_day(); - assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); - game.r#continue().r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + game.execute().title().wolf_pack_kill(); let elder = game.character_by_player_id(elder_player_id); game.mark_and_check(elder.character_id()); @@ -113,10 +104,7 @@ fn elder_doesnt_die_first_try_night_knows() { let elder = game.character_by_player_id(elder_player_id); assert_eq!(elder.died_to().cloned(), None); - assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness); - game.r#continue().r#continue(); - - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + game.execute().title().wolf_pack_kill(); game.mark_and_check(elder.character_id()); game.r#continue().sleep(); @@ -167,10 +155,7 @@ fn elder_executed_doesnt_know() { game.mark_for_execution(elder.character_id()); - game.execute().title().cover_of_darkness(); - game.r#continue().r#continue(); - - game.next().title().wolf_pack_kill(); + game.execute().title().wolf_pack_kill(); assert_eq!( game.character_by_player_id(elder_player_id) @@ -234,10 +219,7 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() { game.next_expect_day(); - game.execute().title().cover_of_darkness(); - game.r#continue().r#continue(); - - game.next().title().wolf_pack_kill(); + game.execute().title().wolf_pack_kill(); game.mark(villagers.next().unwrap()); game.r#continue().sleep(); @@ -269,10 +251,7 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() { ); game.mark_for_execution(game.character_by_player_id(elder_player_id).character_id()); - game.execute().title().cover_of_darkness(); - game.r#continue().r#continue(); - - game.next().title().wolf_pack_kill(); + game.execute().title().wolf_pack_kill(); game.mark(game.character_by_player_id(hunter_player_id).character_id()); game.r#continue().sleep(); diff --git a/werewolves-proto/src/game_test/role/mason.rs b/werewolves-proto/src/game_test/role/mason.rs new file mode 100644 index 0000000..b76512d --- /dev/null +++ b/werewolves-proto/src/game_test/role/mason.rs @@ -0,0 +1,69 @@ +use core::num::NonZeroU8; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + game::{Game, GameSettings, SetupRole}, + game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, + message::night::{ActionPrompt, ActionPromptTitle}, +}; + +#[test] +fn mason_recruits_decrement() { + let players = gen_players(1..10); + let mason_leader_player_id = players[0].player_id; + let wolf_player_id = players[1].player_id; + let sacrificial_wolf_player_id = players[2].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign( + SetupRole::MasonLeader { + recruits_available: NonZeroU8::new(1).unwrap(), + }, + mason_leader_player_id, + ); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.add_and_assign(SetupRole::Werewolf, sacrificial_wolf_player_id); + 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(); + + assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill); + game.mark( + game.living_villager_excl(mason_leader_player_id) + .character_id(), + ); + game.r#continue().sleep(); + + let recruited = game.living_villager_excl(mason_leader_player_id); + assert_eq!(game.next().title(), ActionPromptTitle::MasonLeaderRecruit); + game.mark(recruited.character_id()); + game.r#continue().r#continue(); + assert_eq!( + game.next(), + ActionPrompt::MasonsWake { + character_id: game + .character_by_player_id(mason_leader_player_id) + .identity(), + masons: Box::new([game + .character_by_player_id(recruited.player_id()) + .identity()]) + } + ); + game.r#continue().sleep(); + game.next_expect_day(); + + assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill); + game.mark( + game.living_villager_excl(recruited.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().masons_wake(); + game.r#continue().sleep(); + + game.next_expect_day(); +} diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs index 42bb379..ca5a526 100644 --- a/werewolves-proto/src/game_test/role/mod.rs +++ b/werewolves-proto/src/game_test/role/mod.rs @@ -1,3 +1,4 @@ mod elder; +mod mason; mod scapegoat; mod shapeshifter; diff --git a/werewolves-proto/src/game_test/role/scapegoat.rs b/werewolves-proto/src/game_test/role/scapegoat.rs index 0cebac3..0496447 100644 --- a/werewolves-proto/src/game_test/role/scapegoat.rs +++ b/werewolves-proto/src/game_test/role/scapegoat.rs @@ -3,7 +3,7 @@ use core::num::NonZero; use crate::{ diedto::DiedTo, game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange}, - game_test::{ActionResultExt, GameExt, gen_players}, + game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, message::night::{ActionPrompt, ActionPromptTitle}, role::{Alignment, RoleTitle}, }; @@ -72,9 +72,7 @@ fn redeemed_scapegoat_role_changes() { assert_eq!(game.r#continue().seer(), Alignment::Wolves); game.next_expect_day(); - assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); - game.r#continue().r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + game.execute().title().wolf_pack_kill(); let seer = game .village() .characters() @@ -103,10 +101,8 @@ fn redeemed_scapegoat_role_changes() { night: NonZero::new(1).unwrap() } ); - assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); - game.r#continue().r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill); let wolf_target_2 = game .village() .characters() @@ -153,3 +149,43 @@ fn redeemed_scapegoat_role_changes() { .unwrap(); assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer); } + +#[test] +fn redeemed_scapegoat_cannot_redeem_into_wolf() { + let players = gen_players(1..10); + let scapegoat_player_id = players[0].player_id; + let wolf_player_id = players[1].player_id; + let sacrificial_wolf_player_id = players[2].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign( + SetupRole::Scapegoat { + redeemed: OrRandom::Determined(true), + }, + scapegoat_player_id, + ); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.add_and_assign(SetupRole::Werewolf, sacrificial_wolf_player_id); + 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.mark_for_execution( + game.character_by_player_id(sacrificial_wolf_player_id) + .character_id(), + ); + + game.execute().title().wolf_pack_kill(); + game.mark_and_check( + game.living_villager_excl(scapegoat_player_id) + .character_id(), + ); + game.r#continue().sleep(); + + game.next_expect_day(); + + let day_scapegoat = game.character_by_player_id(scapegoat_player_id); + assert_eq!(day_scapegoat.role().title(), RoleTitle::Scapegoat); +} diff --git a/werewolves-proto/src/game_test/role/shapeshifter.rs b/werewolves-proto/src/game_test/role/shapeshifter.rs index b974319..713a4f4 100644 --- a/werewolves-proto/src/game_test/role/shapeshifter.rs +++ b/werewolves-proto/src/game_test/role/shapeshifter.rs @@ -2,13 +2,13 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use crate::{ + character::CharacterId, game::{Game, GameSettings, SetupRole}, game_test::{ActionResultExt, GameExt, ServerToHostMessageExt, gen_players, init_log}, message::{ host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, }, - player::CharacterId, role::RoleTitle, }; @@ -186,9 +186,8 @@ fn only_1_shapeshift_prompt_if_first_shifts() { let (_, marked, _) = game.mark_for_execution(target); let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]); assert_eq!(target_list, marked); - assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); - game.r#continue().r#continue(); - assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + + assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill); let target = game .village() .characters() diff --git a/werewolves-proto/src/lib.rs b/werewolves-proto/src/lib.rs index 7ab5568..224d142 100644 --- a/werewolves-proto/src/lib.rs +++ b/werewolves-proto/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::new_without_default)] +pub mod character; pub mod diedto; pub mod error; pub mod game; diff --git a/werewolves-proto/src/message.rs b/werewolves-proto/src/message.rs index 8dff05c..24d0c29 100644 --- a/werewolves-proto/src/message.rs +++ b/werewolves-proto/src/message.rs @@ -7,7 +7,7 @@ use core::num::NonZeroU8; pub use ident::*; use serde::{Deserialize, Serialize}; -use crate::{game::GameOver, player::CharacterId, role::RoleTitle}; +use crate::{character::CharacterId, game::GameOver, role::RoleTitle}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ClientMessage { diff --git a/werewolves-proto/src/message/host.rs b/werewolves-proto/src/message/host.rs index 0d63df5..f5ea91c 100644 --- a/werewolves-proto/src/message/host.rs +++ b/werewolves-proto/src/message/host.rs @@ -3,13 +3,14 @@ use core::num::NonZeroU8; use serde::{Deserialize, Serialize}; use crate::{ + character::CharacterId, error::GameError, game::{GameOver, GameSettings}, message::{ CharacterIdentity, night::{ActionPrompt, ActionResponse, ActionResult}, }, - player::{CharacterId, PlayerId}, + player::PlayerId, }; use super::{CharacterState, PlayerState}; diff --git a/werewolves-proto/src/message/ident.rs b/werewolves-proto/src/message/ident.rs index 7a49c23..7b35b4d 100644 --- a/werewolves-proto/src/message/ident.rs +++ b/werewolves-proto/src/message/ident.rs @@ -2,11 +2,7 @@ use core::{fmt::Display, num::NonZeroU8}; use serde::{Deserialize, Serialize}; -use crate::{ - diedto::DiedTo, - player::{CharacterId, PlayerId}, - role::RoleTitle, -}; +use crate::{character::CharacterId, diedto::DiedTo, player::PlayerId, role::RoleTitle}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Identification { diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index 127aa20..5305369 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -1,10 +1,13 @@ +use core::num::NonZeroU8; + use serde::{Deserialize, Serialize}; use werewolves_macros::{ChecksAs, Titles}; use crate::{ + character::CharacterId, + diedto::DiedToTitle, error::GameError, message::CharacterIdentity, - player::CharacterId, role::{Alignment, PreviousGuardianAction, RoleTitle}, }; @@ -19,7 +22,11 @@ pub enum ActionType { Direwolf, OtherWolf, Block, + Intel, Other, + MasonRecruit, + MasonsWake, + Beholder, RoleChange, } @@ -50,7 +57,7 @@ pub enum ActionPrompt { }, #[checks(ActionType::RoleChange)] ElderReveal { character_id: CharacterIdentity }, - #[checks(ActionType::Other)] + #[checks(ActionType::Intel)] Seer { character_id: CharacterIdentity, living_players: Box<[CharacterIdentity]>, @@ -62,13 +69,13 @@ pub enum ActionPrompt { targets: Box<[CharacterIdentity]>, marked: Option, }, - #[checks(ActionType::Other)] + #[checks(ActionType::Intel)] Arcanist { character_id: CharacterIdentity, living_players: Box<[CharacterIdentity]>, marked: (Option, Option), }, - #[checks(ActionType::Other)] + #[checks(ActionType::Intel)] Gravedigger { character_id: CharacterIdentity, dead_players: Box<[CharacterIdentity]>, @@ -101,6 +108,61 @@ pub enum ActionPrompt { living_players: Box<[CharacterIdentity]>, marked: Option, }, + #[checks(ActionType::Intel)] + Adjudicator { + character_id: CharacterIdentity, + living_players: Box<[CharacterIdentity]>, + marked: Option, + }, + #[checks(ActionType::Intel)] + PowerSeer { + character_id: CharacterIdentity, + living_players: Box<[CharacterIdentity]>, + marked: Option, + }, + #[checks(ActionType::Intel)] + Mortician { + character_id: CharacterIdentity, + dead_players: Box<[CharacterIdentity]>, + marked: Option, + }, + #[checks(ActionType::Beholder)] + Beholder { + character_id: CharacterIdentity, + living_players: Box<[CharacterIdentity]>, + marked: Option, + }, + #[checks(ActionType::MasonsWake)] + MasonsWake { + character_id: CharacterIdentity, + masons: Box<[CharacterIdentity]>, + }, + #[checks(ActionType::MasonRecruit)] + MasonLeaderRecruit { + character_id: CharacterIdentity, + recruits_left: NonZeroU8, + potential_recruits: Box<[CharacterIdentity]>, + marked: Option, + }, + #[checks(ActionType::Intel)] + Empath { + character_id: CharacterIdentity, + living_players: Box<[CharacterIdentity]>, + marked: Option, + }, + #[checks(ActionType::Protect)] + Vindicator { + character_id: CharacterIdentity, + living_players: Box<[CharacterIdentity]>, + marked: Option, + }, + #[checks(ActionType::Other)] + PyreMaster { + character_id: CharacterIdentity, + living_players: Box<[CharacterIdentity]>, + marked: Option, + }, + #[checks(ActionType::WolfPackKill)] WolfPackKill { living_villagers: Box<[CharacterIdentity]>, @@ -123,10 +185,41 @@ pub enum ActionPrompt { } impl ActionPrompt { + pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool { + match self { + ActionPrompt::Seer { character_id, .. } + | ActionPrompt::Arcanist { character_id, .. } + | ActionPrompt::Gravedigger { character_id, .. } + | ActionPrompt::Adjudicator { character_id, .. } + | ActionPrompt::PowerSeer { character_id, .. } + | ActionPrompt::Mortician { character_id, .. } + | ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target, + + ActionPrompt::Beholder { .. } + | ActionPrompt::CoverOfDarkness + | ActionPrompt::WolvesIntro { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::ElderReveal { .. } + | ActionPrompt::Protector { .. } + | ActionPrompt::Hunter { .. } + | ActionPrompt::Militia { .. } + | ActionPrompt::MapleWolf { .. } + | ActionPrompt::Guardian { .. } + | ActionPrompt::PyreMaster { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::AlphaWolf { .. } + | ActionPrompt::DireWolf { .. } + | ActionPrompt::Empath { .. } + | ActionPrompt::MasonsWake { .. } + | ActionPrompt::MasonLeaderRecruit { .. } + | ActionPrompt::WolfPackKill { .. } => false, + } + } pub(crate) fn with_mark(&self, mark: CharacterId) -> Result { let mut prompt = self.clone(); match &mut prompt { - ActionPrompt::ElderReveal { .. } + ActionPrompt::MasonsWake { .. } + | ActionPrompt::ElderReveal { .. } | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } | ActionPrompt::Shapeshifter { .. } @@ -195,7 +288,47 @@ impl ActionPrompt { Ok(prompt) } - ActionPrompt::Protector { + ActionPrompt::Adjudicator { + living_players: targets, + marked, + .. + } + | ActionPrompt::PowerSeer { + living_players: targets, + marked, + .. + } + | ActionPrompt::Mortician { + dead_players: targets, + marked, + .. + } + | ActionPrompt::Beholder { + living_players: targets, + marked, + .. + } + | ActionPrompt::MasonLeaderRecruit { + potential_recruits: targets, + marked, + .. + } + | ActionPrompt::Empath { + living_players: targets, + marked, + .. + } + | ActionPrompt::Vindicator { + living_players: targets, + marked, + .. + } + | ActionPrompt::PyreMaster { + living_players: targets, + marked, + .. + } + | ActionPrompt::Protector { targets, marked, .. } | ActionPrompt::Seer { @@ -274,17 +407,6 @@ impl PartialOrd for ActionPrompt { #[derive(Debug, Clone, Serialize, PartialEq, Deserialize)] pub enum ActionResponse { - // Seer(CharacterId), - // Arcanist(Option, Option), - // Gravedigger(CharacterId), - // Hunter(CharacterId), - // Militia(Option), - // MapleWolf(Option), - // Guardian(CharacterId), - // WolfPackKillVote(CharacterId), - // AlphaWolf(Option), - // Direwolf(CharacterId), - // Protector(CharacterId), MarkTarget(CharacterId), Shapeshift, Continue, @@ -294,8 +416,12 @@ pub enum ActionResponse { pub enum ActionResult { RoleBlocked, Seer(Alignment), + PowerSeer { powerful: bool }, + Adjudicator { killer: bool }, Arcanist { same: bool }, GraveDigger(Option), + Mortician(DiedToTitle), + Empath { scapegoat: bool }, GoBackToSleep, Continue, } diff --git a/werewolves-proto/src/player.rs b/werewolves-proto/src/player.rs index ae30f77..0cb8d40 100644 --- a/werewolves-proto/src/player.rs +++ b/werewolves-proto/src/player.rs @@ -1,15 +1,10 @@ -use core::{fmt::Display, num::NonZeroU8}; +use core::fmt::Display; -use rand::seq::SliceRandom; use serde::{Deserialize, Serialize}; use crate::{ - diedto::DiedTo, - error::GameError, - game::{DateTime, Village}, - message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, - modifier::Modifier, - role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, + character::CharacterId, + role::{Role, RoleTitle}, }; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -30,24 +25,6 @@ impl Display for PlayerId { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct CharacterId(uuid::Uuid); - -impl CharacterId { - pub fn new() -> Self { - Self(uuid::Uuid::new_v4()) - } - pub const fn from_u128(v: u128) -> Self { - Self(uuid::Uuid::from_u128(v)) - } -} - -impl Display for CharacterId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Player { id: PlayerId, @@ -67,6 +44,7 @@ impl Player { pub enum Protection { Guardian { source: CharacterId, guarding: bool }, Protector { source: CharacterId }, + Vindicator { source: CharacterId }, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] @@ -77,361 +55,7 @@ pub enum KillOutcome { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RoleChange { - role: Role, - new_role: RoleTitle, - changed_on_night: u8, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Character { - player_id: PlayerId, - identity: CharacterIdentity, - role: Role, - modifier: Option, - died_to: Option, - role_changes: Vec, -} - -impl Character { - pub fn new( - Identification { - player_id, - public: - PublicIdentity { - name, - pronouns, - number, - }, - }: Identification, - role: Role, - ) -> Option { - Some(Self { - role, - identity: CharacterIdentity { - character_id: CharacterId::new(), - name, - pronouns, - number: number?, - }, - player_id, - modifier: None, - died_to: None, - role_changes: Vec::new(), - }) - } - - pub const fn is_power_role(&self) -> bool { - match &self.role { - Role::Scapegoat { .. } | Role::Villager => false, - - Role::Seer - | Role::Arcanist - | Role::Gravedigger - | Role::Hunter { .. } - | Role::Militia { .. } - | Role::MapleWolf { .. } - | Role::Guardian { .. } - | Role::Protector { .. } - | Role::Apprentice(..) - | Role::Elder { .. } - | Role::Werewolf - | Role::AlphaWolf { .. } - | Role::DireWolf - | Role::Shapeshifter { .. } => true, - } - } - - pub fn identity(&self) -> CharacterIdentity { - self.identity.clone() - } - - pub fn name(&self) -> &str { - self.identity.name.as_str() - } - - pub const fn number(&self) -> NonZeroU8 { - self.identity.number - } - - pub const fn pronouns(&self) -> Option<&str> { - match self.identity.pronouns.as_ref() { - Some(p) => Some(p.as_str()), - None => None, - } - } - - pub fn died_to(&self) -> Option<&DiedTo> { - self.died_to.as_ref() - } - - pub fn kill(&mut self, died_to: DiedTo) { - match (&mut self.role, died_to.date_time()) { - ( - Role::Elder { - lost_protection_night: Some(_), - .. - }, - _, - ) => {} - ( - Role::Elder { - lost_protection_night, - .. - }, - DateTime::Night { number: night }, - ) => { - *lost_protection_night = lost_protection_night - .is_none() - .then_some(night) - .and_then(NonZeroU8::new); - return; - } - _ => {} - } - match &self.died_to { - Some(_) => {} - None => self.died_to = Some(died_to), - } - } - - pub const fn alive(&self) -> bool { - self.died_to.is_none() - } - - pub fn execute(&mut self, day: NonZeroU8) -> Result<(), GameError> { - if self.died_to.is_some() { - return Err(GameError::CharacterAlreadyDead); - } - self.died_to = Some(DiedTo::Execution { day }); - Ok(()) - } - - pub const fn character_id(&self) -> CharacterId { - self.identity.character_id - } - - pub const fn player_id(&self) -> PlayerId { - self.player_id - } - - pub const fn role(&self) -> &Role { - &self.role - } - - pub const fn gravedigger_dig(&self) -> Option { - match &self.role { - Role::Shapeshifter { - shifted_into: Some(_), - } => None, - _ => Some(self.role.title()), - } - } - - pub const fn alignment(&self) -> Alignment { - self.role.alignment() - } - - pub const fn role_mut(&mut self) -> &mut Role { - &mut self.role - } - - pub fn elder_reveal(&mut self) { - if let Role::Elder { - woken_for_reveal, .. - } = &mut self.role - { - *woken_for_reveal = true - } - } - - pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<(), GameError> { - let mut role = new_role.title_to_role_excl_apprentice(); - core::mem::swap(&mut role, &mut self.role); - self.role_changes.push(RoleChange { - role, - new_role, - changed_on_night: match at { - DateTime::Day { number: _ } => return Err(GameError::NotNight), - DateTime::Night { number } => number, - }, - }); - - Ok(()) - } - - pub const fn is_wolf(&self) -> bool { - self.role.wolf() - } - - pub const fn is_village(&self) -> bool { - !self.is_wolf() - } - - pub fn night_action_prompt( - &self, - village: &Village, - ) -> Result, GameError> { - if !self.alive() || !self.role.wakes(village) { - return Ok(None); - } - let night = match village.date_time() { - DateTime::Day { number: _ } => return Err(GameError::NotNight), - DateTime::Night { number } => number, - }; - Ok(Some(match &self.role { - Role::Shapeshifter { - shifted_into: Some(_), - } - | Role::AlphaWolf { killed: Some(_) } - | Role::Militia { targeted: Some(_) } - | Role::Scapegoat { redeemed: false } - | Role::Elder { - woken_for_reveal: true, - .. - } - | Role::Villager => return Ok(None), - Role::Scapegoat { redeemed: true } => { - let mut dead = village.dead_characters(); - dead.shuffle(&mut rand::rng()); - if let Some(pr) = dead - .into_iter() - .find_map(|d| d.is_power_role().then_some(d.role().title())) - { - ActionPrompt::RoleChange { - character_id: self.identity(), - new_role: pr, - } - } else { - return Ok(None); - } - } - Role::Seer => ActionPrompt::Seer { - character_id: self.identity(), - living_players: village.living_players_excluding(self.character_id()), - marked: None, - }, - Role::Arcanist => ActionPrompt::Arcanist { - character_id: self.identity(), - living_players: village.living_players_excluding(self.character_id()), - marked: (None, None), - }, - Role::Protector { - last_protected: Some(last_protected), - } => ActionPrompt::Protector { - character_id: self.identity(), - targets: village.living_players_excluding(*last_protected), - marked: None, - }, - Role::Protector { - last_protected: None, - } => ActionPrompt::Protector { - character_id: self.identity(), - targets: village.living_players_excluding(self.character_id()), - marked: None, - }, - Role::Apprentice(role) => { - let current_night = match village.date_time() { - DateTime::Day { number: _ } => return Ok(None), - DateTime::Night { number } => number, - }; - return Ok(village - .characters() - .into_iter() - .filter(|c| c.role().title() == *role) - .filter_map(|char| char.died_to) - .any(|died_to| match died_to.date_time() { - DateTime::Day { number } => number.get() + 1 >= current_night, - DateTime::Night { number } => number + 1 >= current_night, - }) - .then(|| ActionPrompt::RoleChange { - character_id: self.identity(), - new_role: *role, - })); - } - Role::Elder { - knows_on_night, - woken_for_reveal: false, - .. - } => { - let current_night = match village.date_time() { - DateTime::Day { number: _ } => return Ok(None), - DateTime::Night { number } => number, - }; - return Ok((current_night >= knows_on_night.get()).then_some({ - ActionPrompt::ElderReveal { - character_id: self.identity(), - } - })); - } - Role::Militia { targeted: None } => ActionPrompt::Militia { - character_id: self.identity(), - living_players: village.living_players_excluding(self.character_id()), - marked: None, - }, - Role::Werewolf => ActionPrompt::WolfPackKill { - living_villagers: village.living_players(), - marked: None, - }, - Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf { - character_id: self.identity(), - living_villagers: village.living_players_excluding(self.character_id()), - marked: None, - }, - Role::DireWolf => ActionPrompt::DireWolf { - character_id: self.identity(), - living_players: village.living_players(), - marked: None, - }, - Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter { - character_id: self.identity(), - }, - Role::Gravedigger => { - let dead = village.dead_targets(); - if dead.is_empty() { - return Ok(None); - } - ActionPrompt::Gravedigger { - character_id: self.identity(), - dead_players: village.dead_targets(), - marked: None, - } - } - Role::Hunter { target } => ActionPrompt::Hunter { - character_id: self.identity(), - current_target: target.as_ref().and_then(|t| village.target_by_id(*t)), - living_players: village.living_players_excluding(self.character_id()), - marked: None, - }, - Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf { - character_id: self.identity(), - kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night, - living_players: village.living_players_excluding(self.character_id()), - marked: None, - }, - Role::Guardian { - last_protected: Some(PreviousGuardianAction::Guard(prev_target)), - } => ActionPrompt::Guardian { - character_id: self.identity(), - previous: Some(PreviousGuardianAction::Guard(prev_target.clone())), - living_players: village.living_players_excluding(prev_target.character_id), - marked: None, - }, - Role::Guardian { - last_protected: Some(PreviousGuardianAction::Protect(prev_target)), - } => ActionPrompt::Guardian { - character_id: self.identity(), - previous: Some(PreviousGuardianAction::Protect(prev_target.clone())), - living_players: village.living_players(), - marked: None, - }, - Role::Guardian { - last_protected: None, - } => ActionPrompt::Guardian { - character_id: self.identity(), - previous: None, - living_players: village.living_players(), - marked: None, - }, - })) - } + pub role: Role, + pub new_role: RoleTitle, + pub changed_on_night: u8, } diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index d24cc94..8b548e2 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -4,9 +4,9 @@ use serde::{Deserialize, Serialize}; use werewolves_macros::{ChecksAs, Titles}; use crate::{ + character::CharacterId, game::{DateTime, Village}, message::CharacterIdentity, - player::CharacterId, }; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)] @@ -28,6 +28,53 @@ pub enum Role { #[checks(Alignment::Village)] #[checks("powerful")] #[checks("is_mentor")] + Adjudicator, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + PowerSeer, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + Mortician, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + Beholder, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + MasonLeader { + recruits_available: u8, + recruits: Box<[CharacterId]>, + }, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + Empath { cursed: bool }, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + Vindicator, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + Diseased, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + BlackKnight { attacked: bool }, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + Weightlifter, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] + PyreMaster { villagers_killed: u8 }, + #[checks(Alignment::Village)] + #[checks("powerful")] + #[checks("is_mentor")] Gravedigger, #[checks(Alignment::Village)] #[checks("killer")] @@ -101,7 +148,12 @@ impl Role { pub const fn wakes_night_zero(&self) -> bool { match self { - Role::DireWolf | Role::Arcanist | Role::Seer => true, + Role::PowerSeer + | Role::Beholder + | Role::Adjudicator + | Role::DireWolf + | Role::Arcanist + | Role::Seer => true, Role::Shapeshifter { .. } | Role::Werewolf @@ -115,6 +167,14 @@ impl Role { | Role::Apprentice(_) | Role::Villager | Role::Scapegoat { .. } + | Role::Mortician + | Role::MasonLeader { .. } + | Role::Empath { .. } + | Role::Vindicator + | Role::Diseased + | Role::BlackKnight { .. } + | Role::Weightlifter + | Role::PyreMaster { .. } | Role::Protector { .. } => false, } } @@ -132,9 +192,20 @@ impl Role { | Role::Werewolf | Role::Scapegoat { redeemed: false } | Role::Militia { targeted: Some(_) } + | Role::Diseased + | Role::BlackKnight { .. } | Role::Villager => false, - Role::Scapegoat { redeemed: true } + Role::PowerSeer + | Role::Mortician + | Role::Beholder + | Role::MasonLeader { .. } + | Role::Empath { .. } + | Role::Vindicator + | Role::Weightlifter + | Role::PyreMaster { .. } + | Role::Adjudicator + | Role::Scapegoat { redeemed: true } | Role::Shapeshifter { .. } | Role::DireWolf | Role::AlphaWolf { killed: None } @@ -150,7 +221,7 @@ impl Role { Role::Apprentice(title) => village .characters() .iter() - .any(|c| c.role().title() == *title), + .any(|c| c.role_title() == *title), Role::Elder { knows_on_night, diff --git a/werewolves-server/src/game.rs b/werewolves-server/src/game.rs index f79a06b..0fa3c30 100644 --- a/werewolves-server/src/game.rs +++ b/werewolves-server/src/game.rs @@ -10,13 +10,14 @@ use crate::{ }; use tokio::time::Instant; use werewolves_proto::{ + character::Character, error::GameError, game::{Game, GameOver, Village}, message::{ ClientMessage, Identification, ServerMessage, host::{HostGameMessage, HostMessage, ServerToHostMessage}, }, - player::{Character, PlayerId}, + player::PlayerId, }; type Result = core::result::Result; @@ -70,7 +71,7 @@ impl GameRunner { if let Err(err) = self.player_sender.send_if_present( char.player_id(), ServerMessage::GameStart { - role: char.role().initial_shown_role(), + role: char.initial_shown_role(), }, ) { log::warn!( @@ -110,7 +111,7 @@ impl GameRunner { .send_if_present( player_id, ServerMessage::GameStart { - role: char.role().initial_shown_role(), + role: char.initial_shown_role(), }, ) .log_debug(); @@ -171,7 +172,7 @@ impl GameRunner { { sender .send(ServerMessage::GameStart { - role: char.role().initial_shown_role(), + role: char.initial_shown_role(), }) .log_debug(); } else if let Some(sender) = self.joined_players.get_sender(player_id).await { diff --git a/werewolves/src/clients/host/host.rs b/werewolves/src/clients/host/host.rs index f529b1e..5a6ff0c 100644 --- a/werewolves/src/clients/host/host.rs +++ b/werewolves/src/clients/host/host.rs @@ -9,6 +9,7 @@ use gloo::net::websocket::{self, futures::WebSocket}; use instant::Instant; use serde::Serialize; use werewolves_proto::{ + character::CharacterId, error::GameError, game::{GameOver, GameSettings}, message::{ @@ -19,7 +20,7 @@ use werewolves_proto::{ }, night::{ActionPrompt, ActionResult}, }, - player::{CharacterId, PlayerId}, + player::PlayerId, }; use yew::{html::Scope, prelude::*}; diff --git a/werewolves/src/components/action/picker.rs b/werewolves/src/components/action/picker.rs index edb4f61..7b66ab8 100644 --- a/werewolves/src/components/action/picker.rs +++ b/werewolves/src/components/action/picker.rs @@ -1,6 +1,6 @@ use werewolves_proto::{ + character::CharacterId, message::{CharacterIdentity, PublicIdentity}, - player::CharacterId, }; use yew::prelude::*; diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index ef8c456..f125cbe 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -1,12 +1,12 @@ use core::ops::Not; use werewolves_proto::{ + character::CharacterId, message::{ CharacterIdentity, PublicIdentity, host::{HostGameMessage, HostMessage, HostNightMessage}, night::{ActionPrompt, ActionResponse}, }, - player::CharacterId, role::PreviousGuardianAction, }; use yew::prelude::*; @@ -57,6 +57,14 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { ))); }) }); + + let cont = continue_callback.clone().map(|continue_callback| { + html! { + + } + }); let (character_id, targets, marked, role_info) = match &props.prompt { ActionPrompt::CoverOfDarkness => { return html! { @@ -72,13 +80,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }; } ActionPrompt::ElderReveal { character_id } => { - let cont = continue_callback.map(|continue_callback| { - html! { - - } - }); return html! {
{identity_html(props, Some(character_id))} @@ -91,13 +92,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { character_id, new_role, } => { - let cont = continue_callback.map(|continue_callback| { - html! { - - } - }); return html! {
{identity_html(props, Some(character_id))} @@ -108,6 +102,31 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }; } + ActionPrompt::MasonsWake { + character_id, + masons, + } => { + let masons = masons + .into_iter() + .map(|c| { + let ident: PublicIdentity = c.into(); + html! { + + } + }) + .collect::(); + return html! { +
+ {identity_html(props, Some(character_id))} +

{"these are the masons"}

+
+ {masons} +
+ {cont} +
+ }; + } + ActionPrompt::Guardian { character_id, previous, @@ -147,6 +166,92 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }; } + ActionPrompt::Adjudicator { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect::>(), + html! {{"adjudicator"}}, + ), + ActionPrompt::Beholder { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect::>(), + html! {{"beholder"}}, + ), + ActionPrompt::Empath { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect::>(), + html! {{"empath"}}, + ), + ActionPrompt::MasonLeaderRecruit { + character_id, + recruits_left, + potential_recruits, + marked, + } => ( + Some(character_id), + potential_recruits, + marked.iter().cloned().collect::>(), + html! { +
+ {"mason leader recruit"} + {"("}{recruits_left.get()}{" remaining)"} +
+ }, + ), + ActionPrompt::Mortician { + character_id, + dead_players, + marked, + } => ( + Some(character_id), + dead_players, + marked.iter().cloned().collect::>(), + html! {{"mortician"}}, + ), + ActionPrompt::PowerSeer { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect::>(), + html! {{"power seer"}}, + ), + ActionPrompt::PyreMaster { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect::>(), + html! {{"pyremaster"}}, + ), + ActionPrompt::Vindicator { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect::>(), + html! {{"vindicator"}}, + ), ActionPrompt::Seer { character_id, living_players, diff --git a/werewolves/src/components/action/result.rs b/werewolves/src/components/action/result.rs index 96ffa5a..f44ce76 100644 --- a/werewolves/src/components/action/result.rs +++ b/werewolves/src/components/action/result.rs @@ -40,36 +40,64 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { .big_screen .not() .then(|| html! {}); - match &props.result { + let body = match &props.result { + ActionResult::PowerSeer { powerful } => { + let inactive = powerful.not().then_some("inactive"); + let text = if *powerful { + "powerful" + } else { + "not powerful" + }; + html! { + <> + +

{text}

+ + } + } + ActionResult::Adjudicator { killer } => { + let inactive = killer.not().then_some("inactive"); + let text = if *killer { "killer" } else { "not a killer" }; + html! { + <> + +

{text}

+ + } + } + ActionResult::Mortician(died_to) => html! { +

{"cause of death: "}{died_to.to_string().to_case(Case::Title)}

+ }, + ActionResult::Empath { scapegoat: true } => html! { + <> +

{"was the scapegoat!"}

+

{"tag! you're it!"}

+ + }, + ActionResult::Empath { scapegoat: false } => html! { +

{"not the scapegoat"}

+ }, ActionResult::RoleBlocked => { html! { -
- {ident} -

{"you were role blocked"}

- {cont} -
+

{"you were role blocked"}

} } ActionResult::Seer(alignment) => html! { -
- {ident} + <>

{"the alignment was"}

{match alignment { Alignment::Village => "village", Alignment::Wolves => "wolfpack", }}

- {cont} -
+ }, ActionResult::Arcanist { same } => { let outcome = if *same { "same" } else { "different" }; html! { -
- {ident} + <>

{"the alignments are:"}

{outcome}

- {cont} -
+ } } ActionResult::GraveDigger(role_title) => { @@ -77,12 +105,10 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { .map(|r| r.to_string().to_case(Case::Title)) .unwrap_or_else(|| String::from("an empty grave")); html! { -
- {ident} + <>

{"you see:"}

{dig}

- {cont} -
+ } } ActionResult::GoBackToSleep => { @@ -94,17 +120,25 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { ))) } }); - html! { + return html! { {"continue"} - } + }; } ActionResult::Continue => { props.on_complete.emit(HostMessage::GetState); - html! { + return html! { - } + }; } + }; + + html! { +
+ {ident} + {body} + {cont} +
} } diff --git a/werewolves/src/components/action/target.rs b/werewolves/src/components/action/target.rs index acfe162..b28f39c 100644 --- a/werewolves/src/components/action/target.rs +++ b/werewolves/src/components/action/target.rs @@ -1,353 +1,353 @@ -use core::{fmt::Debug, ops::Not}; +// use core::{fmt::Debug, ops::Not}; -use werewolves_proto::{ - message::{CharacterIdentity, PublicIdentity}, - player::CharacterId, -}; -use yew::prelude::*; +// use werewolves_proto::{ +// message::{CharacterIdentity, PublicIdentity}, +// player::CharacterId, +// }; +// use yew::prelude::*; -use crate::components::{Button, Identity}; +// use crate::components::{Button, Identity}; -#[derive(Debug, Clone, PartialEq, Properties)] -pub struct TwoTargetProps { - pub targets: Box<[CharacterIdentity]>, - #[prop_or_default] - pub headline: &'static str, - #[prop_or_default] - pub target_selection: Option>, -} +// #[derive(Debug, Clone, PartialEq, Properties)] +// pub struct TwoTargetProps { +// pub targets: Box<[CharacterIdentity]>, +// #[prop_or_default] +// pub headline: &'static str, +// #[prop_or_default] +// pub target_selection: Option>, +// } -#[derive(Clone)] -enum TwoTargetSelection { - None, - One(CharacterId), - Two(CharacterId, CharacterId), -} +// #[derive(Clone)] +// enum TwoTargetSelection { +// None, +// One(CharacterId), +// Two(CharacterId, CharacterId), +// } -impl TwoTargetSelection { - fn is_selected(&self, id: &CharacterId) -> bool { - match self { - TwoTargetSelection::None => false, - TwoTargetSelection::One(character_id) => id == character_id, - TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2, - } - } -} +// impl TwoTargetSelection { +// fn is_selected(&self, id: &CharacterId) -> bool { +// match self { +// TwoTargetSelection::None => false, +// TwoTargetSelection::One(character_id) => id == character_id, +// TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2, +// } +// } +// } -pub struct TwoTarget(TwoTargetSelection); +// pub struct TwoTarget(TwoTargetSelection); -impl Component for TwoTarget { - type Message = CharacterId; +// impl Component for TwoTarget { +// type Message = CharacterId; - type Properties = TwoTargetProps; +// type Properties = TwoTargetProps; - fn create(_: &Context) -> Self { - Self(TwoTargetSelection::None) - } +// fn create(_: &Context) -> Self { +// Self(TwoTargetSelection::None) +// } - fn view(&self, ctx: &Context) -> Html { - let TwoTargetProps { - targets, - headline, - target_selection, - } = ctx.props(); - let mut targets = targets.clone(); - targets.sort_by(|l, r| l.number.cmp(&r.number)); +// fn view(&self, ctx: &Context) -> Html { +// let TwoTargetProps { +// targets, +// headline, +// target_selection, +// } = ctx.props(); +// let mut targets = targets.clone(); +// targets.sort_by(|l, r| l.number.cmp(&r.number)); - let target_selection = target_selection.clone(); - let scope = ctx.link().clone(); - let card_select = Callback::from(move |target| { - scope.send_message(target); - }); - let targets = targets - .iter() - .map(|t| { - html! { - - } - }) - .collect::(); - let headline = headline - .trim() - .is_empty() - .not() - .then(|| html!(

{headline}

)); +// let target_selection = target_selection.clone(); +// let scope = ctx.link().clone(); +// let card_select = Callback::from(move |target| { +// scope.send_message(target); +// }); +// let targets = targets +// .iter() +// .map(|t| { +// html! { +// +// } +// }) +// .collect::(); +// let headline = headline +// .trim() +// .is_empty() +// .not() +// .then(|| html!(

{headline}

)); - let submit = target_selection.as_ref().map(|target_selection| { - let selected = match &self.0 { - TwoTargetSelection::None | TwoTargetSelection::One(_) => None, - TwoTargetSelection::Two(t1, t2) => Some((*t1, *t2)), - }; - let target_selection = target_selection.clone(); - let disabled = selected.is_none(); - let on_click = - selected.map(|(t1, t2)| move |_| target_selection.emit((t1, t2))); - html! { -
- -
- } - }); +// let submit = target_selection.as_ref().map(|target_selection| { +// let selected = match &self.0 { +// TwoTargetSelection::None | TwoTargetSelection::One(_) => None, +// TwoTargetSelection::Two(t1, t2) => Some((*t1, *t2)), +// }; +// let target_selection = target_selection.clone(); +// let disabled = selected.is_none(); +// let on_click = +// selected.map(|(t1, t2)| move |_| target_selection.emit((t1, t2))); +// html! { +//
+// +//
+// } +// }); - html! { -
- {headline} -
- {targets} -
- {submit} -
- } - } +// html! { +//
+// {headline} +//
+// {targets} +//
+// {submit} +//
+// } +// } - fn update(&mut self, _: &Context, msg: Self::Message) -> bool { - match &self.0 { - TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg), - TwoTargetSelection::One(character_id) => { - if character_id == &msg { - self.0 = TwoTargetSelection::None - } else { - self.0 = TwoTargetSelection::Two(*character_id, msg) - } - } - TwoTargetSelection::Two(t1, t2) => { - if &msg == t1 { - self.0 = TwoTargetSelection::One(*t2); - } else if &msg == t2 { - self.0 = TwoTargetSelection::One(*t1); - } else { - self.0 = TwoTargetSelection::Two(*t1, msg); - } - } - } +// fn update(&mut self, _: &Context, msg: Self::Message) -> bool { +// match &self.0 { +// TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg), +// TwoTargetSelection::One(character_id) => { +// if character_id == &msg { +// self.0 = TwoTargetSelection::None +// } else { +// self.0 = TwoTargetSelection::Two(*character_id, msg) +// } +// } +// TwoTargetSelection::Two(t1, t2) => { +// if &msg == t1 { +// self.0 = TwoTargetSelection::One(*t2); +// } else if &msg == t2 { +// self.0 = TwoTargetSelection::One(*t1); +// } else { +// self.0 = TwoTargetSelection::Two(*t1, msg); +// } +// } +// } - true - } -} +// true +// } +// } -#[derive(Debug, Clone, PartialEq, Properties)] -pub struct OptionalSingleTargetProps { - pub targets: Box<[CharacterIdentity]>, - #[prop_or_default] - pub headline: &'static str, - #[prop_or_default] - pub target_selection: Option>>, - #[prop_or_default] - pub children: Html, -} +// #[derive(Debug, Clone, PartialEq, Properties)] +// pub struct OptionalSingleTargetProps { +// pub targets: Box<[CharacterIdentity]>, +// #[prop_or_default] +// pub headline: &'static str, +// #[prop_or_default] +// pub target_selection: Option>>, +// #[prop_or_default] +// pub children: Html, +// } -pub struct OptionalSingleTarget(Option); +// pub struct OptionalSingleTarget(Option); -impl Component for OptionalSingleTarget { - type Message = CharacterId; +// impl Component for OptionalSingleTarget { +// type Message = CharacterId; - type Properties = OptionalSingleTargetProps; +// type Properties = OptionalSingleTargetProps; - fn create(_: &Context) -> Self { - Self(None) - } +// fn create(_: &Context) -> Self { +// Self(None) +// } - fn view(&self, ctx: &Context) -> Html { - let OptionalSingleTargetProps { - targets, - headline, - target_selection, - children, - } = ctx.props(); - let mut targets = targets.clone(); - targets.sort_by(|l, r| l.number.cmp(&r.number)); +// fn view(&self, ctx: &Context) -> Html { +// let OptionalSingleTargetProps { +// targets, +// headline, +// target_selection, +// children, +// } = ctx.props(); +// let mut targets = targets.clone(); +// targets.sort_by(|l, r| l.number.cmp(&r.number)); - let target_selection = target_selection.clone(); - let scope = ctx.link().clone(); - let card_select = Callback::from(move |target| { - scope.send_message(target); - }); - let targets = targets - .iter() - .map(|t| { - html! { - - } - }) - .collect::(); - let headline = headline - .trim() - .is_empty() - .not() - .then(|| html!(

{headline}

)); +// let target_selection = target_selection.clone(); +// let scope = ctx.link().clone(); +// let card_select = Callback::from(move |target| { +// scope.send_message(target); +// }); +// let targets = targets +// .iter() +// .map(|t| { +// html! { +// +// } +// }) +// .collect::(); +// let headline = headline +// .trim() +// .is_empty() +// .not() +// .then(|| html!(

{headline}

)); - let submit = target_selection.as_ref().map(|target_selection| { - let target_selection = target_selection.clone(); - let sel = self.0; - let on_click = move |_| target_selection.emit(sel); - html! { -
- -
- } - }); +// let submit = target_selection.as_ref().map(|target_selection| { +// let target_selection = target_selection.clone(); +// let sel = self.0; +// let on_click = move |_| target_selection.emit(sel); +// html! { +//
+// +//
+// } +// }); - html! { -
- {headline} - {children.clone()} -
- {targets} -
- {submit} -
- } - } +// html! { +//
+// {headline} +// {children.clone()} +//
+// {targets} +//
+// {submit} +//
+// } +// } - fn update(&mut self, _: &Context, msg: Self::Message) -> bool { - match &self.0 { - Some(t) => { - if t == &msg { - self.0 = None - } else { - self.0 = Some(msg); - } - } - None => self.0 = Some(msg), - } - true - } -} +// fn update(&mut self, _: &Context, msg: Self::Message) -> bool { +// match &self.0 { +// Some(t) => { +// if t == &msg { +// self.0 = None +// } else { +// self.0 = Some(msg); +// } +// } +// None => self.0 = Some(msg), +// } +// true +// } +// } -#[derive(Debug, Clone, PartialEq, Properties)] -pub struct SingleTargetProps { - pub targets: Box<[CharacterIdentity]>, - #[prop_or_default] - pub headline: &'static str, - #[prop_or_default] - pub target_selection: Option>, - #[prop_or_default] - pub children: Html, -} +// #[derive(Debug, Clone, PartialEq, Properties)] +// pub struct SingleTargetProps { +// pub targets: Box<[CharacterIdentity]>, +// #[prop_or_default] +// pub headline: &'static str, +// #[prop_or_default] +// pub target_selection: Option>, +// #[prop_or_default] +// pub children: Html, +// } -pub struct SingleTarget { - selected: Option, -} +// pub struct SingleTarget { +// selected: Option, +// } -impl Component for SingleTarget { - type Message = CharacterId; +// impl Component for SingleTarget { +// type Message = CharacterId; - type Properties = SingleTargetProps; +// type Properties = SingleTargetProps; - fn create(_: &Context) -> Self { - Self { selected: None } - } +// fn create(_: &Context) -> Self { +// Self { selected: None } +// } - fn view(&self, ctx: &Context) -> Html { - let SingleTargetProps { - headline, - targets, - target_selection, - children, - } = ctx.props(); - let mut targets = targets.clone(); - targets.sort_by(|l, r| l.number.cmp(&r.number)); - let target_selection = target_selection.clone(); - let scope = ctx.link().clone(); - let card_select = Callback::from(move |target| { - scope.send_message(target); - }); - let targets = targets - .iter() - .map(|t| { - html! { - - } - }) - .collect::(); - let headline = headline - .trim() - .is_empty() - .not() - .then(|| html!(

{headline}

)); +// fn view(&self, ctx: &Context) -> Html { +// let SingleTargetProps { +// headline, +// targets, +// target_selection, +// children, +// } = ctx.props(); +// let mut targets = targets.clone(); +// targets.sort_by(|l, r| l.number.cmp(&r.number)); +// let target_selection = target_selection.clone(); +// let scope = ctx.link().clone(); +// let card_select = Callback::from(move |target| { +// scope.send_message(target); +// }); +// let targets = targets +// .iter() +// .map(|t| { +// html! { +// +// } +// }) +// .collect::(); +// let headline = headline +// .trim() +// .is_empty() +// .not() +// .then(|| html!(

{headline}

)); - let submit = target_selection.as_ref().map(|target_selection| { - let disabled = self.selected.is_none().then_some("pick a target"); - let target_selection = target_selection.clone(); - let on_click = self - .selected - .map(|t| Callback::from(move |_| target_selection.emit(t))) - .unwrap_or_default(); - html! { -
- -
- } - }); +// let submit = target_selection.as_ref().map(|target_selection| { +// let disabled = self.selected.is_none().then_some("pick a target"); +// let target_selection = target_selection.clone(); +// let on_click = self +// .selected +// .map(|t| Callback::from(move |_| target_selection.emit(t))) +// .unwrap_or_default(); +// html! { +//
+// +//
+// } +// }); - html! { -
- {headline} - {children.clone()} -
- {targets} -
- {submit} -
- } - } +// html! { +//
+// {headline} +// {children.clone()} +//
+// {targets} +//
+// {submit} +//
+// } +// } - fn update(&mut self, _: &Context, msg: Self::Message) -> bool { - match &self.selected { - Some(current) => { - if current == &msg { - self.selected = None; - } else { - self.selected = Some(msg); - } - } - None => self.selected = Some(msg), - } - true - } -} +// fn update(&mut self, _: &Context, msg: Self::Message) -> bool { +// match &self.selected { +// Some(current) => { +// if current == &msg { +// self.selected = None; +// } else { +// self.selected = Some(msg); +// } +// } +// None => self.selected = Some(msg), +// } +// true +// } +// } -#[derive(Debug, Clone, PartialEq, Properties)] -pub struct TargetCardProps { - pub target: CharacterIdentity, - pub selected: bool, - pub on_select: Callback, -} +// #[derive(Debug, Clone, PartialEq, Properties)] +// pub struct TargetCardProps { +// pub target: CharacterIdentity, +// pub selected: bool, +// pub on_select: Callback, +// } -#[function_component] -fn TargetCard(props: &TargetCardProps) -> Html { - let character_id = props.target.character_id; - let on_select = props.on_select.clone(); - let on_click = Callback::from(move |_| on_select.emit(character_id)); +// #[function_component] +// fn TargetCard(props: &TargetCardProps) -> Html { +// let character_id = props.target.character_id; +// let on_select = props.on_select.clone(); +// let on_click = Callback::from(move |_| on_select.emit(character_id)); - let marked = props.selected.then_some("marked"); - let ident: PublicIdentity = props.target.clone().into(); - html! { - - } -} +// let marked = props.selected.then_some("marked"); +// let ident: PublicIdentity = props.target.clone().into(); +// html! { +// +// } +// } diff --git a/werewolves/src/components/host/daytime.rs b/werewolves/src/components/host/daytime.rs index f035c6a..45bfde7 100644 --- a/werewolves/src/components/host/daytime.rs +++ b/werewolves/src/components/host/daytime.rs @@ -1,8 +1,8 @@ use core::{num::NonZeroU8, ops::Not}; use werewolves_proto::{ + character::CharacterId, message::{CharacterState, PublicIdentity}, - player::CharacterId, }; use yew::prelude::*; diff --git a/werewolves/src/components/host/setup.rs b/werewolves/src/components/host/setup.rs index df354f1..87dccc3 100644 --- a/werewolves/src/components/host/setup.rs +++ b/werewolves/src/components/host/setup.rs @@ -1,6 +1,7 @@ use core::ops::Not; use std::collections::HashMap; +use convert_case::{Case, Casing}; use rand::Rng; use werewolves_proto::{ game::{Category, GameSettings, SetupRole, SetupRoleTitle}, @@ -133,7 +134,7 @@ pub fn SetupCategory(
{count} - {r.to_string()} + {r.to_string().to_case(Case::Title)}
@@ -153,7 +154,9 @@ pub fn SetupCategory( html! {
{roles_count} -
{category.to_string()}
+
+ {category.to_string().to_case(Case::Title)} +
{all_roles}
diff --git a/werewolves/src/components/settings.rs b/werewolves/src/components/settings.rs index bc7a6a4..512e748 100644 --- a/werewolves/src/components/settings.rs +++ b/werewolves/src/components/settings.rs @@ -378,6 +378,59 @@ fn setup_options_for_slot( slot_field_open: UseStateHandle, ) -> Html { let setup_options_for_role = match &slot.role { + SetupRole::MasonLeader { recruits_available } => { + let next = { + let mut s = slot.clone(); + match &mut s.role { + SetupRole::MasonLeader { recruits_available } => { + *recruits_available = + NonZeroU8::new(recruits_available.get().checked_add(1).unwrap_or(1)) + .unwrap() + } + _ => unreachable!(), + } + s + }; + let prev = recruits_available + .get() + .checked_sub(1) + .and_then(NonZeroU8::new) + .map(|new_avail| { + let mut s = slot.clone(); + match &mut s.role { + SetupRole::MasonLeader { recruits_available } => { + *recruits_available = new_avail + } + _ => unreachable!(), + } + s + }); + let increment_update = update.clone(); + let on_increment = Callback::from(move |_| { + increment_update.emit(SettingSlotAction::Update(next.clone())) + }); + + let decrement_update = update.clone(); + let on_decrement = prev + .clone() + .map(|prev| { + Callback::from(move |_| { + decrement_update.emit(SettingSlotAction::Update(prev.clone())) + }) + }) + .unwrap_or_default(); + let decrement_disabled_reason = prev.is_none().then_some("at minimum"); + Some(html! { + <> + +
+ + + +
+ + }) + } SetupRole::Scapegoat { redeemed } => { let next = { let next_redeemed = match redeemed {