more roles, no longer exposing role through char

This commit is contained in:
emilis 2025-10-06 20:45:15 +01:00
parent 0889acca6a
commit a8506c5881
No known key found for this signature in database
33 changed files with 2289 additions and 1037 deletions

View File

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

View File

@ -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<Self> {
// let type_path = input.parse::<syn::TypePath>()?;
// let matching = input.parse::<syn::PatStruct>()?;
let name = input.parse::<syn::Ident>()?;
// panic!("{type_path:?}\n\n{matching:?}");
Ok(Self { name })
}
}
impl ToTokens for RefAndMut {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(quote! {});
}
}

View File

@ -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<T> = core::result::Result<T, GameError>;
#[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<Modifier>,
died_to: Option<DiedTo>,
role_changes: Vec<RoleChange>,
}
impl Character {
pub fn new(
Identification {
player_id,
public:
PublicIdentity {
name,
pronouns,
number,
},
}: Identification,
role: Role,
) -> Option<Self> {
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<RoleTitle> {
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<Box<[ActionPrompt]>> {
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::<Result<Box<[CharacterIdentity]>>>()?,
})
.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<Hunter<'a>> {
match &self.role {
Role::Hunter { target } => Ok(Hunter(target)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Hunter,
got: self.role_title(),
}),
}
}
pub const fn hunter_mut<'a>(&'a mut self) -> Result<HunterMut<'a>> {
let title = self.role.title();
match &mut self.role {
Role::Hunter { target } => Ok(HunterMut(target)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Hunter,
got: title,
}),
}
}
pub const fn shapeshifter<'a>(&'a self) -> Result<Shapeshifter<'a>> {
match &self.role {
Role::Shapeshifter { shifted_into } => Ok(Shapeshifter(shifted_into)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Shapeshifter,
got: self.role_title(),
}),
}
}
pub const fn shapeshifter_mut<'a>(&'a mut self) -> Result<ShapeshifterMut<'a>> {
let title = self.role.title();
match &mut self.role {
Role::Shapeshifter { shifted_into } => Ok(ShapeshifterMut(shifted_into)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Shapeshifter,
got: title,
}),
}
}
pub const fn mason_leader<'a>(&'a self) -> Result<MasonLeader<'a>> {
match &self.role {
Role::MasonLeader {
recruits_available,
recruits,
} => Ok(MasonLeader(recruits_available, recruits)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::MasonLeader,
got: self.role_title(),
}),
}
}
pub const fn mason_leader_mut<'a>(&'a mut self) -> Result<MasonLeaderMut<'a>> {
let title = self.role.title();
match &mut self.role {
Role::MasonLeader {
recruits_available,
recruits,
} => Ok(MasonLeaderMut(recruits_available, recruits)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::MasonLeader,
got: title,
}),
}
}
pub const fn scapegoat<'a>(&'a self) -> Result<Scapegoat<'a>> {
match &self.role {
Role::Scapegoat { redeemed } => Ok(Scapegoat(redeemed)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Scapegoat,
got: self.role_title(),
}),
}
}
pub const fn scapegoat_mut<'a>(&'a mut self) -> Result<ScapegoatMut<'a>> {
let title = self.role.title();
match &mut self.role {
Role::Scapegoat { redeemed } => Ok(ScapegoatMut(redeemed)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Scapegoat,
got: title,
}),
}
}
pub const fn empath<'a>(&'a self) -> Result<Empath<'a>> {
match &self.role {
Role::Empath { cursed } => Ok(Empath(cursed)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Empath,
got: self.role_title(),
}),
}
}
pub const fn empath_mut<'a>(&'a mut self) -> Result<EmpathMut<'a>> {
let title = self.role.title();
match &mut self.role {
Role::Empath { cursed } => Ok(EmpathMut(cursed)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Empath,
got: title,
}),
}
}
pub const fn 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<CharacterId>;
Shapeshifter, ShapeshifterMut: Option<CharacterId>;
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;
}
}
}

