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 {
|
||||
KillOutcome::Single(character_id, died_to) => {
|
||||
village
|
||||
.character_by_id_mut(&character_id)
|
||||
.character_by_id_mut(character_id)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.kill(died_to);
|
||||
Ok(())
|
||||
|
|
@ -40,10 +40,10 @@ impl KillOutcome {
|
|||
// check if guardian exists before we mutably borrow killer, which would
|
||||
// prevent us from borrowing village to check after.
|
||||
village
|
||||
.character_by_id(&guardian)
|
||||
.character_by_id(guardian)
|
||||
.ok_or(GameError::InvalidTarget)?;
|
||||
village
|
||||
.character_by_id_mut(&original_killer)
|
||||
.character_by_id_mut(original_killer)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.kill(DiedTo::GuardianProtecting {
|
||||
night,
|
||||
|
|
@ -53,7 +53,7 @@ impl KillOutcome {
|
|||
protecting_from_cause: Box::new(original_kill.clone()),
|
||||
});
|
||||
village
|
||||
.character_by_id_mut(&guardian)
|
||||
.character_by_id_mut(guardian)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.kill(original_kill);
|
||||
Ok(())
|
||||
|
|
@ -115,7 +115,7 @@ pub fn resolve_kill(
|
|||
&& let Some(ss_source) = changes.shapeshifter()
|
||||
{
|
||||
let killing_wolf = village
|
||||
.character_by_id(killing_wolf)
|
||||
.character_by_id(*killing_wolf)
|
||||
.ok_or(GameError::InvalidTarget)?;
|
||||
|
||||
match changes.protected_take(target) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use core::num::NonZeroU8;
|
||||
use core::{num::NonZeroU8, ops::Not};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -40,6 +40,9 @@ pub enum NightChange {
|
|||
target: CharacterId,
|
||||
protection: Protection,
|
||||
},
|
||||
ElderReveal {
|
||||
elder: CharacterId,
|
||||
},
|
||||
}
|
||||
|
||||
enum BlockResolvedOutcome {
|
||||
|
|
@ -108,9 +111,17 @@ impl Night {
|
|||
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
|
||||
.characters()
|
||||
.into_iter()
|
||||
.filter(filter)
|
||||
.map(|c| c.night_action_prompt(&village))
|
||||
.collect::<Result<Box<[_]>>>()?
|
||||
.into_iter()
|
||||
|
|
@ -141,30 +152,8 @@ impl Night {
|
|||
current_prompt: ActionPrompt::CoverOfDarkness,
|
||||
current_result: None,
|
||||
};
|
||||
let mut changes = Vec::new();
|
||||
if let Some(night_nz) = NonZeroU8::new(night) {
|
||||
// TODO: prob should be an end-of-night thing
|
||||
changes = village
|
||||
.dead_characters()
|
||||
.into_iter()
|
||||
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
||||
.filter_map(|(c, d)| match c.role() {
|
||||
Role::Hunter { target } => target.clone().map(|t| (c, t, d)),
|
||||
_ => None,
|
||||
})
|
||||
.filter_map(|(c, t, d)| match d.date_time() {
|
||||
DateTime::Day { number } => (number.get() == night).then_some((c, t)),
|
||||
DateTime::Night { number: _ } => None,
|
||||
})
|
||||
.map(|(c, target)| NightChange::Kill {
|
||||
target,
|
||||
died_to: DiedTo::Hunter {
|
||||
killer: c.character_id().clone(),
|
||||
night: night_nz,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let changes = Self::automatic_changes(&village, night);
|
||||
|
||||
Ok(Self {
|
||||
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<()> {
|
||||
let prev_act = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
|
||||
log::info!("loading previous prompt: {prev_act:?}");
|
||||
|
|
@ -224,13 +246,17 @@ impl Night {
|
|||
let mut changes = ChangesLookup::new(&self.changes);
|
||||
for change in self.changes.iter() {
|
||||
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
|
||||
.character_by_id_mut(character_id)
|
||||
.unwrap()
|
||||
.character_by_id_mut(*character_id)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.role_change(*role_title, DateTime::Night { number: self.night })?,
|
||||
NightChange::HunterTarget { source, target } => {
|
||||
if let Role::Hunter { target: t } =
|
||||
new_village.character_by_id_mut(source).unwrap().role_mut()
|
||||
new_village.character_by_id_mut(*source).unwrap().role_mut()
|
||||
{
|
||||
t.replace(target.clone());
|
||||
}
|
||||
|
|
@ -239,7 +265,7 @@ impl Night {
|
|||
&& changes.protected(target).is_none()
|
||||
{
|
||||
new_village
|
||||
.character_by_id_mut(target)
|
||||
.character_by_id_mut(*target)
|
||||
.unwrap()
|
||||
.kill(DiedTo::Hunter {
|
||||
killer: source.clone(),
|
||||
|
|
@ -262,7 +288,7 @@ impl Night {
|
|||
if let Some(target) = changes.wolf_pack_kill_target()
|
||||
&& 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() {
|
||||
Role::Shapeshifter { shifted_into } => {
|
||||
*shifted_into = Some(target.clone())
|
||||
|
|
@ -343,7 +369,7 @@ impl Night {
|
|||
new_role: RoleTitle::Werewolf,
|
||||
character_id: self
|
||||
.village
|
||||
.character_by_id(&kill_target)
|
||||
.character_by_id(kill_target)
|
||||
.ok_or(GameError::NoMatchingCharacterFound)?
|
||||
.identity(),
|
||||
});
|
||||
|
|
@ -593,13 +619,22 @@ impl Night {
|
|||
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 {
|
||||
marked: Some(marked),
|
||||
..
|
||||
} => {
|
||||
let alignment = self
|
||||
.village
|
||||
.character_by_id(marked)
|
||||
.character_by_id(*marked)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.alignment();
|
||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
|
|
@ -628,12 +663,12 @@ impl Night {
|
|||
} => {
|
||||
let same = self
|
||||
.village
|
||||
.character_by_id(marked1)
|
||||
.character_by_id(*marked1)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?
|
||||
.alignment()
|
||||
== self
|
||||
.village
|
||||
.character_by_id(marked2)
|
||||
.character_by_id(*marked2)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?
|
||||
.alignment();
|
||||
|
||||
|
|
@ -649,7 +684,7 @@ impl Night {
|
|||
} => {
|
||||
let dig_role = self
|
||||
.village
|
||||
.character_by_id(marked)
|
||||
.character_by_id(*marked)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?
|
||||
.gravedigger_dig();
|
||||
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 {
|
||||
NightState::Active {
|
||||
current_prompt,
|
||||
current_result: _,
|
||||
} => match current_prompt {
|
||||
ActionPrompt::RoleChange { character_id, .. }
|
||||
ActionPrompt::ElderReveal { character_id }
|
||||
| ActionPrompt::RoleChange { character_id, .. }
|
||||
| ActionPrompt::Seer { character_id, .. }
|
||||
| ActionPrompt::Protector { character_id, .. }
|
||||
| ActionPrompt::Arcanist { character_id, .. }
|
||||
|
|
@ -888,7 +924,7 @@ impl Night {
|
|||
| ActionPrompt::Guardian { character_id, .. }
|
||||
| ActionPrompt::Shapeshifter { 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::WolfPackKill { .. }
|
||||
| ActionPrompt::CoverOfDarkness => None,
|
||||
|
|
@ -940,3 +976,15 @@ pub enum ServerAction {
|
|||
Prompt(ActionPrompt),
|
||||
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 {
|
||||
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 {
|
||||
self.roles
|
||||
.iter()
|
||||
|
|
@ -38,6 +54,10 @@ impl GameSettings {
|
|||
&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 {
|
||||
log::warn!(
|
||||
"wolves: {} total: {}",
|
||||
|
|
|
|||
|
|
@ -148,7 +148,8 @@ impl SetupRole {
|
|||
}
|
||||
SetupRole::Elder { knows_on_night } => Role::Elder {
|
||||
knows_on_night,
|
||||
has_protection: true,
|
||||
woken_for_reveal: false,
|
||||
lost_protection_night: None,
|
||||
},
|
||||
SetupRole::Werewolf => Role::Werewolf,
|
||||
SetupRole::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use rand::{Rng, seq::SliceRandom};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{DateTime, GameOver, GameSettings},
|
||||
message::{CharacterIdentity, Identification},
|
||||
|
|
@ -62,7 +63,7 @@ impl Village {
|
|||
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
|
||||
.iter()
|
||||
.find(|c| c.character_id() == character_id)
|
||||
|
|
@ -70,7 +71,7 @@ impl Village {
|
|||
|
||||
pub fn find_by_character_id_mut(
|
||||
&mut self,
|
||||
character_id: &CharacterId,
|
||||
character_id: CharacterId,
|
||||
) -> Option<&mut Character> {
|
||||
self.characters
|
||||
.iter_mut()
|
||||
|
|
@ -115,7 +116,7 @@ impl Village {
|
|||
let targets = self
|
||||
.characters
|
||||
.iter_mut()
|
||||
.filter(|c| characters.contains(c.character_id()))
|
||||
.filter(|c| characters.contains(&c.character_id()))
|
||||
.collect::<Box<[_]>>();
|
||||
for t in targets {
|
||||
t.execute(day)?;
|
||||
|
|
@ -162,7 +163,7 @@ impl Village {
|
|||
.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)
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +175,7 @@ impl Village {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[CharacterIdentity]> {
|
||||
pub fn living_players_excluding(&self, exclude: CharacterId) -> Box<[CharacterIdentity]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.alive() && c.character_id() != exclude)
|
||||
|
|
@ -194,6 +195,21 @@ impl Village {
|
|||
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]> {
|
||||
self.characters
|
||||
.iter()
|
||||
|
|
@ -206,19 +222,19 @@ impl Village {
|
|||
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
|
||||
.iter_mut()
|
||||
.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
|
||||
.iter()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +250,8 @@ impl RoleTitle {
|
|||
RoleTitle::Arcanist => Role::Arcanist,
|
||||
RoleTitle::Elder => Role::Elder {
|
||||
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::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
mod night_order;
|
||||
mod role;
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange},
|
||||
game::{Game, GameSettings, OrRandom, SetupRole, SetupSlot, night::NightChange},
|
||||
message::{
|
||||
CharacterState, Identification, PublicIdentity,
|
||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
||||
},
|
||||
player::{CharacterId, PlayerId},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
role::{Alignment, Role, RoleTitle},
|
||||
};
|
||||
use colored::Colorize;
|
||||
|
|
@ -22,7 +23,92 @@ use core::{
|
|||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
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 r#continue(&self);
|
||||
fn seer(&self) -> Alignment;
|
||||
|
|
@ -45,7 +131,7 @@ impl ActionResultExt for ActionResult {
|
|||
}
|
||||
}
|
||||
|
||||
trait ServerToHostMessageExt {
|
||||
pub trait ServerToHostMessageExt {
|
||||
fn prompt(self) -> ActionPrompt;
|
||||
fn result(self) -> ActionResult;
|
||||
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 r#continue(&mut self) -> ActionResult;
|
||||
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
||||
fn mark(&mut self, mark: &CharacterId) -> ActionPrompt;
|
||||
fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool);
|
||||
fn mark(&mut self, mark: CharacterId) -> ActionPrompt;
|
||||
fn mark_and_check(&mut self, mark: CharacterId);
|
||||
fn response(&mut self, resp: ActionResponse) -> ActionResult;
|
||||
fn execute(&mut self) -> ActionPrompt;
|
||||
fn mark_for_execution(
|
||||
&mut self,
|
||||
target: CharacterId,
|
||||
) -> (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 {
|
||||
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 {
|
||||
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||
ActionResponse::Continue,
|
||||
|
|
@ -106,7 +224,7 @@ impl GameExt for Game {
|
|||
.result()
|
||||
}
|
||||
|
||||
fn mark(&mut self, mark: &CharacterId) -> ActionPrompt {
|
||||
fn mark(&mut self, mark: CharacterId) -> ActionPrompt {
|
||||
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||
ActionResponse::MarkTarget(mark.clone()),
|
||||
)))
|
||||
|
|
@ -114,10 +232,66 @@ impl GameExt for Game {
|
|||
.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);
|
||||
if !check(&prompt) {
|
||||
panic!("unexpected prompt: {prompt:?}");
|
||||
match 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()
|
||||
.filter_level(log::LevelFilter::Debug)
|
||||
.format(|f, record| match record.file() {
|
||||
|
|
@ -206,7 +380,7 @@ fn init_log() {
|
|||
.try_init();
|
||||
}
|
||||
|
||||
fn gen_players(range: Range<u8>) -> Box<[Identification]> {
|
||||
pub fn gen_players(range: Range<u8>) -> Box<[Identification]> {
|
||||
range
|
||||
.into_iter()
|
||||
.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]
|
||||
fn wolfpack_kill_all_targets_valid() {
|
||||
init_log();
|
||||
|
|
@ -590,228 +613,3 @@ fn wolfpack_kill_all_targets_valid() {
|
|||
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,
|
||||
new_role: RoleTitle,
|
||||
},
|
||||
#[checks(ActionType::RoleChange)]
|
||||
ElderReveal { character_id: CharacterIdentity },
|
||||
#[checks(ActionType::Other)]
|
||||
Seer {
|
||||
character_id: CharacterIdentity,
|
||||
|
|
@ -124,7 +126,8 @@ impl ActionPrompt {
|
|||
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
||||
let mut prompt = self.clone();
|
||||
match &mut prompt {
|
||||
ActionPrompt::WolvesIntro { .. }
|
||||
ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::Shapeshifter { .. }
|
||||
| ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
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);
|
||||
|
||||
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);
|
||||
|
||||
impl CharacterId {
|
||||
|
|
@ -165,6 +165,29 @@ impl Character {
|
|||
}
|
||||
|
||||
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 {
|
||||
Some(_) => {}
|
||||
None => self.died_to = Some(died_to),
|
||||
|
|
@ -183,12 +206,12 @@ impl Character {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn character_id(&self) -> &CharacterId {
|
||||
&self.identity.character_id
|
||||
pub const fn character_id(&self) -> CharacterId {
|
||||
self.identity.character_id
|
||||
}
|
||||
|
||||
pub const fn player_id(&self) -> &PlayerId {
|
||||
&self.player_id
|
||||
pub const fn player_id(&self) -> PlayerId {
|
||||
self.player_id
|
||||
}
|
||||
|
||||
pub const fn role(&self) -> &Role {
|
||||
|
|
@ -212,6 +235,15 @@ impl Character {
|
|||
&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> {
|
||||
let mut role = new_role.title_to_role_excl_apprentice();
|
||||
core::mem::swap(&mut role, &mut self.role);
|
||||
|
|
@ -253,6 +285,10 @@ impl Character {
|
|||
| Role::AlphaWolf { killed: Some(_) }
|
||||
| Role::Militia { targeted: Some(_) }
|
||||
| Role::Scapegoat { redeemed: false }
|
||||
| Role::Elder {
|
||||
woken_for_reveal: true,
|
||||
..
|
||||
}
|
||||
| Role::Villager => return Ok(None),
|
||||
Role::Scapegoat { redeemed: true } => {
|
||||
let mut dead = village.dead_characters();
|
||||
|
|
@ -283,7 +319,7 @@ impl Character {
|
|||
last_protected: Some(last_protected),
|
||||
} => ActionPrompt::Protector {
|
||||
character_id: self.identity(),
|
||||
targets: village.living_players_excluding(last_protected),
|
||||
targets: village.living_players_excluding(*last_protected),
|
||||
marked: None,
|
||||
},
|
||||
Role::Protector {
|
||||
|
|
@ -312,15 +348,18 @@ impl Character {
|
|||
new_role: *role,
|
||||
}));
|
||||
}
|
||||
Role::Elder { knows_on_night, .. } => {
|
||||
Role::Elder {
|
||||
knows_on_night,
|
||||
woken_for_reveal: false,
|
||||
..
|
||||
} => {
|
||||
let current_night = match village.date_time() {
|
||||
DateTime::Day { number: _ } => return Ok(None),
|
||||
DateTime::Night { number } => number,
|
||||
};
|
||||
return Ok((current_night == knows_on_night.get()).then_some({
|
||||
ActionPrompt::RoleChange {
|
||||
return Ok((current_night >= knows_on_night.get()).then_some({
|
||||
ActionPrompt::ElderReveal {
|
||||
character_id: self.identity(),
|
||||
new_role: RoleTitle::Elder,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
@ -359,7 +398,7 @@ impl Character {
|
|||
}
|
||||
Role::Hunter { target } => ActionPrompt::Hunter {
|
||||
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()),
|
||||
marked: None,
|
||||
},
|
||||
|
|
@ -374,7 +413,7 @@ impl Character {
|
|||
} => ActionPrompt::Guardian {
|
||||
character_id: self.identity(),
|
||||
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,
|
||||
},
|
||||
Role::Guardian {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
player::CharacterId,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
||||
pub enum Role {
|
||||
#[checks(Alignment::Village)]
|
||||
Villager,
|
||||
|
|
@ -64,7 +64,8 @@ pub enum Role {
|
|||
#[checks("is_mentor")]
|
||||
Elder {
|
||||
knows_on_night: NonZeroU8,
|
||||
has_protection: bool,
|
||||
woken_for_reveal: bool,
|
||||
lost_protection_night: Option<NonZeroU8>,
|
||||
},
|
||||
|
||||
#[checks(Alignment::Wolves)]
|
||||
|
|
@ -147,10 +148,17 @@ impl Role {
|
|||
.iter()
|
||||
.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(),
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ impl JoinedPlayers {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn is_connected(&self, player_id: &PlayerId) -> bool {
|
||||
self.players.lock().await.contains_key(player_id)
|
||||
pub async fn is_connected(&self, player_id: PlayerId) -> bool {
|
||||
self.players.lock().await.contains_key(&player_id)
|
||||
}
|
||||
|
||||
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)| {
|
||||
(id == player_id).then(|| PublicIdentity {
|
||||
(*id == player_id).then(|| PublicIdentity {
|
||||
name: p.name.clone(),
|
||||
pronouns: p.pronouns.clone(),
|
||||
number: p.number,
|
||||
|
|
@ -139,11 +139,11 @@ impl JoinedPlayers {
|
|||
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
|
||||
.lock()
|
||||
.await
|
||||
.get(player_id)
|
||||
.get(&player_id)
|
||||
.map(|c| c.sender.clone())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ impl GameRunner {
|
|||
.log_err();
|
||||
};
|
||||
(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) {
|
||||
sender
|
||||
.send_if_present(
|
||||
|
|
@ -133,7 +133,7 @@ impl GameRunner {
|
|||
match msg {
|
||||
Message::Host(HostMessage::ForceRoleAckFor(char_id)) => {
|
||||
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;
|
||||
(notify_of_role)(c.player_id(), self.game.village(), &self.player_sender);
|
||||
|
|
@ -152,12 +152,12 @@ impl GameRunner {
|
|||
message: ClientMessage::GetState,
|
||||
}) => {
|
||||
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
|
||||
} else {
|
||||
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
|
||||
sender.send(ServerMessage::Sleep).log_debug();
|
||||
continue;
|
||||
|
|
@ -167,14 +167,14 @@ impl GameRunner {
|
|||
.village()
|
||||
.characters()
|
||||
.iter()
|
||||
.find(|c| c.player_id() == &player_id)
|
||||
.find(|c| c.player_id() == player_id)
|
||||
{
|
||||
sender
|
||||
.send(ServerMessage::GameStart {
|
||||
role: char.role().initial_shown_role(),
|
||||
})
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -187,15 +187,15 @@ impl GameRunner {
|
|||
message: ClientMessage::RoleAck,
|
||||
}) => {
|
||||
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;
|
||||
self.player_sender
|
||||
.send_if_present(&player_id, ServerMessage::Sleep)
|
||||
.send_if_present(player_id, ServerMessage::Sleep)
|
||||
.log_debug();
|
||||
}
|
||||
(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();
|
||||
}
|
||||
}
|
||||
|
|
@ -206,11 +206,11 @@ impl GameRunner {
|
|||
public: _,
|
||||
},
|
||||
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) => {
|
||||
let newly_connected = c.iter().filter(|c| connect_list.contains(*c));
|
||||
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;
|
||||
}
|
||||
|
|
@ -332,7 +332,7 @@ impl GameEnd {
|
|||
self.game()
|
||||
.unwrap()
|
||||
.player_sender
|
||||
.send_if_present(&identity.player_id, ServerMessage::GameOver(result))
|
||||
.send_if_present(identity.player_id, ServerMessage::GameOver(result))
|
||||
.log_debug();
|
||||
}
|
||||
Message::ConnectedList(_) => {}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ impl Lobby {
|
|||
for (player, _) in self.players_in_lobby.iter() {
|
||||
players.push(PlayerState {
|
||||
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
|
||||
.players_in_lobby
|
||||
.send_if_present(&player_id, ServerMessage::InvalidMessageForGameState);
|
||||
.send_if_present(player_id, ServerMessage::InvalidMessageForGameState);
|
||||
}
|
||||
Err((
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
|
|
@ -140,7 +140,7 @@ impl Lobby {
|
|||
log::error!("processing message from {public} [{player_id}]: {err}");
|
||||
let _ = self
|
||||
.players_in_lobby
|
||||
.send_if_present(&player_id, ServerMessage::Reset);
|
||||
.send_if_present(player_id, ServerMessage::Reset);
|
||||
}
|
||||
Err((Message::ConnectedList(_), _)) => {}
|
||||
}
|
||||
|
|
@ -178,7 +178,7 @@ impl Lobby {
|
|||
.players_in_lobby
|
||||
.iter_mut()
|
||||
.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;
|
||||
}
|
||||
|
|
@ -217,7 +217,7 @@ impl Lobby {
|
|||
// Already have the player
|
||||
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.send_lobby_info_to_clients().await;
|
||||
self.send_lobby_info_to_host().await?;
|
||||
|
|
@ -264,7 +264,7 @@ impl Lobby {
|
|||
.map(|(id, _)| id.public.clone())
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -321,14 +321,14 @@ impl DerefMut for 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()
|
||||
.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(
|
||||
&self,
|
||||
player_id: &PlayerId,
|
||||
player_id: PlayerId,
|
||||
message: ServerMessage,
|
||||
) -> Result<(), GameError> {
|
||||
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 {
|
||||
character_id,
|
||||
new_role,
|
||||
|
|
|
|||
Loading…
Reference in New Issue