elder death protection & lynching

also made PlayerId and CharacterId Copy
This commit is contained in:
emilis 2025-10-05 10:52:37 +01:00
parent 97e1ca8a39
commit 88665302f6
No known key found for this signature in database
17 changed files with 1108 additions and 490 deletions

View File

@ -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) {

View File

@ -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()
}
}

View File

@ -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: {}",

View File

@ -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 },

View File

@ -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 },

View File

@ -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);
}

View File

@ -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
);
}

View File

@ -0,0 +1,3 @@
mod elder;
mod scapegoat;
mod shapeshifter;

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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),

View File

@ -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 {

View File

@ -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 {
DateTime::Night { number } => number == knows_on_night.get(), knows_on_night,
_ => false, woken_for_reveal,
}, ..
} => {
!woken_for_reveal
&& match village.date_time() {
DateTime::Night { number } => number == knows_on_night.get(),
_ => false,
}
}
} }
} }
} }

View File

@ -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())
} }

View File

@ -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(_) => {}

View File

@ -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) {

View File

@ -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,