// Copyright (C) 2025-2026 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::{num::NonZeroU8, ops::Deref};
use serde::{Deserialize, Serialize};
use werewolves_macros::{ChecksAs, Extract, Titles};
use crate::{
character::CharacterId,
diedto::DiedToTitle,
error::GameError,
message::CharacterIdentity,
role::{Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleTitle},
};
type Result = core::result::Result;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, ChecksAs)]
pub enum ActionType {
Cover,
#[checks("is_wolfy")]
WolvesIntro,
DamnedIntro,
RoleChange,
Protect,
#[checks("is_wolfy")]
WolfPackKill,
#[checks("is_wolfy")]
Shapeshifter,
#[checks("is_wolfy")]
AlphaWolfKill,
#[checks("is_wolfy")]
OtherWolf,
#[checks("is_wolfy")]
Direwolf,
LoneWolfKill,
Block,
VillageKill,
BeholderChooses,
Intel,
MasonRecruit,
MasonsWake,
Insomniac,
BeholderWakes,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles, Extract)]
pub enum ActionPrompt {
#[checks(ActionType::Cover)]
CoverOfDarkness,
#[checks(ActionType::WolfPackKill)]
WolvesIntro {
wolves: Box<[(CharacterIdentity, RoleTitle)]>,
},
#[checks(ActionType::RoleChange)]
RoleChange {
character_id: CharacterIdentity,
new_role: RoleTitle,
},
#[checks(ActionType::RoleChange)]
ElderReveal { character_id: CharacterIdentity },
#[checks(ActionType::Intel)]
Seer {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Protect)]
Protector {
character_id: CharacterIdentity,
targets: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Intel)]
Arcanist {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: (Option, Option),
},
#[checks(ActionType::Intel)]
Gravedigger {
character_id: CharacterIdentity,
dead_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::VillageKill)]
Hunter {
character_id: CharacterIdentity,
current_target: Option,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::VillageKill)]
Militia {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::VillageKill)]
MapleWolf {
character_id: CharacterIdentity,
nights_til_starvation: u8,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Protect)]
Guardian {
character_id: CharacterIdentity,
previous: Option,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Intel)]
Adjudicator {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Intel)]
PowerSeer {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Intel)]
Mortician {
character_id: CharacterIdentity,
dead_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::BeholderChooses)]
BeholderChooses {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::MasonsWake)]
MasonsWake {
leader: CharacterIdentity,
masons: Box<[CharacterIdentity]>,
},
#[checks(ActionType::MasonRecruit)]
MasonLeaderRecruit {
character_id: CharacterIdentity,
recruits_left: NonZeroU8,
potential_recruits: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Intel)]
Empath {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Protect)]
Vindicator {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::VillageKill)]
PyreMaster {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::WolfPackKill)]
WolfPackKill {
living_villagers: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Shapeshifter)]
Shapeshifter { character_id: CharacterIdentity },
#[checks(ActionType::AlphaWolfKill)]
AlphaWolf {
character_id: CharacterIdentity,
living_villagers: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Direwolf)]
DireWolf {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::LoneWolfKill)]
LoneWolfKill {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::Insomniac)]
Insomniac { character_id: CharacterIdentity },
#[checks(ActionType::OtherWolf)]
Bloodletter {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option,
},
#[checks(ActionType::DamnedIntro)]
DamnedIntro { character_id: CharacterIdentity },
#[checks(ActionType::BeholderWakes)]
BeholderWakes { character_id: CharacterIdentity },
}
impl ActionPrompt {
pub const fn is_beholdable(&self) -> bool {
match self {
ActionPrompt::BeholderChooses { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::Protector { .. }
| ActionPrompt::Hunter { .. }
| ActionPrompt::Militia { .. }
| ActionPrompt::MapleWolf { .. }
| ActionPrompt::Guardian { .. }
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::MasonLeaderRecruit { .. }
| ActionPrompt::Vindicator { .. }
| ActionPrompt::PyreMaster { .. }
| ActionPrompt::WolfPackKill { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::AlphaWolf { .. }
| ActionPrompt::DireWolf { .. }
| ActionPrompt::LoneWolfKill { .. }
| ActionPrompt::Bloodletter { .. }
| ActionPrompt::DamnedIntro { .. }
| ActionPrompt::CoverOfDarkness => false,
ActionPrompt::BeholderWakes { .. }
| ActionPrompt::Seer { .. }
| ActionPrompt::Arcanist { .. }
| ActionPrompt::Gravedigger { .. }
| ActionPrompt::Adjudicator { .. }
| ActionPrompt::PowerSeer { .. }
| ActionPrompt::Mortician { .. }
| ActionPrompt::Empath { .. }
| ActionPrompt::Insomniac { .. } => true,
}
}
pub const fn marked(&self) -> Option<(CharacterId, Option)> {
match self {
ActionPrompt::Seer { marked, .. }
| ActionPrompt::Protector { marked, .. }
| ActionPrompt::Gravedigger { marked, .. }
| ActionPrompt::Hunter { marked, .. }
| ActionPrompt::Militia { marked, .. }
| ActionPrompt::MapleWolf { marked, .. }
| ActionPrompt::Guardian { marked, .. }
| ActionPrompt::Adjudicator { marked, .. }
| ActionPrompt::PowerSeer { marked, .. }
| ActionPrompt::Mortician { marked, .. }
| ActionPrompt::BeholderChooses { marked, .. }
| ActionPrompt::MasonLeaderRecruit { marked, .. }
| ActionPrompt::Empath { marked, .. }
| ActionPrompt::Vindicator { marked, .. }
| ActionPrompt::PyreMaster { marked, .. }
| ActionPrompt::WolfPackKill { marked, .. }
| ActionPrompt::AlphaWolf { marked, .. }
| ActionPrompt::DireWolf { marked, .. }
| ActionPrompt::LoneWolfKill { marked, .. }
| ActionPrompt::Bloodletter { marked, .. } => match *marked {
Some(marked) => Some((marked, None)),
None => None,
},
ActionPrompt::Arcanist {
marked: (None, Some(marked)),
..
}
| ActionPrompt::Arcanist {
marked: (Some(marked), None),
..
} => Some((*marked, None)),
ActionPrompt::Arcanist {
marked: (Some(marked1), Some(marked2)),
..
} => Some((*marked1, Some(*marked2))),
ActionPrompt::Arcanist {
marked: (None, None),
..
}
| ActionPrompt::BeholderWakes { .. }
| ActionPrompt::CoverOfDarkness
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::Insomniac { .. }
| ActionPrompt::DamnedIntro { .. } => None,
}
}
pub(crate) const fn character_id(&self) -> Option {
match self {
ActionPrompt::BeholderWakes { character_id }
| ActionPrompt::DamnedIntro { character_id }
| ActionPrompt::Insomniac { character_id, .. }
| ActionPrompt::LoneWolfKill { character_id, .. }
| ActionPrompt::ElderReveal { character_id }
| ActionPrompt::RoleChange { character_id, .. }
| ActionPrompt::Seer { character_id, .. }
| ActionPrompt::Protector { character_id, .. }
| ActionPrompt::Arcanist { character_id, .. }
| ActionPrompt::Gravedigger { character_id, .. }
| ActionPrompt::Hunter { character_id, .. }
| ActionPrompt::Militia { character_id, .. }
| ActionPrompt::MapleWolf { character_id, .. }
| ActionPrompt::Guardian { character_id, .. }
| ActionPrompt::Shapeshifter { character_id }
| ActionPrompt::AlphaWolf { character_id, .. }
| ActionPrompt::Adjudicator { character_id, .. }
| ActionPrompt::PowerSeer { character_id, .. }
| ActionPrompt::Mortician { character_id, .. }
| ActionPrompt::BeholderChooses { character_id, .. }
| ActionPrompt::MasonLeaderRecruit { character_id, .. }
| ActionPrompt::Empath { character_id, .. }
| ActionPrompt::Vindicator { character_id, .. }
| ActionPrompt::PyreMaster { character_id, .. }
| ActionPrompt::Bloodletter { character_id, .. }
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
ActionPrompt::WolvesIntro { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::WolfPackKill { .. }
| ActionPrompt::CoverOfDarkness => None,
}
}
pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool {
match self {
ActionPrompt::Insomniac { character_id, .. }
| ActionPrompt::Bloodletter { character_id, .. }
| ActionPrompt::Seer { character_id, .. }
| ActionPrompt::Arcanist { character_id, .. }
| ActionPrompt::Gravedigger { character_id, .. }
| ActionPrompt::Adjudicator { character_id, .. }
| ActionPrompt::PowerSeer { character_id, .. }
| ActionPrompt::Mortician { character_id, .. }
| ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target,
ActionPrompt::BeholderWakes { .. }
| ActionPrompt::DamnedIntro { .. }
| ActionPrompt::BeholderChooses { .. }
| ActionPrompt::CoverOfDarkness
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::Protector { .. }
| ActionPrompt::Hunter { .. }
| ActionPrompt::Militia { .. }
| ActionPrompt::MapleWolf { .. }
| ActionPrompt::Guardian { .. }
| ActionPrompt::PyreMaster { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::AlphaWolf { .. }
| ActionPrompt::DireWolf { .. }
| ActionPrompt::Empath { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::MasonLeaderRecruit { .. }
| ActionPrompt::WolfPackKill { .. }
| ActionPrompt::LoneWolfKill { .. } => false,
}
}
pub fn interactive(&self) -> bool {
match self {
Self::Shapeshifter { .. } => true,
_ => !matches!(
self.with_mark(CharacterId::new()),
Err(GameError::PromptDoesntMark)
),
}
}
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result {
let mut prompt = self.clone();
match &mut prompt {
ActionPrompt::BeholderWakes { .. }
| ActionPrompt::DamnedIntro { .. }
| ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::CoverOfDarkness => Err(GameError::PromptDoesntMark),
ActionPrompt::Guardian {
previous,
living_players,
marked,
..
} => {
if !living_players.iter().any(|c| c.character_id == mark)
|| previous
.as_ref()
.and_then(|p| match p {
PreviousGuardianAction::Protect(_) => None,
PreviousGuardianAction::Guard(c) => Some(c.character_id == mark),
})
.unwrap_or_default()
{
// not in target list OR guarded target previous night
return Err(GameError::InvalidTarget);
}
match marked.as_mut() {
Some(marked_cid) => {
if marked_cid == &mark {
marked.take();
} else {
marked.replace(mark);
}
}
None => {
marked.replace(mark);
}
}
Ok(prompt)
}
ActionPrompt::Arcanist {
living_players: targets,
marked,
..
} => {
if !targets.iter().any(|t| t.character_id == mark) {
return Err(GameError::InvalidTarget);
}
match marked {
(None, Some(m)) | (Some(m), None) => {
if *m == mark {
*marked = (None, None);
} else {
*marked = (Some(*m), Some(mark));
}
}
(None, None) => *marked = (Some(mark), None),
(Some(m1), Some(m2)) => {
if *m1 == mark {
*marked = (Some(*m2), None);
} else if *m2 == mark {
*marked = (Some(*m1), None);
} else {
*marked = (Some(*m2), Some(mark));
}
}
}
Ok(prompt)
}
ActionPrompt::Bloodletter {
living_players: targets,
marked,
..
}
| ActionPrompt::LoneWolfKill {
living_players: targets,
marked,
..
}
| ActionPrompt::Adjudicator {
living_players: targets,
marked,
..
}
| ActionPrompt::PowerSeer {
living_players: targets,
marked,
..
}
| ActionPrompt::Mortician {
dead_players: targets,
marked,
..
}
| ActionPrompt::BeholderChooses {
living_players: targets,
marked,
..
}
| ActionPrompt::MasonLeaderRecruit {
potential_recruits: targets,
marked,
..
}
| ActionPrompt::Empath {
living_players: targets,
marked,
..
}
| ActionPrompt::Vindicator {
living_players: targets,
marked,
..
}
| ActionPrompt::PyreMaster {
living_players: targets,
marked,
..
}
| ActionPrompt::Protector {
targets, marked, ..
}
| ActionPrompt::Seer {
living_players: targets,
marked,
..
}
| ActionPrompt::Gravedigger {
dead_players: targets,
marked,
..
}
| ActionPrompt::Hunter {
living_players: targets,
marked,
..
}
| ActionPrompt::Militia {
living_players: targets,
marked,
..
}
| ActionPrompt::MapleWolf {
living_players: targets,
marked,
..
}
| ActionPrompt::WolfPackKill {
living_villagers: targets,
marked,
..
}
| ActionPrompt::AlphaWolf {
living_villagers: targets,
marked,
..
}
| ActionPrompt::DireWolf {
living_players: targets,
marked,
..
} => {
if !targets.iter().any(|t| t.character_id == mark) {
return Err(GameError::InvalidTarget);
}
if let Some(marked_char) = marked.as_ref()
&& *marked_char == mark
{
marked.take();
} else {
marked.replace(mark);
}
Ok(prompt)
}
}
}
pub const fn is_wolfy(&self) -> bool {
self.action_type().is_wolfy()
|| match self {
ActionPrompt::RoleChange {
character_id: _,
new_role,
} => new_role.wolf(),
_ => false,
}
}
#[rustfmt::skip]
pub fn targets(&self) -> Option<&[CharacterIdentity]> {
match self {
ActionPrompt::Seer { living_players: targets,.. }
| ActionPrompt::Protector { targets,.. }
| ActionPrompt::Arcanist { living_players: targets,.. }
| ActionPrompt::Gravedigger { dead_players: targets,.. }
| ActionPrompt::Hunter { living_players: targets,.. }
| ActionPrompt::Militia { living_players: targets,.. }
| ActionPrompt::MapleWolf { living_players: targets,.. }
| ActionPrompt::Guardian { living_players: targets,.. }
| ActionPrompt::Adjudicator { living_players: targets,.. }
| ActionPrompt::PowerSeer { living_players: targets,.. }
| ActionPrompt::Mortician { dead_players: targets,.. }
| ActionPrompt::BeholderChooses { living_players: targets,.. }
| ActionPrompt::MasonLeaderRecruit { potential_recruits: targets,.. }
| ActionPrompt::Empath { living_players: targets,.. }
| ActionPrompt::Vindicator { living_players: targets,.. }
| ActionPrompt::PyreMaster { living_players: targets,.. }
| ActionPrompt::WolfPackKill { living_villagers: targets,.. }
| ActionPrompt::AlphaWolf { living_villagers: targets,.. }
| ActionPrompt::DireWolf { living_players: targets,.. }
| ActionPrompt::LoneWolfKill { living_players: targets, .. }
| ActionPrompt::Bloodletter {
living_players: targets,
..
} => Some(&**targets),
ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::Insomniac { .. }
| ActionPrompt::CoverOfDarkness
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::BeholderWakes { .. }
| ActionPrompt::DamnedIntro { .. } => None,
}
}
}
impl PartialOrd for ActionPrompt {
fn partial_cmp(&self, other: &Self) -> Option {
self.action_type().partial_cmp(&other.action_type())
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
pub enum ActionResponse {
MarkTarget(CharacterId),
Shapeshift,
Continue,
ContinueToResult,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
pub enum ActionResult {
RoleBlocked,
Drunk,
Seer(CharacterIdentity, Alignment),
PowerSeer {
target: CharacterIdentity,
powerful: Powerful,
},
Adjudicator {
target: CharacterIdentity,
killer: Killer,
},
Arcanist((CharacterIdentity, CharacterIdentity), AlignmentEq),
GraveDigger(CharacterIdentity, Option),
Mortician(CharacterIdentity, DiedToTitle),
Insomniac(Visits),
Empath {
target: CharacterIdentity,
scapegoat: bool,
},
BeholderSawNothing,
BeholderSawEverything,
GoBackToSleep,
ShiftFailed,
Continue,
SkippedByHost,
}
impl ActionResult {
pub fn insane(&self) -> Option {
Some(match self {
ActionResult::Seer(c, Alignment::Village) => {
ActionResult::Seer(c.clone(), Alignment::Wolves)
}
ActionResult::Seer(c, Alignment::Damned) | ActionResult::Seer(c, Alignment::Wolves) => {
ActionResult::Seer(c.clone(), Alignment::Village)
}
ActionResult::PowerSeer { target, powerful } => ActionResult::PowerSeer {
target: target.clone(),
powerful: !*powerful,
},
ActionResult::Adjudicator { target, killer } => ActionResult::Adjudicator {
target: target.clone(),
killer: !*killer,
},
ActionResult::Arcanist(targets, alignment_eq) => {
ActionResult::Arcanist(targets.clone(), !*alignment_eq)
}
ActionResult::Empath { target, scapegoat } => ActionResult::Empath {
target: target.clone(),
scapegoat: !*scapegoat,
},
ActionResult::BeholderSawNothing => ActionResult::BeholderSawEverything,
ActionResult::BeholderSawEverything => ActionResult::BeholderSawNothing,
ActionResult::ShiftFailed
| ActionResult::RoleBlocked
| ActionResult::Drunk
| ActionResult::GraveDigger(_, _)
| ActionResult::Mortician(_, _)
| ActionResult::Insomniac(_)
| ActionResult::GoBackToSleep
| ActionResult::Continue
| ActionResult::SkippedByHost => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Visits(Box<[CharacterIdentity]>);
impl Visits {
pub const fn new(visits: Box<[CharacterIdentity]>) -> Self {
Self(visits)
}
}
impl Deref for Visits {
type Target = [CharacterIdentity];
fn deref(&self) -> &Self::Target {
&self.0
}
}