View File

@ -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<DiedTo>,
night: NonZeroU8,
},
PyreMaster {
killer: CharacterId,
night: NonZeroU8,
},
MasonLeaderRecruitFail {
tried_recruiting: CharacterId,
night: u8,
},
}
impl DiedTo {
pub const fn killer(&self) -> Option<CharacterId> {
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 },
}
}
}

View File

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

View File

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

View File

@ -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<NightChange>,
pub unless: Option<Unless>,
}
impl From<ActionComplete> for ResponseOutcome {
fn from(value: ActionComplete) -> Self {
ResponseOutcome::ActionComplete(value)
}
}
impl ActionPrompt {
fn unless(&self) -> Option<Unless> {
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<ActionPrompt>,
used_actions: Vec<ActionPrompt>,
used_actions: Vec<(ActionPrompt, ActionResult)>,
changes: Vec<NightChange>,
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::<Result<Box<[_]>>>()?
.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<ActionResult> {
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,11 +679,9 @@ impl Night {
) -> Result<BlockResolvedOutcome> {
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)),
}) => {
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: _,
@ -528,11 +698,7 @@ impl Night {
Ok(BlockResolvedOutcome::ActionComplete(result, change))
}
}
ResponseOutcome::ActionComplete(ActionComplete {
result,
change,
unless: Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)),
}) => {
Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => {
if self.changes.iter().any(|c| match c {
NightChange::RoleBlock {
source: _,
@ -549,12 +715,9 @@ impl Night {
Ok(BlockResolvedOutcome::ActionComplete(result, change))
}
}
ResponseOutcome::ActionComplete(ActionComplete {
result,
change,
unless: None,
}) => Ok(BlockResolvedOutcome::ActionComplete(result, change)),
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

View File

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

View File

@ -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<SetupRole> 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<RoleTitle> 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,
}
}
}

View File

@ -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::<Box<[_]>>();
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<CharacterIdentity> {
pub fn target_by_id(&self, character_id: CharacterId) -> Result<CharacterIdentity> {
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()
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,
},
}
}
}

View File

