2025-06-23 09:48:28 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
2025-09-30 13:07:59 +01:00
|
|
|
use werewolves_macros::{ChecksAs, Titles};
|
2025-06-23 09:48:28 +01:00
|
|
|
|
|
|
|
|
use crate::{
|
2025-10-03 22:47:38 +01:00
|
|
|
error::GameError,
|
2025-09-30 13:07:59 +01:00
|
|
|
message::CharacterIdentity,
|
2025-06-23 09:48:28 +01:00
|
|
|
player::CharacterId,
|
2025-09-30 13:07:59 +01:00
|
|
|
role::{Alignment, PreviousGuardianAction, RoleTitle},
|
2025-06-23 09:48:28 +01:00
|
|
|
};
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
type Result<T> = core::result::Result<T, GameError>;
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
|
2025-09-28 02:13:34 +01:00
|
|
|
pub enum ActionType {
|
2025-09-30 13:07:59 +01:00
|
|
|
Cover,
|
|
|
|
|
WolvesIntro,
|
|
|
|
|
Protect,
|
|
|
|
|
WolfPackKill,
|
|
|
|
|
Direwolf,
|
|
|
|
|
OtherWolf,
|
|
|
|
|
Block,
|
|
|
|
|
Other,
|
|
|
|
|
RoleChange,
|
2025-09-28 02:13:34 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
impl ActionType {
|
|
|
|
|
const fn is_wolfy(&self) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
self,
|
|
|
|
|
ActionType::Direwolf
|
|
|
|
|
| ActionType::OtherWolf
|
|
|
|
|
| ActionType::WolfPackKill
|
|
|
|
|
| ActionType::WolvesIntro
|
|
|
|
|
)
|
2025-09-28 02:13:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles)]
|
2025-06-23 09:48:28 +01:00
|
|
|
pub enum ActionPrompt {
|
2025-09-30 13:07:59 +01:00
|
|
|
#[checks(ActionType::Cover)]
|
|
|
|
|
CoverOfDarkness,
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::WolfPackKill)]
|
2025-10-02 17:52:12 +01:00
|
|
|
WolvesIntro {
|
|
|
|
|
wolves: Box<[(CharacterIdentity, RoleTitle)]>,
|
|
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::RoleChange)]
|
2025-09-30 13:07:59 +01:00
|
|
|
RoleChange {
|
|
|
|
|
character_id: CharacterIdentity,
|
|
|
|
|
new_role: RoleTitle,
|
|
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Other)]
|
2025-09-30 13:07:59 +01:00
|
|
|
Seer {
|
|
|
|
|
character_id: CharacterIdentity,
|
2025-10-02 17:52:12 +01:00
|
|
|
living_players: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-09-30 13:07:59 +01:00
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Protect)]
|
2025-09-30 13:07:59 +01:00
|
|
|
Protector {
|
|
|
|
|
character_id: CharacterIdentity,
|
2025-10-02 17:52:12 +01:00
|
|
|
targets: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-09-30 13:07:59 +01:00
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Other)]
|
2025-09-30 13:07:59 +01:00
|
|
|
Arcanist {
|
|
|
|
|
character_id: CharacterIdentity,
|
2025-10-02 17:52:12 +01:00
|
|
|
living_players: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: (Option<CharacterId>, Option<CharacterId>),
|
2025-09-30 13:07:59 +01:00
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Other)]
|
2025-09-30 13:07:59 +01:00
|
|
|
Gravedigger {
|
|
|
|
|
character_id: CharacterIdentity,
|
2025-10-02 17:52:12 +01:00
|
|
|
dead_players: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-09-30 13:07:59 +01:00
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Other)]
|
2025-06-23 09:48:28 +01:00
|
|
|
Hunter {
|
2025-09-30 13:07:59 +01:00
|
|
|
character_id: CharacterIdentity,
|
2025-10-02 17:52:12 +01:00
|
|
|
current_target: Option<CharacterIdentity>,
|
|
|
|
|
living_players: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-06-23 09:48:28 +01:00
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Other)]
|
2025-09-30 13:07:59 +01:00
|
|
|
Militia {
|
|
|
|
|
character_id: CharacterIdentity,
|
2025-10-02 17:52:12 +01:00
|
|
|
living_players: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-09-30 13:07:59 +01:00
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Other)]
|
2025-06-23 09:48:28 +01:00
|
|
|
MapleWolf {
|
2025-09-30 13:07:59 +01:00
|
|
|
character_id: CharacterIdentity,
|
2025-06-23 09:48:28 +01:00
|
|
|
kill_or_die: bool,
|
2025-10-02 17:52:12 +01:00
|
|
|
living_players: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-06-23 09:48:28 +01:00
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Protect)]
|
2025-06-23 09:48:28 +01:00
|
|
|
Guardian {
|
2025-09-30 13:07:59 +01:00
|
|
|
character_id: CharacterIdentity,
|
2025-06-23 09:48:28 +01:00
|
|
|
previous: Option<PreviousGuardianAction>,
|
2025-10-02 17:52:12 +01:00
|
|
|
living_players: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-06-23 09:48:28 +01:00
|
|
|
},
|
2025-09-30 13:07:59 +01:00
|
|
|
#[checks(ActionType::WolfPackKill)]
|
2025-10-02 17:52:12 +01:00
|
|
|
WolfPackKill {
|
|
|
|
|
living_villagers: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-10-02 17:52:12 +01:00
|
|
|
},
|
2025-09-30 13:07:59 +01:00
|
|
|
#[checks(ActionType::OtherWolf)]
|
|
|
|
|
Shapeshifter { character_id: CharacterIdentity },
|
|
|
|
|
#[checks(ActionType::OtherWolf)]
|
|
|
|
|
AlphaWolf {
|
|
|
|
|
character_id: CharacterIdentity,
|
2025-10-02 17:52:12 +01:00
|
|
|
living_villagers: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-09-30 13:07:59 +01:00
|
|
|
},
|
2025-09-28 02:13:34 +01:00
|
|
|
#[checks(ActionType::Direwolf)]
|
2025-09-30 13:07:59 +01:00
|
|
|
DireWolf {
|
|
|
|
|
character_id: CharacterIdentity,
|
2025-10-02 17:52:12 +01:00
|
|
|
living_players: Box<[CharacterIdentity]>,
|
2025-10-03 22:47:38 +01:00
|
|
|
marked: Option<CharacterId>,
|
2025-09-30 13:07:59 +01:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ActionPrompt {
|
2025-10-03 22:47:38 +01:00
|
|
|
pub fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
|
|
|
|
let mut prompt = self.clone();
|
|
|
|
|
match &mut prompt {
|
|
|
|
|
ActionPrompt::WolvesIntro { .. }
|
|
|
|
|
| ActionPrompt::RoleChange { .. }
|
|
|
|
|
| ActionPrompt::Shapeshifter { .. }
|
|
|
|
|
| ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState),
|
|
|
|
|
|
|
|
|
|
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.clone()), Some(mark));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(None, None) => *marked = (Some(mark), None),
|
|
|
|
|
(Some(m1), Some(m2)) => {
|
|
|
|
|
if *m1 == mark {
|
|
|
|
|
*marked = (Some(m2.clone()), None);
|
|
|
|
|
} else if *m2 == mark {
|
|
|
|
|
*marked = (Some(m1.clone()), None);
|
|
|
|
|
} else {
|
|
|
|
|
*marked = (Some(m2.clone()), Some(mark));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(prompt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
pub const fn is_wolfy(&self) -> bool {
|
|
|
|
|
self.action_type().is_wolfy()
|
|
|
|
|
|| match self {
|
|
|
|
|
ActionPrompt::RoleChange {
|
|
|
|
|
character_id: _,
|
|
|
|
|
new_role,
|
|
|
|
|
} => new_role.wolf(),
|
|
|
|
|
_ => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PartialOrd for ActionPrompt {
|
|
|
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
2025-09-28 02:13:34 +01:00
|
|
|
self.action_type().partial_cmp(&other.action_type())
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
|
2025-06-23 09:48:28 +01:00
|
|
|
pub enum ActionResponse {
|
2025-10-03 22:47:38 +01:00
|
|
|
// Seer(CharacterId),
|
|
|
|
|
// Arcanist(Option<CharacterId>, Option<CharacterId>),
|
|
|
|
|
// Gravedigger(CharacterId),
|
|
|
|
|
// Hunter(CharacterId),
|
|
|
|
|
// Militia(Option<CharacterId>),
|
|
|
|
|
// MapleWolf(Option<CharacterId>),
|
|
|
|
|
// Guardian(CharacterId),
|
|
|
|
|
// WolfPackKillVote(CharacterId),
|
|
|
|
|
// AlphaWolf(Option<CharacterId>),
|
|
|
|
|
// Direwolf(CharacterId),
|
|
|
|
|
// Protector(CharacterId),
|
|
|
|
|
MarkTarget(CharacterId),
|
|
|
|
|
Shapeshift,
|
|
|
|
|
Continue,
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
pub enum ActionResult {
|
|
|
|
|
RoleBlocked,
|
|
|
|
|
Seer(Alignment),
|
|
|
|
|
Arcanist { same: bool },
|
|
|
|
|
GraveDigger(Option<RoleTitle>),
|
|
|
|
|
GoBackToSleep,
|
2025-09-30 13:07:59 +01:00
|
|
|
Continue,
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|