// 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 } }