elder death protection & lynching
also made PlayerId and CharacterId Copy
This commit is contained in:
parent
97e1ca8a39
commit
88665302f6
|
|
@ -25,7 +25,7 @@ impl KillOutcome {
|
||||||
match self {
|
match self {
|
||||||
KillOutcome::Single(character_id, died_to) => {
|
KillOutcome::Single(character_id, died_to) => {
|
||||||
village
|
village
|
||||||
.character_by_id_mut(&character_id)
|
.character_by_id_mut(character_id)
|
||||||
.ok_or(GameError::InvalidTarget)?
|
.ok_or(GameError::InvalidTarget)?
|
||||||
.kill(died_to);
|
.kill(died_to);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -40,10 +40,10 @@ impl KillOutcome {
|
||||||
// check if guardian exists before we mutably borrow killer, which would
|
// check if guardian exists before we mutably borrow killer, which would
|
||||||
// prevent us from borrowing village to check after.
|
// prevent us from borrowing village to check after.
|
||||||
village
|
village
|
||||||
.character_by_id(&guardian)
|
.character_by_id(guardian)
|
||||||
.ok_or(GameError::InvalidTarget)?;
|
.ok_or(GameError::InvalidTarget)?;
|
||||||
village
|
village
|
||||||
.character_by_id_mut(&original_killer)
|
.character_by_id_mut(original_killer)
|
||||||
.ok_or(GameError::InvalidTarget)?
|
.ok_or(GameError::InvalidTarget)?
|
||||||
.kill(DiedTo::GuardianProtecting {
|
.kill(DiedTo::GuardianProtecting {
|
||||||
night,
|
night,
|
||||||
|
|
@ -53,7 +53,7 @@ impl KillOutcome {
|
||||||
protecting_from_cause: Box::new(original_kill.clone()),
|
protecting_from_cause: Box::new(original_kill.clone()),
|
||||||
});
|
});
|
||||||
village
|
village
|
||||||
.character_by_id_mut(&guardian)
|
.character_by_id_mut(guardian)
|
||||||
.ok_or(GameError::InvalidTarget)?
|
.ok_or(GameError::InvalidTarget)?
|
||||||
.kill(original_kill);
|
.kill(original_kill);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -115,7 +115,7 @@ pub fn resolve_kill(
|
||||||
&& let Some(ss_source) = changes.shapeshifter()
|
&& let Some(ss_source) = changes.shapeshifter()
|
||||||
{
|
{
|
||||||
let killing_wolf = village
|
let killing_wolf = village
|
||||||
.character_by_id(killing_wolf)
|
.character_by_id(*killing_wolf)
|
||||||
.ok_or(GameError::InvalidTarget)?;
|
.ok_or(GameError::InvalidTarget)?;
|
||||||
|
|
||||||
match changes.protected_take(target) {
|
match changes.protected_take(target) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use core::num::NonZeroU8;
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -40,6 +40,9 @@ pub enum NightChange {
|
||||||
target: CharacterId,
|
target: CharacterId,
|
||||||
protection: Protection,
|
protection: Protection,
|
||||||
},
|
},
|
||||||
|
ElderReveal {
|
||||||
|
elder: CharacterId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BlockResolvedOutcome {
|
enum BlockResolvedOutcome {
|
||||||
|
|
@ -108,9 +111,17 @@ impl Night {
|
||||||
DateTime::Night { number } => number,
|
DateTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
let mut action_queue = village
|
let mut action_queue = village
|
||||||
.characters()
|
.characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(filter)
|
||||||
.map(|c| c.night_action_prompt(&village))
|
.map(|c| c.night_action_prompt(&village))
|
||||||
.collect::<Result<Box<[_]>>>()?
|
.collect::<Result<Box<[_]>>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -141,30 +152,8 @@ impl Night {
|
||||||
current_prompt: ActionPrompt::CoverOfDarkness,
|
current_prompt: ActionPrompt::CoverOfDarkness,
|
||||||
current_result: None,
|
current_result: None,
|
||||||
};
|
};
|
||||||
let mut changes = Vec::new();
|
|
||||||
if let Some(night_nz) = NonZeroU8::new(night) {
|
let changes = Self::automatic_changes(&village, 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 {
|
Ok(Self {
|
||||||
night,
|
night,
|
||||||
|
|
@ -176,6 +165,39 @@ impl Night {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// changes that require no input (such as hunter firing)
|
||||||
|
fn automatic_changes(village: &Village, night: u8) -> Vec<NightChange> {
|
||||||
|
let mut changes = Vec::new();
|
||||||
|
let night = match NonZeroU8::new(night) {
|
||||||
|
Some(night) => night,
|
||||||
|
None => return changes,
|
||||||
|
};
|
||||||
|
if !village.executed_known_elder() {
|
||||||
|
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.get()).then_some((c, t)),
|
||||||
|
DateTime::Night { number: _ } => None,
|
||||||
|
})
|
||||||
|
.map(|(c, target)| NightChange::Kill {
|
||||||
|
target,
|
||||||
|
died_to: DiedTo::Hunter {
|
||||||
|
night,
|
||||||
|
killer: c.character_id().clone(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.for_each(|c| changes.push(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
|
||||||
pub fn previous_state(&mut self) -> Result<()> {
|
pub fn previous_state(&mut self) -> Result<()> {
|
||||||
let prev_act = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
|
let prev_act = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
|
||||||
log::info!("loading previous prompt: {prev_act:?}");
|
log::info!("loading previous prompt: {prev_act:?}");
|
||||||
|
|
@ -224,13 +246,17 @@ impl Night {
|
||||||
let mut changes = ChangesLookup::new(&self.changes);
|
let mut changes = ChangesLookup::new(&self.changes);
|
||||||
for change in self.changes.iter() {
|
for change in self.changes.iter() {
|
||||||
match change {
|
match change {
|
||||||
|
NightChange::ElderReveal { elder } => new_village
|
||||||
|
.character_by_id_mut(*elder)
|
||||||
|
.ok_or(GameError::InvalidTarget)?
|
||||||
|
.elder_reveal(),
|
||||||
NightChange::RoleChange(character_id, role_title) => new_village
|
NightChange::RoleChange(character_id, role_title) => new_village
|
||||||
.character_by_id_mut(character_id)
|
.character_by_id_mut(*character_id)
|
||||||
.unwrap()
|
.ok_or(GameError::InvalidTarget)?
|
||||||
.role_change(*role_title, DateTime::Night { number: self.night })?,
|
.role_change(*role_title, DateTime::Night { number: self.night })?,
|
||||||
NightChange::HunterTarget { source, target } => {
|
NightChange::HunterTarget { source, target } => {
|
||||||
if let Role::Hunter { target: t } =
|
if let Role::Hunter { target: t } =
|
||||||
new_village.character_by_id_mut(source).unwrap().role_mut()
|
new_village.character_by_id_mut(*source).unwrap().role_mut()
|
||||||
{
|
{
|
||||||
t.replace(target.clone());
|
t.replace(target.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +265,7 @@ impl Night {
|
||||||
&& changes.protected(target).is_none()
|
&& changes.protected(target).is_none()
|
||||||
{
|
{
|
||||||
new_village
|
new_village
|
||||||
.character_by_id_mut(target)
|
.character_by_id_mut(*target)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.kill(DiedTo::Hunter {
|
.kill(DiedTo::Hunter {
|
||||||
killer: source.clone(),
|
killer: source.clone(),
|
||||||
|
|
@ -262,7 +288,7 @@ impl Night {
|
||||||
if let Some(target) = changes.wolf_pack_kill_target()
|
if let Some(target) = changes.wolf_pack_kill_target()
|
||||||
&& changes.protected(target).is_none()
|
&& changes.protected(target).is_none()
|
||||||
{
|
{
|
||||||
let ss = new_village.character_by_id_mut(source).unwrap();
|
let ss = new_village.character_by_id_mut(*source).unwrap();
|
||||||
match ss.role_mut() {
|
match ss.role_mut() {
|
||||||
Role::Shapeshifter { shifted_into } => {
|
Role::Shapeshifter { shifted_into } => {
|
||||||
*shifted_into = Some(target.clone())
|
*shifted_into = Some(target.clone())
|
||||||
|
|
@ -343,7 +369,7 @@ impl Night {
|
||||||
new_role: RoleTitle::Werewolf,
|
new_role: RoleTitle::Werewolf,
|
||||||
character_id: self
|
character_id: self
|
||||||
.village
|
.village
|
||||||
.character_by_id(&kill_target)
|
.character_by_id(kill_target)
|
||||||
.ok_or(GameError::NoMatchingCharacterFound)?
|
.ok_or(GameError::NoMatchingCharacterFound)?
|
||||||
.identity(),
|
.identity(),
|
||||||
});
|
});
|
||||||
|
|
@ -593,13 +619,22 @@ impl Night {
|
||||||
unless: None,
|
unless: None,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
ActionPrompt::ElderReveal { character_id } => {
|
||||||
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::ElderReveal {
|
||||||
|
elder: character_id.character_id.clone(),
|
||||||
|
}),
|
||||||
|
unless: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
ActionPrompt::Seer {
|
ActionPrompt::Seer {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let alignment = self
|
let alignment = self
|
||||||
.village
|
.village
|
||||||
.character_by_id(marked)
|
.character_by_id(*marked)
|
||||||
.ok_or(GameError::InvalidTarget)?
|
.ok_or(GameError::InvalidTarget)?
|
||||||
.alignment();
|
.alignment();
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
|
@ -628,12 +663,12 @@ impl Night {
|
||||||
} => {
|
} => {
|
||||||
let same = self
|
let same = self
|
||||||
.village
|
.village
|
||||||
.character_by_id(marked1)
|
.character_by_id(*marked1)
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?
|
.ok_or(GameError::InvalidMessageForGameState)?
|
||||||
.alignment()
|
.alignment()
|
||||||
== self
|
== self
|
||||||
.village
|
.village
|
||||||
.character_by_id(marked2)
|
.character_by_id(*marked2)
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?
|
.ok_or(GameError::InvalidMessageForGameState)?
|
||||||
.alignment();
|
.alignment();
|
||||||
|
|
||||||
|
|
@ -649,7 +684,7 @@ impl Night {
|
||||||
} => {
|
} => {
|
||||||
let dig_role = self
|
let dig_role = self
|
||||||
.village
|
.village
|
||||||
.character_by_id(marked)
|
.character_by_id(*marked)
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?
|
.ok_or(GameError::InvalidMessageForGameState)?
|
||||||
.gravedigger_dig();
|
.gravedigger_dig();
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
|
@ -871,13 +906,14 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn current_character_id(&self) -> Option<&CharacterId> {
|
pub const fn current_character_id(&self) -> Option<CharacterId> {
|
||||||
match &self.night_state {
|
match &self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_prompt,
|
current_prompt,
|
||||||
current_result: _,
|
current_result: _,
|
||||||
} => match current_prompt {
|
} => match current_prompt {
|
||||||
ActionPrompt::RoleChange { character_id, .. }
|
ActionPrompt::ElderReveal { character_id }
|
||||||
|
| ActionPrompt::RoleChange { character_id, .. }
|
||||||
| ActionPrompt::Seer { character_id, .. }
|
| ActionPrompt::Seer { character_id, .. }
|
||||||
| ActionPrompt::Protector { character_id, .. }
|
| ActionPrompt::Protector { character_id, .. }
|
||||||
| ActionPrompt::Arcanist { character_id, .. }
|
| ActionPrompt::Arcanist { character_id, .. }
|
||||||
|
|
@ -888,7 +924,7 @@ impl Night {
|
||||||
| ActionPrompt::Guardian { character_id, .. }
|
| ActionPrompt::Guardian { character_id, .. }
|
||||||
| ActionPrompt::Shapeshifter { character_id }
|
| ActionPrompt::Shapeshifter { character_id }
|
||||||
| ActionPrompt::AlphaWolf { character_id, .. }
|
| ActionPrompt::AlphaWolf { character_id, .. }
|
||||||
| ActionPrompt::DireWolf { character_id, .. } => Some(&character_id.character_id),
|
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
|
||||||
ActionPrompt::WolvesIntro { wolves: _ }
|
ActionPrompt::WolvesIntro { wolves: _ }
|
||||||
| ActionPrompt::WolfPackKill { .. }
|
| ActionPrompt::WolfPackKill { .. }
|
||||||
| ActionPrompt::CoverOfDarkness => None,
|
| ActionPrompt::CoverOfDarkness => None,
|
||||||
|
|
@ -940,3 +976,15 @@ pub enum ServerAction {
|
||||||
Prompt(ActionPrompt),
|
Prompt(ActionPrompt),
|
||||||
Result(ActionResult),
|
Result(ActionResult),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod filter {
|
||||||
|
use crate::player::Character;
|
||||||
|
|
||||||
|
pub fn no_filter(_: &Character) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_village(c: &Character) -> bool {
|
||||||
|
!c.is_village()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,22 @@ impl Default for GameSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameSettings {
|
impl GameSettings {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
roles: vec![],
|
||||||
|
next_order: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_remaining_slots_with_villagers(&mut self, player_count: usize) {
|
||||||
|
if self.roles.len() >= player_count {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for _ in 0..(player_count - self.roles.len()) {
|
||||||
|
self.new_slot(RoleTitle::Villager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn wolves_count(&self) -> usize {
|
pub fn wolves_count(&self) -> usize {
|
||||||
self.roles
|
self.roles
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -38,6 +54,10 @@ impl GameSettings {
|
||||||
&self.roles
|
&self.roles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_slot_by_id(&self, slot_id: SlotId) -> Option<&SetupSlot> {
|
||||||
|
self.roles.iter().find(|s| s.slot_id == slot_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn village_roles_count(&self) -> usize {
|
pub fn village_roles_count(&self) -> usize {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"wolves: {} total: {}",
|
"wolves: {} total: {}",
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,8 @@ impl SetupRole {
|
||||||
}
|
}
|
||||||
SetupRole::Elder { knows_on_night } => Role::Elder {
|
SetupRole::Elder { knows_on_night } => Role::Elder {
|
||||||
knows_on_night,
|
knows_on_night,
|
||||||
has_protection: true,
|
woken_for_reveal: false,
|
||||||
|
lost_protection_night: None,
|
||||||
},
|
},
|
||||||
SetupRole::Werewolf => Role::Werewolf,
|
SetupRole::Werewolf => Role::Werewolf,
|
||||||
SetupRole::AlphaWolf => Role::AlphaWolf { killed: None },
|
SetupRole::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use rand::{Rng, seq::SliceRandom};
|
use rand::Rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{DateTime, GameOver, GameSettings},
|
game::{DateTime, GameOver, GameSettings},
|
||||||
message::{CharacterIdentity, Identification},
|
message::{CharacterIdentity, Identification},
|
||||||
|
|
@ -62,7 +63,7 @@ impl Village {
|
||||||
self.date_time
|
self.date_time
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_character_id(&self, character_id: &CharacterId) -> Option<&Character> {
|
pub fn find_by_character_id(&self, character_id: CharacterId) -> Option<&Character> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| c.character_id() == character_id)
|
.find(|c| c.character_id() == character_id)
|
||||||
|
|
@ -70,7 +71,7 @@ impl Village {
|
||||||
|
|
||||||
pub fn find_by_character_id_mut(
|
pub fn find_by_character_id_mut(
|
||||||
&mut self,
|
&mut self,
|
||||||
character_id: &CharacterId,
|
character_id: CharacterId,
|
||||||
) -> Option<&mut Character> {
|
) -> Option<&mut Character> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
|
|
@ -115,7 +116,7 @@ impl Village {
|
||||||
let targets = self
|
let targets = self
|
||||||
.characters
|
.characters
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.filter(|c| characters.contains(c.character_id()))
|
.filter(|c| characters.contains(&c.character_id()))
|
||||||
.collect::<Box<[_]>>();
|
.collect::<Box<[_]>>();
|
||||||
for t in targets {
|
for t in targets {
|
||||||
t.execute(day)?;
|
t.execute(day)?;
|
||||||
|
|
@ -162,7 +163,7 @@ impl Village {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn target_by_id(&self, character_id: &CharacterId) -> Option<CharacterIdentity> {
|
pub fn target_by_id(&self, character_id: CharacterId) -> Option<CharacterIdentity> {
|
||||||
self.character_by_id(character_id).map(Character::identity)
|
self.character_by_id(character_id).map(Character::identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,7 +175,7 @@ impl Village {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[CharacterIdentity]> {
|
pub fn living_players_excluding(&self, exclude: CharacterId) -> Box<[CharacterIdentity]> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.alive() && c.character_id() != exclude)
|
.filter(|c| c.alive() && c.character_id() != exclude)
|
||||||
|
|
@ -194,6 +195,21 @@ impl Village {
|
||||||
self.characters.iter().filter(|c| !c.alive()).collect()
|
self.characters.iter().filter(|c| !c.alive()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn executed_known_elder(&self) -> bool {
|
||||||
|
self.characters.iter().any(|d| {
|
||||||
|
matches!(
|
||||||
|
d.role(),
|
||||||
|
Role::Elder {
|
||||||
|
woken_for_reveal: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
) && d
|
||||||
|
.died_to()
|
||||||
|
.map(|d| matches!(d, DiedTo::Execution { .. }))
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> {
|
pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -206,19 +222,19 @@ impl Village {
|
||||||
self.characters.iter().cloned().collect()
|
self.characters.iter().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn character_by_id_mut(&mut self, character_id: &CharacterId) -> Option<&mut Character> {
|
pub fn character_by_id_mut(&mut self, character_id: CharacterId) -> Option<&mut Character> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|c| c.character_id() == character_id)
|
.find(|c| c.character_id() == character_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn character_by_id(&self, character_id: &CharacterId) -> Option<&Character> {
|
pub fn character_by_id(&self, character_id: CharacterId) -> Option<&Character> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| c.character_id() == character_id)
|
.find(|c| c.character_id() == character_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn character_by_player_id(&self, player_id: &PlayerId) -> Option<&Character> {
|
pub fn character_by_player_id(&self, player_id: PlayerId) -> Option<&Character> {
|
||||||
self.characters.iter().find(|c| c.player_id() == player_id)
|
self.characters.iter().find(|c| c.player_id() == player_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +250,8 @@ impl RoleTitle {
|
||||||
RoleTitle::Arcanist => Role::Arcanist,
|
RoleTitle::Arcanist => Role::Arcanist,
|
||||||
RoleTitle::Elder => Role::Elder {
|
RoleTitle::Elder => Role::Elder {
|
||||||
knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(),
|
knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(),
|
||||||
has_protection: true,
|
woken_for_reveal: false,
|
||||||
|
lost_protection_night: None,
|
||||||
},
|
},
|
||||||
RoleTitle::Werewolf => Role::Werewolf,
|
RoleTitle::Werewolf => Role::Werewolf,
|
||||||
RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
mod night_order;
|
mod night_order;
|
||||||
|
mod role;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange},
|
game::{Game, GameSettings, OrRandom, SetupRole, SetupSlot, night::NightChange},
|
||||||
message::{
|
message::{
|
||||||
CharacterState, Identification, PublicIdentity,
|
CharacterState, Identification, PublicIdentity,
|
||||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
||||||
},
|
},
|
||||||
player::{CharacterId, PlayerId},
|
player::{Character, CharacterId, PlayerId},
|
||||||
role::{Alignment, Role, RoleTitle},
|
role::{Alignment, Role, RoleTitle},
|
||||||
};
|
};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
@ -22,7 +23,92 @@ use core::{
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
trait ActionResultExt {
|
pub trait SettingsExt {
|
||||||
|
fn add_role(&mut self, role: SetupRole, modify: impl FnOnce(&mut SetupSlot));
|
||||||
|
fn add_and_assign(&mut self, role: SetupRole, assignee: PlayerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SettingsExt for GameSettings {
|
||||||
|
fn add_role(&mut self, role: SetupRole, modify: impl FnOnce(&mut SetupSlot)) {
|
||||||
|
let slot_id = self.new_slot(role.clone().into());
|
||||||
|
let mut slot = self.get_slot_by_id(slot_id).unwrap().clone();
|
||||||
|
slot.role = role;
|
||||||
|
modify(&mut slot);
|
||||||
|
self.update_slot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_and_assign(&mut self, role: SetupRole, assignee: PlayerId) {
|
||||||
|
self.add_role(role, |slot| slot.assign_to = Some(assignee));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ActionPromptTitleExt {
|
||||||
|
fn wolf_pack_kill(&self);
|
||||||
|
fn cover_of_darkness(&self);
|
||||||
|
fn wolves_intro(&self);
|
||||||
|
fn role_change(&self);
|
||||||
|
fn seer(&self);
|
||||||
|
fn protector(&self);
|
||||||
|
fn arcanist(&self);
|
||||||
|
fn gravedigger(&self);
|
||||||
|
fn hunter(&self);
|
||||||
|
fn militia(&self);
|
||||||
|
fn maplewolf(&self);
|
||||||
|
fn guardian(&self);
|
||||||
|
fn shapeshifter(&self);
|
||||||
|
fn alphawolf(&self);
|
||||||
|
fn direwolf(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
|
fn cover_of_darkness(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::CoverOfDarkness);
|
||||||
|
}
|
||||||
|
fn wolves_intro(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::WolvesIntro);
|
||||||
|
}
|
||||||
|
fn role_change(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::RoleChange);
|
||||||
|
}
|
||||||
|
fn seer(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Seer);
|
||||||
|
}
|
||||||
|
fn protector(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Protector);
|
||||||
|
}
|
||||||
|
fn arcanist(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Arcanist);
|
||||||
|
}
|
||||||
|
fn gravedigger(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Gravedigger);
|
||||||
|
}
|
||||||
|
fn hunter(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Hunter);
|
||||||
|
}
|
||||||
|
fn militia(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Militia);
|
||||||
|
}
|
||||||
|
fn maplewolf(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::MapleWolf);
|
||||||
|
}
|
||||||
|
fn guardian(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Guardian);
|
||||||
|
}
|
||||||
|
fn shapeshifter(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Shapeshifter);
|
||||||
|
}
|
||||||
|
fn alphawolf(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::AlphaWolf);
|
||||||
|
}
|
||||||
|
fn direwolf(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::DireWolf);
|
||||||
|
}
|
||||||
|
fn wolf_pack_kill(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::WolfPackKill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ActionResultExt {
|
||||||
fn sleep(&self);
|
fn sleep(&self);
|
||||||
fn r#continue(&self);
|
fn r#continue(&self);
|
||||||
fn seer(&self) -> Alignment;
|
fn seer(&self) -> Alignment;
|
||||||
|
|
@ -45,7 +131,7 @@ impl ActionResultExt for ActionResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ServerToHostMessageExt {
|
pub trait ServerToHostMessageExt {
|
||||||
fn prompt(self) -> ActionPrompt;
|
fn prompt(self) -> ActionPrompt;
|
||||||
fn result(self) -> ActionResult;
|
fn result(self) -> ActionResult;
|
||||||
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
||||||
|
|
@ -83,21 +169,53 @@ impl ServerToHostMessageExt for ServerToHostMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait GameExt {
|
pub trait GameExt {
|
||||||
|
fn villager_character_ids(&self) -> Box<[CharacterId]>;
|
||||||
|
fn character_by_player_id(&self, player_id: PlayerId) -> Character;
|
||||||
fn next(&mut self) -> ActionPrompt;
|
fn next(&mut self) -> ActionPrompt;
|
||||||
fn r#continue(&mut self) -> ActionResult;
|
fn r#continue(&mut self) -> ActionResult;
|
||||||
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
||||||
fn mark(&mut self, mark: &CharacterId) -> ActionPrompt;
|
fn mark(&mut self, mark: CharacterId) -> ActionPrompt;
|
||||||
fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool);
|
fn mark_and_check(&mut self, mark: CharacterId);
|
||||||
fn response(&mut self, resp: ActionResponse) -> ActionResult;
|
fn response(&mut self, resp: ActionResponse) -> ActionResult;
|
||||||
fn execute(&mut self) -> ActionPrompt;
|
fn execute(&mut self) -> ActionPrompt;
|
||||||
fn mark_for_execution(
|
fn mark_for_execution(
|
||||||
&mut self,
|
&mut self,
|
||||||
target: CharacterId,
|
target: CharacterId,
|
||||||
) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
||||||
|
fn living_villager_excl(&self, excl: PlayerId) -> Character;
|
||||||
|
#[allow(unused)]
|
||||||
|
fn get_state(&mut self) -> ServerToHostMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameExt for Game {
|
impl GameExt for Game {
|
||||||
|
fn get_state(&mut self) -> ServerToHostMessage {
|
||||||
|
self.process(HostGameMessage::GetState).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn living_villager_excl(&self, excl: PlayerId) -> Character {
|
||||||
|
self.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.alive() && matches!(c.role(), Role::Villager) && c.player_id() != excl)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn villager_character_ids(&self) -> Box<[CharacterId]> {
|
||||||
|
self.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|c| matches!(c.role(), Role::Villager).then_some(c.character_id().clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn character_by_player_id(&self, player_id: PlayerId) -> Character {
|
||||||
|
self.village()
|
||||||
|
.character_by_player_id(player_id)
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn r#continue(&mut self) -> ActionResult {
|
fn r#continue(&mut self) -> ActionResult {
|
||||||
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
ActionResponse::Continue,
|
ActionResponse::Continue,
|
||||||
|
|
@ -106,7 +224,7 @@ impl GameExt for Game {
|
||||||
.result()
|
.result()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark(&mut self, mark: &CharacterId) -> ActionPrompt {
|
fn mark(&mut self, mark: CharacterId) -> ActionPrompt {
|
||||||
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
ActionResponse::MarkTarget(mark.clone()),
|
ActionResponse::MarkTarget(mark.clone()),
|
||||||
)))
|
)))
|
||||||
|
|
@ -114,10 +232,66 @@ impl GameExt for Game {
|
||||||
.prompt()
|
.prompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool) {
|
fn mark_and_check(&mut self, mark: CharacterId) {
|
||||||
let prompt = self.mark(mark);
|
let prompt = self.mark(mark);
|
||||||
if !check(&prompt) {
|
match prompt {
|
||||||
panic!("unexpected prompt: {prompt:?}");
|
ActionPrompt::ElderReveal { .. }
|
||||||
|
| ActionPrompt::CoverOfDarkness
|
||||||
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
||||||
|
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
||||||
|
ActionPrompt::Seer {
|
||||||
|
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::WolfPackKill {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::AlphaWolf {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::DireWolf {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => assert_eq!(marked, mark, "marked character"),
|
||||||
|
|
||||||
|
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::WolfPackKill { marked: None, .. }
|
||||||
|
| ActionPrompt::AlphaWolf { marked: None, .. }
|
||||||
|
| ActionPrompt::DireWolf { marked: None, .. } => panic!("no mark"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,7 +349,7 @@ impl GameExt for Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_log() {
|
pub fn init_log() {
|
||||||
let _ = pretty_env_logger::formatted_builder()
|
let _ = pretty_env_logger::formatted_builder()
|
||||||
.filter_level(log::LevelFilter::Debug)
|
.filter_level(log::LevelFilter::Debug)
|
||||||
.format(|f, record| match record.file() {
|
.format(|f, record| match record.file() {
|
||||||
|
|
@ -206,7 +380,7 @@ fn init_log() {
|
||||||
.try_init();
|
.try_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gen_players(range: Range<u8>) -> Box<[Identification]> {
|
pub fn gen_players(range: Range<u8>) -> Box<[Identification]> {
|
||||||
range
|
range
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|num| Identification {
|
.map(|num| Identification {
|
||||||
|
|
@ -365,157 +539,6 @@ fn yes_wolf_kill_n2() {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn protect_stops_shapeshift() {
|
|
||||||
init_log();
|
|
||||||
let players = gen_players(1..10);
|
|
||||||
let mut settings = GameSettings::default();
|
|
||||||
settings.new_slot(RoleTitle::Shapeshifter);
|
|
||||||
settings.new_slot(RoleTitle::Protector);
|
|
||||||
for _ in 0..7 {
|
|
||||||
settings.new_slot(RoleTitle::Villager);
|
|
||||||
}
|
|
||||||
if let Some(slot) = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
|
||||||
{
|
|
||||||
settings.remove_slot(slot.slot_id);
|
|
||||||
}
|
|
||||||
let mut game = Game::new(&players, settings).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
|
||||||
ActionResponse::Continue,
|
|
||||||
)))
|
|
||||||
.unwrap(),
|
|
||||||
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
|
|
||||||
);
|
|
||||||
assert!(matches!(
|
|
||||||
game.process(HostGameMessage::Night(HostNightMessage::Next))
|
|
||||||
.unwrap(),
|
|
||||||
ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ })
|
|
||||||
));
|
|
||||||
assert_eq!(
|
|
||||||
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
|
||||||
ActionResponse::Continue
|
|
||||||
)))
|
|
||||||
.unwrap(),
|
|
||||||
ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep),
|
|
||||||
);
|
|
||||||
assert!(matches!(
|
|
||||||
game.process(HostGameMessage::Night(HostNightMessage::Next))
|
|
||||||
.unwrap(),
|
|
||||||
ServerToHostMessage::Daytime {
|
|
||||||
characters: _,
|
|
||||||
marked: _,
|
|
||||||
day: _,
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
let execution_target = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.find(|v| v.is_village() && !matches!(v.role().title(), RoleTitle::Protector))
|
|
||||||
.unwrap()
|
|
||||||
.character_id()
|
|
||||||
.clone();
|
|
||||||
match game
|
|
||||||
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
|
|
||||||
execution_target.clone(),
|
|
||||||
)))
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
ServerToHostMessage::Daytime {
|
|
||||||
characters: _,
|
|
||||||
marked,
|
|
||||||
day: _,
|
|
||||||
} => assert_eq!(marked.to_vec(), vec![execution_target]),
|
|
||||||
resp => panic!("unexpected server message: {resp:#?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
game.process(HostGameMessage::Day(HostDayMessage::Execute))
|
|
||||||
.unwrap()
|
|
||||||
.prompt()
|
|
||||||
.title(),
|
|
||||||
ActionPromptTitle::CoverOfDarkness
|
|
||||||
);
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
let (prot_and_wolf_target, prot_char_id) = match game
|
|
||||||
.process(HostGameMessage::Night(HostNightMessage::Next))
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
ServerToHostMessage::ActionPrompt(ActionPrompt::Protector {
|
|
||||||
character_id: prot_char_id,
|
|
||||||
targets,
|
|
||||||
marked: None,
|
|
||||||
}) => (
|
|
||||||
targets
|
|
||||||
.into_iter()
|
|
||||||
.map(|c| game.village().character_by_id(&c.character_id).unwrap())
|
|
||||||
.find(|c| c.is_village())
|
|
||||||
.unwrap()
|
|
||||||
.character_id()
|
|
||||||
.clone(),
|
|
||||||
prot_char_id,
|
|
||||||
),
|
|
||||||
_ => panic!("first n2 prompt isn't protector"),
|
|
||||||
};
|
|
||||||
let target = game
|
|
||||||
.village()
|
|
||||||
.character_by_id(&prot_and_wolf_target)
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
log::info!("target: {target:#?}");
|
|
||||||
|
|
||||||
match game
|
|
||||||
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
|
||||||
ActionResponse::MarkTarget(prot_and_wolf_target.clone()),
|
|
||||||
)))
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
ServerToHostMessage::ActionPrompt(ActionPrompt::Protector {
|
|
||||||
marked: Some(mark), ..
|
|
||||||
}) => assert_eq!(mark, prot_and_wolf_target, "marked target"),
|
|
||||||
resp => panic!("unexpected response: {resp:?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
game.r#continue().sleep();
|
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
|
|
||||||
game.mark_and_check(&prot_and_wolf_target, |c| match c {
|
|
||||||
ActionPrompt::WolfPackKill {
|
|
||||||
marked: Some(mark), ..
|
|
||||||
} => prot_and_wolf_target == *mark,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,);
|
|
||||||
|
|
||||||
game.response(ActionResponse::Shapeshift);
|
|
||||||
|
|
||||||
game.next_expect_day();
|
|
||||||
|
|
||||||
let target = game
|
|
||||||
.village()
|
|
||||||
.character_by_id(target.character_id())
|
|
||||||
.unwrap();
|
|
||||||
assert!(target.is_village());
|
|
||||||
assert!(target.alive());
|
|
||||||
|
|
||||||
let prot = game
|
|
||||||
.village()
|
|
||||||
.character_by_id(&prot_char_id.character_id)
|
|
||||||
.unwrap();
|
|
||||||
assert!(prot.is_village());
|
|
||||||
assert!(prot.alive());
|
|
||||||
assert_eq!(prot.role().title(), RoleTitle::Protector);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wolfpack_kill_all_targets_valid() {
|
fn wolfpack_kill_all_targets_valid() {
|
||||||
init_log();
|
init_log();
|
||||||
|
|
@ -590,228 +613,3 @@ fn wolfpack_kill_all_targets_valid() {
|
||||||
assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter);
|
assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_1_shapeshift_prompt_if_first_shifts() {
|
|
||||||
let players = gen_players(1..10);
|
|
||||||
let mut settings = GameSettings::default();
|
|
||||||
settings.new_slot(RoleTitle::Shapeshifter);
|
|
||||||
settings.new_slot(RoleTitle::Shapeshifter);
|
|
||||||
for _ in 0..7 {
|
|
||||||
settings.new_slot(RoleTitle::Villager);
|
|
||||||
}
|
|
||||||
if let Some(slot) = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
|
||||||
{
|
|
||||||
settings.remove_slot(slot.slot_id);
|
|
||||||
}
|
|
||||||
let mut game = Game::new(&players, settings).unwrap();
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
|
||||||
game.r#continue().sleep();
|
|
||||||
game.next_expect_day();
|
|
||||||
let target = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.find_map(|c| c.is_village().then_some(c.character_id().clone()))
|
|
||||||
.unwrap();
|
|
||||||
let (_, marked, _) = game.mark_for_execution(target.clone());
|
|
||||||
let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]);
|
|
||||||
assert_eq!(target_list, marked);
|
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
let target = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.find_map(|c| (c.is_village() && c.alive()).then_some(c.character_id().clone()))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
game.mark_and_check(&target, |p| match p {
|
|
||||||
ActionPrompt::WolfPackKill {
|
|
||||||
marked: Some(t), ..
|
|
||||||
} => *t == target,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter);
|
|
||||||
game.response(ActionResponse::Shapeshift).r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::RoleChange);
|
|
||||||
game.r#continue().sleep();
|
|
||||||
|
|
||||||
game.next_expect_day();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn redeemed_scapegoat_role_changes() {
|
|
||||||
let players = gen_players(1..10);
|
|
||||||
let scapegoat_player_id = players[0].player_id.clone();
|
|
||||||
let seer_player_id = players[1].player_id.clone();
|
|
||||||
let wolf_player_id = players[2].player_id.clone();
|
|
||||||
let wolf_target_2_player_id = players[3].player_id.clone();
|
|
||||||
let mut settings = GameSettings::default();
|
|
||||||
{
|
|
||||||
let scapegoat_slot = settings.new_slot(RoleTitle::Scapegoat);
|
|
||||||
let mut scapegoat_slot = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.slot_id == scapegoat_slot)
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
scapegoat_slot.role = SetupRole::Scapegoat {
|
|
||||||
redeemed: OrRandom::Determined(true),
|
|
||||||
};
|
|
||||||
scapegoat_slot.assign_to = Some(scapegoat_player_id.clone());
|
|
||||||
settings.update_slot(scapegoat_slot);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let mut slot = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
slot.assign_to = Some(wolf_player_id.clone());
|
|
||||||
settings.update_slot(slot);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let slot = settings.new_slot(RoleTitle::Seer);
|
|
||||||
let mut slot = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.slot_id == slot)
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
slot.assign_to = Some(seer_player_id.clone());
|
|
||||||
settings.update_slot(slot);
|
|
||||||
}
|
|
||||||
for _ in 0..6 {
|
|
||||||
settings.new_slot(RoleTitle::Villager);
|
|
||||||
}
|
|
||||||
let mut game = Game::new(&players, settings).unwrap();
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
|
||||||
game.r#continue().sleep();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
|
|
||||||
let wolf_char_id = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.find(|c| c.player_id() == &wolf_player_id)
|
|
||||||
.unwrap()
|
|
||||||
.character_id()
|
|
||||||
.clone();
|
|
||||||
game.mark_and_check(&wolf_char_id, |p| match p {
|
|
||||||
ActionPrompt::Seer {
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => marked == &wolf_char_id,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
|
||||||
game.next_expect_day();
|
|
||||||
|
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
let seer = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.find(|c| c.player_id() == &seer_player_id)
|
|
||||||
.unwrap()
|
|
||||||
.character_id()
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
game.mark_and_check(&seer, |p| match p {
|
|
||||||
ActionPrompt::WolfPackKill {
|
|
||||||
marked: Some(t), ..
|
|
||||||
} => *t == seer,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
game.r#continue().sleep();
|
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
|
|
||||||
game.mark_and_check(&wolf_char_id, |p| match p {
|
|
||||||
ActionPrompt::Seer {
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => marked == &wolf_char_id,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
|
||||||
game.next_expect_day();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*game
|
|
||||||
.village()
|
|
||||||
.character_by_id(&seer)
|
|
||||||
.unwrap()
|
|
||||||
.died_to()
|
|
||||||
.unwrap(),
|
|
||||||
DiedTo::Wolfpack {
|
|
||||||
killing_wolf: wolf_char_id.clone(),
|
|
||||||
night: NonZero::new(1).unwrap()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
let wolf_target_2 = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.iter()
|
|
||||||
.find(|c| c.player_id() == &wolf_target_2_player_id)
|
|
||||||
.unwrap()
|
|
||||||
.character_id()
|
|
||||||
.clone();
|
|
||||||
game.mark_and_check(&wolf_target_2, |r| match r {
|
|
||||||
ActionPrompt::WolfPackKill {
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => marked == &wolf_target_2,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
game.r#continue().sleep();
|
|
||||||
let scapegoat = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.find(|c| c.player_id() == &scapegoat_player_id)
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
assert_eq!(
|
|
||||||
game.next(),
|
|
||||||
ActionPrompt::RoleChange {
|
|
||||||
character_id: scapegoat.identity(),
|
|
||||||
new_role: RoleTitle::Seer
|
|
||||||
}
|
|
||||||
);
|
|
||||||
game.r#continue().sleep();
|
|
||||||
|
|
||||||
match game.game_state() {
|
|
||||||
crate::game::GameState::Night { night } => night
|
|
||||||
.changes()
|
|
||||||
.iter()
|
|
||||||
.find(|c| match c {
|
|
||||||
NightChange::RoleChange(char, role) => {
|
|
||||||
char == scapegoat.character_id() && role == &RoleTitle::Seer
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
.expect("no role change"),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
game.next_expect_day();
|
|
||||||
let day_scapegoat = game
|
|
||||||
.village()
|
|
||||||
.character_by_id(scapegoat.character_id())
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
|
role::{Alignment, Role},
|
||||||
|
};
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elder_doesnt_die_first_try_night_doesnt_know() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let elder_player_id = players[0].player_id.clone();
|
||||||
|
let wolf_player_id = players[2].player_id.clone();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_role(
|
||||||
|
SetupRole::Elder {
|
||||||
|
knows_on_night: NonZeroU8::new(3).unwrap(),
|
||||||
|
},
|
||||||
|
|slot| {
|
||||||
|
slot.assign_to = Some(elder_player_id.clone());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
settings.add_role(SetupRole::Werewolf, |slot| {
|
||||||
|
slot.assign_to = Some(wolf_player_id.clone())
|
||||||
|
});
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
|
||||||
|
game.mark_and_check(elder.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
assert_eq!(elder.died_to().cloned(), None);
|
||||||
|
|
||||||
|
assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
game.mark_and_check(elder.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(elder_player_id)
|
||||||
|
.died_to()
|
||||||
|
.cloned(),
|
||||||
|
Some(DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game
|
||||||
|
.character_by_player_id(wolf_player_id)
|
||||||
|
.character_id()
|
||||||
|
.clone(),
|
||||||
|
night: NonZeroU8::new(2).unwrap(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elder_doesnt_die_first_try_night_knows() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let elder_player_id = players[0].player_id.clone();
|
||||||
|
let wolf_player_id = players[2].player_id.clone();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_role(
|
||||||
|
SetupRole::Elder {
|
||||||
|
knows_on_night: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
|slot| {
|
||||||
|
slot.assign_to = Some(elder_player_id.clone());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
settings.add_role(SetupRole::Werewolf, |slot| {
|
||||||
|
slot.assign_to = Some(wolf_player_id.clone())
|
||||||
|
});
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
|
||||||
|
game.mark_and_check(elder.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.next(),
|
||||||
|
ActionPrompt::ElderReveal {
|
||||||
|
character_id: elder.identity()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
assert_eq!(elder.died_to().cloned(), None);
|
||||||
|
|
||||||
|
assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
game.mark_and_check(elder.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(elder_player_id)
|
||||||
|
.died_to()
|
||||||
|
.cloned(),
|
||||||
|
Some(DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf_player_id).character_id(),
|
||||||
|
night: NonZeroU8::new(2).unwrap(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elder_executed_doesnt_know() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let elder_player_id = players[0].player_id;
|
||||||
|
let seer_player_id = players[1].player_id;
|
||||||
|
let wolf_player_id = players[2].player_id;
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_role(
|
||||||
|
SetupRole::Elder {
|
||||||
|
knows_on_night: NonZeroU8::new(3).unwrap(),
|
||||||
|
},
|
||||||
|
|slot| {
|
||||||
|
slot.assign_to = Some(elder_player_id.clone());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
settings.add_and_assign(SetupRole::Seer, seer_player_id.clone());
|
||||||
|
settings.add_role(SetupRole::Werewolf, |slot| {
|
||||||
|
slot.assign_to = Some(wolf_player_id.clone())
|
||||||
|
});
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
let mut villagers = game.villager_character_ids().into_iter();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(villagers.next().unwrap());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Village);
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
|
||||||
|
game.mark_for_execution(elder.character_id());
|
||||||
|
|
||||||
|
game.execute().title().cover_of_darkness();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(elder_player_id)
|
||||||
|
.died_to()
|
||||||
|
.cloned(),
|
||||||
|
Some(DiedTo::Execution {
|
||||||
|
day: NonZeroU8::new(1).unwrap()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(elder_player_id).role().clone(),
|
||||||
|
Role::Elder {
|
||||||
|
knows_on_night: NonZeroU8::new(3).unwrap(),
|
||||||
|
woken_for_reveal: false,
|
||||||
|
lost_protection_night: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
game.mark(villagers.next().unwrap());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(villagers.next().unwrap());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Village);
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elder_executed_knows_no_powers_incl_hunter_activation() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let elder_player_id = players[0].player_id;
|
||||||
|
let seer_player_id = players[1].player_id;
|
||||||
|
let wolf_player_id = players[2].player_id;
|
||||||
|
let hunter_player_id = players[3].player_id;
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_role(
|
||||||
|
SetupRole::Elder {
|
||||||
|
knows_on_night: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
|slot| {
|
||||||
|
slot.assign_to = Some(elder_player_id.clone());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
settings.add_and_assign(SetupRole::Seer, seer_player_id.clone());
|
||||||
|
settings.add_role(SetupRole::Werewolf, |slot| {
|
||||||
|
slot.assign_to = Some(wolf_player_id.clone())
|
||||||
|
});
|
||||||
|
settings.add_and_assign(SetupRole::Hunter, hunter_player_id);
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
let mut villagers = game.villager_character_ids().into_iter();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.living_villager_excl(seer_player_id).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Village);
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().cover_of_darkness();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(villagers.next().unwrap());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.living_villager_excl(seer_player_id).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Village);
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(wolf_player_id).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.next(),
|
||||||
|
ActionPrompt::ElderReveal {
|
||||||
|
character_id: game.character_by_player_id(elder_player_id).identity()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(elder_player_id).role().clone(),
|
||||||
|
Role::Elder {
|
||||||
|
knows_on_night: NonZeroU8::new(1).unwrap(),
|
||||||
|
woken_for_reveal: true,
|
||||||
|
lost_protection_night: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
game.mark_for_execution(game.character_by_player_id(elder_player_id).character_id());
|
||||||
|
game.execute().title().cover_of_darkness();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(game.character_by_player_id(hunter_player_id).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(hunter_player_id)
|
||||||
|
.died_to()
|
||||||
|
.cloned(),
|
||||||
|
Some(DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf_player_id).character_id(),
|
||||||
|
night: NonZeroU8::new(2).unwrap()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(wolf_player_id)
|
||||||
|
.died_to()
|
||||||
|
.cloned(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod elder;
|
||||||
|
mod scapegoat;
|
||||||
|
mod shapeshifter;
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
use core::num::NonZero;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange},
|
||||||
|
game_test::{ActionResultExt, GameExt, gen_players},
|
||||||
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
|
role::{Alignment, RoleTitle},
|
||||||
|
};
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn redeemed_scapegoat_role_changes() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let scapegoat_player_id = players[0].player_id.clone();
|
||||||
|
let seer_player_id = players[1].player_id.clone();
|
||||||
|
let wolf_player_id = players[2].player_id.clone();
|
||||||
|
let wolf_target_2_player_id = players[3].player_id.clone();
|
||||||
|
let mut settings = GameSettings::default();
|
||||||
|
{
|
||||||
|
let scapegoat_slot = settings.new_slot(RoleTitle::Scapegoat);
|
||||||
|
let mut scapegoat_slot = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.slot_id == scapegoat_slot)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
scapegoat_slot.role = SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(true),
|
||||||
|
};
|
||||||
|
scapegoat_slot.assign_to = Some(scapegoat_player_id.clone());
|
||||||
|
settings.update_slot(scapegoat_slot);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut slot = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
slot.assign_to = Some(wolf_player_id.clone());
|
||||||
|
settings.update_slot(slot);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let slot = settings.new_slot(RoleTitle::Seer);
|
||||||
|
let mut slot = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.slot_id == slot)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
slot.assign_to = Some(seer_player_id.clone());
|
||||||
|
settings.update_slot(slot);
|
||||||
|
}
|
||||||
|
for _ in 0..6 {
|
||||||
|
settings.new_slot(RoleTitle::Villager);
|
||||||
|
}
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
|
||||||
|
let wolf_char_id = game
|
||||||
|
.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.player_id() == wolf_player_id)
|
||||||
|
.unwrap()
|
||||||
|
.character_id()
|
||||||
|
.clone();
|
||||||
|
game.mark_and_check(wolf_char_id);
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
let seer = game
|
||||||
|
.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.player_id() == seer_player_id)
|
||||||
|
.unwrap()
|
||||||
|
.character_id()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
game.mark_and_check(seer);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
|
||||||
|
game.mark_and_check(wolf_char_id);
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game
|
||||||
|
.village()
|
||||||
|
.character_by_id(seer)
|
||||||
|
.unwrap()
|
||||||
|
.died_to()
|
||||||
|
.unwrap(),
|
||||||
|
DiedTo::Wolfpack {
|
||||||
|
killing_wolf: wolf_char_id.clone(),
|
||||||
|
night: NonZero::new(1).unwrap()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
let wolf_target_2 = game
|
||||||
|
.village()
|
||||||
|
.characters()
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.player_id() == wolf_target_2_player_id)
|
||||||
|
.unwrap()
|
||||||
|
.character_id()
|
||||||
|
.clone();
|
||||||
|
game.mark_and_check(wolf_target_2);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
let scapegoat = game
|
||||||
|
.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.player_id() == scapegoat_player_id)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
assert_eq!(
|
||||||
|
game.next(),
|
||||||
|
ActionPrompt::RoleChange {
|
||||||
|
character_id: scapegoat.identity(),
|
||||||
|
new_role: RoleTitle::Seer
|
||||||
|
}
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
match game.game_state() {
|
||||||
|
crate::game::GameState::Night { night } => night
|
||||||
|
.changes()
|
||||||
|
.iter()
|
||||||
|
.find(|c| match c {
|
||||||
|
NightChange::RoleChange(char, role) => {
|
||||||
|
char == &scapegoat.character_id() && role == &RoleTitle::Seer
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
.expect("no role change"),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
let day_scapegoat = game
|
||||||
|
.village()
|
||||||
|
.character_by_id(scapegoat.character_id())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{ActionResultExt, GameExt, ServerToHostMessageExt, gen_players, init_log},
|
||||||
|
message::{
|
||||||
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
|
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
||||||
|
},
|
||||||
|
player::CharacterId,
|
||||||
|
role::RoleTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protect_stops_shapeshift() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mut settings = GameSettings::default();
|
||||||
|
settings.new_slot(RoleTitle::Shapeshifter);
|
||||||
|
settings.new_slot(RoleTitle::Protector);
|
||||||
|
for _ in 0..7 {
|
||||||
|
settings.new_slot(RoleTitle::Villager);
|
||||||
|
}
|
||||||
|
if let Some(slot) = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
||||||
|
{
|
||||||
|
settings.remove_slot(slot.slot_id);
|
||||||
|
}
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::Continue,
|
||||||
|
)))
|
||||||
|
.unwrap(),
|
||||||
|
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
game.process(HostGameMessage::Night(HostNightMessage::Next))
|
||||||
|
.unwrap(),
|
||||||
|
ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ })
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::Continue
|
||||||
|
)))
|
||||||
|
.unwrap(),
|
||||||
|
ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep),
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
game.process(HostGameMessage::Night(HostNightMessage::Next))
|
||||||
|
.unwrap(),
|
||||||
|
ServerToHostMessage::Daytime {
|
||||||
|
characters: _,
|
||||||
|
marked: _,
|
||||||
|
day: _,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let execution_target = game
|
||||||
|
.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find(|v| v.is_village() && !matches!(v.role().title(), RoleTitle::Protector))
|
||||||
|
.unwrap()
|
||||||
|
.character_id()
|
||||||
|
.clone();
|
||||||
|
match game
|
||||||
|
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
|
||||||
|
execution_target.clone(),
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::Daytime {
|
||||||
|
characters: _,
|
||||||
|
marked,
|
||||||
|
day: _,
|
||||||
|
} => assert_eq!(marked.to_vec(), vec![execution_target]),
|
||||||
|
resp => panic!("unexpected server message: {resp:#?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.process(HostGameMessage::Day(HostDayMessage::Execute))
|
||||||
|
.unwrap()
|
||||||
|
.prompt()
|
||||||
|
.title(),
|
||||||
|
ActionPromptTitle::CoverOfDarkness
|
||||||
|
);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
let (prot_and_wolf_target, prot_char_id) = match game
|
||||||
|
.process(HostGameMessage::Night(HostNightMessage::Next))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::ActionPrompt(ActionPrompt::Protector {
|
||||||
|
character_id: prot_char_id,
|
||||||
|
targets,
|
||||||
|
marked: None,
|
||||||
|
}) => (
|
||||||
|
targets
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| game.village().character_by_id(c.character_id).unwrap())
|
||||||
|
.find(|c| c.is_village())
|
||||||
|
.unwrap()
|
||||||
|
.character_id()
|
||||||
|
.clone(),
|
||||||
|
prot_char_id,
|
||||||
|
),
|
||||||
|
_ => panic!("first n2 prompt isn't protector"),
|
||||||
|
};
|
||||||
|
let target = game
|
||||||
|
.village()
|
||||||
|
.character_by_id(prot_and_wolf_target)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
log::info!("target: {target:#?}");
|
||||||
|
|
||||||
|
match game
|
||||||
|
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::MarkTarget(prot_and_wolf_target.clone()),
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::ActionPrompt(ActionPrompt::Protector {
|
||||||
|
marked: Some(mark), ..
|
||||||
|
}) => assert_eq!(mark, prot_and_wolf_target, "marked target"),
|
||||||
|
resp => panic!("unexpected response: {resp:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
|
||||||
|
game.mark_and_check(prot_and_wolf_target);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,);
|
||||||
|
|
||||||
|
game.response(ActionResponse::Shapeshift);
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
let target = game
|
||||||
|
.village()
|
||||||
|
.character_by_id(target.character_id())
|
||||||
|
.unwrap();
|
||||||
|
assert!(target.is_village());
|
||||||
|
assert!(target.alive());
|
||||||
|
|
||||||
|
let prot = game
|
||||||
|
.village()
|
||||||
|
.character_by_id(prot_char_id.character_id)
|
||||||
|
.unwrap();
|
||||||
|
assert!(prot.is_village());
|
||||||
|
assert!(prot.alive());
|
||||||
|
assert_eq!(prot.role().title(), RoleTitle::Protector);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_1_shapeshift_prompt_if_first_shifts() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mut settings = GameSettings::default();
|
||||||
|
settings.new_slot(RoleTitle::Shapeshifter);
|
||||||
|
settings.new_slot(RoleTitle::Shapeshifter);
|
||||||
|
for _ in 0..7 {
|
||||||
|
settings.new_slot(RoleTitle::Villager);
|
||||||
|
}
|
||||||
|
if let Some(slot) = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
||||||
|
{
|
||||||
|
settings.remove_slot(slot.slot_id);
|
||||||
|
}
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
let target = game
|
||||||
|
.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|c| c.is_village().then_some(c.character_id().clone()))
|
||||||
|
.unwrap();
|
||||||
|
let (_, marked, _) = game.mark_for_execution(target.clone());
|
||||||
|
let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]);
|
||||||
|
assert_eq!(target_list, marked);
|
||||||
|
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
let target = game
|
||||||
|
.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|c| (c.is_village() && c.alive()).then_some(c.character_id().clone()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
game.mark_and_check(target);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter);
|
||||||
|
game.response(ActionResponse::Shapeshift).r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::RoleChange);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,8 @@ pub enum ActionPrompt {
|
||||||
character_id: CharacterIdentity,
|
character_id: CharacterIdentity,
|
||||||
new_role: RoleTitle,
|
new_role: RoleTitle,
|
||||||
},
|
},
|
||||||
|
#[checks(ActionType::RoleChange)]
|
||||||
|
ElderReveal { character_id: CharacterIdentity },
|
||||||
#[checks(ActionType::Other)]
|
#[checks(ActionType::Other)]
|
||||||
Seer {
|
Seer {
|
||||||
character_id: CharacterIdentity,
|
character_id: CharacterIdentity,
|
||||||
|
|
@ -124,7 +126,8 @@ impl ActionPrompt {
|
||||||
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
||||||
let mut prompt = self.clone();
|
let mut prompt = self.clone();
|
||||||
match &mut prompt {
|
match &mut prompt {
|
||||||
ActionPrompt::WolvesIntro { .. }
|
ActionPrompt::ElderReveal { .. }
|
||||||
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
| ActionPrompt::Shapeshifter { .. }
|
| ActionPrompt::Shapeshifter { .. }
|
||||||
| ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState),
|
| ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState),
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::{
|
||||||
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
pub struct PlayerId(uuid::Uuid);
|
pub struct PlayerId(uuid::Uuid);
|
||||||
|
|
||||||
impl PlayerId {
|
impl PlayerId {
|
||||||
|
|
@ -30,7 +30,7 @@ impl Display for PlayerId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
pub struct CharacterId(uuid::Uuid);
|
pub struct CharacterId(uuid::Uuid);
|
||||||
|
|
||||||
impl CharacterId {
|
impl CharacterId {
|
||||||
|
|
@ -165,6 +165,29 @@ impl Character {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kill(&mut self, died_to: DiedTo) {
|
pub fn kill(&mut self, died_to: DiedTo) {
|
||||||
|
match (&mut self.role, died_to.date_time()) {
|
||||||
|
(
|
||||||
|
Role::Elder {
|
||||||
|
lost_protection_night: Some(_),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
) => {}
|
||||||
|
(
|
||||||
|
Role::Elder {
|
||||||
|
lost_protection_night,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
DateTime::Night { number: night },
|
||||||
|
) => {
|
||||||
|
*lost_protection_night = lost_protection_night
|
||||||
|
.is_none()
|
||||||
|
.then_some(night)
|
||||||
|
.and_then(NonZeroU8::new);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
match &self.died_to {
|
match &self.died_to {
|
||||||
Some(_) => {}
|
Some(_) => {}
|
||||||
None => self.died_to = Some(died_to),
|
None => self.died_to = Some(died_to),
|
||||||
|
|
@ -183,12 +206,12 @@ impl Character {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn character_id(&self) -> &CharacterId {
|
pub const fn character_id(&self) -> CharacterId {
|
||||||
&self.identity.character_id
|
self.identity.character_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn player_id(&self) -> &PlayerId {
|
pub const fn player_id(&self) -> PlayerId {
|
||||||
&self.player_id
|
self.player_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn role(&self) -> &Role {
|
pub const fn role(&self) -> &Role {
|
||||||
|
|
@ -212,6 +235,15 @@ impl Character {
|
||||||
&mut self.role
|
&mut self.role
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn elder_reveal(&mut self) {
|
||||||
|
if let Role::Elder {
|
||||||
|
woken_for_reveal, ..
|
||||||
|
} = &mut self.role
|
||||||
|
{
|
||||||
|
*woken_for_reveal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<(), GameError> {
|
pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<(), GameError> {
|
||||||
let mut role = new_role.title_to_role_excl_apprentice();
|
let mut role = new_role.title_to_role_excl_apprentice();
|
||||||
core::mem::swap(&mut role, &mut self.role);
|
core::mem::swap(&mut role, &mut self.role);
|
||||||
|
|
@ -253,6 +285,10 @@ impl Character {
|
||||||
| Role::AlphaWolf { killed: Some(_) }
|
| Role::AlphaWolf { killed: Some(_) }
|
||||||
| Role::Militia { targeted: Some(_) }
|
| Role::Militia { targeted: Some(_) }
|
||||||
| Role::Scapegoat { redeemed: false }
|
| Role::Scapegoat { redeemed: false }
|
||||||
|
| Role::Elder {
|
||||||
|
woken_for_reveal: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
| Role::Villager => return Ok(None),
|
| Role::Villager => return Ok(None),
|
||||||
Role::Scapegoat { redeemed: true } => {
|
Role::Scapegoat { redeemed: true } => {
|
||||||
let mut dead = village.dead_characters();
|
let mut dead = village.dead_characters();
|
||||||
|
|
@ -283,7 +319,7 @@ impl Character {
|
||||||
last_protected: Some(last_protected),
|
last_protected: Some(last_protected),
|
||||||
} => ActionPrompt::Protector {
|
} => ActionPrompt::Protector {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
targets: village.living_players_excluding(last_protected),
|
targets: village.living_players_excluding(*last_protected),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
},
|
||||||
Role::Protector {
|
Role::Protector {
|
||||||
|
|
@ -312,15 +348,18 @@ impl Character {
|
||||||
new_role: *role,
|
new_role: *role,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Role::Elder { knows_on_night, .. } => {
|
Role::Elder {
|
||||||
|
knows_on_night,
|
||||||
|
woken_for_reveal: false,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
let current_night = match village.date_time() {
|
let current_night = match village.date_time() {
|
||||||
DateTime::Day { number: _ } => return Ok(None),
|
DateTime::Day { number: _ } => return Ok(None),
|
||||||
DateTime::Night { number } => number,
|
DateTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
return Ok((current_night == knows_on_night.get()).then_some({
|
return Ok((current_night >= knows_on_night.get()).then_some({
|
||||||
ActionPrompt::RoleChange {
|
ActionPrompt::ElderReveal {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
new_role: RoleTitle::Elder,
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -359,7 +398,7 @@ impl Character {
|
||||||
}
|
}
|
||||||
Role::Hunter { target } => ActionPrompt::Hunter {
|
Role::Hunter { target } => ActionPrompt::Hunter {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
current_target: target.as_ref().and_then(|t| village.target_by_id(t)),
|
current_target: target.as_ref().and_then(|t| village.target_by_id(*t)),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
},
|
||||||
|
|
@ -374,7 +413,7 @@ impl Character {
|
||||||
} => ActionPrompt::Guardian {
|
} => ActionPrompt::Guardian {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
||||||
living_players: village.living_players_excluding(&prev_target.character_id),
|
living_players: village.living_players_excluding(prev_target.character_id),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
},
|
||||||
Role::Guardian {
|
Role::Guardian {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
player::CharacterId,
|
player::CharacterId,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
Villager,
|
Villager,
|
||||||
|
|
@ -64,7 +64,8 @@ pub enum Role {
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Elder {
|
Elder {
|
||||||
knows_on_night: NonZeroU8,
|
knows_on_night: NonZeroU8,
|
||||||
has_protection: bool,
|
woken_for_reveal: bool,
|
||||||
|
lost_protection_night: Option<NonZeroU8>,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
|
|
@ -147,10 +148,17 @@ impl Role {
|
||||||
.iter()
|
.iter()
|
||||||
.any(|c| c.role().title() == *title),
|
.any(|c| c.role().title() == *title),
|
||||||
|
|
||||||
Role::Elder { knows_on_night, .. } => match village.date_time() {
|
Role::Elder {
|
||||||
|
knows_on_night,
|
||||||
|
woken_for_reveal,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
!woken_for_reveal
|
||||||
|
&& match village.date_time() {
|
||||||
DateTime::Night { number } => number == knows_on_night.get(),
|
DateTime::Night { number } => number == knows_on_night.get(),
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,8 @@ impl JoinedPlayers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_connected(&self, player_id: &PlayerId) -> bool {
|
pub async fn is_connected(&self, player_id: PlayerId) -> bool {
|
||||||
self.players.lock().await.contains_key(player_id)
|
self.players.lock().await.contains_key(&player_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&self, player_id: &PlayerId, f: impl FnOnce(&mut JoinedPlayer)) {
|
pub async fn update(&self, player_id: &PlayerId, f: impl FnOnce(&mut JoinedPlayer)) {
|
||||||
|
|
@ -110,9 +110,9 @@ impl JoinedPlayers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_player_identity(&self, player_id: &PlayerId) -> Option<PublicIdentity> {
|
pub async fn get_player_identity(&self, player_id: PlayerId) -> Option<PublicIdentity> {
|
||||||
self.players.lock().await.iter().find_map(|(id, p)| {
|
self.players.lock().await.iter().find_map(|(id, p)| {
|
||||||
(id == player_id).then(|| PublicIdentity {
|
(*id == player_id).then(|| PublicIdentity {
|
||||||
name: p.name.clone(),
|
name: p.name.clone(),
|
||||||
pronouns: p.pronouns.clone(),
|
pronouns: p.pronouns.clone(),
|
||||||
number: p.number,
|
number: p.number,
|
||||||
|
|
@ -139,11 +139,11 @@ impl JoinedPlayers {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_sender(&self, player_id: &PlayerId) -> Option<Sender<ServerMessage>> {
|
pub async fn get_sender(&self, player_id: PlayerId) -> Option<Sender<ServerMessage>> {
|
||||||
self.players
|
self.players
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.get(player_id)
|
.get(&player_id)
|
||||||
.map(|c| c.sender.clone())
|
.map(|c| c.sender.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ impl GameRunner {
|
||||||
.log_err();
|
.log_err();
|
||||||
};
|
};
|
||||||
(update_host)(&acks, &mut self.comms);
|
(update_host)(&acks, &mut self.comms);
|
||||||
let notify_of_role = |player_id: &PlayerId, village: &Village, sender: &LobbyPlayers| {
|
let notify_of_role = |player_id: PlayerId, village: &Village, sender: &LobbyPlayers| {
|
||||||
if let Some(char) = village.character_by_player_id(player_id) {
|
if let Some(char) = village.character_by_player_id(player_id) {
|
||||||
sender
|
sender
|
||||||
.send_if_present(
|
.send_if_present(
|
||||||
|
|
@ -133,7 +133,7 @@ impl GameRunner {
|
||||||
match msg {
|
match msg {
|
||||||
Message::Host(HostMessage::ForceRoleAckFor(char_id)) => {
|
Message::Host(HostMessage::ForceRoleAckFor(char_id)) => {
|
||||||
if let Some((c, ackd)) =
|
if let Some((c, ackd)) =
|
||||||
acks.iter_mut().find(|(c, _)| c.character_id() == &char_id)
|
acks.iter_mut().find(|(c, _)| c.character_id() == char_id)
|
||||||
{
|
{
|
||||||
*ackd = true;
|
*ackd = true;
|
||||||
(notify_of_role)(c.player_id(), self.game.village(), &self.player_sender);
|
(notify_of_role)(c.player_id(), self.game.village(), &self.player_sender);
|
||||||
|
|
@ -152,12 +152,12 @@ impl GameRunner {
|
||||||
message: ClientMessage::GetState,
|
message: ClientMessage::GetState,
|
||||||
}) => {
|
}) => {
|
||||||
let sender =
|
let sender =
|
||||||
if let Some(sender) = self.joined_players.get_sender(&player_id).await {
|
if let Some(sender) = self.joined_players.get_sender(player_id).await {
|
||||||
sender
|
sender
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if acks.iter().any(|(c, d)| c.player_id() == &player_id && *d) {
|
if acks.iter().any(|(c, d)| c.player_id() == player_id && *d) {
|
||||||
// already ack'd just sleep
|
// already ack'd just sleep
|
||||||
sender.send(ServerMessage::Sleep).log_debug();
|
sender.send(ServerMessage::Sleep).log_debug();
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -167,14 +167,14 @@ impl GameRunner {
|
||||||
.village()
|
.village()
|
||||||
.characters()
|
.characters()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| c.player_id() == &player_id)
|
.find(|c| c.player_id() == player_id)
|
||||||
{
|
{
|
||||||
sender
|
sender
|
||||||
.send(ServerMessage::GameStart {
|
.send(ServerMessage::GameStart {
|
||||||
role: char.role().initial_shown_role(),
|
role: char.role().initial_shown_role(),
|
||||||
})
|
})
|
||||||
.log_debug();
|
.log_debug();
|
||||||
} else if let Some(sender) = self.joined_players.get_sender(&player_id).await {
|
} else if let Some(sender) = self.joined_players.get_sender(player_id).await {
|
||||||
sender.send(ServerMessage::GameInProgress).log_debug();
|
sender.send(ServerMessage::GameInProgress).log_debug();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,15 +187,15 @@ impl GameRunner {
|
||||||
message: ClientMessage::RoleAck,
|
message: ClientMessage::RoleAck,
|
||||||
}) => {
|
}) => {
|
||||||
if let Some((_, ackd)) =
|
if let Some((_, ackd)) =
|
||||||
acks.iter_mut().find(|(t, _)| t.player_id() == &player_id)
|
acks.iter_mut().find(|(t, _)| t.player_id() == player_id)
|
||||||
{
|
{
|
||||||
*ackd = true;
|
*ackd = true;
|
||||||
self.player_sender
|
self.player_sender
|
||||||
.send_if_present(&player_id, ServerMessage::Sleep)
|
.send_if_present(player_id, ServerMessage::Sleep)
|
||||||
.log_debug();
|
.log_debug();
|
||||||
}
|
}
|
||||||
(update_host)(&acks, &mut self.comms);
|
(update_host)(&acks, &mut self.comms);
|
||||||
if let Some(sender) = self.joined_players.get_sender(&player_id).await {
|
if let Some(sender) = self.joined_players.get_sender(player_id).await {
|
||||||
sender.send(ServerMessage::Sleep).log_debug();
|
sender.send(ServerMessage::Sleep).log_debug();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,11 +206,11 @@ impl GameRunner {
|
||||||
public: _,
|
public: _,
|
||||||
},
|
},
|
||||||
message: _,
|
message: _,
|
||||||
}) => (notify_of_role)(&player_id, self.game.village(), &self.player_sender),
|
}) => (notify_of_role)(player_id, self.game.village(), &self.player_sender),
|
||||||
Message::ConnectedList(c) => {
|
Message::ConnectedList(c) => {
|
||||||
let newly_connected = c.iter().filter(|c| connect_list.contains(*c));
|
let newly_connected = c.iter().filter(|c| connect_list.contains(*c));
|
||||||
for connected in newly_connected {
|
for connected in newly_connected {
|
||||||
(notify_of_role)(connected, self.game.village(), &self.player_sender)
|
(notify_of_role)(*connected, self.game.village(), &self.player_sender)
|
||||||
}
|
}
|
||||||
connect_list = c;
|
connect_list = c;
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +332,7 @@ impl GameEnd {
|
||||||
self.game()
|
self.game()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.player_sender
|
.player_sender
|
||||||
.send_if_present(&identity.player_id, ServerMessage::GameOver(result))
|
.send_if_present(identity.player_id, ServerMessage::GameOver(result))
|
||||||
.log_debug();
|
.log_debug();
|
||||||
}
|
}
|
||||||
Message::ConnectedList(_) => {}
|
Message::ConnectedList(_) => {}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ impl Lobby {
|
||||||
for (player, _) in self.players_in_lobby.iter() {
|
for (player, _) in self.players_in_lobby.iter() {
|
||||||
players.push(PlayerState {
|
players.push(PlayerState {
|
||||||
identification: player.clone(),
|
identification: player.clone(),
|
||||||
connected: self.joined_players.is_connected(&player.player_id).await,
|
connected: self.joined_players.is_connected(player.player_id).await,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +128,7 @@ impl Lobby {
|
||||||
)) => {
|
)) => {
|
||||||
let _ = self
|
let _ = self
|
||||||
.players_in_lobby
|
.players_in_lobby
|
||||||
.send_if_present(&player_id, ServerMessage::InvalidMessageForGameState);
|
.send_if_present(player_id, ServerMessage::InvalidMessageForGameState);
|
||||||
}
|
}
|
||||||
Err((
|
Err((
|
||||||
Message::Client(IdentifiedClientMessage {
|
Message::Client(IdentifiedClientMessage {
|
||||||
|
|
@ -140,7 +140,7 @@ impl Lobby {
|
||||||
log::error!("processing message from {public} [{player_id}]: {err}");
|
log::error!("processing message from {public} [{player_id}]: {err}");
|
||||||
let _ = self
|
let _ = self
|
||||||
.players_in_lobby
|
.players_in_lobby
|
||||||
.send_if_present(&player_id, ServerMessage::Reset);
|
.send_if_present(player_id, ServerMessage::Reset);
|
||||||
}
|
}
|
||||||
Err((Message::ConnectedList(_), _)) => {}
|
Err((Message::ConnectedList(_), _)) => {}
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +178,7 @@ impl Lobby {
|
||||||
.players_in_lobby
|
.players_in_lobby
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find_map(|(c, _)| (c.player_id == pid).then_some(c))
|
.find_map(|(c, _)| (c.player_id == pid).then_some(c))
|
||||||
&& let Some(joined_id) = self.joined_players.get_player_identity(&pid).await
|
&& let Some(joined_id) = self.joined_players.get_player_identity(pid).await
|
||||||
{
|
{
|
||||||
p.public = joined_id;
|
p.public = joined_id;
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +217,7 @@ impl Lobby {
|
||||||
// Already have the player
|
// Already have the player
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if let Some(sender) = self.joined_players.get_sender(&identity.player_id).await {
|
if let Some(sender) = self.joined_players.get_sender(identity.player_id).await {
|
||||||
self.players_in_lobby.push((identity, sender.clone()));
|
self.players_in_lobby.push((identity, sender.clone()));
|
||||||
self.send_lobby_info_to_clients().await;
|
self.send_lobby_info_to_clients().await;
|
||||||
self.send_lobby_info_to_host().await?;
|
self.send_lobby_info_to_host().await?;
|
||||||
|
|
@ -264,7 +264,7 @@ impl Lobby {
|
||||||
.map(|(id, _)| id.public.clone())
|
.map(|(id, _)| id.public.clone())
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
if let Some(sender) = self.joined_players.get_sender(&player_id).await {
|
if let Some(sender) = self.joined_players.get_sender(player_id).await {
|
||||||
sender.send(msg).log_debug();
|
sender.send(msg).log_debug();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -321,14 +321,14 @@ impl DerefMut for LobbyPlayers {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LobbyPlayers {
|
impl LobbyPlayers {
|
||||||
pub fn find(&self, player_id: &PlayerId) -> Option<&Sender<ServerMessage>> {
|
pub fn find(&self, player_id: PlayerId) -> Option<&Sender<ServerMessage>> {
|
||||||
self.iter()
|
self.iter()
|
||||||
.find_map(|(id, s)| (&id.player_id == player_id).then_some(s))
|
.find_map(|(id, s)| (id.player_id == player_id).then_some(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_if_present(
|
pub fn send_if_present(
|
||||||
&self,
|
&self,
|
||||||
player_id: &PlayerId,
|
player_id: PlayerId,
|
||||||
message: ServerMessage,
|
message: ServerMessage,
|
||||||
) -> Result<(), GameError> {
|
) -> Result<(), GameError> {
|
||||||
if let Some(sender) = self.find(player_id) {
|
if let Some(sender) = self.find(player_id) {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,22 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
/>
|
/>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
ActionPrompt::ElderReveal { character_id } => {
|
||||||
|
let cont = continue_callback.map(|continue_callback| {
|
||||||
|
html! {
|
||||||
|
<Button on_click={continue_callback}>
|
||||||
|
{"continue"}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return html! {
|
||||||
|
<div class="role-change">
|
||||||
|
{identity_html(props, Some(character_id))}
|
||||||
|
<h1>{"you are the elder"}</h1>
|
||||||
|
{cont}
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
}
|
||||||
ActionPrompt::RoleChange {
|
ActionPrompt::RoleChange {
|
||||||
character_id,
|
character_id,
|
||||||
new_role,
|
new_role,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue