werewolves/werewolves-proto/src/game/settings/settings_role.rs

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",
}
}
}