@ -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 {
assert_eq!(
self.process(HostGameMessage::Day(HostDayMessage::Execute))
.unwrap()
.prompt()
.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: _,

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
mod elder;
mod mason;
mod scapegoat;
mod shapeshifter;

View File

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

View File

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

View File

@ -1,5 +1,6 @@
#![allow(clippy::new_without_default)]
pub mod character;
pub mod diedto;
pub mod error;
pub mod game;

View File

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

View File

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

View File

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

View File

@ -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<CharacterId>,
},
#[checks(ActionType::Other)]
#[checks(ActionType::Intel)]
Arcanist {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: (Option<CharacterId>, Option<CharacterId>),
},
#[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<CharacterId>,
},
#[checks(ActionType::Intel)]
Adjudicator {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Intel)]
PowerSeer {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Intel)]
Mortician {
character_id: CharacterIdentity,
dead_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Beholder)]
Beholder {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[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<CharacterId>,
},
#[checks(ActionType::Intel)]
Empath {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Protect)]
Vindicator {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Other)]
PyreMaster {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[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<ActionPrompt> {
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<CharacterId>, Option<CharacterId>),
// Gravedigger(CharacterId),
// Hunter(CharacterId),
// Militia(Option<CharacterId>),
// MapleWolf(Option<CharacterId>),
// Guardian(CharacterId),
// WolfPackKillVote(CharacterId),
// AlphaWolf(Option<CharacterId>),
// 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<RoleTitle>),
Mortician(DiedToTitle),
Empath { scapegoat: bool },
GoBackToSleep,
Continue,
}

View File

@ -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<Modifier>,
died_to: Option<DiedTo>,
role_changes: Vec<RoleChange>,
}
impl Character {
pub fn new(
Identification {
player_id,
public:
PublicIdentity {
name,
pronouns,
number,
},
}: Identification,
role: Role,
) -> Option<Self> {
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<RoleTitle> {
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<Option<ActionPrompt>, 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,
}

View File

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

View File

@ -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<T> = core::result::Result<T, GameError>;
@ -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 {

View File

@ -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::*};

View File

@ -1,6 +1,6 @@
use werewolves_proto::{
character::CharacterId,
message::{CharacterIdentity, PublicIdentity},
player::CharacterId,
};
use yew::prelude::*;

View File

@ -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! {
<Button on_click={continue_callback}>
{"continue"}
</Button>
}
});
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! {
<Button on_click={continue_callback}>
{"continue"}
</Button>
}
});
return html! {
<div class="role-change">
{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! {
<Button on_click={continue_callback}>
{"continue"}
</Button>
}
});
return html! {
<div class="role-change">
{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! {
<Identity ident={ident}/>
}
})
.collect::<Html>();
return html! {
<div class="masons">
{identity_html(props, Some(character_id))}
<h2>{"these are the masons"}</h2>
<div class="mason-list">
{masons}
</div>
{cont}
</div>
};
}
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::<Box<[CharacterId]>>(),
html! {{"adjudicator"}},
),
ActionPrompt::Beholder {
character_id,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {{"beholder"}},
),
ActionPrompt::Empath {
character_id,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {{"empath"}},
),
ActionPrompt::MasonLeaderRecruit {
character_id,
recruits_left,
potential_recruits,
marked,
} => (
Some(character_id),
potential_recruits,
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {
<div>
<span>{"mason leader recruit"}</span>
<span>{"("}{recruits_left.get()}{" remaining)"}</span>
</div>
},
),
ActionPrompt::Mortician {
character_id,
dead_players,
marked,
} => (
Some(character_id),
dead_players,
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {{"mortician"}},
),
ActionPrompt::PowerSeer {
character_id,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {{"power seer"}},
),
ActionPrompt::PyreMaster {
character_id,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {{"pyremaster"}},
),
ActionPrompt::Vindicator {
character_id,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {{"vindicator"}},
),
ActionPrompt::Seer {
character_id,
living_players,

View File

@ -40,36 +40,64 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
.big_screen
.not()
.then(|| html! {<Button on_click={on_complete}>{"continue"}</Button>});
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! {
<>
<img src="/img/powerful.svg" class={classes!(inactive)}/>
<h3>{text}</h3>
</>
}
}
ActionResult::Adjudicator { killer } => {
let inactive = killer.not().then_some("inactive");
let text = if *killer { "killer" } else { "not a killer" };
html! {
<>
<img src="/img/killer.svg" class={classes!(inactive)}/>
<h3>{text}</h3>
</>
}
}
ActionResult::Mortician(died_to) => html! {
<h2>{"cause of death: "}{died_to.to_string().to_case(Case::Title)}</h2>
},
ActionResult::Empath { scapegoat: true } => html! {
<>
<h2>{"was the scapegoat!"}</h2>
<h3>{"tag! you're it!"}</h3>
</>
},
ActionResult::Empath { scapegoat: false } => html! {
<h2>{"not the scapegoat"}</h2>
},
ActionResult::RoleBlocked => {
html! {
<div class="result">
{ident}
<h2>{"you were role blocked"}</h2>
{cont}
</div>
}
}
ActionResult::Seer(alignment) => html! {
<div class="result">
{ident}
<>
<h2>{"the alignment was"}</h2>
<p>{match alignment {
Alignment::Village => "village",
Alignment::Wolves => "wolfpack",
}}</p>
{cont}
</div>
</>
},
ActionResult::Arcanist { same } => {
let outcome = if *same { "same" } else { "different" };
html! {
<div class="result">
{ident}
<>
<h2>{"the alignments are:"}</h2>
<p>{outcome}</p>
{cont}
</div>
</>
}
}
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! {
<div class="result">
{ident}
<>
<h2>{"you see:"}</h2>
<p>{dig}</p>
{cont}
</div>
</>
}
}
ActionResult::GoBackToSleep => {
@ -94,17 +120,25 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
)))
}
});
html! {
return html! {
<CoverOfDarkness message={"go to sleep"} next={next}>
{"continue"}
</CoverOfDarkness>
}
};
}
ActionResult::Continue => {
props.on_complete.emit(HostMessage::GetState);
html! {
return html! {
<CoverOfDarkness />
}
}
};
}
};
html! {
<div class="result">
{ident}
{body}
{cont}
</div>
}
}

