// Copyright (C) 2025 Emilis Bliūdžius // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use core::{ fmt::Display, num::NonZeroU8, ops::{Deref, Not}, }; use rand::seq::SliceRandom; use serde::{Deserialize, Serialize}; use crate::{ aura::{Aura, AuraTitle, Auras}, diedto::DiedTo, error::GameError, game::{GameTime, Village}, message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, player::{PlayerId, RoleChange}, role::{ Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful, PreviousGuardianAction, Role, RoleTitle, }, }; type Result = core::result::Result; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct CharacterId(uuid::Uuid); impl CharacterId { pub fn new() -> Self { Self(uuid::Uuid::new_v4()) } pub const fn from_u128(v: u128) -> Self { Self(uuid::Uuid::from_u128(v)) } } impl Display for CharacterId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Character { player_id: PlayerId, identity: CharacterIdentity, role: Role, auras: Auras, died_to: Option, role_changes: Vec, } impl Character { pub fn new( Identification { player_id, public: PublicIdentity { name, pronouns, number, }, }: Identification, role: Role, auras: Vec, ) -> Option { Some(Self { role, player_id, died_to: None, auras: Auras::new(auras), role_changes: Vec::new(), identity: CharacterIdentity { character_id: CharacterId::new(), name, pronouns, number: number?, }, }) } pub const fn scapegoat_can_redeem_into(&self) -> bool { !self.role.wolf() && !matches!( &self.role, Role::Scapegoat { .. } | Role::Villager | Role::Apprentice(_) ) } pub fn identity(&self) -> CharacterIdentity { 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) { if self.died_to.is_some() { return; } match (&mut self.role, died_to.date_time()) { (Role::BlackKnight { attacked }, GameTime::Night { .. }) => { 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 { match &self.role { Role::Shapeshifter { shifted_into: Some(_), } => None, _ => Some(self.role.title()), } } pub fn elder_reveal(&mut self) { if let Role::Elder { woken_for_reveal, .. } = &mut self.role { *woken_for_reveal = true } } pub fn purge_expired_auras(&mut self, village: &Village) -> Box<[Aura]> { self.auras.purge_expired(village) } pub fn auras(&self) -> &[Aura] { self.auras.list() } pub fn auras_mut(&mut self) -> &mut [Aura] { self.auras.list_mut() } 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(()) } #[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> { if !self.role.wakes(village) { return Ok(Vec::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::>(); 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()) } /// Returns a copy of this character with their role replaced /// in a read-only type pub fn as_role(&self, role: Role) -> AsCharacter { let mut char = self.clone(); char.role = role; AsCharacter(char) } pub fn apply_aura(&mut self, aura: Aura) { self.auras.add(aura); } pub fn remove_aura(&mut self, aura: AuraTitle) { self.auras.remove_aura(aura); } pub fn night_action_prompts(&self, village: &Village) -> Result> { let mut prompts = Vec::new(); if let Role::MasonLeader { .. } = &self.role { // add them here so masons wake up even with a dead leader prompts.append(&mut self.mason_prompts(village)?); } let night = match village.time() { GameTime::Day { number: _ } => return Err(GameError::NotNight), GameTime::Night { number } => number, }; if night == 0 && self.auras.list().contains(&Aura::Traitor) { log::info!("adding traitor prompt for {}", self.identity()); prompts.push(ActionPrompt::TraitorIntro { character_id: self.identity(), }); } if !self.alive() || !self.role.wakes(village) { return Ok(prompts.into_boxed_slice()); } 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 => {} Role::Insomniac => prompts.push(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.scapegoat_can_redeem_into().then_some(d.role_title())) { prompts.push(ActionPrompt::RoleChange { character_id: self.identity(), new_role: pr, }); } } Role::Bloodletter => prompts.push(ActionPrompt::Bloodletter { character_id: self.identity(), living_players: village.living_villagers(), marked: None, }), Role::Seer => prompts.push(ActionPrompt::Seer { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }), Role::Arcanist => prompts.push(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), } => prompts.push(ActionPrompt::Protector { character_id: self.identity(), targets: village.living_players_excluding(*last_protected), marked: None, }), Role::Protector { last_protected: None, } => prompts.push(ActionPrompt::Protector { character_id: self.identity(), targets: village.living_players(), marked: None, }), Role::Apprentice(role) => match village.time() { GameTime::Day { number: _ } => {} GameTime::Night { number: current_night, } => { if 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, }) { prompts.push(ActionPrompt::RoleChange { character_id: self.identity(), new_role: *role, }); } } }, Role::Elder { knows_on_night, woken_for_reveal: false, .. } => match village.time() { GameTime::Day { number: _ } => {} GameTime::Night { number } => { if number >= knows_on_night.get() { prompts.push(ActionPrompt::ElderReveal { character_id: self.identity(), }); } } }, Role::Militia { targeted: None } => prompts.push(ActionPrompt::Militia { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }), Role::Werewolf => prompts.push(ActionPrompt::WolfPackKill { living_villagers: village.living_players(), marked: None, }), Role::AlphaWolf { killed: None } => prompts.push(ActionPrompt::AlphaWolf { character_id: self.identity(), living_villagers: village.living_players_excluding(self.character_id()), marked: None, }), Role::DireWolf { last_blocked: Some(last_blocked), } => prompts.push(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 { .. } => prompts.push(ActionPrompt::DireWolf { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }), Role::Shapeshifter { shifted_into: None } => prompts.push(ActionPrompt::Shapeshifter { character_id: self.identity(), }), Role::Gravedigger => { let dead = village.dead_targets(); if !dead.is_empty() { prompts.push(ActionPrompt::Gravedigger { character_id: self.identity(), dead_players: village.dead_targets(), marked: None, }); } } Role::Hunter { target } => prompts.push(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 } => prompts.push(ActionPrompt::MapleWolf { character_id: self.identity(), nights_til_starvation: (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)), } => prompts.push(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)), } => prompts.push(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, } => prompts.push(ActionPrompt::Guardian { character_id: self.identity(), previous: None, living_players: village.living_players(), marked: None, }), Role::Adjudicator => prompts.push(ActionPrompt::Adjudicator { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }), Role::PowerSeer => prompts.push(ActionPrompt::PowerSeer { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }), Role::Mortician => { let dead = village.dead_targets(); if !dead.is_empty() { prompts.push(ActionPrompt::Mortician { character_id: self.identity(), dead_players: dead, marked: None, }); } } Role::Beholder => prompts.push(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" ); } Role::Empath { cursed: false } => prompts.push(ActionPrompt::Empath { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }), Role::Vindicator => { if night != 0 && let Some(last_day) = NonZeroU8::new(night) && village .executions_on_day(last_day) .iter() .any(|c| c.alignment().village()) { prompts.push(ActionPrompt::Vindicator { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }); } } Role::PyreMaster { .. } => prompts.push(ActionPrompt::PyreMaster { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }), Role::LoneWolf => prompts.push(ActionPrompt::LoneWolfKill { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), marked: None, }), } Ok(prompts.into_boxed_slice()) } pub const fn killing_wolf_order(&self) -> Option { self.role.killing_wolf_order() } pub fn black_knight_kill(&mut self) -> Result<()> { let attacked = self.black_knight_ref()?.attacked; if let Some(attacked) = attacked.as_ref() && let Some(next) = attacked.next_night() && self.died_to.is_none() { self.died_to = Some(next); } Ok(()) } pub fn alignment(&self) -> Alignment { if let Some(alignment) = self.auras.overrides_alignment() { return alignment; } if let Role::Empath { cursed: true } = &self.role { return Alignment::Wolves; } self.role.alignment() } pub fn killer(&self) -> Killer { if let Some(killer) = self.auras.overrides_killer() { return killer; } if let Role::Empath { cursed: true } = &self.role { return Killer::Killer; } self.role.killer() } pub fn powerful(&self) -> Powerful { if let Some(powerful) = self.auras.overrides_powerful() { return powerful; } if let Role::Empath { cursed: true } = &self.role { return Powerful::Powerful; } self.role.powerful() } pub const fn initial_shown_role(&self) -> RoleTitle { self.role.initial_shown_role() } #[doc(hidden)] pub fn role(&self) -> &Role { &self.role } #[doc(hidden)] pub fn role_mut(&mut self) -> &mut Role { &mut self.role } } // pub struct MasonLeaderMut<'a>(&'a mut u8, &'a mut Box<[CharacterId]>); impl crate::role::MasonLeaderMut<'_> { pub fn recruit(self, target: CharacterId) { let mut recruits = self.recruits.to_vec(); recruits.push(target); *self.recruits = recruits.into_boxed_slice(); *self.recruits_available = self.recruits_available.saturating_sub(1); } } pub struct AsCharacter(Character); impl Deref for AsCharacter { type Target = Character; fn deref(&self) -> &Self::Target { &self.0 } }