werewolves/werewolves-proto/src/character.rs

837 lines
28 KiB
Rust
Raw Normal View History

use core::{fmt::Display, num::NonZeroU8, ops::Not};
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use crate::{
diedto::DiedTo,
error::GameError,
game::{GameTime, Village},
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
modifier::Modifier,
player::{PlayerId, RoleChange},
role::{
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful,
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, PartialEq, 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 {
2025-10-06 21:59:44 +01:00
!matches!(&self.role, Role::Scapegoat { .. } | Role::Villager)
}
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) {
2025-10-06 21:59:44 +01:00
if self.died_to.is_some() {
return;
}
match (&mut self.role, died_to.date_time()) {
(Role::BlackKnight { attacked }, GameTime::Night { .. }) => {
2025-10-06 21:59:44 +01:00
attacked.replace(died_to);
return;
}
(
Role::Elder {
lost_protection_night: Some(_),
..
},
_,
) => {}
(
Role::Elder {
lost_protection_night,
..
},
GameTime::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: GameTime) -> 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 {
GameTime::Day { number: _ } => return Err(GameError::NotNight),
GameTime::Night { number } => number,
},
});
Ok(())
}
2025-10-07 21:18:31 +01:00
#[cfg(test)]
pub fn role_changes(&self) -> &[RoleChange] {
&self.role_changes
}
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,
..
}
)
}
fn mason_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
if !self.role.wakes(village) {
return Ok(Box::new([]));
}
let (recruits, recruits_available) = match &self.role {
Role::MasonLeader {
recruits,
recruits_available,
} => (recruits, *recruits_available),
_ => {
return Err(GameError::InvalidRole {
expected: RoleTitle::MasonLeader,
got: self.role_title(),
});
}
};
let recruits = recruits
.iter()
.filter_map(|r| village.character_by_id(*r).ok())
.filter_map(|c| c.is_village().then_some(c.identity()))
.chain((self.is_village() && self.alive()).then_some(self.identity()))
.collect::<Box<[_]>>();
Ok(recruits
.is_empty()
.not()
.then_some(ActionPrompt::MasonsWake {
leader: self.identity(),
masons: recruits.clone(),
})
.into_iter()
.chain(
self.alive()
.then_some(())
.and_then(|_| 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.iter().any(|r| r.character_id == c.character_id))
.collect(),
marked: None,
}),
)
.collect())
}
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
if self.mason_leader().is_ok() {
return self.mason_prompts(village);
}
if !self.alive() || !self.role.wakes(village) {
return Ok(Box::new([]));
}
let night = match village.time() {
GameTime::Day { number: _ } => return Err(GameError::NotNight),
GameTime::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::Insomniac => ActionPrompt::Insomniac {
character_id: self.identity(),
},
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.time() {
GameTime::Day { number: _ } => return Ok(Box::new([])),
GameTime::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() {
GameTime::Day { number } => number.get() + 1 >= current_night,
GameTime::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.time() {
GameTime::Day { number: _ } => return Ok(Box::new([])),
GameTime::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 {
last_blocked: Some(last_blocked),
} => ActionPrompt::DireWolf {
character_id: self.identity(),
living_players: village
.living_players_excluding(self.character_id())
.into_iter()
.filter(|c| c.character_id != *last_blocked)
.collect(),
marked: None,
},
Role::DireWolf { .. } => ActionPrompt::DireWolf {
character_id: self.identity(),
living_players: village.living_players_excluding(self.character_id()),
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(),
2025-10-07 01:47:59 +01:00
dead_players: {
let dead = village.dead_targets();
if dead.is_empty() {
return Ok(Box::new([]));
}
dead
},
marked: None,
},
Role::Beholder => ActionPrompt::Beholder {
character_id: self.identity(),
living_players: village.living_players_excluding(self.character_id()),
marked: None,
},
Role::MasonLeader { .. } => {
log::error!(
"night_action_prompts got to MasonLeader, should be handled before the living check"
);
return Ok(Box::new([]));
}
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.time() {
GameTime::Day { .. } => {
log::error!(
"vindicator trying to get a prompt during the day? village state: {village:?}"
);
return Ok(Box::new([]));
}
GameTime::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,
},
2025-10-07 02:52:06 +01:00
Role::LoneWolf => ActionPrompt::LoneWolfKill {
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 killing_wolf_order(&self) -> Option<KillingWolfOrder> {
self.role.killing_wolf_order()
}
pub const fn killer(&self) -> Killer {
if let Role::Empath { cursed: true } = &self.role {
return Killer::Killer;
}
self.role.killer()
}
pub const fn powerful(&self) -> Powerful {
if let Role::Empath { cursed: true } = &self.role {
return Powerful::Powerful;
}
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,
}),
}
}
2025-10-06 21:59:44 +01:00
pub const fn black_knight<'a>(&'a self) -> Result<BlackKnight<'a>> {
match &self.role {
Role::BlackKnight { attacked } => Ok(BlackKnight(attacked)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::BlackKnight,
got: self.role_title(),
}),
}
}
pub const fn black_knight_kill<'a>(&'a mut self) -> Result<BlackKnightKill<'a>> {
let title = self.role.title();
match &self.role {
Role::BlackKnight { attacked } => Ok(BlackKnightKill {
attacked,
died_to: &mut self.died_to,
}),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::BlackKnight,
got: title,
}),
}
}
pub const fn black_knight_mut<'a>(&'a mut self) -> Result<BlackKnightMut<'a>> {
let title = self.role.title();
match &mut self.role {
Role::BlackKnight { attacked } => Ok(BlackKnightMut(attacked)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::BlackKnight,
got: title,
}),
}
}
pub const fn guardian<'a>(&'a self) -> Result<Guardian<'a>> {
let title = self.role.title();
match &self.role {
Role::Guardian { last_protected } => Ok(Guardian(last_protected)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Guardian,
got: title,
}),
}
}
pub const fn guardian_mut<'a>(&'a mut self) -> Result<GuardianMut<'a>> {
let title = self.role.title();
match &mut self.role {
Role::Guardian { last_protected } => Ok(GuardianMut(last_protected)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::Guardian,
got: title,
}),
}
}
pub const fn direwolf<'a>(&'a self) -> Result<Direwolf<'a>> {
let title = self.role.title();
match &self.role {
Role::DireWolf { last_blocked } => Ok(Direwolf(last_blocked)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::DireWolf,
got: title,
}),
}
}
pub const fn direwolf_mut<'a>(&'a mut self) -> Result<DirewolfMut<'a>> {
let title = self.role.title();
match &mut self.role {
Role::DireWolf { last_blocked } => Ok(DirewolfMut(last_blocked)),
_ => Err(GameError::InvalidRole {
expected: RoleTitle::DireWolf,
got: title,
}),
}
}
pub const fn 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;
2025-10-06 21:59:44 +01:00
BlackKnight, BlackKnightMut: Option<DiedTo>;
Guardian, GuardianMut: Option<PreviousGuardianAction>;
Direwolf, DirewolfMut: Option<CharacterId>;
);
2025-10-06 21:59:44 +01:00
pub struct BlackKnightKill<'a> {
attacked: &'a Option<DiedTo>,
died_to: &'a mut Option<DiedTo>,
}
impl BlackKnightKill<'_> {
pub fn kill(self) {
if let Some(attacked) = self.attacked.as_ref().and_then(|a| a.next_night())
&& self.died_to.is_none()
{
self.died_to.replace(attacked.clone());
}
}
}
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;
}
}
}