490 lines
17 KiB
Rust
490 lines
17 KiB
Rust
// 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 <https://www.gnu.org/licenses/>.
|
|
use core::{
|
|
fmt::{Debug, Display},
|
|
num::NonZeroU8,
|
|
};
|
|
|
|
use rand::distr::{Distribution, StandardUniform};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
use werewolves_macros::{All, ChecksAs, Titles};
|
|
|
|
use crate::{
|
|
aura::AuraTitle,
|
|
character::Character,
|
|
error::GameError,
|
|
message::Identification,
|
|
player::PlayerId,
|
|
role::{Role, RoleTitle},
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
|
pub enum OrRandom<T>
|
|
where
|
|
T: Debug + Clone + PartialEq,
|
|
StandardUniform: Distribution<T>,
|
|
{
|
|
Determined(T),
|
|
#[default]
|
|
Random,
|
|
}
|
|
|
|
impl<T> OrRandom<T>
|
|
where
|
|
for<'a> T: Debug + Clone + PartialEq,
|
|
StandardUniform: Distribution<T>,
|
|
{
|
|
pub fn into_concrete(self) -> T {
|
|
match self {
|
|
Self::Determined(value) => value,
|
|
Self::Random => rand::random(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Debug, PartialOrd, Ord, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ChecksAs, All,
|
|
)]
|
|
pub enum Category {
|
|
#[checks]
|
|
Wolves,
|
|
Villager,
|
|
Intel,
|
|
Defensive,
|
|
Offensive,
|
|
StartsAsVillager,
|
|
}
|
|
impl Category {
|
|
pub fn entire_category(&self) -> Box<[SetupRoleTitle]> {
|
|
SetupRoleTitle::ALL
|
|
.iter()
|
|
.filter(|r| r.category() == *self)
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl Display for Category {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(match self {
|
|
Category::Wolves => "Wolves",
|
|
Category::Villager => "Villager",
|
|
Category::Intel => "Intel",
|
|
Category::Defensive => "Defensive",
|
|
Category::Offensive => "Offensive",
|
|
Category::StartsAsVillager => "Starts As Villager",
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChecksAs, Titles)]
|
|
pub enum SetupRole {
|
|
#[checks(Category::Villager)]
|
|
Villager,
|
|
#[checks(Category::Villager)]
|
|
Scapegoat { redeemed: OrRandom<bool> },
|
|
#[checks(Category::Intel)]
|
|
Seer,
|
|
#[checks(Category::Intel)]
|
|
Arcanist,
|
|
#[checks(Category::Intel)]
|
|
Gravedigger,
|
|
#[checks(Category::Offensive)]
|
|
Hunter,
|
|
#[checks(Category::Offensive)]
|
|
Militia,
|
|
#[checks(Category::Offensive)]
|
|
MapleWolf,
|
|
#[checks(Category::Defensive)]
|
|
Guardian,
|
|
#[checks(Category::Defensive)]
|
|
Protector,
|
|
#[checks(Category::StartsAsVillager)]
|
|
Apprentice { to: Option<RoleTitle> },
|
|
#[checks(Category::StartsAsVillager)]
|
|
Elder { knows_on_night: NonZeroU8 },
|
|
|
|
#[checks(Category::Wolves)]
|
|
Werewolf,
|
|
#[checks(Category::Wolves)]
|
|
AlphaWolf,
|
|
#[checks(Category::Wolves)]
|
|
DireWolf,
|
|
#[checks(Category::Wolves)]
|
|
Shapeshifter,
|
|
#[checks(Category::Wolves)]
|
|
LoneWolf,
|
|
#[checks(Category::Wolves)]
|
|
Bloodletter,
|
|
|
|
#[checks(Category::Intel)]
|
|
Adjudicator,
|
|
#[checks(Category::Intel)]
|
|
Insomniac,
|
|
#[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 fn can_assign_aura(&self, aura: AuraTitle) -> bool {
|
|
if self.into_role().title().wolf() {
|
|
return match aura {
|
|
AuraTitle::Scapegoat
|
|
| AuraTitle::RedeemableScapegoat
|
|
| AuraTitle::VindictiveScapegoat
|
|
| AuraTitle::SpitefulScapegoat
|
|
| AuraTitle::InevitableScapegoat
|
|
| AuraTitle::Notorious
|
|
| AuraTitle::Traitor
|
|
| AuraTitle::Bloodlet
|
|
| AuraTitle::Insane => false,
|
|
AuraTitle::Drunk => !matches!(self, SetupRoleTitle::Werewolf),
|
|
};
|
|
}
|
|
match aura {
|
|
AuraTitle::Traitor => true,
|
|
AuraTitle::Drunk => {
|
|
matches!(
|
|
self.category(),
|
|
Category::StartsAsVillager
|
|
| Category::Defensive
|
|
| Category::Intel
|
|
| Category::Offensive
|
|
) && !matches!(
|
|
self,
|
|
Self::Elder
|
|
| Self::BlackKnight
|
|
| Self::Diseased
|
|
| Self::Weightlifter
|
|
| Self::Mortician
|
|
)
|
|
}
|
|
AuraTitle::Insane => {
|
|
matches!(self.category(), Category::Intel)
|
|
&& !matches!(
|
|
self,
|
|
Self::MasonLeader
|
|
| Self::Empath
|
|
| Self::Insomniac
|
|
| Self::Mortician
|
|
| Self::Gravedigger
|
|
)
|
|
}
|
|
AuraTitle::Bloodlet => false,
|
|
AuraTitle::InevitableScapegoat => {
|
|
matches!(self.category(), Category::Intel)
|
|
&& !matches!(
|
|
self,
|
|
SetupRoleTitle::Mortician
|
|
| SetupRoleTitle::Gravedigger
|
|
| SetupRoleTitle::Empath
|
|
| SetupRoleTitle::Insomniac
|
|
)
|
|
}
|
|
AuraTitle::Notorious => {
|
|
!matches!(self, SetupRoleTitle::Villager | SetupRoleTitle::Scapegoat)
|
|
}
|
|
AuraTitle::RedeemableScapegoat => matches!(self, SetupRoleTitle::Villager),
|
|
AuraTitle::VindictiveScapegoat | AuraTitle::SpitefulScapegoat => true,
|
|
AuraTitle::Scapegoat => matches!(self, SetupRoleTitle::Villager),
|
|
}
|
|
}
|
|
pub fn into_role(self) -> Role {
|
|
match self {
|
|
SetupRoleTitle::Bloodletter => Role::Bloodletter,
|
|
SetupRoleTitle::Insomniac => Role::Insomniac,
|
|
SetupRoleTitle::LoneWolf => Role::LoneWolf,
|
|
SetupRoleTitle::Villager => Role::Villager,
|
|
SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false },
|
|
SetupRoleTitle::Seer => Role::Seer,
|
|
SetupRoleTitle::Arcanist => Role::Arcanist,
|
|
SetupRoleTitle::Gravedigger => Role::Gravedigger,
|
|
SetupRoleTitle::Hunter => Role::Hunter { target: None },
|
|
SetupRoleTitle::Militia => Role::Militia { targeted: None },
|
|
SetupRoleTitle::MapleWolf => Role::MapleWolf {
|
|
last_kill_on_night: 0,
|
|
},
|
|
SetupRoleTitle::Guardian => Role::Guardian {
|
|
last_protected: None,
|
|
},
|
|
SetupRoleTitle::Protector => Role::Protector {
|
|
last_protected: None,
|
|
},
|
|
SetupRoleTitle::Apprentice => Role::Apprentice(RoleTitle::Arcanist),
|
|
SetupRoleTitle::Elder => Role::Elder {
|
|
woken_for_reveal: false,
|
|
lost_protection_night: None,
|
|
knows_on_night: NonZeroU8::new(1).unwrap(),
|
|
},
|
|
SetupRoleTitle::Werewolf => Role::Werewolf,
|
|
SetupRoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
|
SetupRoleTitle::DireWolf => Role::DireWolf { last_blocked: None },
|
|
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: None },
|
|
SetupRoleTitle::Weightlifter => Role::Weightlifter,
|
|
SetupRoleTitle::PyreMaster => Role::PyreMaster {
|
|
villagers_killed: 0,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for SetupRole {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(match self {
|
|
SetupRole::Bloodletter => "Bloodletter",
|
|
SetupRole::Insomniac => "Insomniac",
|
|
SetupRole::LoneWolf => "Lone Wolf",
|
|
SetupRole::Villager => "Villager",
|
|
SetupRole::Scapegoat { .. } => "Scapegoat",
|
|
SetupRole::Seer => "Seer",
|
|
SetupRole::Arcanist => "Arcanist",
|
|
SetupRole::Gravedigger => "Gravedigger",
|
|
SetupRole::Hunter => "Hunter",
|
|
SetupRole::Militia => "Militia",
|
|
SetupRole::MapleWolf => "MapleWolf",
|
|
SetupRole::Guardian => "Guardian",
|
|
SetupRole::Protector => "Protector",
|
|
SetupRole::Apprentice { .. } => "Apprentice",
|
|
SetupRole::Elder { .. } => "Elder",
|
|
SetupRole::Werewolf => "Werewolf",
|
|
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",
|
|
})
|
|
}
|
|
}
|
|
|
|
impl SetupRole {
|
|
pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> {
|
|
Ok(match self {
|
|
Self::Bloodletter => Role::Bloodletter,
|
|
SetupRole::Insomniac => Role::Insomniac,
|
|
SetupRole::LoneWolf => Role::LoneWolf,
|
|
SetupRole::Villager => Role::Villager,
|
|
SetupRole::Scapegoat { redeemed } => Role::Scapegoat {
|
|
redeemed: redeemed.into_concrete(),
|
|
},
|
|
SetupRole::Seer => Role::Seer,
|
|
SetupRole::Arcanist => Role::Arcanist,
|
|
SetupRole::Gravedigger => Role::Gravedigger,
|
|
SetupRole::Hunter => Role::Hunter { target: None },
|
|
SetupRole::Militia => Role::Militia { targeted: None },
|
|
SetupRole::MapleWolf => Role::MapleWolf {
|
|
last_kill_on_night: 0,
|
|
},
|
|
SetupRole::Guardian => Role::Guardian {
|
|
last_protected: None,
|
|
},
|
|
SetupRole::Protector => Role::Protector {
|
|
last_protected: None,
|
|
},
|
|
SetupRole::Apprentice { to: Some(role) } => Role::Apprentice(role),
|
|
SetupRole::Apprentice { to: None } => {
|
|
let mentors = roles_in_game
|
|
.iter()
|
|
.filter(|r| r.is_mentor())
|
|
.collect::<Box<[_]>>();
|
|
if mentors.is_empty() {
|
|
return Err(GameError::NoApprenticeMentor);
|
|
}
|
|
let mentor = *mentors[rand::random_range(0..mentors.len())];
|
|
Role::Apprentice(mentor)
|
|
}
|
|
SetupRole::Elder { knows_on_night } => Role::Elder {
|
|
knows_on_night,
|
|
woken_for_reveal: false,
|
|
lost_protection_night: None,
|
|
},
|
|
SetupRole::Werewolf => Role::Werewolf,
|
|
SetupRole::AlphaWolf => Role::AlphaWolf { killed: None },
|
|
SetupRole::DireWolf => Role::DireWolf { last_blocked: None },
|
|
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: None },
|
|
SetupRole::Weightlifter => Role::Weightlifter,
|
|
SetupRole::PyreMaster => Role::PyreMaster {
|
|
villagers_killed: 0,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
impl From<SetupRole> for RoleTitle {
|
|
fn from(value: SetupRole) -> Self {
|
|
match value {
|
|
SetupRole::Scapegoat { .. } => RoleTitle::Scapegoat,
|
|
SetupRole::Apprentice { .. } => RoleTitle::Apprentice,
|
|
other => other
|
|
.into_role(&[])
|
|
.map(|r| r.title())
|
|
.unwrap_or(RoleTitle::Villager),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RoleTitle> for SetupRole {
|
|
fn from(value: RoleTitle) -> Self {
|
|
match value {
|
|
RoleTitle::Bloodletter => SetupRole::Bloodletter,
|
|
RoleTitle::Insomniac => SetupRole::Insomniac,
|
|
RoleTitle::LoneWolf => SetupRole::LoneWolf,
|
|
RoleTitle::Villager => SetupRole::Villager,
|
|
RoleTitle::Scapegoat => SetupRole::Scapegoat {
|
|
redeemed: Default::default(),
|
|
},
|
|
RoleTitle::Seer => SetupRole::Seer,
|
|
RoleTitle::Arcanist => SetupRole::Arcanist,
|
|
RoleTitle::Gravedigger => SetupRole::Gravedigger,
|
|
RoleTitle::Hunter => SetupRole::Hunter,
|
|
RoleTitle::Militia => SetupRole::Militia,
|
|
RoleTitle::MapleWolf => SetupRole::MapleWolf,
|
|
RoleTitle::Guardian => SetupRole::Guardian,
|
|
RoleTitle::Protector => SetupRole::Protector,
|
|
RoleTitle::Apprentice => SetupRole::Apprentice { to: None },
|
|
RoleTitle::Elder => SetupRole::Elder {
|
|
knows_on_night: NonZeroU8::new(3).unwrap(),
|
|
},
|
|
RoleTitle::Werewolf => SetupRole::Werewolf,
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub struct SlotId(Uuid);
|
|
|
|
impl SlotId {
|
|
pub fn new() -> Self {
|
|
Self(Uuid::new_v4())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SetupSlot {
|
|
pub slot_id: SlotId,
|
|
pub role: SetupRole,
|
|
pub auras: Vec<AuraTitle>,
|
|
pub assign_to: Option<PlayerId>,
|
|
pub created_order: u32,
|
|
}
|
|
|
|
impl SetupSlot {
|
|
pub fn new(title: RoleTitle, created_order: u32) -> Self {
|
|
Self {
|
|
created_order,
|
|
assign_to: None,
|
|
role: title.into(),
|
|
auras: Vec::new(),
|
|
slot_id: SlotId::new(),
|
|
}
|
|
}
|
|
|
|
pub fn into_character(
|
|
self,
|
|
ident: Identification,
|
|
roles_in_game: &[RoleTitle],
|
|
) -> Result<Character, GameError> {
|
|
Character::new(
|
|
ident.clone(),
|
|
self.role.into_role(roles_in_game)?,
|
|
self.auras
|
|
.into_iter()
|
|
.map(|aura| aura.into_aura())
|
|
.collect(),
|
|
)
|
|
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
|
}
|
|
}
|
|
|
|
impl Category {
|
|
pub const fn class(&self) -> &'static str {
|
|
match self {
|
|
Category::Wolves => "wolves",
|
|
Category::Villager => "village",
|
|
Category::Intel => "intel",
|
|
Category::Defensive => "defensive",
|
|
Category::Offensive => "offensive",
|
|
Category::StartsAsVillager => "starts-as-villager",
|
|
}
|
|
}
|
|
}
|