View File

@ -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<Callback<(CharacterId, CharacterId)>>,
}
// #[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<Callback<(CharacterId, CharacterId)>>,
// }
#[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 {
Self(TwoTargetSelection::None)
}
// fn create(_: &Context<Self>) -> Self {
// Self(TwoTargetSelection::None)
// }
fn view(&self, ctx: &Context<Self>) -> 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<Self>) -> 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! {
<TargetCard
target={t.clone()}
selected={self.0.is_selected(&t.character_id)}
on_select={card_select.clone()}
/>
}
})
.collect::<Html>();
let headline = headline
.trim()
.is_empty()
.not()
.then(|| html!(<h2>{headline}</h2>));
// 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! {
// <TargetCard
// target={t.clone()}
// selected={self.0.is_selected(&t.character_id)}
// on_select={card_select.clone()}
// />
// }
// })
// .collect::<Html>();
// let headline = headline
// .trim()
// .is_empty()
// .not()
// .then(|| html!(<h2>{headline}</h2>));
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! {
<div class="button-container sp-ace">
<button
disabled={disabled}
onclick={on_click}
>
{"submit"}
</button>
</div>
}
});
// 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! {
// <div class="button-container sp-ace">
// <button
// disabled={disabled}
// onclick={on_click}
// >
// {"submit"}
// </button>
// </div>
// }
// });
html! {
<div class="column-list">
{headline}
<div class="row-list">
{targets}
</div>
{submit}
</div>
}
}
// html! {
// <div class="column-list">
// {headline}
// <div class="row-list">
// {targets}
// </div>
// {submit}
// </div>
// }
// }
fn update(&mut self, _: &Context<Self>, 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<Self>, 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<Callback<Option<CharacterId>>>,
#[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<Callback<Option<CharacterId>>>,
// #[prop_or_default]
// pub children: Html,
// }
pub struct OptionalSingleTarget(Option<CharacterId>);
// pub struct OptionalSingleTarget(Option<CharacterId>);
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 {
Self(None)
}
// fn create(_: &Context<Self>) -> Self {
// Self(None)
// }
fn view(&self, ctx: &Context<Self>) -> 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<Self>) -> 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! {
<TargetCard
target={t.clone()}
selected={self.0.as_ref().map(|c| c == &t.character_id).unwrap_or_default()}
on_select={card_select.clone()}
/>
}
})
.collect::<Html>();
let headline = headline
.trim()
.is_empty()
.not()
.then(|| html!(<h2>{headline}</h2>));
// 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! {
// <TargetCard
// target={t.clone()}
// selected={self.0.as_ref().map(|c| c == &t.character_id).unwrap_or_default()}
// on_select={card_select.clone()}
// />
// }
// })
// .collect::<Html>();
// let headline = headline
// .trim()
// .is_empty()
// .not()
// .then(|| html!(<h2>{headline}</h2>));
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! {
<div class="button-container sp-ace">
<button
onclick={on_click}
>
{"submit"}
</button>
</div>
}
});
// 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! {
// <div class="button-container sp-ace">
// <button
// onclick={on_click}
// >
// {"submit"}
// </button>
// </div>
// }
// });
html! {
<div class="column-list">
{headline}
{children.clone()}
<div class="row-list">
{targets}
</div>
{submit}
</div>
}
}
// html! {
// <div class="column-list">
// {headline}
// {children.clone()}
// <div class="row-list">
// {targets}
// </div>
// {submit}
// </div>
// }
// }
fn update(&mut self, _: &Context<Self>, 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<Self>, 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<Callback<CharacterId>>,
#[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<Callback<CharacterId>>,
// #[prop_or_default]
// pub children: Html,
// }
pub struct SingleTarget {
selected: Option<CharacterId>,
}
// pub struct SingleTarget {
// selected: Option<CharacterId>,
// }
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 {
Self { selected: None }
}
// fn create(_: &Context<Self>) -> Self {
// Self { selected: None }
// }
fn view(&self, ctx: &Context<Self>) -> 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! {
<TargetCard
target={t.clone()}
selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
on_select={card_select.clone()}
/>
}
})
.collect::<Html>();
let headline = headline
.trim()
.is_empty()
.not()
.then(|| html!(<h2>{headline}</h2>));
// fn view(&self, ctx: &Context<Self>) -> 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! {
// <TargetCard
// target={t.clone()}
// selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
// on_select={card_select.clone()}
// />
// }
// })
// .collect::<Html>();
// let headline = headline
// .trim()
// .is_empty()
// .not()
// .then(|| html!(<h2>{headline}</h2>));
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! {
<div class="button-container sp-ace">
<Button disabled_reason={disabled} on_click={on_click}>
{"submit"}
</Button>
</div>
}
});
// 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! {
// <div class="button-container sp-ace">
// <Button disabled_reason={disabled} on_click={on_click}>
// {"submit"}
// </Button>
// </div>
// }
// });
html! {
<div class="character-picker">
{headline}
{children.clone()}
<div class="row-list">
{targets}
</div>
{submit}
</div>
}
}
// html! {
// <div class="character-picker">
// {headline}
// {children.clone()}
// <div class="row-list">
// {targets}
// </div>
// {submit}
// </div>
// }
// }
fn update(&mut self, _: &Context<Self>, 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<Self>, 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<CharacterId>,
}
// #[derive(Debug, Clone, PartialEq, Properties)]
// pub struct TargetCardProps {
// pub target: CharacterIdentity,
// pub selected: bool,
// pub on_select: Callback<CharacterId>,
// }
#[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! {
<Button on_click={on_click} classes={classes!(marked, "character")}>
<Identity ident={ident}/>
</Button>
}
}
// let marked = props.selected.then_some("marked");
// let ident: PublicIdentity = props.target.clone().into();
// html! {
// <Button on_click={on_click} classes={classes!(marked, "character")}>
// <Identity ident={ident}/>
// </Button>
// }
// }

