2025-06-23 09:48:28 +01:00
|
|
|
use core::{num::NonZeroU8, ops::Not};
|
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use werewolves_macros::Extract;
|
|
|
|
|
|
|
|
|
|
use super::Result;
|
|
|
|
|
use crate::{
|
|
|
|
|
diedto::DiedTo,
|
|
|
|
|
error::GameError,
|
|
|
|
|
game::{DateTime, Village},
|
|
|
|
|
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
|
|
|
|
player::{Character, CharacterId, Protection},
|
|
|
|
|
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Night {
|
|
|
|
|
village: Village,
|
|
|
|
|
night: u8,
|
|
|
|
|
action_queue: VecDeque<(ActionPrompt, Character)>,
|
|
|
|
|
changes: Vec<NightChange>,
|
|
|
|
|
night_state: NightState,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ResponseOutcome {
|
|
|
|
|
pub result: ActionResult,
|
|
|
|
|
pub change: Option<NightChange>,
|
|
|
|
|
pub unless: Option<Unless>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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_char: CharacterId,
|
|
|
|
|
current_result: Option<ActionResult>,
|
|
|
|
|
},
|
|
|
|
|
Complete,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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).map(|prompt| (prompt, c)))
|
|
|
|
|
.collect::<Result<Box<[_]>>>()?
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter_map(|(prompt, char)| prompt.map(|p| (p, char)))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
action_queue.sort_by(|(left_prompt, _), (right_prompt, _)| {
|
|
|
|
|
left_prompt
|
|
|
|
|
.partial_cmp(right_prompt)
|
|
|
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
|
|
|
});
|
2025-09-26 21:15:52 +01:00
|
|
|
let action_queue = VecDeque::from(action_queue);
|
2025-06-23 09:48:28 +01:00
|
|
|
let (current_prompt, current_char) = if night == 0 {
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::WolvesIntro {
|
|
|
|
|
wolves: village
|
|
|
|
|
.living_wolf_pack_players()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|w| (w.target(), w.role().title()))
|
|
|
|
|
.collect(),
|
|
|
|
|
},
|
|
|
|
|
village
|
|
|
|
|
.living_wolf_pack_players()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.next()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.character_id()
|
|
|
|
|
.clone(),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
2025-09-26 21:15:52 +01:00
|
|
|
(
|
|
|
|
|
ActionPrompt::WolfPackKill {
|
|
|
|
|
living_villagers: village.living_villagers(),
|
|
|
|
|
},
|
|
|
|
|
village
|
|
|
|
|
.living_wolf_pack_players()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.next()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.character_id()
|
|
|
|
|
.clone(),
|
|
|
|
|
)
|
2025-06-23 09:48:28 +01:00
|
|
|
};
|
|
|
|
|
let night_state = NightState::Active {
|
|
|
|
|
current_char,
|
|
|
|
|
current_prompt,
|
|
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 changes = ChangesLookup(&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 DiedTo::MapleWolf {
|
|
|
|
|
source,
|
|
|
|
|
night,
|
|
|
|
|
starves_if_fails: true,
|
|
|
|
|
} = died_to
|
|
|
|
|
&& changes.protected(target).is_some()
|
|
|
|
|
{
|
|
|
|
|
// kill maple first, then act as if they get their kill attempt
|
|
|
|
|
new_village
|
|
|
|
|
.character_by_id_mut(source)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.kill(DiedTo::MapleWolfStarved { night: *night });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(prot) = changes.protected(target) {
|
|
|
|
|
match prot {
|
|
|
|
|
Protection::Guardian {
|
|
|
|
|
source,
|
|
|
|
|
guarding: true,
|
|
|
|
|
} => {
|
|
|
|
|
let kill_source = match died_to {
|
|
|
|
|
DiedTo::MapleWolfStarved { night } => {
|
|
|
|
|
new_village
|
|
|
|
|
.character_by_id_mut(target)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.kill(DiedTo::MapleWolfStarved { night: *night });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
DiedTo::Execution { day: _ } => unreachable!(),
|
|
|
|
|
DiedTo::MapleWolf {
|
|
|
|
|
source,
|
|
|
|
|
night: _,
|
|
|
|
|
starves_if_fails: _,
|
|
|
|
|
}
|
|
|
|
|
| DiedTo::Militia {
|
|
|
|
|
killer: source,
|
|
|
|
|
night: _,
|
|
|
|
|
}
|
|
|
|
|
| DiedTo::AlphaWolf {
|
|
|
|
|
killer: source,
|
|
|
|
|
night: _,
|
|
|
|
|
}
|
|
|
|
|
| DiedTo::Hunter {
|
|
|
|
|
killer: source,
|
|
|
|
|
night: _,
|
|
|
|
|
} => source.clone(),
|
|
|
|
|
DiedTo::Wolfpack { night: _ } => {
|
|
|
|
|
if let Some(wolf_to_kill) = new_village
|
|
|
|
|
.living_wolf_pack_players()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find(|w| matches!(w.role(), Role::Werewolf))
|
|
|
|
|
.map(|w| w.character_id().clone())
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
new_village
|
|
|
|
|
.living_wolf_pack_players()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.next()
|
|
|
|
|
.map(|w| w.character_id().clone())
|
|
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
wolf_to_kill
|
|
|
|
|
} else {
|
|
|
|
|
// No wolves? Game over?
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
DiedTo::Shapeshift { into: _, night: _ } => target.clone(),
|
|
|
|
|
DiedTo::Guardian {
|
|
|
|
|
killer: _,
|
|
|
|
|
night: _,
|
|
|
|
|
} => continue,
|
|
|
|
|
};
|
|
|
|
|
new_village.character_by_id_mut(&kill_source).unwrap().kill(
|
|
|
|
|
DiedTo::Guardian {
|
|
|
|
|
killer: source.clone(),
|
|
|
|
|
night: NonZeroU8::new(self.night).unwrap(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
new_village.character_by_id_mut(source).unwrap().kill(
|
|
|
|
|
DiedTo::Wolfpack {
|
|
|
|
|
night: NonZeroU8::new(self.night).unwrap(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
Protection::Guardian {
|
|
|
|
|
source: _,
|
|
|
|
|
guarding: false,
|
|
|
|
|
} => continue,
|
|
|
|
|
Protection::Protector { source: _ } => continue,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new_village
|
|
|
|
|
.character_by_id_mut(target)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.kill(DiedTo::Wolfpack {
|
|
|
|
|
night: NonZeroU8::new(self.night).unwrap(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
NightChange::Shapeshift { source } => {
|
|
|
|
|
// TODO: shapeshift should probably notify immediately after it happens
|
|
|
|
|
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(),
|
|
|
|
|
});
|
|
|
|
|
let target = new_village.find_by_character_id_mut(target).unwrap();
|
|
|
|
|
target
|
|
|
|
|
.role_change(
|
|
|
|
|
RoleTitle::Werewolf,
|
|
|
|
|
DateTime::Night { number: self.night },
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
NightChange::RoleBlock {
|
|
|
|
|
source: _,
|
|
|
|
|
target: _,
|
|
|
|
|
block_type: _,
|
|
|
|
|
}
|
|
|
|
|
| NightChange::Protection {
|
|
|
|
|
target: _,
|
|
|
|
|
protection: _,
|
|
|
|
|
} => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-26 21:15:52 +01:00
|
|
|
if new_village.is_game_over().is_none() {
|
|
|
|
|
new_village.to_day()?;
|
|
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
Ok(new_village)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ActionResult> {
|
|
|
|
|
if let (
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: ActionPrompt::WolvesIntro { wolves: _ },
|
|
|
|
|
current_char: _,
|
|
|
|
|
current_result,
|
|
|
|
|
},
|
|
|
|
|
ActionResponse::WolvesIntroAck,
|
|
|
|
|
) = (&mut self.night_state, &resp)
|
|
|
|
|
{
|
|
|
|
|
*current_result = Some(ActionResult::WolvesIntroDone);
|
|
|
|
|
return Ok(ActionResult::WolvesIntroDone);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match self.received_response_with_role_blocks(resp) {
|
|
|
|
|
Ok((result, Some(change))) => {
|
|
|
|
|
match &mut self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: _,
|
|
|
|
|
current_char: _,
|
|
|
|
|
current_result,
|
|
|
|
|
} => current_result.replace(result.clone()),
|
|
|
|
|
NightState::Complete => return Err(GameError::NightOver),
|
|
|
|
|
};
|
|
|
|
|
self.changes.push(change);
|
|
|
|
|
Ok(result)
|
|
|
|
|
}
|
|
|
|
|
Ok((result, None)) => {
|
|
|
|
|
match &mut self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: _,
|
|
|
|
|
current_char: _,
|
|
|
|
|
current_result,
|
|
|
|
|
} => {
|
|
|
|
|
current_result.replace(result.clone());
|
|
|
|
|
}
|
|
|
|
|
NightState::Complete => return Err(GameError::NightOver),
|
|
|
|
|
};
|
|
|
|
|
Ok(result)
|
|
|
|
|
}
|
|
|
|
|
Err(err) => Err(err),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fn received_response_with_role_blocks(
|
|
|
|
|
&self,
|
|
|
|
|
resp: ActionResponse,
|
|
|
|
|
) -> Result<(ActionResult, Option<NightChange>)> {
|
|
|
|
|
match self.received_response_inner(resp) {
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
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((ActionResult::RoleBlocked, None))
|
|
|
|
|
} else {
|
|
|
|
|
Ok((result, change))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
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((ActionResult::RoleBlocked, None))
|
|
|
|
|
} else {
|
|
|
|
|
Ok((result, change))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result,
|
|
|
|
|
change,
|
|
|
|
|
unless: None,
|
|
|
|
|
}) => Ok((result, change)),
|
|
|
|
|
Err(err) => Err(err),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn received_response_inner(&self, resp: ActionResponse) -> Result<ResponseOutcome> {
|
|
|
|
|
let (current_prompt, current_char) = match &self.night_state {
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: _,
|
|
|
|
|
current_char: _,
|
|
|
|
|
current_result: Some(_),
|
|
|
|
|
} => return Err(GameError::NightNeedsNext),
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt,
|
|
|
|
|
current_char,
|
|
|
|
|
current_result: None,
|
|
|
|
|
} => (current_prompt, current_char),
|
|
|
|
|
NightState::Complete => return Err(GameError::NightOver),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match (current_prompt, resp) {
|
|
|
|
|
(ActionPrompt::RoleChange { new_role }, ActionResponse::RoleChangeAck) => {
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::RoleChange(current_char.clone(), *new_role)),
|
|
|
|
|
unless: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(ActionPrompt::Seer { living_players }, ActionResponse::Seer(target)) => {
|
|
|
|
|
if !living_players.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::Seer(
|
|
|
|
|
self.village
|
|
|
|
|
.character_by_id(&target)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.role()
|
|
|
|
|
.alignment(),
|
|
|
|
|
),
|
|
|
|
|
change: None,
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::Arcanist { living_players },
|
|
|
|
|
ActionResponse::Arcanist(target1, target2),
|
|
|
|
|
) => {
|
|
|
|
|
if !(living_players.iter().any(|p| p.character_id == target1)
|
|
|
|
|
&& living_players.iter().any(|p| p.character_id == target2))
|
|
|
|
|
{
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
let target1_align = self
|
|
|
|
|
.village
|
|
|
|
|
.character_by_id(&target1)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.role()
|
|
|
|
|
.alignment();
|
|
|
|
|
let target2_align = self
|
|
|
|
|
.village
|
|
|
|
|
.character_by_id(&target2)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.role()
|
|
|
|
|
.alignment();
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::Arcanist {
|
|
|
|
|
same: target1_align == target2_align,
|
|
|
|
|
},
|
|
|
|
|
change: None,
|
|
|
|
|
unless: Some(Unless::TargetsBlocked(target1, target2)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(ActionPrompt::Gravedigger { dead_players }, ActionResponse::Gravedigger(target)) => {
|
|
|
|
|
if !dead_players.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
let target_role = self.village.character_by_id(&target).unwrap().role();
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GraveDigger(
|
|
|
|
|
matches!(
|
|
|
|
|
target_role,
|
|
|
|
|
Role::Shapeshifter {
|
|
|
|
|
shifted_into: Some(_)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
.not()
|
|
|
|
|
.then(|| target_role.title()),
|
|
|
|
|
),
|
|
|
|
|
change: None,
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::Hunter {
|
|
|
|
|
current_target: _,
|
|
|
|
|
living_players,
|
|
|
|
|
},
|
|
|
|
|
ActionResponse::Hunter(target),
|
|
|
|
|
) => {
|
|
|
|
|
if !living_players.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::HunterTarget {
|
|
|
|
|
target: target.clone(),
|
|
|
|
|
source: current_char.clone(),
|
|
|
|
|
}),
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(ActionPrompt::Militia { living_players }, ActionResponse::Militia(Some(target))) => {
|
|
|
|
|
if !living_players.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
let night = if let Some(night) = NonZeroU8::new(self.night) {
|
|
|
|
|
night
|
|
|
|
|
} else {
|
|
|
|
|
return Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
unless: None,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Kill {
|
|
|
|
|
target: target.clone(),
|
|
|
|
|
died_to: DiedTo::Militia {
|
|
|
|
|
night,
|
|
|
|
|
killer: current_char.clone(),
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(ActionPrompt::Militia { living_players: _ }, ActionResponse::Militia(None)) => {
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
unless: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::MapleWolf {
|
|
|
|
|
kill_or_die,
|
|
|
|
|
living_players,
|
|
|
|
|
},
|
|
|
|
|
ActionResponse::MapleWolf(Some(target)),
|
|
|
|
|
) => {
|
|
|
|
|
if !living_players.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
let night = if let Some(night) = NonZeroU8::new(self.night) {
|
|
|
|
|
night
|
|
|
|
|
} else {
|
|
|
|
|
return Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
unless: None,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Kill {
|
|
|
|
|
target: target.clone(),
|
|
|
|
|
died_to: DiedTo::MapleWolf {
|
|
|
|
|
night,
|
|
|
|
|
source: current_char.clone(),
|
|
|
|
|
starves_if_fails: *kill_or_die,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::MapleWolf {
|
|
|
|
|
kill_or_die: true,
|
|
|
|
|
living_players: _,
|
|
|
|
|
},
|
|
|
|
|
ActionResponse::MapleWolf(None),
|
|
|
|
|
) => {
|
|
|
|
|
let night = if let Some(night) = NonZeroU8::new(self.night) {
|
|
|
|
|
night
|
|
|
|
|
} else {
|
|
|
|
|
return Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
unless: None,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Kill {
|
|
|
|
|
target: current_char.clone(),
|
|
|
|
|
died_to: DiedTo::MapleWolfStarved { night },
|
|
|
|
|
}),
|
|
|
|
|
unless: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::MapleWolf {
|
|
|
|
|
kill_or_die: false,
|
|
|
|
|
living_players: _,
|
|
|
|
|
},
|
|
|
|
|
ActionResponse::MapleWolf(None),
|
|
|
|
|
) => Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
unless: None,
|
|
|
|
|
}),
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::Guardian {
|
|
|
|
|
previous: Some(previous),
|
|
|
|
|
living_players,
|
|
|
|
|
},
|
|
|
|
|
ActionResponse::Guardian(target),
|
|
|
|
|
) => {
|
|
|
|
|
if !living_players.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
let guarding = match previous {
|
|
|
|
|
PreviousGuardianAction::Protect(prev_target) => {
|
|
|
|
|
prev_target.character_id == target
|
|
|
|
|
}
|
|
|
|
|
PreviousGuardianAction::Guard(prev_target) => {
|
|
|
|
|
if prev_target.character_id == target {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Protection {
|
|
|
|
|
target: target.clone(),
|
|
|
|
|
protection: Protection::Guardian {
|
|
|
|
|
source: current_char.clone(),
|
|
|
|
|
guarding,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::Guardian {
|
|
|
|
|
previous: None,
|
|
|
|
|
living_players,
|
|
|
|
|
},
|
|
|
|
|
ActionResponse::Guardian(target),
|
|
|
|
|
) => {
|
|
|
|
|
if !living_players.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Protection {
|
|
|
|
|
target: target.clone(),
|
|
|
|
|
protection: Protection::Guardian {
|
|
|
|
|
source: current_char.clone(),
|
|
|
|
|
guarding: false,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::WolfPackKill { living_villagers },
|
|
|
|
|
ActionResponse::WolfPackKillVote(target),
|
|
|
|
|
) => {
|
|
|
|
|
let night = match NonZeroU8::new(self.night) {
|
|
|
|
|
Some(night) => night,
|
|
|
|
|
None => {
|
|
|
|
|
return Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
unless: None,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if !living_villagers.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Kill {
|
|
|
|
|
target: target.clone(),
|
|
|
|
|
died_to: DiedTo::Wolfpack { night },
|
|
|
|
|
}),
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(ActionPrompt::Shapeshifter, ActionResponse::Shapeshifter(true)) => {
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Shapeshift {
|
|
|
|
|
source: current_char.clone(),
|
|
|
|
|
}),
|
|
|
|
|
unless: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::AlphaWolf {
|
|
|
|
|
living_villagers: _,
|
|
|
|
|
},
|
|
|
|
|
ActionResponse::AlphaWolf(None),
|
|
|
|
|
)
|
|
|
|
|
| (ActionPrompt::Shapeshifter, ActionResponse::Shapeshifter(false)) => {
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
unless: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
ActionPrompt::AlphaWolf { living_villagers },
|
|
|
|
|
ActionResponse::AlphaWolf(Some(target)),
|
|
|
|
|
) => {
|
|
|
|
|
let night = match NonZeroU8::new(self.night) {
|
|
|
|
|
Some(night) => night,
|
|
|
|
|
None => {
|
|
|
|
|
return Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: None,
|
|
|
|
|
unless: None,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if !living_villagers.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Kill {
|
|
|
|
|
target: target.clone(),
|
|
|
|
|
died_to: DiedTo::AlphaWolf {
|
|
|
|
|
killer: current_char.clone(),
|
|
|
|
|
night,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(ActionPrompt::DireWolf { living_players }, ActionResponse::Direwolf(target)) => {
|
|
|
|
|
if !living_players.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::RoleBlock {
|
|
|
|
|
source: current_char.clone(),
|
|
|
|
|
target,
|
|
|
|
|
block_type: RoleBlock::Direwolf,
|
|
|
|
|
}),
|
|
|
|
|
unless: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
(ActionPrompt::Protector { targets }, ActionResponse::Protector(target)) => {
|
|
|
|
|
if !targets.iter().any(|p| p.character_id == target) {
|
|
|
|
|
return Err(GameError::InvalidTarget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(ResponseOutcome {
|
|
|
|
|
result: ActionResult::GoBackToSleep,
|
|
|
|
|
change: Some(NightChange::Protection {
|
|
|
|
|
target: target.clone(),
|
|
|
|
|
protection: Protection::Protector {
|
|
|
|
|
source: current_char.clone(),
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
unless: Some(Unless::TargetBlocked(target)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For other responses that are invalid -- this allows the match to error
|
|
|
|
|
// if a new prompt is added
|
|
|
|
|
(ActionPrompt::RoleChange { new_role: _ }, _)
|
|
|
|
|
| (ActionPrompt::Seer { living_players: _ }, _)
|
|
|
|
|
| (ActionPrompt::Protector { targets: _ }, _)
|
|
|
|
|
| (ActionPrompt::Arcanist { living_players: _ }, _)
|
|
|
|
|
| (ActionPrompt::Gravedigger { dead_players: _ }, _)
|
|
|
|
|
| (
|
|
|
|
|
ActionPrompt::Hunter {
|
|
|
|
|
current_target: _,
|
|
|
|
|
living_players: _,
|
|
|
|
|
},
|
|
|
|
|
_,
|
|
|
|
|
)
|
|
|
|
|
| (ActionPrompt::Militia { living_players: _ }, _)
|
|
|
|
|
| (
|
|
|
|
|
ActionPrompt::MapleWolf {
|
|
|
|
|
kill_or_die: _,
|
|
|
|
|
living_players: _,
|
|
|
|
|
},
|
|
|
|
|
_,
|
|
|
|
|
)
|
|
|
|
|
| (
|
|
|
|
|
ActionPrompt::Guardian {
|
|
|
|
|
previous: _,
|
|
|
|
|
living_players: _,
|
|
|
|
|
},
|
|
|
|
|
_,
|
|
|
|
|
)
|
|
|
|
|
| (
|
|
|
|
|
ActionPrompt::WolfPackKill {
|
|
|
|
|
living_villagers: _,
|
|
|
|
|
},
|
|
|
|
|
_,
|
|
|
|
|
)
|
|
|
|
|
| (ActionPrompt::Shapeshifter, _)
|
|
|
|
|
| (
|
|
|
|
|
ActionPrompt::AlphaWolf {
|
|
|
|
|
living_villagers: _,
|
|
|
|
|
},
|
|
|
|
|
_,
|
|
|
|
|
)
|
|
|
|
|
| (ActionPrompt::DireWolf { living_players: _ }, _) => {
|
|
|
|
|
Err(GameError::InvalidMessageForGameState)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(ActionPrompt::WolvesIntro { wolves: _ }, _) => {
|
|
|
|
|
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_char: _,
|
|
|
|
|
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_char: _,
|
|
|
|
|
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_char,
|
|
|
|
|
current_result: _,
|
|
|
|
|
} => Some(current_char),
|
|
|
|
|
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_char: _,
|
|
|
|
|
current_result: Some(_),
|
|
|
|
|
} => {}
|
|
|
|
|
NightState::Active {
|
|
|
|
|
current_prompt: _,
|
|
|
|
|
current_char: _,
|
|
|
|
|
current_result: None,
|
|
|
|
|
} => return Err(GameError::AwaitingResponse),
|
|
|
|
|
NightState::Complete => return Err(GameError::NightOver),
|
|
|
|
|
}
|
|
|
|
|
if let Some((prompt, character)) = self.action_queue.pop_front() {
|
|
|
|
|
self.night_state = NightState::Active {
|
|
|
|
|
current_prompt: prompt,
|
|
|
|
|
current_char: character.character_id().clone(),
|
|
|
|
|
current_result: None,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
self.night_state = NightState::Complete;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const fn changes(&self) -> &[NightChange] {
|
|
|
|
|
self.changes.as_slice()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ChangesLookup<'a>(&'a [NightChange]);
|
|
|
|
|
|
|
|
|
|
impl<'a> ChangesLookup<'a> {
|
|
|
|
|
pub fn killed(&self, target: &CharacterId) -> Option<&'a DiedTo> {
|
|
|
|
|
self.0.iter().find_map(|c| match c {
|
|
|
|
|
NightChange::Kill { target: t, died_to } => (t == target).then_some(died_to),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
|
|
|
|
self.0.iter().find_map(|c| match c {
|
|
|
|
|
NightChange::Protection {
|
|
|
|
|
target: t,
|
|
|
|
|
protection,
|
|
|
|
|
} => (t == target).then_some(protection),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> {
|
|
|
|
|
self.0.iter().find_map(|c| match c {
|
|
|
|
|
NightChange::Kill {
|
|
|
|
|
target,
|
|
|
|
|
died_to: DiedTo::Wolfpack { night: _ },
|
|
|
|
|
} => Some(target),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|