2025-11-05 19:45:29 +00:00
|
|
|
use core::{
|
|
|
|
|
fmt::Display,
|
|
|
|
|
num::NonZeroU8,
|
|
|
|
|
ops::{Deref, Not},
|
|
|
|
|
};
|
2025-10-06 20:45:15 +01:00
|
|
|
|
|
|
|
|
use rand::seq::SliceRandom;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
diedto::DiedTo,
|
|
|
|
|
error::GameError,
|
2025-10-12 23:48:52 +01:00
|
|
|
game::{GameTime, Village},
|
2025-10-06 20:45:15 +01:00
|
|
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
|
|
|
|
modifier::Modifier,
|
|
|
|
|
player::{PlayerId, RoleChange},
|
2025-10-12 23:48:52 +01:00
|
|
|
role::{
|
|
|
|
|
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful,
|
|
|
|
|
PreviousGuardianAction, Role, RoleTitle,
|
|
|
|
|
},
|
2025-10-06 20:45:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
2025-10-06 20:45:15 +01:00
|
|
|
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)
|
2025-10-06 20:45:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-10-06 20:45:15 +01:00
|
|
|
match (&mut self.role, died_to.date_time()) {
|
2025-10-12 23:48:52 +01:00
|
|
|
(Role::BlackKnight { attacked }, GameTime::Night { .. }) => {
|
2025-10-06 21:59:44 +01:00
|
|
|
attacked.replace(died_to);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-06 20:45:15 +01:00
|
|
|
(
|
|
|
|
|
Role::Elder {
|
|
|
|
|
lost_protection_night: Some(_),
|
|
|
|
|
..
|
|
|
|
|
},
|
|
|
|
|
_,
|
|
|
|
|
) => {}
|
|
|
|
|
(
|
|
|
|
|
Role::Elder {
|
|
|
|
|
lost_protection_night,
|
|
|
|
|
..
|
|
|
|
|
},
|
2025-10-12 23:48:52 +01:00
|
|
|
GameTime::Night { number: night },
|
2025-10-06 20:45:15 +01:00
|
|
|
) => {
|
|
|
|
|
*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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
pub fn role_change(&mut self, new_role: RoleTitle, at: GameTime) -> Result<()> {
|
2025-10-06 20:45:15 +01:00
|
|
|
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 {
|
2025-10-12 23:48:52 +01:00
|
|
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
|
|
|
|
GameTime::Night { number } => number,
|
2025-10-06 20:45:15 +01:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-10-07 21:18:31 +01:00
|
|
|
#[cfg(test)]
|
|
|
|
|
pub fn role_changes(&self) -> &[RoleChange] {
|
|
|
|
|
&self.role_changes
|
|
|
|
|
}
|
2025-10-06 20:45:15 +01:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-07 17:45:21 +01:00
|
|
|
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 {
|
2025-10-13 23:29:10 +01:00
|
|
|
leader: self.identity(),
|
2025-10-07 17:45:21 +01:00
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 19:45:29 +00:00
|
|
|
/// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
2025-10-07 17:45:21 +01:00
|
|
|
if self.mason_leader().is_ok() {
|
|
|
|
|
return self.mason_prompts(village);
|
|
|
|
|
}
|
2025-10-06 20:45:15 +01:00
|
|
|
if !self.alive() || !self.role.wakes(village) {
|
|
|
|
|
return Ok(Box::new([]));
|
|
|
|
|
}
|
2025-10-12 23:48:52 +01:00
|
|
|
let night = match village.time() {
|
|
|
|
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
|
|
|
|
GameTime::Night { number } => number,
|
2025-10-06 20:45:15 +01:00
|
|
|
};
|
|
|
|
|
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([])),
|
2025-10-07 17:45:21 +01:00
|
|
|
|
|
|
|
|
Role::Insomniac => ActionPrompt::Insomniac {
|
|
|
|
|
character_id: self.identity(),
|
|
|
|
|
},
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
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) => {
|
2025-10-12 23:48:52 +01:00
|
|
|
let current_night = match village.time() {
|
|
|
|
|
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
|
|
|
|
GameTime::Night { number } => number,
|
2025-10-06 20:45:15 +01:00
|
|
|
};
|
|
|
|
|
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() {
|
2025-10-12 23:48:52 +01:00
|
|
|
GameTime::Day { number } => number.get() + 1 >= current_night,
|
|
|
|
|
GameTime::Night { number } => number + 1 >= current_night,
|
2025-10-06 20:45:15 +01:00
|
|
|
})
|
|
|
|
|
.then(|| ActionPrompt::RoleChange {
|
|
|
|
|
character_id: self.identity(),
|
|
|
|
|
new_role: *role,
|
|
|
|
|
})
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect());
|
|
|
|
|
}
|
|
|
|
|
Role::Elder {
|
|
|
|
|
knows_on_night,
|
|
|
|
|
woken_for_reveal: false,
|
|
|
|
|
..
|
|
|
|
|
} => {
|
2025-10-12 23:48:52 +01:00
|
|
|
let current_night = match village.time() {
|
|
|
|
|
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
|
|
|
|
GameTime::Night { number } => number,
|
2025-10-06 20:45:15 +01:00
|
|
|
};
|
|
|
|
|
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,
|
|
|
|
|
},
|
2025-10-13 23:29:10 +01:00
|
|
|
Role::DireWolf {
|
|
|
|
|
last_blocked: Some(last_blocked),
|
|
|
|
|
} => ActionPrompt::DireWolf {
|
2025-10-06 20:45:15 +01:00
|
|
|
character_id: self.identity(),
|
2025-10-13 23:29:10 +01:00
|
|
|
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()),
|
2025-10-06 20:45:15 +01:00
|
|
|
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
|
|
|
|
|
},
|
2025-10-06 20:45:15 +01:00
|
|
|
marked: None,
|
|
|
|
|
},
|
|
|
|
|
Role::Beholder => ActionPrompt::Beholder {
|
|
|
|
|
character_id: self.identity(),
|
|
|
|
|
living_players: village.living_players_excluding(self.character_id()),
|
|
|
|
|
marked: None,
|
|
|
|
|
},
|
2025-10-07 17:45:21 +01:00
|
|
|
Role::MasonLeader { .. } => {
|
|
|
|
|
log::error!(
|
|
|
|
|
"night_action_prompts got to MasonLeader, should be handled before the living check"
|
|
|
|
|
);
|
|
|
|
|
return Ok(Box::new([]));
|
2025-10-06 20:45:15 +01:00
|
|
|
}
|
|
|
|
|
Role::Empath { cursed: false } => ActionPrompt::Empath {
|
|
|
|
|
character_id: self.identity(),
|
|
|
|
|
living_players: village.living_players_excluding(self.character_id()),
|
|
|
|
|
marked: None,
|
|
|
|
|
},
|
|
|
|
|
Role::Vindicator => {
|
2025-10-12 23:48:52 +01:00
|
|
|
let last_day = match village.time() {
|
|
|
|
|
GameTime::Day { .. } => {
|
2025-10-06 20:45:15 +01:00
|
|
|
log::error!(
|
|
|
|
|
"vindicator trying to get a prompt during the day? village state: {village:?}"
|
|
|
|
|
);
|
|
|
|
|
return Ok(Box::new([]));
|
|
|
|
|
}
|
2025-10-12 23:48:52 +01:00
|
|
|
GameTime::Night { number } => {
|
2025-10-06 20:45:15 +01:00
|
|
|
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,
|
|
|
|
|
},
|
2025-10-06 20:45:15 +01:00
|
|
|
}]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
pub const fn role(&self) -> &Role {
|
|
|
|
|
&self.role
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
pub const fn killing_wolf_order(&self) -> Option<KillingWolfOrder> {
|
|
|
|
|
self.role.killing_wolf_order()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const fn killer(&self) -> Killer {
|
2025-10-06 20:45:15 +01:00
|
|
|
if let Role::Empath { cursed: true } = &self.role {
|
2025-10-12 23:48:52 +01:00
|
|
|
return Killer::Killer;
|
2025-10-06 20:45:15 +01:00
|
|
|
}
|
|
|
|
|
self.role.killer()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
pub const fn powerful(&self) -> Powerful {
|
2025-10-06 20:45:15 +01:00
|
|
|
if let Role::Empath { cursed: true } = &self.role {
|
2025-10-12 23:48:52 +01:00
|
|
|
return Powerful::Powerful;
|
2025-10-06 20:45:15 +01:00
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 23:29:10 +01:00
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
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>;
|
2025-10-12 23:48:52 +01:00
|
|
|
Guardian, GuardianMut: Option<PreviousGuardianAction>;
|
2025-10-13 23:29:10 +01:00
|
|
|
Direwolf, DirewolfMut: Option<CharacterId>;
|
2025-10-06 20:45:15 +01:00
|
|
|
);
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-05 19:45:29 +00:00
|
|
|
|
|
|
|
|
pub struct AsCharacter(Character);
|
|
|
|
|
|
|
|
|
|
impl Deref for AsCharacter {
|
|
|
|
|
type Target = Character;
|
|
|
|
|
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
|
&self.0
|
|
|
|
|
}
|
|
|
|
|
}
|