View File

@ -1,8 +1,8 @@
use core::{num::NonZeroU8, ops::Not};
use werewolves_proto::{
character::CharacterId,
message::{CharacterState, PublicIdentity},
player::CharacterId,
};
use yew::prelude::*;

View File

@ -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(
<div class={classes!("slot")}>
<div class={classes!("role", wakes, r.category().class())}>
{count}
{r.to_string()}
{r.to_string().to_case(Case::Title)}
</div>
<div class="attributes">
<div class="alignment">
@ -153,7 +154,9 @@ pub fn SetupCategory(
html! {
<div class="category">
{roles_count}
<div class={classes!("title", category.class())}>{category.to_string()}</div>
<div class={classes!("title", category.class())}>
{category.to_string().to_case(Case::Title)}
</div>
<div class={classes!("category-list")}>
{all_roles}
</div>

View File

@ -378,6 +378,59 @@ fn setup_options_for_slot(
slot_field_open: UseStateHandle<bool>,
) -> 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! {
<>
<label>{"recruits"}</label>
<div class={classes!("increment-decrement")}>
<Button on_click={on_decrement} disabled_reason={decrement_disabled_reason}>{"-"}</Button>
<label>{recruits_available.get().to_string()}</label>
<Button on_click={on_increment}>{"+"}</Button>
</div>
</>
})
}
SetupRole::Scapegoat { redeemed } => {
let next = {
let next_redeemed = match redeemed {