werewolves/werewolves-proto/src/game/night.rs

928 lines
34 KiB
Rust
Raw Normal View History

use core::num::NonZeroU8;
use std::collections::VecDeque;
use serde::{Deserialize, Serialize};
use werewolves_macros::Extract;
use super::Result;
use crate::{
diedto::DiedTo,
error::GameError,
game::{
DateTime, Village,
kill::{self, ChangesLookup},
},
2025-10-03 00:00:39 +01:00
message::night::{ActionPrompt, ActionResponse, ActionResult},
player::{Character, CharacterId, Protection},
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
};
#[derive(Debug, Clone, Serialize, Deserialize, Extract)]
pub enum NightChange {
RoleChange(CharacterId, RoleTitle),
HunterTarget {
source: CharacterId,
target: CharacterId,
},
Kill {
target: CharacterId,
died_to: DiedTo,
},
RoleBlock {
source: CharacterId,
target: CharacterId,
block_type: RoleBlock,
},
Shapeshift {
source: CharacterId,
},
Protection {
target: CharacterId,
protection: Protection,
},
}
enum BlockResolvedOutcome {
PromptUpdate(ActionPrompt),
ActionComplete(ActionResult, Option<NightChange>),
}
enum ResponseOutcome {
PromptUpdate(ActionPrompt),
ActionComplete(ActionComplete),
}
struct ActionComplete {
pub result: ActionResult,
pub change: Option<NightChange>,
pub unless: Option<Unless>,
}
impl Default for ActionComplete {
fn default() -> Self {
Self {
result: ActionResult::GoBackToSleep,
change: None,
unless: None,
}
}
}
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>,
},
Complete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Night {
village: Village,
night: u8,
action_queue: VecDeque<ActionPrompt>,
used_actions: Vec<ActionPrompt>,
changes: Vec<NightChange>,
night_state: NightState,
}
impl Night {
pub fn new(village: Village) -> Result<Self> {
let night = match village.date_time() {
DateTime::Day { number: _ } => return Err(GameError::NotNight),
DateTime::Night { number } => number,
};
let mut action_queue = village
.characters()
.into_iter()
.map(|c| c.night_action_prompt(&village))
.collect::<Result<Box<[_]>>>()?
.into_iter()
.flatten()
.chain((night > 0).then(|| ActionPrompt::WolfPackKill {
marked: None,
living_villagers: village.living_villagers(),
}))
.collect::<Vec<_>>();
action_queue.sort_by(|left_prompt, right_prompt| {
left_prompt
.partial_cmp(right_prompt)
.unwrap_or(core::cmp::Ordering::Equal)
});
let mut action_queue = VecDeque::from(action_queue);
if night == 0 {
action_queue.push_front(ActionPrompt::WolvesIntro {
wolves: village
.living_wolf_pack_players()
.into_iter()
.map(|w| (w.identity(), w.role().title()))
.collect(),
});
}
// let current_prompt = action_queue.pop_front().ok_or(GameError::NoNightActions)?;
let night_state = NightState::Active {
current_prompt: ActionPrompt::CoverOfDarkness,
current_result: None,
};
let mut changes = Vec::new();
if let Some(night_nz) = NonZeroU8::new(night) {
// TODO: prob should be an end-of-night thing
changes = village
.dead_characters()
.into_iter()
.filter_map(|c| c.died_to().map(|d| (c, d)))
.filter_map(|(c, d)| match c.role() {
Role::Hunter { target } => target.clone().map(|t| (c, t, d)),
_ => None,
})
.filter_map(|(c, t, d)| match d.date_time() {
DateTime::Day { number } => (number.get() == night).then_some((c, t)),
DateTime::Night { number: _ } => None,
})
.map(|(c, target)| NightChange::Kill {
target,
died_to: DiedTo::Hunter {
killer: c.character_id().clone(),
night: night_nz,
},
})
.collect();
}
Ok(Self {
night,
changes,
village,
night_state,
action_queue,
used_actions: Vec::new(),
})
}
pub fn previous_state(&mut self) -> Result<()> {
let prev_act = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
log::info!("loading previous prompt: {prev_act:?}");
match &self.night_state {
NightState::Active {
current_prompt,
current_result: Some(current_result),
} => {
log::info!("removing current result: {current_result:?}");
self.night_state = NightState::Active {
current_prompt: current_prompt.clone(),
current_result: None,
};
}
NightState::Active {
current_prompt,
current_result: None,
} => {
log::info!("pushing current prompt to front of action queue: {current_prompt:?}");
self.action_queue.push_front(current_prompt.clone());
self.night_state = NightState::Active {
current_prompt: prev_act,
current_result: None,
}
}
NightState::Complete => {
self.night_state = NightState::Active {
current_prompt: prev_act,
current_result: None,
};
}
}
Ok(())
}
#[cfg(test)]
pub fn action_queue(&self) -> Box<[ActionPrompt]> {
self.action_queue.iter().cloned().collect()
}
pub fn collect_completed(&self) -> Result<Village> {
if !matches!(self.night_state, NightState::Complete) {
return Err(GameError::NotEndOfNight);
}
let mut new_village = self.village.clone();
let mut changes = ChangesLookup::new(&self.changes);
for change in self.changes.iter() {
match change {
NightChange::RoleChange(character_id, role_title) => new_village
.character_by_id_mut(character_id)
.unwrap()
.role_change(*role_title, DateTime::Night { number: self.night })?,
NightChange::HunterTarget { source, target } => {
if let Role::Hunter { target: t } =
new_village.character_by_id_mut(source).unwrap().role_mut()
{
t.replace(target.clone());
}
if changes.killed(source).is_some()
&& changes.protected(source).is_none()
&& changes.protected(target).is_none()
{
new_village
.character_by_id_mut(target)
.unwrap()
.kill(DiedTo::Hunter {
killer: source.clone(),
night: NonZeroU8::new(self.night).unwrap(),
})
}
}
NightChange::Kill { target, died_to } => {
if let Some(kill) = kill::resolve_kill(
&mut changes,
target,
died_to,
self.night,
&self.village,
)? {
kill.apply_to_village(&mut new_village)?;
}
}
NightChange::Shapeshift { source } => {
if let Some(target) = changes.wolf_pack_kill_target()
&& changes.protected(target).is_none()
{
let ss = new_village.character_by_id_mut(source).unwrap();
match ss.role_mut() {
Role::Shapeshifter { shifted_into } => {
*shifted_into = Some(target.clone())
}
_ => unreachable!(),
}
ss.kill(DiedTo::Shapeshift {
into: target.clone(),
night: NonZeroU8::new(self.night).unwrap(),
});
// role change pushed in [apply_shapeshift]
}
}
NightChange::RoleBlock {
source: _,
target: _,
block_type: _,
}
| NightChange::Protection {
target: _,
protection: _,
} => {}
}
}
if new_village.is_game_over().is_none() {
new_village.to_day()?;
}
Ok(new_village)
}
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> {
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
NightChange::Kill {
target,
died_to:
DiedTo::Wolfpack {
night: _,
killing_wolf: _,
},
} => Some(target.clone()),
_ => None,
}) {
if self.changes.iter().any(|c| match c {
NightChange::Protection {
target,
protection: _,
} => target == &kill_target,
_ => false,
}) {
// there is protection, so the kill doesn't happen -> no shapeshift
return Ok(());
}
if self.changes.iter_mut().any(|c| {
matches!(
c,
NightChange::Kill {
target: _,
died_to: DiedTo::Wolfpack {
night: _,
killing_wolf: _
}
}
)
}) {
self.changes.push(NightChange::Kill {
target: source.clone(),
died_to: DiedTo::Shapeshift {
into: kill_target.clone(),
night: NonZeroU8::new(self.night).unwrap(),
},
});
}
self.changes.push(NightChange::Shapeshift {
source: source.clone(),
});
self.action_queue.push_front(ActionPrompt::RoleChange {
new_role: RoleTitle::Werewolf,
character_id: self
.village
.character_by_id(&kill_target)
.ok_or(GameError::NoMatchingCharacterFound)?
.identity(),
});
}
// 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(())
}
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
match self.received_response_with_role_blocks(resp)? {
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
NightState::Active {
current_result: Some(_),
..
2025-10-04 17:50:29 +01:00
} => Err(GameError::AwaitingResponse),
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),
},
BlockResolvedOutcome::ActionComplete(result, Some(change)) => {
match &mut self.night_state {
NightState::Active {
current_prompt: _,
current_result,
} => current_result.replace(result.clone()),
NightState::Complete => return Err(GameError::NightOver),
};
if let NightChange::Shapeshift { source } = &change {
// needs to be resolved _now_ so that the target can be woken
// for the role change with the wolves
self.apply_shapeshift(source)?;
return Ok(ServerAction::Result(
self.action_queue
.iter()
.next()
.and_then(|a| a.is_wolfy().then_some(ActionResult::Continue))
.unwrap_or(ActionResult::GoBackToSleep),
));
}
self.changes.push(change);
Ok(ServerAction::Result(result))
}
BlockResolvedOutcome::ActionComplete(result, None) => {
match &mut self.night_state {
NightState::Active {
current_prompt: _,
current_result,
} => {
current_result.replace(result.clone());
}
NightState::Complete => return Err(GameError::NightOver),
};
Ok(ServerAction::Result(result))
}
}
}
fn received_response_consecutive_wolves_dont_sleep(
&self,
resp: ActionResponse,
) -> Result<ResponseOutcome> {
let (current_cover, current_wolfy) = self
.current_prompt()
.map(|current_prompt| {
(
*current_prompt == ActionPrompt::CoverOfDarkness,
current_prompt.is_wolfy(),
)
})
.unwrap_or_default();
let next_wolfy = self
.action_queue
.iter()
.next()
.map(|a| a.is_wolfy())
.unwrap_or_default();
if current_cover && let ActionResponse::Continue = &resp {
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Continue,
change: None,
unless: None,
}));
}
match (
self.received_response_inner(resp)?,
current_wolfy,
next_wolfy,
) {
(ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)),
(
ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Shapeshift { source }),
unless,
}),
true,
_,
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Continue,
change: Some(NightChange::Shapeshift { source }),
unless,
})),
(
ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change,
unless,
}),
true,
true,
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Continue,
change,
unless,
})),
(outcome, _, _) => Ok(outcome),
}
}
fn received_response_with_role_blocks(
&self,
resp: ActionResponse,
) -> Result<BlockResolvedOutcome> {
match self.received_response_consecutive_wolves_dont_sleep(resp)? {
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
ResponseOutcome::ActionComplete(ActionComplete {
result,
change,
unless: Some(Unless::TargetBlocked(unless_blocked)),
}) => {
if self.changes.iter().any(|c| match c {
NightChange::RoleBlock {
source: _,
target,
block_type: _,
} => target == &unless_blocked,
_ => false,
}) {
Ok(BlockResolvedOutcome::ActionComplete(
ActionResult::RoleBlocked,
None,
))
} else {
Ok(BlockResolvedOutcome::ActionComplete(result, change))
}
}
ResponseOutcome::ActionComplete(ActionComplete {
result,
change,
unless: Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)),
}) => {
if self.changes.iter().any(|c| match c {
NightChange::RoleBlock {
source: _,
target,
block_type: _,
} => target == &unless_blocked1 || target == &unless_blocked2,
_ => false,
}) {
Ok(BlockResolvedOutcome::ActionComplete(
ActionResult::RoleBlocked,
None,
))
} else {
Ok(BlockResolvedOutcome::ActionComplete(result, change))
}
}
ResponseOutcome::ActionComplete(ActionComplete {
result,
change,
unless: None,
}) => Ok(BlockResolvedOutcome::ActionComplete(result, change)),
}
}
fn received_response_inner(&self, resp: ActionResponse) -> Result<ResponseOutcome> {
let current_prompt = match &self.night_state {
NightState::Active {
current_prompt: _,
current_result: Some(_),
} => return Err(GameError::NightNeedsNext),
NightState::Active {
current_prompt,
current_result: None,
} => current_prompt,
NightState::Complete => return Err(GameError::NightOver),
};
match resp {
ActionResponse::MarkTarget(mark) => {
return Ok(ResponseOutcome::PromptUpdate(
current_prompt.with_mark(mark)?,
));
}
ActionResponse::Shapeshift => {
return match current_prompt {
ActionPrompt::Shapeshifter {
character_id: source,
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Shapeshift {
source: source.character_id.clone(),
}),
unless: None,
})),
_ => Err(GameError::InvalidMessageForGameState),
};
}
ActionResponse::Continue => {}
};
match current_prompt {
ActionPrompt::RoleChange { .. }
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::CoverOfDarkness => {
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: None,
unless: None,
}))
}
ActionPrompt::Seer {
marked: Some(marked),
..
} => {
let alignment = self
.village
.character_by_id(marked)
.ok_or(GameError::InvalidTarget)?
.alignment();
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Seer(alignment),
change: None,
unless: Some(Unless::TargetBlocked(marked.clone())),
}))
}
ActionPrompt::Protector {
marked: Some(marked),
character_id,
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: marked.clone(),
protection: Protection::Protector {
source: character_id.character_id.clone(),
},
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::Arcanist {
marked: (Some(marked1), Some(marked2)),
..
} => {
let same = self
.village
.character_by_id(marked1)
.ok_or(GameError::InvalidMessageForGameState)?
.alignment()
== self
.village
.character_by_id(marked2)
.ok_or(GameError::InvalidMessageForGameState)?
.alignment();
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Arcanist { same },
change: None,
unless: Some(Unless::TargetsBlocked(marked1.clone(), marked2.clone())),
}))
}
ActionPrompt::Gravedigger {
marked: Some(marked),
..
} => {
let dig_role = self
.village
.character_by_id(marked)
.ok_or(GameError::InvalidMessageForGameState)?
.gravedigger_dig();
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GraveDigger(dig_role),
change: None,
unless: Some(Unless::TargetBlocked(marked.clone())),
}))
}
ActionPrompt::Hunter {
character_id,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::HunterTarget {
source: character_id.character_id.clone(),
target: marked.clone(),
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::Militia {
character_id,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: marked.clone(),
died_to: DiedTo::Militia {
killer: character_id.character_id.clone(),
night: NonZeroU8::new(self.night)
.ok_or(GameError::InvalidMessageForGameState)?,
},
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::Militia { marked: None, .. } => {
Ok(ResponseOutcome::ActionComplete(Default::default()))
}
ActionPrompt::MapleWolf {
character_id,
kill_or_die,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: marked.clone(),
died_to: DiedTo::MapleWolf {
source: character_id.character_id.clone(),
night: NonZeroU8::new(self.night)
.ok_or(GameError::InvalidMessageForGameState)?,
starves_if_fails: *kill_or_die,
},
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::MapleWolf { marked: None, .. } => {
Ok(ResponseOutcome::ActionComplete(Default::default()))
}
ActionPrompt::Guardian {
character_id,
previous: None,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: marked.clone(),
protection: Protection::Guardian {
source: character_id.character_id.clone(),
guarding: false,
},
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::Guardian {
character_id,
previous: Some(PreviousGuardianAction::Guard(prev_target)),
marked: Some(marked),
..
} => {
if prev_target.character_id == *marked {
return Err(GameError::InvalidTarget);
}
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: marked.clone(),
protection: Protection::Guardian {
source: character_id.character_id.clone(),
guarding: false,
},
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
}))
}
ActionPrompt::Guardian {
character_id,
previous: Some(PreviousGuardianAction::Protect(prev_protect)),
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: marked.clone(),
protection: Protection::Guardian {
source: character_id.character_id.clone(),
guarding: prev_protect.character_id == *marked,
},
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::WolfPackKill {
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: marked.clone(),
died_to: DiedTo::Wolfpack {
killing_wolf: self
.village
.killing_wolf()
.ok_or(GameError::NoWolves)?
.character_id()
.clone(),
night: NonZeroU8::new(self.night)
.ok_or(GameError::InvalidMessageForGameState)?,
},
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::Shapeshifter { character_id } => {
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Shapeshift {
source: character_id.character_id.clone(),
}),
unless: None,
}))
}
ActionPrompt::AlphaWolf {
character_id,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: marked.clone(),
died_to: DiedTo::AlphaWolf {
killer: character_id.character_id.clone(),
night: NonZeroU8::new(self.night)
.ok_or(GameError::InvalidMessageForGameState)?,
},
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::AlphaWolf { marked: None, .. } => {
Ok(ResponseOutcome::ActionComplete(Default::default()))
}
ActionPrompt::DireWolf {
character_id,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::RoleBlock {
source: character_id.character_id.clone(),
target: marked.clone(),
block_type: RoleBlock::Direwolf,
}),
unless: Some(Unless::TargetBlocked(marked.clone())),
})),
ActionPrompt::Protector { marked: None, .. }
| ActionPrompt::Arcanist {
marked: (None, None),
..
}
| ActionPrompt::Arcanist {
marked: (None, Some(_)),
..
}
| ActionPrompt::Arcanist {
marked: (Some(_), None),
..
}
| ActionPrompt::Gravedigger { marked: None, .. }
| ActionPrompt::Hunter { marked: None, .. }
| ActionPrompt::Guardian { marked: None, .. }
| ActionPrompt::WolfPackKill { marked: None, .. }
| ActionPrompt::DireWolf { marked: None, .. }
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState),
}
}
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,
} => current_result.as_ref(),
NightState::Complete => None,
}
}
pub const fn current_prompt(&self) -> Option<&ActionPrompt> {
match &self.night_state {
NightState::Active {
current_prompt,
current_result: _,
} => Some(current_prompt),
NightState::Complete => None,
}
}
pub const fn current_character_id(&self) -> Option<&CharacterId> {
match &self.night_state {
NightState::Active {
current_prompt,
current_result: _,
} => match current_prompt {
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::DireWolf { character_id, .. } => Some(&character_id.character_id),
ActionPrompt::WolvesIntro { wolves: _ }
| ActionPrompt::WolfPackKill { .. }
| ActionPrompt::CoverOfDarkness => None,
},
NightState::Complete => None,
}
}
pub fn current_character(&self) -> Option<&Character> {
self.current_character_id()
.and_then(|id| self.village.character_by_id(id))
}
pub const fn complete(&self) -> bool {
matches!(self.night_state, NightState::Complete)
}
pub fn next(&mut self) -> Result<()> {
match &self.night_state {
NightState::Active {
current_prompt: _,
current_result: Some(_),
} => {}
NightState::Active {
current_prompt: _,
current_result: None,
} => return Err(GameError::AwaitingResponse),
NightState::Complete => return Err(GameError::NightOver),
}
if let Some(prompt) = self.action_queue.pop_front() {
self.used_actions.push(prompt.clone());
self.night_state = NightState::Active {
current_prompt: prompt,
current_result: None,
};
} else {
self.night_state = NightState::Complete;
}
Ok(())
}
pub const fn changes(&self) -> &[NightChange] {
self.changes.as_slice()
}
}
pub enum ServerAction {
Prompt(ActionPrompt),
Result(ActionResult),
}