// 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::{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,
TraitorIntro,
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,
Intel,
Other,
MasonRecruit,
MasonsWake,
Insomniac,
Beholder,
}
#[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::Other)]
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::Other)]
MapleWolf {
character_id: CharacterIdentity,
kill_or_die: bool,
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::Beholder)]
Beholder {
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::TraitorIntro)]
TraitorIntro { character_id: CharacterIdentity },
}
impl ActionPrompt {
pub(crate) 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::Beholder { 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::CoverOfDarkness
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::Insomniac { .. }
| ActionPrompt::TraitorIntro { .. } => None,
}
}
pub(crate) const fn character_id(&self) -> Option {
match self {
ActionPrompt::TraitorIntro { 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::Beholder { 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::TraitorIntro { .. }
| ActionPrompt::Beholder { .. }
| 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::RoleDoesntMark)
),
}
}
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result {
let mut prompt = self.clone();
match &mut prompt {
ActionPrompt::TraitorIntro { .. }
| ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::CoverOfDarkness => Err(GameError::RoleDoesntMark),
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::Beholder {
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,
}
}
}
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)]
pub enum ActionResult {
RoleBlocked,
Drunk,
Seer(Alignment),
PowerSeer { powerful: Powerful },
Adjudicator { killer: Killer },
Arcanist(AlignmentEq),
GraveDigger(Option),
Mortician(DiedToTitle),
Insomniac(Visits),
Empath { scapegoat: bool },
BeholderSawNothing,
BeholderSawEverything,
GoBackToSleep,
ShiftFailed,
Continue,
}
impl ActionResult {
pub fn insane(&self) -> Option {
Some(match self {
ActionResult::Seer(Alignment::Village) => ActionResult::Seer(Alignment::Wolves),
ActionResult::Seer(Alignment::Traitor) | ActionResult::Seer(Alignment::Wolves) => {
ActionResult::Seer(Alignment::Village)
}
ActionResult::PowerSeer { powerful } => ActionResult::PowerSeer {
powerful: !*powerful,
},
ActionResult::Adjudicator { killer } => ActionResult::Adjudicator { killer: !*killer },
ActionResult::Arcanist(alignment_eq) => ActionResult::Arcanist(!*alignment_eq),
ActionResult::Empath { scapegoat } => ActionResult::Empath {
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 => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Visits(Box<[CharacterIdentity]>);
impl Visits {
pub(crate) const fn new(visits: Box<[CharacterIdentity]>) -> Self {
Self(visits)
}
}
impl Deref for Visits {
type Target = [CharacterIdentity];
fn deref(&self) -> &Self::Target {
&self.0
}
}