2025-10-12 23:48:52 +01:00
|
|
|
pub mod changes;
|
|
|
|
|
mod process;
|
|
|
|
|
|
2025-10-05 10:54:47 +01:00
|
|
|
use core::num::NonZeroU8;
|
2025-06-23 09:48:28 +01:00
|
|
|
use std::collections::VecDeque;
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use werewolves_macros::Extract;
|
|
|
|
|
|
|
|
|
|
use super::Result;
|
|
|
|
|
use crate::{
|
2025-10-06 20:45:15 +01:00
|
|
|
character::{Character, CharacterId},
|
2025-06-23 09:48:28 +01:00
|
|
|
diedto::DiedTo,
|
|
|
|
|
error::GameError,
|
2025-09-28 02:13:34 +01:00
|
|
|
game::{
|
2025-10-12 23:48:52 +01:00
|
|
|
GameTime, Village,
|
|
|
|
|
kill::{self},
|
|
|
|
|
night::changes::{ChangesLookup, NightChange},
|
|
|
|
|
story::NightChoice,
|
2025-10-07 17:45:21 +01:00
|
|
|
},
|
2025-10-12 23:48:52 +01:00
|
|
|
message::night::{ActionPrompt, ActionResponse, ActionResult, Visits},
|
2025-10-06 20:45:15 +01:00
|
|
|
player::Protection,
|
2025-10-12 23:48:52 +01:00
|
|
|
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, RoleBlock, RoleTitle},
|
2025-06-23 09:48:28 +01:00
|
|
|
};
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
enum BlockResolvedOutcome {
|
|
|
|
|
PromptUpdate(ActionPrompt),
|
|
|
|
|
ActionComplete(ActionResult, Option<NightChange>),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum ResponseOutcome {
|
|
|
|
|
PromptUpdate(ActionPrompt),
|
|
|
|
|
ActionComplete(ActionComplete),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ActionComplete {
|
2025-06-23 09:48:28 +01:00
|
|
|
pub result: ActionResult,
|
|
|
|
|
pub change: Option<NightChange>,
|
2025-10-06 20:45:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<ActionComplete> for ResponseOutcome {
|
|
|
|
|
fn from(value: ActionComplete) -> Self {
|
|
|
|
|
ResponseOutcome::ActionComplete(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ActionPrompt {
|
|
|
|
|
fn unless(&self) -> Option<Unless> {
|
|
|
|
|
match &self {
|
2025-10-07 17:45:21 +01:00
|
|
|
ActionPrompt::Insomniac { .. }
|
|
|
|
|
| ActionPrompt::MasonsWake { .. }
|
2025-10-06 20:45:15 +01:00
|
|
|
| ActionPrompt::WolvesIntro { .. }
|
|
|
|
|
| ActionPrompt::RoleChange { .. }
|
|
|
|
|
| ActionPrompt::Shapeshifter { .. }
|
|
|
|
|
| ActionPrompt::ElderReveal { .. }
|
|
|
|
|
| ActionPrompt::CoverOfDarkness => None,
|
|
|
|
|
|
|
|
|
|
ActionPrompt::Arcanist {
|
|
|
|
|
marked: (Some(marked1), Some(marked2)),
|
|
|
|
|
..
|
|
|
|
|
} => Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
|
|
|
|
|
2025-10-07 02:52:06 +01:00
|
|
|
ActionPrompt::LoneWolfKill {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Seer {
|
2025-10-06 20:45:15 +01:00
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Protector {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Gravedigger {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Hunter {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Militia {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::MapleWolf {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Guardian {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Adjudicator {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::PowerSeer {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Mortician {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Beholder {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::MasonLeaderRecruit {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Empath {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Vindicator {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::PyreMaster {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::WolfPackKill {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::AlphaWolf {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::DireWolf {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
} => Some(Unless::TargetBlocked(*marked)),
|
|
|
|
|
|
2025-10-07 02:52:06 +01:00
|
|
|
ActionPrompt::LoneWolfKill { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Seer { marked: None, .. }
|
2025-10-06 20:45:15 +01:00
|
|
|
| ActionPrompt::Protector { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Gravedigger { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Hunter { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Militia { marked: None, .. }
|
|
|
|
|
| ActionPrompt::MapleWolf { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Guardian { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Adjudicator { marked: None, .. }
|
|
|
|
|
| ActionPrompt::PowerSeer { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Mortician { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Beholder { marked: None, .. }
|
|
|
|
|
| ActionPrompt::MasonLeaderRecruit { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Empath { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Vindicator { marked: None, .. }
|
|
|
|
|
| ActionPrompt::PyreMaster { marked: None, .. }
|
|
|
|
|
| ActionPrompt::WolfPackKill { marked: None, .. }
|
|
|
|
|
| ActionPrompt::AlphaWolf { marked: None, .. }
|
|
|
|
|
| ActionPrompt::DireWolf { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Arcanist {
|
|
|
|
|
marked: (Some(_), None),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Arcanist {
|
|
|
|
|
marked: (None, Some(_)),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Arcanist {
|
|
|
|
|
marked: (None, None),
|
|
|
|
|
..
|
|
|
|
|
} => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
impl Default for ActionComplete {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
enum Unless {
|
|
|
|
|
TargetBlocked(CharacterId),
|
|
|
|
|
TargetsBlocked(CharacterId, CharacterId),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Unless> for ActionResult {
|
|
|
|
|
fn from(value: Unless) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
Unless::TargetBlocked(_) | Unless::TargetsBlocked(_, _) => ActionResult::RoleBlocked,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
#[allow(clippy::large_enum_variant)]
|
|
|
|
|
enum NightState {
|
|
|
|
|
Active {
|
|
|
|
|
current_prompt: ActionPrompt,
|
|
|
|
|
current_result: Option<ActionResult>,
|
2025-10-07 21:18:31 +01:00
|
|
|
current_changes: Vec<NightChange>,
|
2025-10-09 22:27:21 +01:00
|
|
|
current_page: usize,
|
2025-06-23 09:48:28 +01:00
|
|
|
},
|
|
|
|
|
Complete,
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Night {
|
|
|
|
|
village: Village,
|
|
|
|
|
night: u8,
|
|
|
|
|
action_queue: VecDeque<ActionPrompt>,
|
2025-10-07 21:18:31 +01:00
|
|
|
used_actions: Vec<(ActionPrompt, ActionResult, Vec<NightChange>)>,
|
2025-09-30 13:07:59 +01:00
|
|
|
night_state: NightState,
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
impl Night {
|
|
|
|
|
pub fn new(village: Village) -> Result<Self> {
|
2025-10-12 23:48:52 +01:00
|
|
|
let night = match village.time() {
|
|
|
|
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
|
|
|
|
GameTime::Night { number } => number,
|
2025-06-23 09:48:28 +01:00
|
|
|
};
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
let filter = if village.executed_known_elder() {
|
|
|
|
|
// there is a lynched elder, remove villager PRs from the prompts
|
|
|
|
|
filter::no_village
|
|
|
|
|
} else {
|
|
|
|
|
filter::no_filter
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
let mut action_queue = village
|
|
|
|
|
.characters()
|
|
|
|
|
.into_iter()
|
2025-10-05 10:52:37 +01:00
|
|
|
.filter(filter)
|
2025-10-06 20:45:15 +01:00
|
|
|
.map(|c| c.night_action_prompts(&village))
|
2025-06-23 09:48:28 +01:00
|
|
|
.collect::<Result<Box<[_]>>>()?
|
|
|
|
|
.into_iter()
|
2025-09-30 13:07:59 +01:00
|
|
|
.flatten()
|
2025-10-06 21:59:44 +01:00
|
|
|
.chain(village.wolf_pack_kill())
|
2025-06-23 09:48:28 +01:00
|
|
|
.collect::<Vec<_>>();
|
2025-09-30 13:07:59 +01:00
|
|
|
action_queue.sort_by(|left_prompt, right_prompt| {
|
2025-06-23 09:48:28 +01:00
|
|
|
left_prompt
|
|
|
|
|
.partial_cmp(right_prompt)
|
|
|
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
|
|
|
});
|
2025-09-30 13:07:59 +01:00
|
|
|
let mut action_queue = VecDeque::from(action_queue);
|
|
|
|
|
|
|
|
|
|
if night == 0 {
|
|
|
|
|
action_queue.push_front(ActionPrompt::WolvesIntro {
|
|
|
|
|
wolves: village
|
2025-09-26 21:15:52 +01:00
|
|
|
.living_wolf_pack_players()
|
|
|
|
|
.into_iter()
|
2025-10-06 20:45:15 +01:00
|
|
|
.map(|w| (w.identity(), w.role_title()))
|
2025-09-30 13:07:59 +01:00
|
|
|
.collect(),
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-10-13 23:16:20 +01:00
|
|
|
if let Some(prompt) = village.wolf_revert_prompt() {
|
|
|
|
|
match &prompt {
|
|
|
|
|
ActionPrompt::RoleChange {
|
|
|
|
|
character_id,
|
|
|
|
|
new_role,
|
|
|
|
|
} => {
|
|
|
|
|
action_queue = Self::remove_reverted_prompts(
|
|
|
|
|
action_queue,
|
|
|
|
|
character_id.character_id,
|
|
|
|
|
*new_role,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
prompt => {
|
|
|
|
|
log::error!(
|
|
|
|
|
"wolf_revert_prompt should have returned a role change, got {prompt:?}"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
action_queue.push_front(prompt);
|
|
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
let night_state = NightState::Active {
|
2025-09-30 13:07:59 +01:00
|
|
|
current_prompt: ActionPrompt::CoverOfDarkness,
|
2025-10-07 21:18:31 +01:00
|
|
|
current_changes: Vec::new(),
|
2025-06-23 09:48:28 +01:00
|
|
|
current_result: None,
|
2025-10-09 22:27:21 +01:00
|
|
|
current_page: 0,
|
2025-06-23 09:48:28 +01:00
|
|
|
};
|
2025-10-05 10:52:37 +01:00
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
night,
|
|
|
|
|
village,
|
|
|
|
|
night_state,
|
|
|
|
|
action_queue,
|
|
|
|
|
used_actions: Vec::new(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 23:16:20 +01:00
|
|
|
fn remove_reverted_prompts(
|
|
|
|
|
mut action_queue: VecDeque<ActionPrompt>,
|
|
|
|
|
reverting: CharacterId,
|
|
|
|
|
reverting_into: RoleTitle,
|
|
|
|
|
) -> VecDeque<ActionPrompt> {
|
|
|
|
|
let mut new_queue = VecDeque::new();
|
|
|
|
|
if let Some(wolves) = action_queue.iter_mut().find_map(|q| match q {
|
|
|
|
|
ActionPrompt::WolvesIntro { wolves } => Some(wolves),
|
|
|
|
|
_ => None,
|
|
|
|
|
}) && let Some(w) = wolves.iter_mut().find(|w| w.0.character_id == reverting)
|
|
|
|
|
{
|
|
|
|
|
w.1 = reverting_into;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// remove prompts by the reverting wolf that are in the queue
|
|
|
|
|
for prompt in action_queue {
|
|
|
|
|
let (wolf_id, prompt) = match prompt {
|
|
|
|
|
ActionPrompt::WolvesIntro { mut wolves } => {
|
|
|
|
|
if let Some(w) = wolves.iter_mut().find(|w| w.0.character_id == reverting) {
|
|
|
|
|
w.1 = reverting_into;
|
|
|
|
|
}
|
|
|
|
|
new_queue.push_front(ActionPrompt::WolvesIntro { wolves });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ActionPrompt::Shapeshifter { character_id } => (
|
|
|
|
|
character_id.character_id,
|
|
|
|
|
ActionPrompt::Shapeshifter { character_id },
|
|
|
|
|
),
|
|
|
|
|
ActionPrompt::AlphaWolf {
|
|
|
|
|
character_id,
|
|
|
|
|
living_villagers,
|
|
|
|
|
marked,
|
|
|
|
|
} => (
|
|
|
|
|
character_id.character_id,
|
|
|
|
|
ActionPrompt::AlphaWolf {
|
|
|
|
|
character_id,
|
|
|
|
|
living_villagers,
|
|
|
|
|
marked,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ActionPrompt::DireWolf {
|
|
|
|
|
character_id,
|
|
|
|
|
living_players,
|
|
|
|
|
marked,
|
|
|
|
|
} => (
|
|
|
|
|
character_id.character_id,
|
|
|
|
|
ActionPrompt::DireWolf {
|
|
|
|
|
character_id,
|
|
|
|
|
living_players,
|
|
|
|
|
marked,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ActionPrompt::LoneWolfKill {
|
|
|
|
|
character_id,
|
|
|
|
|
living_players,
|
|
|
|
|
marked,
|
|
|
|
|
} => (
|
|
|
|
|
character_id.character_id,
|
|
|
|
|
ActionPrompt::LoneWolfKill {
|
|
|
|
|
character_id,
|
|
|
|
|
living_players,
|
|
|
|
|
marked,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
other => {
|
|
|
|
|
new_queue.push_front(other);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if wolf_id != reverting {
|
|
|
|
|
new_queue.push_front(prompt);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new_queue
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
/// changes that require no input (such as hunter firing)
|
2025-10-12 23:48:52 +01:00
|
|
|
fn automatic_changes(&self) -> Vec<NightChange> {
|
2025-06-23 09:48:28 +01:00
|
|
|
let mut changes = Vec::new();
|
2025-10-12 23:48:52 +01:00
|
|
|
let night = match NonZeroU8::new(self.night) {
|
2025-10-05 10:52:37 +01:00
|
|
|
Some(night) => night,
|
|
|
|
|
None => return changes,
|
|
|
|
|
};
|
2025-10-12 23:48:52 +01:00
|
|
|
if !self.village.executed_known_elder() {
|
|
|
|
|
self.village
|
2025-06-23 09:48:28 +01:00
|
|
|
.dead_characters()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
2025-10-06 20:45:15 +01:00
|
|
|
.filter_map(|(c, d)| c.hunter().ok().and_then(|h| *h).map(|t| (c, t, d)))
|
2025-06-23 09:48:28 +01:00
|
|
|
.filter_map(|(c, t, d)| match d.date_time() {
|
2025-10-12 23:48:52 +01:00
|
|
|
GameTime::Day { number } => (number.get() == night.get()).then_some((c, t)),
|
|
|
|
|
GameTime::Night { number: _ } => None,
|
2025-06-23 09:48:28 +01:00
|
|
|
})
|
|
|
|
|
.map(|(c, target)| NightChange::Kill {
|
|
|
|
|
target,
|
|
|
|
|
died_to: DiedTo::Hunter {
|
2025-10-05 10:52:37 +01:00
|
|
|
night,
|
2025-10-05 10:54:47 +01:00
|
|
|
killer: c.character_id(),
|
2025-06-23 09:48:28 +01:00
|
|
|
},
|
|
|
|
|
})
|
2025-10-05 10:52:37 +01:00
|
|
|
.for_each(|c| changes.push(c));
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
changes
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 02:13:34 +01:00
|
|
|
pub fn previous_state(&mut self) -> Result<()> {
|
2025-10-07 21:18:31 +01:00
|
|
|
let (current_prompt, current_result, current_changes) = match &mut self.night_state {
|
2025-09-28 02:13:34 +01:00
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt,
|
2025-10-07 21:18:31 +01:00
|
|
|
current_result,
|
|
|
|
|
current_changes,
|
2025-10-09 22:27:21 +01:00
|
|
|
current_page,
|
|
|
|
|
} => {
|
|
|
|
|
if let Some(page_back) = current_page.checked_sub(1) {
|
|
|
|
|
*current_page = page_back;
|
|
|
|
|
return Ok(());
|
|
|
|
|
} else {
|
|
|
|
|
(current_prompt, current_result, current_changes)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-07 21:18:31 +01:00
|
|
|
NightState::Complete => return Err(GameError::NightOver),
|
|
|
|
|
};
|
2025-10-13 01:21:34 +01:00
|
|
|
if let Some((mut prompt, _, changes)) = self.used_actions.pop() {
|
2025-10-07 21:18:31 +01:00
|
|
|
// Remove the shapeshifter role change from the queue
|
|
|
|
|
if let ActionPrompt::Shapeshifter {
|
|
|
|
|
character_id: ss_char,
|
|
|
|
|
} = &prompt
|
|
|
|
|
&& let Some(change) = changes.first()
|
|
|
|
|
&& let NightChange::Shapeshift { source, into } = change
|
|
|
|
|
&& ss_char.character_id == *source
|
|
|
|
|
&& let Some(next) = self.action_queue.pop_front()
|
|
|
|
|
&& let ActionPrompt::RoleChange {
|
|
|
|
|
character_id: role_change_char,
|
|
|
|
|
..
|
|
|
|
|
} = &next
|
|
|
|
|
&& role_change_char.character_id != *into
|
|
|
|
|
{
|
|
|
|
|
// put it back in
|
|
|
|
|
self.action_queue.push_front(next);
|
2025-09-28 02:13:34 +01:00
|
|
|
}
|
2025-10-13 01:21:34 +01:00
|
|
|
core::mem::swap(&mut prompt, current_prompt);
|
|
|
|
|
let last_prompt = prompt;
|
|
|
|
|
self.action_queue.push_front(last_prompt);
|
2025-10-07 21:18:31 +01:00
|
|
|
*current_result = None;
|
|
|
|
|
*current_changes = Vec::new();
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(GameError::NoPreviousState)
|
2025-09-28 02:13:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
#[cfg(test)]
|
|
|
|
|
pub fn action_queue(&self) -> Box<[ActionPrompt]> {
|
|
|
|
|
self.action_queue.iter().cloned().collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
pub fn collect_changes(&self) -> Result<Box<[NightChange]>> {
|
2025-06-23 09:48:28 +01:00
|
|
|
if !matches!(self.night_state, NightState::Complete) {
|
|
|
|
|
return Err(GameError::NotEndOfNight);
|
|
|
|
|
}
|
2025-10-12 23:48:52 +01:00
|
|
|
let mut all_changes = self.automatic_changes();
|
2025-10-07 21:18:31 +01:00
|
|
|
all_changes.append(&mut self.changes_from_actions().into_vec());
|
2025-10-12 23:48:52 +01:00
|
|
|
Ok(all_changes.into_boxed_slice())
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
fn apply_mason_recruit(
|
|
|
|
|
&mut self,
|
|
|
|
|
mason_leader: CharacterId,
|
|
|
|
|
recruiting: CharacterId,
|
|
|
|
|
) -> Result<ActionResult> {
|
|
|
|
|
if self.village.character_by_id(recruiting)?.is_village() {
|
|
|
|
|
if let Some(masons) = self.action_queue.iter_mut().find_map(|a| match a {
|
2025-10-07 17:45:21 +01:00
|
|
|
ActionPrompt::MasonsWake { leader, masons, .. } => {
|
2025-10-13 01:21:34 +01:00
|
|
|
(leader.character_id == mason_leader).then_some(masons)
|
2025-10-07 17:45:21 +01:00
|
|
|
}
|
2025-10-06 20:45:15 +01:00
|
|
|
_ => None,
|
|
|
|
|
}) {
|
|
|
|
|
let mut ext_masons = masons.to_vec();
|
|
|
|
|
ext_masons.push(self.village.character_by_id(recruiting)?.identity());
|
|
|
|
|
*masons = ext_masons.into_boxed_slice();
|
|
|
|
|
} else {
|
|
|
|
|
self.action_queue.push_front(ActionPrompt::MasonsWake {
|
2025-10-13 01:21:34 +01:00
|
|
|
leader: self.village.character_by_id(mason_leader)?.identity(),
|
2025-10-06 20:45:15 +01:00
|
|
|
masons: Box::new([self.village.character_by_id(recruiting)?.identity()]),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
Ok(ActionResult::Continue)
|
|
|
|
|
} else {
|
|
|
|
|
Ok(ActionResult::GoBackToSleep)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> {
|
2025-10-07 21:18:31 +01:00
|
|
|
if let Some(kill_target) = self
|
|
|
|
|
.changes_from_actions()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find_map(|c| match c {
|
|
|
|
|
NightChange::Kill {
|
|
|
|
|
target,
|
|
|
|
|
died_to:
|
|
|
|
|
DiedTo::Wolfpack {
|
|
|
|
|
night: _,
|
|
|
|
|
killing_wolf: _,
|
|
|
|
|
},
|
|
|
|
|
} => Some(target),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
if self.changes_from_actions().into_iter().any(|c| match c {
|
2025-09-27 00:14:26 +01:00
|
|
|
NightChange::Protection {
|
|
|
|
|
target,
|
|
|
|
|
protection: _,
|
2025-10-07 21:18:31 +01:00
|
|
|
} => target == kill_target,
|
2025-09-27 00:14:26 +01:00
|
|
|
_ => false,
|
|
|
|
|
}) {
|
|
|
|
|
// there is protection, so the kill doesn't happen -> no shapeshift
|
2025-09-30 13:07:59 +01:00
|
|
|
return Ok(());
|
2025-09-27 00:14:26 +01:00
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
|
2025-10-07 21:18:31 +01:00
|
|
|
if self.changes_from_actions().into_iter().any(|c| {
|
2025-09-27 00:14:26 +01:00
|
|
|
matches!(
|
|
|
|
|
c,
|
|
|
|
|
NightChange::Kill {
|
|
|
|
|
target: _,
|
2025-09-28 02:13:34 +01:00
|
|
|
died_to: DiedTo::Wolfpack {
|
|
|
|
|
night: _,
|
|
|
|
|
killing_wolf: _
|
|
|
|
|
}
|
2025-09-27 00:14:26 +01:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}) {
|
2025-10-07 21:18:31 +01:00
|
|
|
match &mut self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_changes, ..
|
|
|
|
|
} => current_changes.push(NightChange::Kill {
|
|
|
|
|
target: *source,
|
|
|
|
|
died_to: DiedTo::Shapeshift {
|
|
|
|
|
into: kill_target,
|
|
|
|
|
night: NonZeroU8::new(self.night).unwrap(),
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
_ => return Err(GameError::InvalidMessageForGameState),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
match &mut self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_changes, ..
|
|
|
|
|
} => current_changes.push(NightChange::Shapeshift {
|
|
|
|
|
source: *source,
|
|
|
|
|
into: kill_target,
|
|
|
|
|
}),
|
|
|
|
|
_ => return Err(GameError::InvalidMessageForGameState),
|
2025-09-27 00:14:26 +01:00
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
self.action_queue.push_front(ActionPrompt::RoleChange {
|
|
|
|
|
new_role: RoleTitle::Werewolf,
|
2025-10-06 20:45:15 +01:00
|
|
|
character_id: self.village.character_by_id(kill_target)?.identity(),
|
2025-09-30 13:07:59 +01:00
|
|
|
});
|
2025-09-27 00:14:26 +01:00
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
// Remove any further shapeshift prompts from the queue
|
|
|
|
|
let mut new_queue = VecDeque::new();
|
|
|
|
|
while let Some(prompt) = self.action_queue.pop_back() {
|
|
|
|
|
match &prompt {
|
|
|
|
|
ActionPrompt::Shapeshifter { character_id: _ } => {}
|
|
|
|
|
_ => new_queue.push_front(prompt),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.action_queue = new_queue;
|
|
|
|
|
Ok(())
|
2025-09-27 00:14:26 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-09 22:27:21 +01:00
|
|
|
pub const fn page(&self) -> Option<usize> {
|
|
|
|
|
match &self.night_state {
|
|
|
|
|
NightState::Active { current_page, .. } => Some(*current_page),
|
|
|
|
|
NightState::Complete => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
|
2025-09-30 13:07:59 +01:00
|
|
|
match self.received_response_with_role_blocks(resp)? {
|
2025-10-03 22:47:38 +01:00
|
|
|
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_result: Some(_),
|
|
|
|
|
..
|
2025-10-04 17:50:29 +01:00
|
|
|
} => Err(GameError::AwaitingResponse),
|
2025-10-03 22:47:38 +01:00
|
|
|
NightState::Active { current_prompt, .. } => {
|
|
|
|
|
*current_prompt = prompt.clone();
|
|
|
|
|
Ok(ServerAction::Prompt(prompt))
|
|
|
|
|
}
|
2025-10-04 17:50:29 +01:00
|
|
|
NightState::Complete => Err(GameError::NightOver),
|
2025-10-03 22:47:38 +01:00
|
|
|
},
|
2025-10-06 20:45:15 +01:00
|
|
|
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
2025-06-23 09:48:28 +01:00
|
|
|
match &mut self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: _,
|
|
|
|
|
current_result,
|
2025-10-07 21:18:31 +01:00
|
|
|
..
|
2025-06-23 09:48:28 +01:00
|
|
|
} => current_result.replace(result.clone()),
|
|
|
|
|
NightState::Complete => return Err(GameError::NightOver),
|
|
|
|
|
};
|
2025-10-09 22:27:21 +01:00
|
|
|
if let NightChange::Shapeshift { source, .. } = &change {
|
2025-09-30 13:07:59 +01:00
|
|
|
// needs to be resolved _now_ so that the target can be woken
|
|
|
|
|
// for the role change with the wolves
|
|
|
|
|
self.apply_shapeshift(source)?;
|
2025-10-03 22:47:38 +01:00
|
|
|
return Ok(ServerAction::Result(
|
|
|
|
|
self.action_queue
|
|
|
|
|
.iter()
|
|
|
|
|
.next()
|
|
|
|
|
.and_then(|a| a.is_wolfy().then_some(ActionResult::Continue))
|
|
|
|
|
.unwrap_or(ActionResult::GoBackToSleep),
|
|
|
|
|
));
|
2025-09-27 00:14:26 +01:00
|
|
|
}
|
2025-10-06 20:45:15 +01:00
|
|
|
if let NightChange::MasonRecruit {
|
|
|
|
|
mason_leader,
|
|
|
|
|
recruiting,
|
|
|
|
|
} = &change
|
|
|
|
|
{
|
|
|
|
|
result = self.apply_mason_recruit(*mason_leader, *recruiting)?;
|
|
|
|
|
}
|
2025-10-07 21:18:31 +01:00
|
|
|
match &mut self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_changes, ..
|
|
|
|
|
} => current_changes.push(change),
|
|
|
|
|
NightState::Complete => return Err(GameError::InvalidMessageForGameState),
|
|
|
|
|
}
|
2025-10-03 22:47:38 +01:00
|
|
|
Ok(ServerAction::Result(result))
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
2025-10-03 22:47:38 +01:00
|
|
|
BlockResolvedOutcome::ActionComplete(result, None) => {
|
2025-06-23 09:48:28 +01:00
|
|
|
match &mut self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: _,
|
|
|
|
|
current_result,
|
2025-10-07 21:18:31 +01:00
|
|
|
..
|
2025-06-23 09:48:28 +01:00
|
|
|
} => {
|
|
|
|
|
current_result.replace(result.clone());
|
|
|
|
|
}
|
|
|
|
|
NightState::Complete => return Err(GameError::NightOver),
|
|
|
|
|
};
|
2025-10-03 22:47:38 +01:00
|
|
|
Ok(ServerAction::Result(result))
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
|
|
|
|
|
fn received_response_consecutive_wolves_dont_sleep(
|
|
|
|
|
&self,
|
|
|
|
|
resp: ActionResponse,
|
|
|
|
|
) -> Result<ResponseOutcome> {
|
2025-10-03 22:47:38 +01:00
|
|
|
let (current_cover, current_wolfy) = self
|
|
|
|
|
.current_prompt()
|
2025-10-09 22:27:21 +01:00
|
|
|
.map(|(current_prompt, _)| {
|
2025-10-03 22:47:38 +01:00
|
|
|
(
|
|
|
|
|
*current_prompt == ActionPrompt::CoverOfDarkness,
|
|
|
|
|
current_prompt.is_wolfy(),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default();
|
2025-09-30 13:07:59 +01:00
|
|
|
let next_wolfy = self
|
|
|
|
|
.action_queue
|
|
|
|
|
.iter()
|
|
|
|
|
.next()
|
|
|
|
|
.map(|a| a.is_wolfy())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
if current_cover && let ActionResponse::Continue = &resp {
|
|
|
|
|
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
|
|
|
result: ActionResult::Continue,
|
|
|
|
|
change: None,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
match (self.process(resp)?, current_wolfy, next_wolfy) {
|
2025-10-03 22:47:38 +01:00
|
|
|
(ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)),
|
2025-09-30 13:07:59 +01:00
|
|
|
(
|
2025-10-03 22:47:38 +01:00
|
|
|
ResponseOutcome::ActionComplete(ActionComplete {
|
2025-09-30 13:07:59 +01:00
|
|
|
result: ActionResult::GoBackToSleep,
|
2025-10-07 21:18:31 +01:00
|
|
|
change: Some(NightChange::Shapeshift { source, into }),
|
2025-10-03 22:47:38 +01:00
|
|
|
}),
|
2025-09-30 13:07:59 +01:00
|
|
|
true,
|
|
|
|
|
_,
|
2025-10-03 22:47:38 +01:00
|
|
|
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
2025-09-30 13:07:59 +01:00
|
|
|
result: ActionResult::Continue,
|
2025-10-07 21:18:31 +01:00
|
|
|
change: Some(NightChange::Shapeshift { source, into }),
|
2025-10-03 22:47:38 +01:00
|
|
|
})),
|
2025-09-30 13:07:59 +01:00
|
|
|
(
|
2025-10-03 22:47:38 +01:00
|
|
|
ResponseOutcome::ActionComplete(ActionComplete {
|
2025-09-30 13:07:59 +01:00
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change,
|
2025-10-03 22:47:38 +01:00
|
|
|
}),
|
2025-09-30 13:07:59 +01:00
|
|
|
true,
|
|
|
|
|
true,
|
2025-10-03 22:47:38 +01:00
|
|
|
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
2025-09-30 13:07:59 +01:00
|
|
|
result: ActionResult::Continue,
|
|
|
|
|
change,
|
2025-10-03 22:47:38 +01:00
|
|
|
})),
|
2025-09-30 13:07:59 +01:00
|
|
|
(outcome, _, _) => Ok(outcome),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
fn received_response_with_role_blocks(
|
|
|
|
|
&self,
|
|
|
|
|
resp: ActionResponse,
|
2025-10-03 22:47:38 +01:00
|
|
|
) -> Result<BlockResolvedOutcome> {
|
2025-09-30 13:07:59 +01:00
|
|
|
match self.received_response_consecutive_wolves_dont_sleep(resp)? {
|
2025-10-03 22:47:38 +01:00
|
|
|
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
|
2025-10-06 20:45:15 +01:00
|
|
|
ResponseOutcome::ActionComplete(ActionComplete { result, change }) => {
|
2025-10-09 22:27:21 +01:00
|
|
|
match self
|
|
|
|
|
.current_prompt()
|
|
|
|
|
.ok_or(GameError::NightOver)?
|
|
|
|
|
.0
|
|
|
|
|
.unless()
|
|
|
|
|
{
|
2025-10-06 20:45:15 +01:00
|
|
|
Some(Unless::TargetBlocked(unless_blocked)) => {
|
2025-10-07 21:18:31 +01:00
|
|
|
if self.changes_from_actions().into_iter().any(|c| match c {
|
2025-10-06 20:45:15 +01:00
|
|
|
NightChange::RoleBlock {
|
|
|
|
|
source: _,
|
|
|
|
|
target,
|
|
|
|
|
block_type: _,
|
2025-10-07 21:18:31 +01:00
|
|
|
} => target == unless_blocked,
|
2025-10-06 20:45:15 +01:00
|
|
|
_ => false,
|
|
|
|
|
}) {
|
|
|
|
|
Ok(BlockResolvedOutcome::ActionComplete(
|
|
|
|
|
ActionResult::RoleBlocked,
|
|
|
|
|
None,
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => {
|
2025-10-07 21:18:31 +01:00
|
|
|
if self.changes_from_actions().into_iter().any(|c| match c {
|
2025-10-06 20:45:15 +01:00
|
|
|
NightChange::RoleBlock {
|
|
|
|
|
source: _,
|
|
|
|
|
target,
|
|
|
|
|
block_type: _,
|
2025-10-07 21:18:31 +01:00
|
|
|
} => target == unless_blocked1 || target == unless_blocked2,
|
2025-10-06 20:45:15 +01:00
|
|
|
_ => false,
|
|
|
|
|
}) {
|
|
|
|
|
Ok(BlockResolvedOutcome::ActionComplete(
|
|
|
|
|
ActionResult::RoleBlocked,
|
|
|
|
|
None,
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => Ok(BlockResolvedOutcome::ActionComplete(result, change)),
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const fn village(&self) -> &Village {
|
|
|
|
|
&self.village
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const fn current_result(&self) -> Option<&ActionResult> {
|
|
|
|
|
match &self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: _,
|
|
|
|
|
current_result,
|
2025-10-07 21:18:31 +01:00
|
|
|
..
|
2025-06-23 09:48:28 +01:00
|
|
|
} => current_result.as_ref(),
|
|
|
|
|
NightState::Complete => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 22:27:21 +01:00
|
|
|
pub const fn current_prompt(&self) -> Option<(&ActionPrompt, usize)> {
|
2025-06-23 09:48:28 +01:00
|
|
|
match &self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt,
|
|
|
|
|
current_result: _,
|
2025-10-09 22:27:21 +01:00
|
|
|
current_page,
|
2025-10-07 21:18:31 +01:00
|
|
|
..
|
2025-10-09 22:27:21 +01:00
|
|
|
} => Some((current_prompt, *current_page)),
|
2025-06-23 09:48:28 +01:00
|
|
|
NightState::Complete => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
pub const fn current_character_id(&self) -> Option<CharacterId> {
|
2025-06-23 09:48:28 +01:00
|
|
|
match &self.night_state {
|
|
|
|
|
NightState::Active {
|
2025-09-30 13:07:59 +01:00
|
|
|
current_prompt,
|
2025-06-23 09:48:28 +01:00
|
|
|
current_result: _,
|
2025-10-07 21:18:31 +01:00
|
|
|
..
|
2025-09-30 13:07:59 +01:00
|
|
|
} => match current_prompt {
|
2025-10-07 17:45:21 +01:00
|
|
|
ActionPrompt::Insomniac { character_id, .. }
|
|
|
|
|
| ActionPrompt::LoneWolfKill { character_id, .. }
|
2025-10-07 02:52:06 +01:00
|
|
|
| ActionPrompt::ElderReveal { character_id }
|
2025-10-05 10:52:37 +01:00
|
|
|
| ActionPrompt::RoleChange { character_id, .. }
|
2025-10-03 22:47:38 +01:00
|
|
|
| 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, .. }
|
2025-09-30 13:07:59 +01:00
|
|
|
| ActionPrompt::Shapeshifter { character_id }
|
2025-10-03 22:47:38 +01:00
|
|
|
| ActionPrompt::AlphaWolf { character_id, .. }
|
2025-10-06 20:45:15 +01:00
|
|
|
| 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, .. }
|
2025-10-05 10:52:37 +01:00
|
|
|
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
|
2025-10-07 17:45:21 +01:00
|
|
|
|
2025-09-30 13:07:59 +01:00
|
|
|
ActionPrompt::WolvesIntro { wolves: _ }
|
2025-10-07 17:45:21 +01:00
|
|
|
| ActionPrompt::MasonsWake { .. }
|
2025-10-03 22:47:38 +01:00
|
|
|
| ActionPrompt::WolfPackKill { .. }
|
2025-09-30 13:07:59 +01:00
|
|
|
| ActionPrompt::CoverOfDarkness => None,
|
|
|
|
|
},
|
2025-06-23 09:48:28 +01:00
|
|
|
NightState::Complete => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn current_character(&self) -> Option<&Character> {
|
|
|
|
|
self.current_character_id()
|
2025-10-06 20:45:15 +01:00
|
|
|
.and_then(|id| self.village.character_by_id(id).ok())
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const fn complete(&self) -> bool {
|
|
|
|
|
matches!(self.night_state, NightState::Complete)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
pub fn used_actions(&self) -> Box<[(ActionPrompt, ActionResult)]> {
|
|
|
|
|
self.used_actions
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(p, r, _)| (p.clone(), r.clone()))
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
pub fn next(&mut self) -> Result<()> {
|
|
|
|
|
match &self.night_state {
|
|
|
|
|
NightState::Active {
|
2025-10-06 20:45:15 +01:00
|
|
|
current_prompt,
|
|
|
|
|
current_result: Some(result),
|
2025-10-07 21:18:31 +01:00
|
|
|
current_changes,
|
2025-10-09 22:27:21 +01:00
|
|
|
..
|
2025-10-06 20:45:15 +01:00
|
|
|
} => {
|
2025-10-07 21:18:31 +01:00
|
|
|
self.used_actions.push((
|
|
|
|
|
current_prompt.clone(),
|
|
|
|
|
result.clone(),
|
|
|
|
|
current_changes.clone(),
|
|
|
|
|
));
|
2025-10-06 20:45:15 +01:00
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: _,
|
|
|
|
|
current_result: None,
|
2025-10-07 21:18:31 +01:00
|
|
|
..
|
2025-06-23 09:48:28 +01:00
|
|
|
} => return Err(GameError::AwaitingResponse),
|
|
|
|
|
NightState::Complete => return Err(GameError::NightOver),
|
|
|
|
|
}
|
2025-09-30 13:07:59 +01:00
|
|
|
if let Some(prompt) = self.action_queue.pop_front() {
|
2025-10-07 17:45:21 +01:00
|
|
|
if let ActionPrompt::Insomniac { character_id } = &prompt
|
|
|
|
|
&& self.get_visits_for(character_id.character_id).is_empty()
|
|
|
|
|
{
|
|
|
|
|
// skip!
|
|
|
|
|
self.used_actions.pop(); // it will be re-added
|
|
|
|
|
return self.next();
|
|
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
self.night_state = NightState::Active {
|
|
|
|
|
current_prompt: prompt,
|
|
|
|
|
current_result: None,
|
2025-10-07 21:18:31 +01:00
|
|
|
current_changes: Vec::new(),
|
2025-10-09 22:27:21 +01:00
|
|
|
current_page: 0,
|
2025-06-23 09:48:28 +01:00
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
self.night_state = NightState::Complete;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-07 21:18:31 +01:00
|
|
|
fn changes_from_actions(&self) -> Box<[NightChange]> {
|
|
|
|
|
self.used_actions
|
|
|
|
|
.iter()
|
2025-10-12 23:48:52 +01:00
|
|
|
.flat_map(|(_, _, act)| act.iter())
|
2025-10-07 21:18:31 +01:00
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
2025-10-07 17:45:21 +01:00
|
|
|
|
|
|
|
|
pub fn get_visits_for(&self, visit_char: CharacterId) -> Visits {
|
|
|
|
|
Visits::new(
|
|
|
|
|
self.used_actions
|
|
|
|
|
.iter()
|
2025-10-07 21:18:31 +01:00
|
|
|
.filter_map(|(prompt, _, _)| match prompt {
|
2025-10-07 17:45:21 +01:00
|
|
|
ActionPrompt::Arcanist {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: (Some(marked1), Some(marked2)),
|
|
|
|
|
..
|
|
|
|
|
} => (*marked1 == visit_char || *marked2 == visit_char)
|
|
|
|
|
.then_some(character_id.clone()),
|
|
|
|
|
ActionPrompt::WolfPackKill {
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
} => (*marked == visit_char)
|
|
|
|
|
.then(|| self.village.killing_wolf().map(|c| c.identity()))
|
|
|
|
|
.flatten(),
|
|
|
|
|
|
|
|
|
|
ActionPrompt::Seer {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Protector {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Gravedigger {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Hunter {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Militia {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::MapleWolf {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Guardian {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Adjudicator {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::PowerSeer {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Mortician {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Beholder {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::MasonLeaderRecruit {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Empath {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::Vindicator {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::PyreMaster {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::AlphaWolf {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::DireWolf {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
| ActionPrompt::LoneWolfKill {
|
|
|
|
|
character_id,
|
|
|
|
|
marked: Some(marked),
|
|
|
|
|
..
|
|
|
|
|
} => (*marked == visit_char).then(|| character_id.clone()),
|
|
|
|
|
|
|
|
|
|
ActionPrompt::WolfPackKill { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Arcanist { marked: _, .. }
|
|
|
|
|
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Seer { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Protector { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Gravedigger { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Hunter { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Militia { marked: None, .. }
|
|
|
|
|
| ActionPrompt::MapleWolf { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Guardian { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Adjudicator { marked: None, .. }
|
|
|
|
|
| ActionPrompt::PowerSeer { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Mortician { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Beholder { marked: None, .. }
|
|
|
|
|
| ActionPrompt::MasonLeaderRecruit { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Empath { marked: None, .. }
|
|
|
|
|
| ActionPrompt::Vindicator { marked: None, .. }
|
|
|
|
|
| ActionPrompt::PyreMaster { marked: None, .. }
|
|
|
|
|
| ActionPrompt::AlphaWolf { marked: None, .. }
|
|
|
|
|
| ActionPrompt::DireWolf { marked: None, .. }
|
|
|
|
|
| ActionPrompt::CoverOfDarkness
|
|
|
|
|
| ActionPrompt::WolvesIntro { .. }
|
|
|
|
|
| ActionPrompt::RoleChange { .. }
|
|
|
|
|
| ActionPrompt::ElderReveal { .. }
|
|
|
|
|
| ActionPrompt::Shapeshifter { .. }
|
|
|
|
|
| ActionPrompt::MasonsWake { .. }
|
|
|
|
|
| ActionPrompt::Insomniac { .. } => None,
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-10-09 22:27:21 +01:00
|
|
|
|
|
|
|
|
pub fn next_page(&mut self) {
|
2025-10-12 23:48:52 +01:00
|
|
|
if let NightState::Active { current_page, .. } = &mut self.night_state {
|
|
|
|
|
*current_page += 1
|
2025-10-09 22:27:21 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
2025-10-03 22:47:38 +01:00
|
|
|
|
|
|
|
|
pub enum ServerAction {
|
|
|
|
|
Prompt(ActionPrompt),
|
|
|
|
|
Result(ActionResult),
|
|
|
|
|
}
|
2025-10-05 10:52:37 +01:00
|
|
|
|
|
|
|
|
mod filter {
|
2025-10-06 20:45:15 +01:00
|
|
|
use crate::character::Character;
|
2025-10-05 10:52:37 +01:00
|
|
|
|
|
|
|
|
pub fn no_filter(_: &Character) -> bool {
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn no_village(c: &Character) -> bool {
|
|
|
|
|
!c.is_village()
|
|
|
|
|
}
|
|
|
|
|
}
|