diff --git a/Cargo.lock b/Cargo.lock index bdbdc2a..99296a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ "axum-core", - "base64", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", @@ -165,6 +165,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -185,6 +191,9 @@ name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -1054,7 +1063,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "headers-core", "http 1.3.1", @@ -1783,6 +1792,18 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + [[package]] name = "route-recognizer" version = "0.3.1" @@ -2459,6 +2480,7 @@ dependencies = [ "mime-sniffer", "pretty_env_logger", "rand", + "ron", "serde", "serde_json", "thiserror 2.0.17", diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index 38d2eee..418ad3e 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -6,11 +6,14 @@ use serde::{Deserialize, Serialize}; use crate::{ diedto::DiedTo, error::GameError, - game::{DateTime, Village, night::NightChange}, + game::{GameTime, Village}, message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, modifier::Modifier, player::{PlayerId, RoleChange}, - role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, + role::{ + Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful, + PreviousGuardianAction, Role, RoleTitle, + }, }; type Result = core::result::Result; @@ -33,7 +36,7 @@ impl Display for CharacterId { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Character { player_id: PlayerId, identity: CharacterIdentity, @@ -103,7 +106,7 @@ impl Character { return; } match (&mut self.role, died_to.date_time()) { - (Role::BlackKnight { attacked }, DateTime::Night { .. }) => { + (Role::BlackKnight { attacked }, GameTime::Night { .. }) => { attacked.replace(died_to); return; } @@ -119,7 +122,7 @@ impl Character { lost_protection_night, .. }, - DateTime::Night { number: night }, + GameTime::Night { number: night }, ) => { *lost_protection_night = lost_protection_night .is_none() @@ -184,15 +187,15 @@ impl Character { } } - pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<()> { + pub fn role_change(&mut self, new_role: RoleTitle, at: GameTime) -> Result<()> { let mut role = new_role.title_to_role_excl_apprentice(); core::mem::swap(&mut role, &mut self.role); self.role_changes.push(RoleChange { role, new_role, changed_on_night: match at { - DateTime::Day { number: _ } => return Err(GameError::NotNight), - DateTime::Night { number } => number, + GameTime::Day { number: _ } => return Err(GameError::NotNight), + GameTime::Night { number } => number, }, }); @@ -276,9 +279,9 @@ impl Character { if !self.alive() || !self.role.wakes(village) { return Ok(Box::new([])); } - let night = match village.date_time() { - DateTime::Day { number: _ } => return Err(GameError::NotNight), - DateTime::Night { number } => number, + let night = match village.time() { + GameTime::Day { number: _ } => return Err(GameError::NotNight), + GameTime::Night { number } => number, }; Ok(Box::new([match &self.role { Role::Empath { cursed: true } @@ -341,9 +344,9 @@ impl Character { marked: None, }, Role::Apprentice(role) => { - let current_night = match village.date_time() { - DateTime::Day { number: _ } => return Ok(Box::new([])), - DateTime::Night { number } => number, + let current_night = match village.time() { + GameTime::Day { number: _ } => return Ok(Box::new([])), + GameTime::Night { number } => number, }; return Ok(village .characters() @@ -351,8 +354,8 @@ impl Character { .filter(|c| c.role_title() == *role) .filter_map(|char| char.died_to) .any(|died_to| match died_to.date_time() { - DateTime::Day { number } => number.get() + 1 >= current_night, - DateTime::Night { number } => number + 1 >= current_night, + GameTime::Day { number } => number.get() + 1 >= current_night, + GameTime::Night { number } => number + 1 >= current_night, }) .then(|| ActionPrompt::RoleChange { character_id: self.identity(), @@ -366,9 +369,9 @@ impl Character { woken_for_reveal: false, .. } => { - let current_night = match village.date_time() { - DateTime::Day { number: _ } => return Ok(Box::new([])), - DateTime::Night { number } => number, + let current_night = match village.time() { + GameTime::Day { number: _ } => return Ok(Box::new([])), + GameTime::Night { number } => number, }; return Ok((current_night >= knows_on_night.get()) .then_some({ @@ -486,14 +489,14 @@ impl Character { marked: None, }, Role::Vindicator => { - let last_day = match village.date_time() { - DateTime::Day { .. } => { + let last_day = match village.time() { + GameTime::Day { .. } => { log::error!( "vindicator trying to get a prompt during the day? village state: {village:?}" ); return Ok(Box::new([])); } - DateTime::Night { number } => { + GameTime::Night { number } => { if number == 0 { return Ok(Box::new([])); } @@ -530,16 +533,20 @@ impl Character { &self.role } - pub const fn killer(&self) -> bool { + pub const fn killing_wolf_order(&self) -> Option { + self.role.killing_wolf_order() + } + + pub const fn killer(&self) -> Killer { if let Role::Empath { cursed: true } = &self.role { - return true; + return Killer::Killer; } self.role.killer() } - pub const fn powerful(&self) -> bool { + pub const fn powerful(&self) -> Powerful { if let Role::Empath { cursed: true } = &self.role { - return true; + return Powerful::Powerful; } self.role.powerful() } @@ -689,6 +696,28 @@ impl Character { } } + pub const fn guardian<'a>(&'a self) -> Result> { + let title = self.role.title(); + match &self.role { + Role::Guardian { last_protected } => Ok(Guardian(last_protected)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Guardian, + got: title, + }), + } + } + + pub const fn guardian_mut<'a>(&'a mut self) -> Result> { + let title = self.role.title(); + match &mut self.role { + Role::Guardian { last_protected } => Ok(GuardianMut(last_protected)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Guardian, + got: title, + }), + } + } + pub const fn initial_shown_role(&self) -> RoleTitle { self.role.initial_shown_role() } @@ -728,6 +757,7 @@ decl_ref_and_mut!( Scapegoat, ScapegoatMut: bool; Empath, EmpathMut: bool; BlackKnight, BlackKnightMut: Option; + Guardian, GuardianMut: Option; ); pub struct BlackKnightKill<'a> { diff --git a/werewolves-proto/src/diedto.rs b/werewolves-proto/src/diedto.rs index 264c223..1cb813e 100644 --- a/werewolves-proto/src/diedto.rs +++ b/werewolves-proto/src/diedto.rs @@ -3,7 +3,7 @@ use core::{fmt::Debug, num::NonZeroU8}; use serde::{Deserialize, Serialize}; use werewolves_macros::Titles; -use crate::{character::CharacterId, game::DateTime}; +use crate::{character::CharacterId, game::GameTime}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)] pub enum DiedTo { @@ -113,9 +113,9 @@ impl DiedTo { | DiedTo::LoneWolf { killer, .. } => Some(*killer), } } - pub const fn date_time(&self) -> DateTime { + pub const fn date_time(&self) -> GameTime { match self { - DiedTo::Execution { day } => DateTime::Day { number: *day }, + DiedTo::Execution { day } => GameTime::Day { number: *day }, DiedTo::GuardianProtecting { source: _, protecting: _, @@ -138,11 +138,11 @@ impl DiedTo { | DiedTo::Shapeshift { into: _, night } | DiedTo::PyreMasterLynchMob { night, .. } | DiedTo::PyreMaster { night, .. } - | DiedTo::Hunter { killer: _, night } => DateTime::Night { + | DiedTo::Hunter { killer: _, night } => GameTime::Night { number: night.get(), }, DiedTo::LoneWolf { night, .. } | DiedTo::MasonLeaderRecruitFail { night, .. } => { - DateTime::Night { number: *night } + GameTime::Night { number: *night } } } } diff --git a/werewolves-proto/src/error.rs b/werewolves-proto/src/error.rs index 7be72fd..21c6b6f 100644 --- a/werewolves-proto/src/error.rs +++ b/werewolves-proto/src/error.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{message::PublicIdentity, player::PlayerId, role::RoleTitle}; +use crate::{game::GameTime, message::PublicIdentity, player::PlayerId, role::RoleTitle}; #[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] pub enum GameError { @@ -75,4 +75,10 @@ pub enum GameError { AssignedPlayerMissing(PlayerId), #[error(" {0} assigned to {1} roles")] AssignedMultipleTimes(PublicIdentity, usize), + #[error("change set for {0} is already set")] + ChangesAlreadySet(GameTime), + #[error("missing {0} in game story")] + MissingTime(GameTime), + #[error("no previous during day")] + NoPreviousDuringDay, } diff --git a/werewolves-proto/src/game/kill.rs b/werewolves-proto/src/game/kill.rs index a02e8b0..3fa6fa1 100644 --- a/werewolves-proto/src/game/kill.rs +++ b/werewolves-proto/src/game/kill.rs @@ -5,7 +5,7 @@ use crate::{ character::CharacterId, diedto::DiedTo, error::GameError, - game::{Village, night::NightChange}, + game::{Village, night::changes::ChangesLookup}, player::Protection, }; @@ -157,93 +157,3 @@ pub fn resolve_kill( | Protection::Protector { .. } => Ok(None), } } - -pub struct ChangesLookup<'a>(&'a [NightChange], Vec); - -impl<'a> ChangesLookup<'a> { - pub fn new(changes: &'a [NightChange]) -> Self { - Self(changes, Vec::new()) - } - - pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> { - self.0.iter().enumerate().find_map(|(idx, c)| { - self.1 - .contains(&idx) - .not() - .then(|| match c { - NightChange::Kill { target: t, died_to } => (*t == target).then_some(died_to), - _ => None, - }) - .flatten() - }) - } - - pub fn protected_take(&mut self, target: CharacterId) -> Option { - if let Some((idx, c)) = self.0.iter().enumerate().find_map(|(idx, c)| { - self.1 - .contains(&idx) - .not() - .then(|| match c { - NightChange::Protection { - target: t, - protection, - } => (*t == target).then_some((idx, protection)), - _ => None, - }) - .flatten() - }) { - self.1.push(idx); - Some(c.clone()) - } else { - None - } - } - pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> { - self.0.iter().enumerate().find_map(|(idx, c)| { - self.1 - .contains(&idx) - .not() - .then(|| match c { - NightChange::Protection { - target: t, - protection, - } => (t == target).then_some(protection), - _ => None, - }) - .flatten() - }) - } - - pub fn shapeshifter(&self) -> Option<&'a CharacterId> { - self.0.iter().enumerate().find_map(|(idx, c)| { - self.1 - .contains(&idx) - .not() - .then_some(match c { - NightChange::Shapeshift { source, .. } => Some(source), - _ => None, - }) - .flatten() - }) - } - - pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> { - self.0.iter().enumerate().find_map(|(idx, c)| { - self.1 - .contains(&idx) - .not() - .then_some(match c { - NightChange::Kill { - target, - died_to: - DiedTo::Wolfpack { - night: _, - killing_wolf: _, - }, - } => Some(target), - _ => None, - }) - .flatten() - }) - } -} diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index 859ca79..b8f43db 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -1,10 +1,11 @@ mod kill; -pub(crate) mod night; +pub mod night; mod settings; +pub mod story; mod village; use core::{ - fmt::Debug, + fmt::{Debug, Display}, num::NonZeroU8, ops::{Deref, Range, RangeBounds}, }; @@ -15,7 +16,10 @@ use serde::{Deserialize, Serialize}; use crate::{ character::CharacterId, error::GameError, - game::night::{Night, ServerAction}, + game::{ + night::{Night, ServerAction}, + story::{DayDetail, GameActions, GameStory, NightDetails}, + }, message::{ CharacterState, Identification, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, @@ -31,18 +35,17 @@ type Result = core::result::Result; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Game { - previous: Vec, - next: Vec, + history: GameStory, state: GameState, } impl Game { pub fn new(players: &[Identification], settings: GameSettings) -> Result { + let village = Village::new(players, settings)?; Ok(Self { - next: Vec::new(), - previous: Vec::new(), + history: GameStory::new(village.clone()), state: GameState::Night { - night: Night::new(Village::new(players, settings)?)?, + night: Night::new(village)?, }, }) } @@ -82,11 +85,28 @@ impl Game { self.process(HostGameMessage::GetState) } (GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => { + let time = village.time(); if let Some(outcome) = village.execute(marked)? { + log::warn!("adding to history for {}", village.time()); + self.history.add( + village.time(), + GameActions::DayDetails( + marked.iter().map(|c| DayDetail::Execute(*c)).collect(), + ), + )?; return Ok(ServerToHostMessage::GameOver(outcome)); } let night = Night::new(village.clone())?; - self.previous.push(self.state.clone()); + log::warn!("adding to history for {time}"); + self.history.add( + time, + GameActions::DayDetails( + marked + .iter() + .map(|mark| DayDetail::Execute(*mark)) + .collect(), + ), + )?; self.state = GameState::Night { night }; self.process(HostGameMessage::GetState) } @@ -106,9 +126,9 @@ impl Game { died_to: c.died_to().cloned(), }) .collect(), - day: match village.date_time() { - DateTime::Day { number } => number, - DateTime::Night { number: _ } => unreachable!(), + day: match village.time() { + GameTime::Day { number } => number, + GameTime::Night { number: _ } => unreachable!(), }, settings: village.settings(), }) @@ -126,8 +146,16 @@ impl Game { match night.next() { Ok(_) => self.process(HostGameMessage::GetState), Err(GameError::NightOver) => { - let village = night.collect_completed()?; - self.previous.push(self.state.clone()); + let changes = night.collect_changes()?; + let village = night.village().with_night_changes(&changes)?; + log::warn!("adding to history for {}", night.village().time()); + self.history.add( + night.village().time(), + GameActions::NightDetails(NightDetails::new( + &night.used_actions(), + changes, + )), + )?; self.state = GameState::Day { village, marked: Vec::new(), @@ -171,19 +199,8 @@ impl Game { }, HostGameMessage::Night(_), ) => Err(GameError::InvalidMessageForGameState), - ( - GameState::Day { - village: _, - marked: _, - }, - HostGameMessage::PreviousState, - ) => { - let mut prev = self.previous.pop().ok_or(GameError::NoPreviousState)?; - log::info!("previous state loaded: {prev:?}"); - core::mem::swap(&mut prev, &mut self.state); - self.next.push(prev); - - self.process(HostGameMessage::GetState) + (GameState::Day { .. }, HostGameMessage::PreviousState) => { + Err(GameError::NoPreviousDuringDay) } (GameState::Night { night }, HostGameMessage::PreviousState) => { night.previous_state()?; @@ -192,6 +209,10 @@ impl Game { } } + pub fn story(&self) -> GameStory { + self.history.clone() + } + pub fn game_over(&self) -> Option { self.state.game_over() } @@ -204,10 +225,6 @@ impl Game { pub fn game_state_mut(&mut self) -> &mut GameState { &mut self.state } - - pub fn previous_game_states(&self) -> &[GameState] { - &self.previous - } } #[allow(clippy::large_enum_variant)] @@ -296,37 +313,58 @@ pub enum Maybe { Maybe, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub enum DateTime { +#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)] +pub enum GameTime { Day { number: NonZeroU8 }, Night { number: u8 }, } -impl Default for DateTime { +impl Display for GameTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GameTime::Day { number } => write!(f, "Day {number}"), + GameTime::Night { number } => write!(f, "Night {number}"), + } + } +} + +impl Default for GameTime { fn default() -> Self { - DateTime::Day { + GameTime::Day { number: NonZeroU8::new(1).unwrap(), } } } -impl DateTime { +impl GameTime { pub const fn is_day(&self) -> bool { - matches!(self, DateTime::Day { number: _ }) + matches!(self, GameTime::Day { number: _ }) } pub const fn is_night(&self) -> bool { - matches!(self, DateTime::Night { number: _ }) + matches!(self, GameTime::Night { number: _ }) } pub const fn next(self) -> Self { match self { - DateTime::Day { number } => DateTime::Night { + GameTime::Day { number } => GameTime::Night { number: number.get(), }, - DateTime::Night { number } => DateTime::Day { + GameTime::Night { number } => GameTime::Day { number: NonZeroU8::new(number + 1).unwrap(), }, } } + + pub const fn previous(self) -> Option { + match self { + GameTime::Day { number } => Some(GameTime::Night { + number: number.get() - 1, + }), + GameTime::Night { number } => match NonZeroU8::new(number) { + Some(number) => Some(GameTime::Day { number }), + None => None, + }, + } + } } diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 9a579d9..a90778a 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -1,3 +1,6 @@ +pub mod changes; +mod process; + use core::num::NonZeroU8; use std::collections::VecDeque; @@ -10,54 +13,16 @@ use crate::{ diedto::DiedTo, error::GameError, game::{ - DateTime, Village, - kill::{self, ChangesLookup}, - }, - message::{ - CharacterIdentity, - night::{ActionPrompt, ActionResponse, ActionResult, Visits}, + GameTime, Village, + kill::{self}, + night::changes::{ChangesLookup, NightChange}, + story::NightChoice, }, + message::night::{ActionPrompt, ActionResponse, ActionResult, Visits}, player::Protection, - role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle}, + role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, RoleBlock, RoleTitle}, }; -#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, Extract)] -pub enum NightChange { - RoleChange(CharacterId, RoleTitle), - HunterTarget { - source: CharacterId, - target: CharacterId, - }, - Kill { - target: CharacterId, - died_to: DiedTo, - }, - RoleBlock { - source: CharacterId, - target: CharacterId, - block_type: RoleBlock, - }, - Shapeshift { - source: CharacterId, - into: CharacterId, - }, - Protection { - target: CharacterId, - protection: Protection, - }, - ElderReveal { - elder: CharacterId, - }, - MasonRecruit { - mason_leader: CharacterId, - recruiting: CharacterId, - }, - EmpathFoundScapegoat { - empath: CharacterId, - scapegoat: CharacterId, - }, -} - enum BlockResolvedOutcome { PromptUpdate(ActionPrompt), ActionComplete(ActionResult, Option), @@ -252,9 +217,9 @@ pub struct Night { impl Night { pub fn new(village: Village) -> Result { - let night = match village.date_time() { - DateTime::Day { number: _ } => return Err(GameError::NotNight), - DateTime::Night { number } => number, + let night = match village.time() { + GameTime::Day { number: _ } => return Err(GameError::NotNight), + GameTime::Night { number } => number, }; let filter = if village.executed_known_elder() { @@ -308,21 +273,21 @@ impl Night { } /// changes that require no input (such as hunter firing) - fn automatic_changes(village: &Village, night: u8) -> Vec { + fn automatic_changes(&self) -> Vec { let mut changes = Vec::new(); - let night = match NonZeroU8::new(night) { + let night = match NonZeroU8::new(self.night) { Some(night) => night, None => return changes, }; - if !village.executed_known_elder() { - village + if !self.village.executed_known_elder() { + self.village .dead_characters() .into_iter() .filter_map(|c| c.died_to().map(|d| (c, d))) .filter_map(|(c, d)| c.hunter().ok().and_then(|h| *h).map(|t| (c, t, d))) .filter_map(|(c, t, d)| match d.date_time() { - DateTime::Day { number } => (number.get() == night.get()).then_some((c, t)), - DateTime::Night { number: _ } => None, + GameTime::Day { number } => (number.get() == night.get()).then_some((c, t)), + GameTime::Night { number: _ } => None, }) .map(|(c, target)| NightChange::Kill { target, @@ -387,143 +352,13 @@ impl Night { self.action_queue.iter().cloned().collect() } - pub fn collect_completed(&self) -> Result { + pub fn collect_changes(&self) -> Result> { if !matches!(self.night_state, NightState::Complete) { return Err(GameError::NotEndOfNight); } - let mut new_village = self.village.clone(); - let mut all_changes = Self::automatic_changes(&self.village, self.night); + let mut all_changes = self.automatic_changes(); all_changes.append(&mut self.changes_from_actions().into_vec()); - let mut changes = ChangesLookup::new(&all_changes); - for change in all_changes.iter() { - match change { - NightChange::ElderReveal { elder } => { - new_village.character_by_id_mut(*elder)?.elder_reveal() - } - NightChange::RoleChange(character_id, role_title) => new_village - .character_by_id_mut(*character_id)? - .role_change(*role_title, DateTime::Night { number: self.night })?, - NightChange::HunterTarget { source, target } => { - let hunter_character = new_village.character_by_id_mut(*source).unwrap(); - hunter_character.hunter_mut()?.replace(*target); - if changes.killed(*source).is_some() - && changes.protected(source).is_none() - && changes.protected(target).is_none() - { - new_village - .character_by_id_mut(*target) - .unwrap() - .kill(DiedTo::Hunter { - killer: *source, - night: NonZeroU8::new(self.night).unwrap(), - }) - } - } - NightChange::Kill { target, died_to } => { - if let Some(kill) = kill::resolve_kill( - &mut changes, - *target, - died_to, - self.night, - &self.village, - )? { - kill.apply_to_village(&mut new_village)?; - } - } - NightChange::Shapeshift { source, into } => { - if let Some(target) = changes.wolf_pack_kill_target() - && changes.protected(target).is_none() - { - if *target != *into { - log::error!("shapeshift into({into}) != target({target})"); - continue; - } - let ss = new_village.character_by_id_mut(*source).unwrap(); - ss.shapeshifter_mut().unwrap().replace(*target); - ss.kill(DiedTo::Shapeshift { - into: *target, - night: NonZeroU8::new(self.night).unwrap(), - }); - // role change pushed in [apply_shapeshift] - } - } - NightChange::RoleBlock { - source: _, - target: _, - block_type: _, - } - | NightChange::Protection { - target: _, - protection: _, - } => {} - NightChange::MasonRecruit { - mason_leader, - recruiting, - } => { - if new_village.character_by_id(*recruiting)?.is_wolf() { - new_village.character_by_id_mut(*mason_leader)?.kill( - DiedTo::MasonLeaderRecruitFail { - tried_recruiting: *recruiting, - night: self.night, - }, - ); - } else { - new_village - .character_by_id_mut(*mason_leader)? - .mason_leader_mut()? - .recruit(*recruiting); - } - } - NightChange::EmpathFoundScapegoat { empath, scapegoat } => { - new_village - .character_by_id_mut(*scapegoat)? - .role_change(RoleTitle::Villager, DateTime::Night { number: self.night })?; - *new_village.character_by_id_mut(*empath)?.empath_mut()? = true; - } - } - } - // black knights death - for knight in new_village - .characters_mut() - .into_iter() - .filter(|k| k.alive()) - .filter(|k| k.black_knight().ok().and_then(|t| (*t).clone()).is_some()) - .filter(|k| changes.killed(k.character_id()).is_none()) - { - knight.black_knight_kill()?.kill(); - } - - // pyre masters death - let village_dead = new_village - .characters() - .into_iter() - .filter(|c| c.is_village()) - .filter_map(|c| c.died_to().cloned()) - .filter_map(|c| c.killer().map(|k| (k, c))) - .collect::>(); - for pyremaster in new_village - .living_characters_by_role_mut(RoleTitle::PyreMaster) - .into_iter() - .filter(|p| { - village_dead - .iter() - .filter(|(k, _)| *k == p.character_id()) - .count() - >= PYREMASTER_VILLAGER_KILLS_TO_DIE.get() as usize - }) - { - if let Some(night) = NonZeroU8::new(self.night) { - pyremaster.kill(DiedTo::PyreMasterLynchMob { - night, - source: pyremaster.character_id(), - }); - } - } - - if new_village.is_game_over().is_none() { - new_village.to_day()?; - } - Ok(new_village) + Ok(all_changes.into_boxed_slice()) } fn apply_mason_recruit( @@ -730,11 +565,7 @@ impl Night { })); } - match ( - self.received_response_inner(resp)?, - current_wolfy, - next_wolfy, - ) { + match (self.process(resp)?, current_wolfy, next_wolfy) { (ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)), ( ResponseOutcome::ActionComplete(ActionComplete { @@ -815,483 +646,6 @@ impl Night { } } - fn received_response_inner(&self, resp: ActionResponse) -> Result { - let current_prompt = match &self.night_state { - NightState::Active { - current_prompt: _, - current_result: Some(_), - .. - } => return Err(GameError::NightNeedsNext), - NightState::Active { - current_prompt, - current_result: None, - .. - } => current_prompt, - NightState::Complete => return Err(GameError::NightOver), - }; - - match resp { - ActionResponse::MarkTarget(mark) => { - return Ok(ResponseOutcome::PromptUpdate( - current_prompt.with_mark(mark)?, - )); - } - ActionResponse::Shapeshift => { - return match current_prompt { - ActionPrompt::Shapeshifter { - character_id: source, - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Shapeshift { - source: source.character_id, - into: self - .changes_from_actions() - .into_iter() - .find_map(|c| match c { - NightChange::Kill { - target, - died_to: DiedTo::Wolfpack { .. }, - } => Some(target), - _ => None, - }) - .ok_or(GameError::InvalidTarget)?, - }), - })), - _ => Err(GameError::InvalidMessageForGameState), - }; - } - ActionResponse::Continue => { - if let ActionPrompt::Insomniac { character_id } = current_prompt { - return Ok(ActionComplete { - result: ActionResult::Insomniac( - self.get_visits_for(character_id.character_id), - ), - change: None, - } - .into()); - } - if let ActionPrompt::RoleChange { - character_id, - new_role, - } = current_prompt - { - return Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::RoleChange( - character_id.character_id, - *new_role, - )), - })); - } - } - }; - - match current_prompt { - ActionPrompt::LoneWolfKill { - character_id, - marked: Some(marked), - .. - } => Ok(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: *marked, - died_to: DiedTo::LoneWolf { - killer: character_id.character_id, - night: self.night, - }, - }), - } - .into()), - ActionPrompt::RoleChange { .. } - | ActionPrompt::WolvesIntro { .. } - | ActionPrompt::CoverOfDarkness => { - Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: None, - })) - } - ActionPrompt::ElderReveal { character_id } => { - Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::ElderReveal { - elder: character_id.character_id, - }), - })) - } - ActionPrompt::Seer { - marked: Some(marked), - .. - } => { - let alignment = self.village.character_by_id(*marked)?.alignment(); - Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::Seer(alignment), - change: None, - })) - } - ActionPrompt::Protector { - marked: Some(marked), - character_id, - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Protection { - target: *marked, - protection: Protection::Protector { - source: character_id.character_id, - }, - }), - })), - ActionPrompt::Arcanist { - marked: (Some(marked1), Some(marked2)), - .. - } => { - let same = self.village.character_by_id(*marked1)?.alignment() - == self.village.character_by_id(*marked2)?.alignment(); - - Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::Arcanist { same }, - change: None, - })) - } - ActionPrompt::Gravedigger { - marked: Some(marked), - .. - } => { - let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig(); - Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GraveDigger(dig_role), - change: None, - })) - } - ActionPrompt::Hunter { - character_id, - marked: Some(marked), - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::HunterTarget { - source: character_id.character_id, - target: *marked, - }), - })), - ActionPrompt::Militia { - character_id, - marked: Some(marked), - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: *marked, - died_to: DiedTo::Militia { - killer: character_id.character_id, - night: NonZeroU8::new(self.night) - .ok_or(GameError::InvalidMessageForGameState)?, - }, - }), - })), - ActionPrompt::Militia { marked: None, .. } => { - Ok(ResponseOutcome::ActionComplete(Default::default())) - } - ActionPrompt::MapleWolf { - character_id, - kill_or_die, - marked: Some(marked), - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: *marked, - died_to: DiedTo::MapleWolf { - source: character_id.character_id, - night: NonZeroU8::new(self.night) - .ok_or(GameError::InvalidMessageForGameState)?, - starves_if_fails: *kill_or_die, - }, - }), - })), - ActionPrompt::MapleWolf { marked: None, .. } => { - Ok(ResponseOutcome::ActionComplete(Default::default())) - } - ActionPrompt::Guardian { - character_id, - previous: None, - marked: Some(marked), - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Protection { - target: *marked, - protection: Protection::Guardian { - source: character_id.character_id, - guarding: false, - }, - }), - })), - ActionPrompt::Guardian { - character_id, - previous: Some(PreviousGuardianAction::Guard(prev_target)), - marked: Some(marked), - .. - } => { - if prev_target.character_id == *marked { - return Err(GameError::InvalidTarget); - } - Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Protection { - target: *marked, - protection: Protection::Guardian { - source: character_id.character_id, - guarding: false, - }, - }), - })) - } - ActionPrompt::Guardian { - character_id, - previous: Some(PreviousGuardianAction::Protect(prev_protect)), - marked: Some(marked), - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Protection { - target: *marked, - protection: Protection::Guardian { - source: character_id.character_id, - guarding: prev_protect.character_id == *marked, - }, - }), - })), - ActionPrompt::WolfPackKill { - marked: Some(marked), - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: *marked, - died_to: DiedTo::Wolfpack { - killing_wolf: self - .village - .killing_wolf() - .ok_or(GameError::NoWolves)? - .character_id(), - night: NonZeroU8::new(self.night) - .ok_or(GameError::InvalidMessageForGameState)?, - }, - }), - })), - ActionPrompt::Shapeshifter { character_id } => { - Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: match &resp { - ActionResponse::Continue => None, - ActionResponse::Shapeshift => Some(NightChange::Shapeshift { - source: character_id.character_id, - into: self - .changes_from_actions() - .into_iter() - .find_map(|c| match c { - NightChange::Kill { - target, - died_to: DiedTo::Wolfpack { .. }, - } => Some(target), - _ => None, - }) - .ok_or(GameError::InvalidTarget)?, - }), - _ => return Err(GameError::InvalidMessageForGameState), - }, - })) - } - ActionPrompt::AlphaWolf { - character_id, - marked: Some(marked), - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: *marked, - died_to: DiedTo::AlphaWolf { - killer: character_id.character_id, - night: NonZeroU8::new(self.night) - .ok_or(GameError::InvalidMessageForGameState)?, - }, - }), - })), - ActionPrompt::AlphaWolf { marked: None, .. } => { - Ok(ResponseOutcome::ActionComplete(Default::default())) - } - ActionPrompt::DireWolf { - character_id, - marked: Some(marked), - .. - } => Ok(ResponseOutcome::ActionComplete(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::RoleBlock { - source: character_id.character_id, - target: *marked, - block_type: RoleBlock::Direwolf, - }), - })), - ActionPrompt::Adjudicator { - marked: Some(marked), - .. - } => Ok(ActionComplete { - result: ActionResult::Adjudicator { - killer: self.village.character_by_id(*marked)?.killer(), - }, - change: None, - } - .into()), - ActionPrompt::PowerSeer { - marked: Some(marked), - .. - } => Ok(ActionComplete { - result: ActionResult::PowerSeer { - powerful: self.village.character_by_id(*marked)?.powerful(), - }, - change: None, - } - .into()), - ActionPrompt::Mortician { - marked: Some(marked), - .. - } => Ok(ActionComplete { - result: ActionResult::Mortician( - self.village - .character_by_id(*marked)? - .died_to() - .ok_or(GameError::InvalidTarget)? - .title(), - ), - change: None, - } - .into()), - ActionPrompt::Beholder { - marked: Some(marked), - .. - } => { - if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| { - prompt.matches_beholding(*marked).then_some(result) - }) { - Ok(ActionComplete { - result: result.clone(), - change: None, - } - .into()) - } else { - Ok(ActionComplete { - result: ActionResult::GoBackToSleep, - change: None, - } - .into()) - } - } - ActionPrompt::MasonsWake { .. } => Ok(ActionComplete { - result: ActionResult::GoBackToSleep, - change: None, - } - .into()), - ActionPrompt::MasonLeaderRecruit { - character_id, - marked: Some(marked), - .. - } => Ok(ActionComplete { - result: ActionResult::Continue, - change: Some(NightChange::MasonRecruit { - mason_leader: character_id.character_id, - recruiting: *marked, - }), - } - .into()), - ActionPrompt::Empath { - character_id, - marked: Some(marked), - .. - } => { - let marked = self.village.character_by_id(*marked)?; - let scapegoat = marked.role_title() == RoleTitle::Scapegoat; - - Ok(ActionComplete { - result: ActionResult::Empath { scapegoat }, - change: scapegoat.then(|| NightChange::EmpathFoundScapegoat { - empath: character_id.character_id, - scapegoat: marked.character_id(), - }), - } - .into()) - } - ActionPrompt::Vindicator { - character_id, - marked: Some(marked), - .. - } => Ok(ActionComplete { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Protection { - target: *marked, - protection: Protection::Vindicator { - source: character_id.character_id, - }, - }), - } - .into()), - ActionPrompt::Insomniac { .. } => Ok(ActionComplete { - result: ActionResult::GoBackToSleep, - change: None, - } - .into()), - ActionPrompt::PyreMaster { - character_id, - marked: Some(marked), - .. - } => Ok(ActionComplete { - result: ActionResult::GoBackToSleep, - change: NonZeroU8::new(self.night).map(|night| NightChange::Kill { - target: *marked, - died_to: DiedTo::PyreMaster { - killer: character_id.character_id, - night, - }, - }), - } - .into()), - - ActionPrompt::PyreMaster { marked: None, .. } - | ActionPrompt::MasonLeaderRecruit { marked: None, .. } => Ok(ActionComplete { - result: ActionResult::GoBackToSleep, - change: None, - } - .into()), - - ActionPrompt::Adjudicator { marked: None, .. } - | ActionPrompt::PowerSeer { marked: None, .. } - | ActionPrompt::Mortician { marked: None, .. } - | ActionPrompt::Beholder { marked: None, .. } - | ActionPrompt::Empath { marked: None, .. } - | ActionPrompt::Vindicator { marked: None, .. } - | ActionPrompt::Protector { marked: None, .. } - | ActionPrompt::Arcanist { - marked: (None, None), - .. - } - | ActionPrompt::Arcanist { - marked: (None, Some(_)), - .. - } - | ActionPrompt::Arcanist { - marked: (Some(_), None), - .. - } - | ActionPrompt::LoneWolfKill { marked: None, .. } - | ActionPrompt::Gravedigger { marked: None, .. } - | ActionPrompt::Hunter { marked: None, .. } - | ActionPrompt::Guardian { marked: None, .. } - | ActionPrompt::WolfPackKill { marked: None, .. } - | ActionPrompt::DireWolf { marked: None, .. } - | ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState), - } - } - pub const fn village(&self) -> &Village { &self.village } @@ -1368,6 +722,13 @@ impl Night { matches!(self.night_state, NightState::Complete) } + pub fn used_actions(&self) -> Box<[(ActionPrompt, ActionResult)]> { + self.used_actions + .iter() + .map(|(p, r, _)| (p.clone(), r.clone())) + .collect() + } + pub fn next(&mut self) -> Result<()> { match &self.night_state { NightState::Active { @@ -1413,8 +774,7 @@ impl Night { fn changes_from_actions(&self) -> Box<[NightChange]> { self.used_actions .iter() - .map(|(_, _, act)| act.into_iter()) - .flatten() + .flat_map(|(_, _, act)| act.iter()) .cloned() .collect() } @@ -1561,9 +921,8 @@ impl Night { } pub fn next_page(&mut self) { - match &mut self.night_state { - NightState::Active { current_page, .. } => *current_page += 1, - _ => {} + if let NightState::Active { current_page, .. } = &mut self.night_state { + *current_page += 1 } } } diff --git a/werewolves-proto/src/game/night/changes.rs b/werewolves-proto/src/game/night/changes.rs new file mode 100644 index 0000000..4e2f510 --- /dev/null +++ b/werewolves-proto/src/game/night/changes.rs @@ -0,0 +1,138 @@ +use core::ops::Not; + +use serde::{Deserialize, Serialize}; +use werewolves_macros::Extract; + +use crate::{ + character::CharacterId, + diedto::DiedTo, + player::Protection, + role::{RoleBlock, RoleTitle}, +}; + +#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, Extract)] +pub enum NightChange { + RoleChange(CharacterId, RoleTitle), + HunterTarget { + source: CharacterId, + target: CharacterId, + }, + Kill { + target: CharacterId, + died_to: DiedTo, + }, + RoleBlock { + source: CharacterId, + target: CharacterId, + block_type: RoleBlock, + }, + Shapeshift { + source: CharacterId, + into: CharacterId, + }, + Protection { + target: CharacterId, + protection: Protection, + }, + ElderReveal { + elder: CharacterId, + }, + MasonRecruit { + mason_leader: CharacterId, + recruiting: CharacterId, + }, + EmpathFoundScapegoat { + empath: CharacterId, + scapegoat: CharacterId, + }, +} + +pub struct ChangesLookup<'a>(&'a [NightChange], Vec); + +impl<'a> ChangesLookup<'a> { + pub fn new(changes: &'a [NightChange]) -> Self { + Self(changes, Vec::new()) + } + + pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> { + self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then(|| match c { + NightChange::Kill { target: t, died_to } => (*t == target).then_some(died_to), + _ => None, + }) + .flatten() + }) + } + + pub fn protected_take(&mut self, target: CharacterId) -> Option { + if let Some((idx, c)) = self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then(|| match c { + NightChange::Protection { + target: t, + protection, + } => (*t == target).then_some((idx, protection)), + _ => None, + }) + .flatten() + }) { + self.1.push(idx); + Some(c.clone()) + } else { + None + } + } + pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> { + self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then(|| match c { + NightChange::Protection { + target: t, + protection, + } => (t == target).then_some(protection), + _ => None, + }) + .flatten() + }) + } + + pub fn shapeshifter(&self) -> Option<&'a CharacterId> { + self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then_some(match c { + NightChange::Shapeshift { source, .. } => Some(source), + _ => None, + }) + .flatten() + }) + } + + pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> { + self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then_some(match c { + NightChange::Kill { + target, + died_to: + DiedTo::Wolfpack { + night: _, + killing_wolf: _, + }, + } => Some(target), + _ => None, + }) + .flatten() + }) + } +} diff --git a/werewolves-proto/src/game/night/process.rs b/werewolves-proto/src/game/night/process.rs new file mode 100644 index 0000000..8864ed9 --- /dev/null +++ b/werewolves-proto/src/game/night/process.rs @@ -0,0 +1,491 @@ +use core::num::NonZeroU8; + +use crate::{ + diedto::DiedTo, + error::GameError, + game::night::{ActionComplete, Night, NightState, ResponseOutcome, changes::NightChange}, + message::night::{ActionPrompt, ActionResponse, ActionResult}, + player::Protection, + role::{AlignmentEq, PreviousGuardianAction, RoleBlock, RoleTitle}, +}; + +type Result = core::result::Result; + +impl Night { + pub(super) fn process(&self, resp: ActionResponse) -> Result { + let current_prompt = match &self.night_state { + NightState::Active { + current_prompt: _, + current_result: Some(_), + .. + } => return Err(GameError::NightNeedsNext), + NightState::Active { + current_prompt, + current_result: None, + .. + } => current_prompt, + NightState::Complete => return Err(GameError::NightOver), + }; + + match resp { + ActionResponse::MarkTarget(mark) => { + return Ok(ResponseOutcome::PromptUpdate( + current_prompt.with_mark(mark)?, + )); + } + ActionResponse::Shapeshift => { + return match current_prompt { + ActionPrompt::Shapeshifter { + character_id: source, + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Shapeshift { + source: source.character_id, + into: self + .changes_from_actions() + .into_iter() + .find_map(|c| match c { + NightChange::Kill { + target, + died_to: DiedTo::Wolfpack { .. }, + } => Some(target), + _ => None, + }) + .ok_or(GameError::InvalidTarget)?, + }), + })), + _ => Err(GameError::InvalidMessageForGameState), + }; + } + ActionResponse::Continue => { + if let ActionPrompt::Insomniac { character_id } = current_prompt { + return Ok(ActionComplete { + result: ActionResult::Insomniac( + self.get_visits_for(character_id.character_id), + ), + change: None, + } + .into()); + } + if let ActionPrompt::RoleChange { + character_id, + new_role, + } = current_prompt + { + return Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::RoleChange( + character_id.character_id, + *new_role, + )), + })); + } + } + }; + + match current_prompt { + ActionPrompt::LoneWolfKill { + character_id, + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: *marked, + died_to: DiedTo::LoneWolf { + killer: character_id.character_id, + night: self.night, + }, + }), + } + .into()), + ActionPrompt::RoleChange { .. } + | ActionPrompt::WolvesIntro { .. } + | ActionPrompt::CoverOfDarkness => { + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + })) + } + ActionPrompt::ElderReveal { character_id } => { + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::ElderReveal { + elder: character_id.character_id, + }), + })) + } + ActionPrompt::Seer { + marked: Some(marked), + .. + } => { + let alignment = self.village.character_by_id(*marked)?.alignment(); + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::Seer(alignment), + change: None, + })) + } + ActionPrompt::Protector { + marked: Some(marked), + character_id, + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: *marked, + protection: Protection::Protector { + source: character_id.character_id, + }, + }), + })), + ActionPrompt::Arcanist { + marked: (Some(marked1), Some(marked2)), + .. + } => { + let same = self.village.character_by_id(*marked1)?.alignment() + == self.village.character_by_id(*marked2)?.alignment(); + + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::Arcanist(AlignmentEq::new(same)), + change: None, + })) + } + ActionPrompt::Gravedigger { + marked: Some(marked), + .. + } => { + let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig(); + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GraveDigger(dig_role), + change: None, + })) + } + ActionPrompt::Hunter { + character_id, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::HunterTarget { + source: character_id.character_id, + target: *marked, + }), + })), + ActionPrompt::Militia { + character_id, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: *marked, + died_to: DiedTo::Militia { + killer: character_id.character_id, + night: NonZeroU8::new(self.night) + .ok_or(GameError::InvalidMessageForGameState)?, + }, + }), + })), + ActionPrompt::Militia { marked: None, .. } => { + Ok(ResponseOutcome::ActionComplete(Default::default())) + } + ActionPrompt::MapleWolf { + character_id, + kill_or_die, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: *marked, + died_to: DiedTo::MapleWolf { + source: character_id.character_id, + night: NonZeroU8::new(self.night) + .ok_or(GameError::InvalidMessageForGameState)?, + starves_if_fails: *kill_or_die, + }, + }), + })), + ActionPrompt::MapleWolf { marked: None, .. } => { + Ok(ResponseOutcome::ActionComplete(Default::default())) + } + ActionPrompt::Guardian { + character_id, + previous: None, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: *marked, + protection: Protection::Guardian { + source: character_id.character_id, + guarding: false, + }, + }), + })), + ActionPrompt::Guardian { + character_id, + previous: Some(PreviousGuardianAction::Guard(prev_target)), + marked: Some(marked), + .. + } => { + if prev_target.character_id == *marked { + return Err(GameError::InvalidTarget); + } + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: *marked, + protection: Protection::Guardian { + source: character_id.character_id, + guarding: false, + }, + }), + })) + } + ActionPrompt::Guardian { + character_id, + previous: Some(PreviousGuardianAction::Protect(prev_protect)), + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: *marked, + protection: Protection::Guardian { + source: character_id.character_id, + guarding: prev_protect.character_id == *marked, + }, + }), + })), + ActionPrompt::WolfPackKill { + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: *marked, + died_to: DiedTo::Wolfpack { + killing_wolf: self + .village + .killing_wolf() + .ok_or(GameError::NoWolves)? + .character_id(), + night: NonZeroU8::new(self.night) + .ok_or(GameError::InvalidMessageForGameState)?, + }, + }), + })), + ActionPrompt::Shapeshifter { character_id } => { + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: match &resp { + ActionResponse::Continue => None, + ActionResponse::Shapeshift => Some(NightChange::Shapeshift { + source: character_id.character_id, + into: self + .changes_from_actions() + .into_iter() + .find_map(|c| match c { + NightChange::Kill { + target, + died_to: DiedTo::Wolfpack { .. }, + } => Some(target), + _ => None, + }) + .ok_or(GameError::InvalidTarget)?, + }), + _ => return Err(GameError::InvalidMessageForGameState), + }, + })) + } + ActionPrompt::AlphaWolf { + character_id, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: *marked, + died_to: DiedTo::AlphaWolf { + killer: character_id.character_id, + night: NonZeroU8::new(self.night) + .ok_or(GameError::InvalidMessageForGameState)?, + }, + }), + })), + ActionPrompt::AlphaWolf { marked: None, .. } => { + Ok(ResponseOutcome::ActionComplete(Default::default())) + } + ActionPrompt::DireWolf { + character_id, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::RoleBlock { + source: character_id.character_id, + target: *marked, + block_type: RoleBlock::Direwolf, + }), + })), + ActionPrompt::Adjudicator { + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::Adjudicator { + killer: self.village.character_by_id(*marked)?.killer(), + }, + change: None, + } + .into()), + ActionPrompt::PowerSeer { + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::PowerSeer { + powerful: self.village.character_by_id(*marked)?.powerful(), + }, + change: None, + } + .into()), + ActionPrompt::Mortician { + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::Mortician( + self.village + .character_by_id(*marked)? + .died_to() + .ok_or(GameError::InvalidTarget)? + .title(), + ), + change: None, + } + .into()), + ActionPrompt::Beholder { + marked: Some(marked), + .. + } => { + if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| { + prompt.matches_beholding(*marked).then_some(result) + }) { + Ok(ActionComplete { + result: result.clone(), + change: None, + } + .into()) + } else { + Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + } + .into()) + } + } + ActionPrompt::MasonsWake { .. } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + } + .into()), + ActionPrompt::MasonLeaderRecruit { + character_id, + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::Continue, + change: Some(NightChange::MasonRecruit { + mason_leader: character_id.character_id, + recruiting: *marked, + }), + } + .into()), + ActionPrompt::Empath { + character_id, + marked: Some(marked), + .. + } => { + let marked = self.village.character_by_id(*marked)?; + let scapegoat = marked.role_title() == RoleTitle::Scapegoat; + + Ok(ActionComplete { + result: ActionResult::Empath { scapegoat }, + change: scapegoat.then(|| NightChange::EmpathFoundScapegoat { + empath: character_id.character_id, + scapegoat: marked.character_id(), + }), + } + .into()) + } + ActionPrompt::Vindicator { + character_id, + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: *marked, + protection: Protection::Vindicator { + source: character_id.character_id, + }, + }), + } + .into()), + ActionPrompt::Insomniac { .. } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + } + .into()), + ActionPrompt::PyreMaster { + character_id, + marked: Some(marked), + .. + } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: NonZeroU8::new(self.night).map(|night| NightChange::Kill { + target: *marked, + died_to: DiedTo::PyreMaster { + killer: character_id.character_id, + night, + }, + }), + } + .into()), + + ActionPrompt::PyreMaster { marked: None, .. } + | ActionPrompt::MasonLeaderRecruit { marked: None, .. } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + } + .into()), + + ActionPrompt::Adjudicator { marked: None, .. } + | ActionPrompt::PowerSeer { marked: None, .. } + | ActionPrompt::Mortician { marked: None, .. } + | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::Empath { marked: None, .. } + | ActionPrompt::Vindicator { marked: None, .. } + | ActionPrompt::Protector { marked: None, .. } + | ActionPrompt::Arcanist { + marked: (None, None), + .. + } + | ActionPrompt::Arcanist { + marked: (None, Some(_)), + .. + } + | ActionPrompt::Arcanist { + marked: (Some(_), None), + .. + } + | ActionPrompt::LoneWolfKill { marked: None, .. } + | ActionPrompt::Gravedigger { marked: None, .. } + | ActionPrompt::Hunter { marked: None, .. } + | ActionPrompt::Guardian { marked: None, .. } + | ActionPrompt::WolfPackKill { marked: None, .. } + | ActionPrompt::DireWolf { marked: None, .. } + | ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState), + } + } +} diff --git a/werewolves-proto/src/game/story.rs b/werewolves-proto/src/game/story.rs new file mode 100644 index 0000000..39308c4 --- /dev/null +++ b/werewolves-proto/src/game/story.rs @@ -0,0 +1,458 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{ + character::CharacterId, + diedto::DiedToTitle, + error::GameError, + game::{GameTime, Village, night::changes::NightChange}, + message::night::{ActionPrompt, ActionResult}, + role::{Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleTitle}, +}; + +type Result = core::result::Result; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum GameActions { + DayDetails(Box<[DayDetail]>), + NightDetails(NightDetails), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum DayDetail { + Execute(CharacterId), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NightDetails { + pub choices: Box<[NightChoice]>, + pub changes: Box<[NightChange]>, +} + +impl NightDetails { + pub fn new(choices: &[(ActionPrompt, ActionResult)], changes: Box<[NightChange]>) -> Self { + Self { + changes, + choices: choices + .iter() + .cloned() + .filter_map(|(prompt, result)| NightChoice::new(prompt, result)) + .collect(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NightChoice { + pub prompt: StoryActionPrompt, + pub result: Option, +} + +impl NightChoice { + pub fn new(prompt: ActionPrompt, result: ActionResult) -> Option { + Some(Self { + prompt: StoryActionPrompt::new(prompt)?, + result: StoryActionResult::new(result), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum StoryActionResult { + RoleBlocked, + Seer(Alignment), + PowerSeer { powerful: Powerful }, + Adjudicator { killer: Killer }, + Arcanist(AlignmentEq), + GraveDigger(Option), + Mortician(DiedToTitle), + Insomniac { visits: Box<[CharacterId]> }, + Empath { scapegoat: bool }, +} + +impl StoryActionResult { + pub fn new(result: ActionResult) -> Option { + Some(match result { + ActionResult::RoleBlocked => Self::RoleBlocked, + ActionResult::Seer(alignment) => Self::Seer(alignment), + ActionResult::PowerSeer { powerful } => Self::PowerSeer { powerful }, + ActionResult::Adjudicator { killer } => Self::Adjudicator { killer }, + ActionResult::Arcanist(same) => Self::Arcanist(same), + ActionResult::GraveDigger(role_title) => Self::GraveDigger(role_title), + ActionResult::Mortician(died_to) => Self::Mortician(died_to), + ActionResult::Insomniac(visits) => Self::Insomniac { + visits: visits.iter().map(|c| c.character_id).collect(), + }, + ActionResult::Empath { scapegoat } => Self::Empath { scapegoat }, + + ActionResult::GoBackToSleep | ActionResult::Continue => return None, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum StoryActionPrompt { + Seer { + character_id: CharacterId, + chosen: CharacterId, + }, + Protector { + character_id: CharacterId, + chosen: CharacterId, + }, + Arcanist { + character_id: CharacterId, + chosen: (CharacterId, CharacterId), + }, + Gravedigger { + character_id: CharacterId, + chosen: CharacterId, + }, + Hunter { + character_id: CharacterId, + chosen: CharacterId, + }, + Militia { + character_id: CharacterId, + chosen: CharacterId, + }, + MapleWolf { + character_id: CharacterId, + chosen: CharacterId, + }, + Guardian { + character_id: CharacterId, + chosen: CharacterId, + guarding: bool, + }, + Adjudicator { + character_id: CharacterId, + chosen: CharacterId, + }, + PowerSeer { + character_id: CharacterId, + chosen: CharacterId, + }, + Mortician { + character_id: CharacterId, + chosen: CharacterId, + }, + Beholder { + character_id: CharacterId, + chosen: CharacterId, + }, + MasonsWake { + leader: CharacterId, + masons: Box<[CharacterId]>, + }, + MasonLeaderRecruit { + character_id: CharacterId, + chosen: CharacterId, + }, + Empath { + character_id: CharacterId, + chosen: CharacterId, + }, + Vindicator { + character_id: CharacterId, + chosen: CharacterId, + }, + PyreMaster { + character_id: CharacterId, + chosen: CharacterId, + }, + WolfPackKill { + chosen: CharacterId, + }, + Shapeshifter { + character_id: CharacterId, + }, + AlphaWolf { + character_id: CharacterId, + chosen: CharacterId, + }, + DireWolf { + character_id: CharacterId, + chosen: CharacterId, + }, + LoneWolfKill { + character_id: CharacterId, + chosen: CharacterId, + }, + Insomniac { + character_id: CharacterId, + }, +} + +impl StoryActionPrompt { + pub fn new(prompt: ActionPrompt) -> Option { + Some(match prompt { + ActionPrompt::Seer { + character_id, + marked: Some(marked), + .. + } => Self::Seer { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Arcanist { + character_id, + marked: (Some(marked1), Some(marked2)), + .. + } => Self::Arcanist { + character_id: character_id.character_id, + chosen: (marked1, marked2), + }, + ActionPrompt::Protector { + character_id, + marked: Some(marked), + .. + } => Self::Protector { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Gravedigger { + character_id, + marked: Some(marked), + .. + } => Self::Gravedigger { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Hunter { + character_id, + marked: Some(marked), + .. + } => Self::Hunter { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Militia { + character_id, + marked: Some(marked), + .. + } => Self::Militia { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::MapleWolf { + character_id, + marked: Some(marked), + .. + } => Self::MapleWolf { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Guardian { + character_id, + previous, + marked: Some(marked), + .. + } => Self::Guardian { + character_id: character_id.character_id, + chosen: marked, + guarding: previous + .map(|prev| match prev { + PreviousGuardianAction::Protect(id) => id.character_id == marked, + _ => false, + }) + .unwrap_or_default(), + }, + ActionPrompt::Adjudicator { + character_id, + marked: Some(marked), + .. + } => Self::Adjudicator { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::PowerSeer { + character_id, + marked: Some(marked), + .. + } => Self::PowerSeer { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Mortician { + character_id, + marked: Some(marked), + .. + } => Self::Mortician { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Beholder { + character_id, + marked: Some(marked), + .. + } => Self::Beholder { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::MasonsWake { leader, masons } => Self::MasonsWake { + leader, + masons: masons.into_iter().map(|c| c.character_id).collect(), + }, + ActionPrompt::MasonLeaderRecruit { + character_id, + marked: Some(marked), + .. + } => Self::MasonLeaderRecruit { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Empath { + character_id, + marked: Some(marked), + .. + } => Self::Empath { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Vindicator { + character_id, + marked: Some(marked), + .. + } => Self::Vindicator { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::PyreMaster { + character_id, + marked: Some(marked), + .. + } => Self::PyreMaster { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::WolfPackKill { + marked: Some(marked), + .. + } => Self::WolfPackKill { chosen: marked }, + ActionPrompt::Shapeshifter { character_id } => Self::Shapeshifter { + character_id: character_id.character_id, + }, + ActionPrompt::AlphaWolf { + character_id, + marked: Some(marked), + .. + } => Self::AlphaWolf { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::DireWolf { + character_id, + marked: Some(marked), + .. + } => Self::DireWolf { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::LoneWolfKill { + character_id, + marked: Some(marked), + .. + } => Self::LoneWolfKill { + character_id: character_id.character_id, + chosen: marked, + }, + ActionPrompt::Insomniac { character_id } => Self::Insomniac { + character_id: character_id.character_id, + }, + + ActionPrompt::Protector { .. } + | ActionPrompt::Gravedigger { .. } + | ActionPrompt::Hunter { .. } + | ActionPrompt::Militia { .. } + | ActionPrompt::MapleWolf { .. } + | ActionPrompt::Guardian { .. } + | ActionPrompt::Adjudicator { .. } + | ActionPrompt::PowerSeer { .. } + | ActionPrompt::Mortician { .. } + | ActionPrompt::Beholder { .. } + | ActionPrompt::MasonLeaderRecruit { .. } + | ActionPrompt::Empath { .. } + | ActionPrompt::Vindicator { .. } + | ActionPrompt::PyreMaster { .. } + | ActionPrompt::WolfPackKill { .. } + | ActionPrompt::AlphaWolf { .. } + | ActionPrompt::DireWolf { .. } + | ActionPrompt::LoneWolfKill { .. } + | ActionPrompt::Seer { .. } + | ActionPrompt::Arcanist { .. } + | ActionPrompt::WolvesIntro { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::ElderReveal { .. } + | ActionPrompt::CoverOfDarkness => return None, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GameStory { + pub starting_village: Village, + pub changes: HashMap, +} + +impl GameStory { + pub fn new(starting_village: Village) -> Self { + Self { + starting_village, + changes: HashMap::new(), + } + } + + pub fn add(&mut self, time: GameTime, changes: GameActions) -> Result<()> { + if self.changes.contains_key(&time) { + return Err(GameError::ChangesAlreadySet(time)); + } + self.changes.insert(time, changes); + + Ok(()) + } + + pub fn village_at(&self, at_time: GameTime) -> Result> { + let mut village = self.starting_village.clone(); + for (time, actions) in self.iter() { + village = match actions { + GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?, + GameActions::NightDetails(night_details) => { + village.with_night_changes(&night_details.changes)? + } + }; + if time == at_time { + return Ok(Some(village)); + } + } + Ok(None) + } + + pub fn iter<'a>(&'a self) -> StoryIterator<'a> { + StoryIterator { + story: self, + time: self.starting_village.time(), + } + } +} + +pub struct StoryIterator<'a> { + story: &'a GameStory, + time: GameTime, +} + +impl<'a> Iterator for StoryIterator<'a> { + type Item = (GameTime, &'a GameActions); + + fn next(&mut self) -> Option { + match self.story.changes.get(&self.time) { + Some(changes) => { + let changes_time = self.time; + self.time = self.time.next(); + Some((changes_time, changes)) + } + None => None, + } + } +} diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index ff564e9..85faf3d 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -1,5 +1,5 @@ +mod apply; use core::num::NonZeroU8; -use std::{rc::Rc, sync::Arc}; use rand::Rng; use serde::{Deserialize, Serialize}; @@ -9,16 +9,16 @@ use crate::{ character::{Character, CharacterId}, diedto::DiedTo, error::GameError, - game::{DateTime, GameOver, GameSettings}, + game::{GameOver, GameSettings, GameTime}, message::{CharacterIdentity, Identification, night::ActionPrompt}, player::PlayerId, role::{Role, RoleTitle}, }; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Village { characters: Box<[Character]>, - date_time: DateTime, + time: GameTime, settings: GameSettings, } @@ -37,7 +37,7 @@ impl Village { Ok(Self { settings, characters, - date_time: DateTime::Night { number: 0 }, + time: GameTime::Night { number: 0 }, }) } @@ -46,30 +46,18 @@ impl Village { } pub fn killing_wolf(&self) -> Option<&Character> { - let wolves = self.characters.iter().filter(|c| c.is_wolf()); - - { - let ww = wolves - .clone() - .filter(|w| matches!(w.role_title(), RoleTitle::Werewolf)) - .collect::>(); - if !ww.is_empty() { - return Some(ww[rand::random_range(0..ww.len())]); - } - } - { - let wolves = wolves.collect::>(); - if wolves.is_empty() { - return None; - } - - Some(wolves[rand::random_range(0..wolves.len())]) - } + let mut wolves = self + .characters + .iter() + .filter(|c| c.is_wolf()) + .collect::>(); + wolves.sort_by_key(|w| w.killing_wolf_order()); + wolves.first().copied() } pub fn wolf_pack_kill(&self) -> Option { - let night = match self.date_time { - DateTime::Day { .. } => return None, - DateTime::Night { number } => number, + let night = match self.time { + GameTime::Day { .. } => return None, + GameTime::Night { number } => number, }; let no_kill_due_to_disease = self .characters @@ -88,8 +76,8 @@ impl Village { }) } - pub const fn date_time(&self) -> DateTime { - self.date_time + pub const fn time(&self) -> GameTime { + self.time } pub fn find_by_character_id_mut( @@ -137,9 +125,9 @@ impl Village { } pub fn execute(&mut self, characters: &[CharacterId]) -> Result> { - let day = match self.date_time { - DateTime::Day { number } => number, - DateTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight), + let day = match self.time { + GameTime::Day { number } => number, + GameTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight), }; let targets = self @@ -151,16 +139,19 @@ impl Village { t.execute(day)?; } - self.date_time = self.date_time.next(); - Ok(self.is_game_over()) + if let Some(game_over) = self.is_game_over() { + return Ok(Some(game_over)); + } + self.time = self.time.next(); + Ok(None) } - pub fn to_day(&mut self) -> Result { - if self.date_time.is_day() { + pub fn to_day(&mut self) -> Result { + if self.time.is_day() { return Err(GameError::AlreadyDaytime); } - self.date_time = self.date_time.next(); - Ok(self.date_time) + self.time = self.time.next(); + Ok(self.time) } pub fn living_wolf_pack_players(&self) -> Box<[Character]> { diff --git a/werewolves-proto/src/game/village/apply.rs b/werewolves-proto/src/game/village/apply.rs new file mode 100644 index 0000000..871ecb8 --- /dev/null +++ b/werewolves-proto/src/game/village/apply.rs @@ -0,0 +1,173 @@ +use core::num::NonZeroU8; + +use crate::{ + diedto::DiedTo, + error::GameError, + game::{ + GameTime, Village, kill, + night::changes::{ChangesLookup, NightChange}, + story::DayDetail, + }, + player::Protection, + role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleTitle}, +}; + +type Result = core::result::Result; + +impl Village { + pub fn with_day_changes(&self, day_changes: &[DayDetail]) -> Result { + let mut new_village = self.clone(); + let executions = day_changes + .iter() + .map(|c| match c { + DayDetail::Execute(c) => *c, + }) + .collect::>(); + + new_village.execute(&executions)?; + + Ok(new_village) + } + pub fn with_night_changes(&self, all_changes: &[NightChange]) -> Result { + let night = match self.time { + GameTime::Day { .. } => return Err(GameError::NotNight), + GameTime::Night { number } => number, + }; + let mut changes = ChangesLookup::new(all_changes); + + let mut new_village = self.clone(); + for change in all_changes { + match change { + NightChange::ElderReveal { elder } => { + new_village.character_by_id_mut(*elder)?.elder_reveal() + } + NightChange::RoleChange(character_id, role_title) => new_village + .character_by_id_mut(*character_id)? + .role_change(*role_title, GameTime::Night { number: night })?, + NightChange::HunterTarget { source, target } => { + let hunter_character = new_village.character_by_id_mut(*source).unwrap(); + hunter_character.hunter_mut()?.replace(*target); + if changes.killed(*source).is_some() + && changes.protected(source).is_none() + && changes.protected(target).is_none() + { + new_village + .character_by_id_mut(*target) + .unwrap() + .kill(DiedTo::Hunter { + killer: *source, + night: NonZeroU8::new(night).unwrap(), + }) + } + } + NightChange::Kill { target, died_to } => { + if let Some(kill) = + kill::resolve_kill(&mut changes, *target, died_to, night, self)? + { + kill.apply_to_village(&mut new_village)?; + } + } + NightChange::Shapeshift { source, into } => { + if let Some(target) = changes.wolf_pack_kill_target() + && changes.protected(target).is_none() + { + if *target != *into { + log::error!("shapeshift into({into}) != target({target})"); + continue; + } + let ss = new_village.character_by_id_mut(*source).unwrap(); + ss.shapeshifter_mut().unwrap().replace(*target); + ss.kill(DiedTo::Shapeshift { + into: *target, + night: NonZeroU8::new(night).unwrap(), + }); + // role change pushed in [apply_shapeshift] + } + } + + NightChange::Protection { + target, + protection: Protection::Guardian { source, guarding }, + } => { + let target = new_village.character_by_id(*target)?.identity(); + new_village + .character_by_id_mut(*source)? + .guardian_mut()? + .replace(if *guarding { + PreviousGuardianAction::Guard(target) + } else { + PreviousGuardianAction::Protect(target) + }); + } + + NightChange::RoleBlock { .. } | NightChange::Protection { .. } => {} + NightChange::MasonRecruit { + mason_leader, + recruiting, + } => { + if new_village.character_by_id(*recruiting)?.is_wolf() { + new_village.character_by_id_mut(*mason_leader)?.kill( + DiedTo::MasonLeaderRecruitFail { + night, + tried_recruiting: *recruiting, + }, + ); + } else { + new_village + .character_by_id_mut(*mason_leader)? + .mason_leader_mut()? + .recruit(*recruiting); + } + } + NightChange::EmpathFoundScapegoat { empath, scapegoat } => { + new_village + .character_by_id_mut(*scapegoat)? + .role_change(RoleTitle::Villager, GameTime::Night { number: night })?; + *new_village.character_by_id_mut(*empath)?.empath_mut()? = true; + } + } + } + // black knights death + for knight in new_village + .characters_mut() + .into_iter() + .filter(|k| k.alive()) + .filter(|k| k.black_knight().ok().and_then(|t| (*t).clone()).is_some()) + .filter(|k| changes.killed(k.character_id()).is_none()) + { + knight.black_knight_kill()?.kill(); + } + + // pyre masters death + let village_dead = new_village + .characters() + .into_iter() + .filter(|c| c.is_village()) + .filter_map(|c| c.died_to().cloned()) + .filter_map(|c| c.killer().map(|k| (k, c))) + .collect::>(); + for pyremaster in new_village + .living_characters_by_role_mut(RoleTitle::PyreMaster) + .into_iter() + .filter(|p| { + village_dead + .iter() + .filter(|(k, _)| *k == p.character_id()) + .count() + >= PYREMASTER_VILLAGER_KILLS_TO_DIE.get() as usize + }) + { + if let Some(night) = NonZeroU8::new(night) { + pyremaster.kill(DiedTo::PyreMasterLynchMob { + night, + source: pyremaster.character_id(), + }); + } + } + + if new_village.is_game_over().is_none() { + new_village.to_day()?; + } + Ok(new_village) + } +} diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 1e64381..a3668c8 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -4,15 +4,16 @@ mod role; use crate::{ character::{Character, CharacterId}, + diedto::DiedToTitle, error::GameError, - game::{Game, GameOver, GameSettings, SetupRole, SetupSlot}, + game::{Game, GameOver, GameSettings, OrRandom, SetupRole, SetupSlot}, message::{ CharacterState, Identification, PublicIdentity, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits}, }, player::PlayerId, - role::{Alignment, RoleTitle}, + role::{Alignment, Killer, Powerful, RoleTitle}, }; use colored::Colorize; use core::{num::NonZeroU8, ops::Range}; @@ -51,7 +52,7 @@ pub trait ActionPromptTitleExt { fn gravedigger(&self); fn hunter(&self); fn militia(&self); - fn maplewolf(&self); + fn maple_wolf(&self); fn guardian(&self); fn shapeshifter(&self); fn alphawolf(&self); @@ -65,9 +66,14 @@ pub trait ActionPromptTitleExt { fn adjudicator(&self); fn lone_wolf(&self); fn insomniac(&self); + fn power_seer(&self); + fn mortician(&self); } impl ActionPromptTitleExt for ActionPromptTitle { + fn mortician(&self) { + assert_eq!(*self, ActionPromptTitle::Mortician); + } fn cover_of_darkness(&self) { assert_eq!(*self, ActionPromptTitle::CoverOfDarkness); } @@ -95,7 +101,7 @@ impl ActionPromptTitleExt for ActionPromptTitle { fn militia(&self) { assert_eq!(*self, ActionPromptTitle::Militia); } - fn maplewolf(&self) { + fn maple_wolf(&self) { assert_eq!(*self, ActionPromptTitle::MapleWolf); } fn guardian(&self) { @@ -131,6 +137,9 @@ impl ActionPromptTitleExt for ActionPromptTitle { fn adjudicator(&self) { assert_eq!(*self, ActionPromptTitle::Adjudicator) } + fn power_seer(&self) { + assert_eq!(*self, ActionPromptTitle::PowerSeer) + } fn empath(&self) { assert_eq!(*self, ActionPromptTitle::Empath) } @@ -148,9 +157,48 @@ pub trait ActionResultExt { fn seer(&self) -> Alignment; fn insomniac(&self) -> Visits; fn arcanist(&self) -> bool; + fn role_blocked(&self); + fn gravedigger(&self) -> Option; + fn power_seer(&self) -> Powerful; + fn adjudicator(&self) -> Killer; + fn mortician(&self) -> DiedToTitle; + fn empath(&self) -> bool; } impl ActionResultExt for ActionResult { + fn empath(&self) -> bool { + match self { + Self::Empath { scapegoat } => *scapegoat, + resp => panic!("expected empath, got {resp:?}"), + } + } + fn mortician(&self) -> DiedToTitle { + match self { + Self::Mortician(role) => *role, + resp => panic!("expected mortician, got {resp:?}"), + } + } + fn gravedigger(&self) -> Option { + match self { + Self::GraveDigger(role) => *role, + resp => panic!("expected gravedigger, got {resp:?}"), + } + } + fn role_blocked(&self) { + assert_eq!(*self, ActionResult::RoleBlocked) + } + fn adjudicator(&self) -> Killer { + match self { + Self::Adjudicator { killer } => *killer, + resp => panic!("expected adjudicator, got {resp:?}"), + } + } + fn power_seer(&self) -> Powerful { + match self { + Self::PowerSeer { powerful } => *powerful, + resp => panic!("expected power seer, got {resp:?}"), + } + } fn sleep(&self) { assert_eq!(*self, ActionResult::GoBackToSleep) } @@ -168,7 +216,7 @@ impl ActionResultExt for ActionResult { fn arcanist(&self) -> bool { match self { - ActionResult::Arcanist { same } => *same, + ActionResult::Arcanist(same) => same.same(), _ => panic!("expected an arcanist result"), } } @@ -734,3 +782,400 @@ fn wolfpack_kill_all_targets_valid() { assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter); } } + +#[test] +fn varied_test() { + init_log(); + let players = (1..32u8) + .filter_map(NonZeroU8::new) + .map(|n| Identification { + player_id: PlayerId::from_u128(n.get() as _), + public: PublicIdentity { + name: format!("Player {n}"), + pronouns: Some("he/him".into()), + number: Some(n), + }, + }) + .collect::>(); + let mut players_iter = players.iter().map(|p| p.player_id); + let ( + werewolf, + dire_wolf, + shapeshifter, + alpha_wolf, + seer, + arcanist, + maple_wolf, + guardian, + vindicator, + adjudicator, + power_seer, + beholder, + gravedigger, + mortician, + insomniac, + empath, + scapegoat, + hunter, + ) = ( + (SetupRole::Werewolf, players_iter.next().unwrap()), + (SetupRole::DireWolf, players_iter.next().unwrap()), + (SetupRole::Shapeshifter, players_iter.next().unwrap()), + (SetupRole::AlphaWolf, players_iter.next().unwrap()), + (SetupRole::Seer, players_iter.next().unwrap()), + (SetupRole::Arcanist, players_iter.next().unwrap()), + (SetupRole::MapleWolf, players_iter.next().unwrap()), + (SetupRole::Guardian, players_iter.next().unwrap()), + (SetupRole::Vindicator, players_iter.next().unwrap()), + (SetupRole::Adjudicator, players_iter.next().unwrap()), + (SetupRole::PowerSeer, players_iter.next().unwrap()), + (SetupRole::Beholder, players_iter.next().unwrap()), + (SetupRole::Gravedigger, players_iter.next().unwrap()), + (SetupRole::Mortician, players_iter.next().unwrap()), + (SetupRole::Insomniac, players_iter.next().unwrap()), + (SetupRole::Empath, players_iter.next().unwrap()), + ( + SetupRole::Scapegoat { + redeemed: OrRandom::Determined(false), + }, + players_iter.next().unwrap(), + ), + (SetupRole::Hunter, players_iter.next().unwrap()), + ); + let mut settings = GameSettings::empty(); + settings.add_and_assign(werewolf.0, werewolf.1); + settings.add_and_assign(dire_wolf.0, dire_wolf.1); + settings.add_and_assign(shapeshifter.0, shapeshifter.1); + settings.add_and_assign(alpha_wolf.0, alpha_wolf.1); + settings.add_and_assign(seer.0, seer.1); + settings.add_and_assign(arcanist.0, arcanist.1); + settings.add_and_assign(maple_wolf.0, maple_wolf.1); + settings.add_and_assign(guardian.0, guardian.1); + settings.add_and_assign(vindicator.0, vindicator.1); + settings.add_and_assign(adjudicator.0, adjudicator.1); + settings.add_and_assign(power_seer.0, power_seer.1); + settings.add_and_assign(beholder.0, beholder.1); + settings.add_and_assign(gravedigger.0, gravedigger.1); + settings.add_and_assign(mortician.0, mortician.1); + settings.add_and_assign(insomniac.0, insomniac.1); + settings.add_and_assign(empath.0, empath.1); + settings.add_and_assign(scapegoat.0, scapegoat.1); + settings.add_and_assign(hunter.0, hunter.1); + settings.fill_remaining_slots_with_villagers(players.len()); + + let ( + werewolf, + dire_wolf, + shapeshifter, + alpha_wolf, + seer, + arcanist, + maple_wolf, + guardian, + vindicator, + adjudicator, + power_seer, + beholder, + gravedigger, + mortician, + insomniac, + empath, + scapegoat, + hunter, + ) = ( + werewolf.1, + dire_wolf.1, + shapeshifter.1, + alpha_wolf.1, + seer.1, + arcanist.1, + maple_wolf.1, + guardian.1, + vindicator.1, + adjudicator.1, + power_seer.1, + beholder.1, + gravedigger.1, + mortician.1, + insomniac.1, + empath.1, + scapegoat.1, + hunter.1, + ); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + game.next().title().wolves_intro(); + game.r#continue().r#continue(); + + game.next().title().direwolf(); + game.mark(game.character_by_player_id(seer).character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(seer).character_id()); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().role_blocked(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().power_seer(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(arcanist).character_id()); + game.r#continue().role_blocked(); + + game.next_expect_day(); + game.mark_for_execution(game.character_by_player_id(dire_wolf).character_id()); + game.mark_for_execution(game.character_by_player_id(alpha_wolf).character_id()); + + game.execute().title().guardian(); + let protect = game.living_villager(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(protect.character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.response(ActionResponse::Shapeshift).sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(seer).character_id()); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().arcanist(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(seer).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().power_seer(); + + game.next().title().gravedigger(); + game.mark(game.character_by_player_id(dire_wolf).character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf)); + + game.next().title().mortician(); + game.mark(game.character_by_player_id(dire_wolf).character_id()); + assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution); + + game.next().title().empath(); + game.mark(game.living_villager().character_id()); + assert!(!game.r#continue().empath()); + + game.next().title().maple_wolf(); + game.mark( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + game.r#continue().insomniac(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(power_seer).character_id()); + game.r#continue().power_seer(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(protect.player_id()).died_to(), + None + ); + + game.mark_for_execution( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.execute().title().guardian(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next().title().vindicator(); + game.mark( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(protect.character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().arcanist(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().power_seer(); + + game.next().title().gravedigger(); + game.mark(game.character_by_player_id(dire_wolf).character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf)); + + game.next().title().mortician(); + game.mark(game.character_by_player_id(dire_wolf).character_id()); + assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution); + + game.next().title().empath(); + game.mark(game.character_by_player_id(scapegoat).character_id()); + assert!(game.r#continue().empath()); + + game.next().title().maple_wolf(); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + game.r#continue().insomniac(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(power_seer).character_id()); + game.r#continue().power_seer(); + + game.next_expect_day(); + game.mark_for_execution( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.execute().title().vindicator(); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.response(ActionResponse::Shapeshift).r#continue(); + + game.next().title().role_change(); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + game.r#continue().arcanist(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().power_seer(); + + game.next().title().gravedigger(); + game.mark(game.character_by_player_id(guardian).character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian)); + + game.next().title().mortician(); + game.mark(game.character_by_player_id(guardian).character_id()); + assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack); + + game.next().title().maple_wolf(); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + game.r#continue().insomniac(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(gravedigger).character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian)); + + game.next_expect_day(); + game.mark_for_execution(game.character_by_player_id(vindicator).character_id()); + game.execute().title().wolf_pack_kill(); + game.mark(game.living_villager().character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().arcanist(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().power_seer(); + + game.next().title().gravedigger(); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + assert_eq!(game.r#continue().gravedigger(), None); + + game.next().title().mortician(); + game.mark(game.character_by_player_id(werewolf).character_id()); + assert_eq!( + game.r#continue().mortician(), + DiedToTitle::GuardianProtecting + ); + + game.next().title().maple_wolf(); + game.mark(game.character_by_player_id(hunter).character_id()); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + game.r#continue().insomniac(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(mortician).character_id()); + assert_eq!( + game.r#continue().mortician(), + DiedToTitle::GuardianProtecting + ); + + game.next_expect_game_over(); +} diff --git a/werewolves-proto/src/game_test/role/beholder.rs b/werewolves-proto/src/game_test/role/beholder.rs index 705aa28..034da7b 100644 --- a/werewolves-proto/src/game_test/role/beholder.rs +++ b/werewolves-proto/src/game_test/role/beholder.rs @@ -6,12 +6,14 @@ use crate::{ game::{Game, GameSettings, SetupRole}, game_test::{ ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players, + init_log, }, message::night::{ActionPrompt, ActionPromptTitle}, }; #[test] fn beholding_seer() { + init_log(); let players = gen_players(1..10); let seer_player_id = players[0].player_id; let wolf_player_id = players[1].player_id; diff --git a/werewolves-proto/src/game_test/role/guardian.rs b/werewolves-proto/src/game_test/role/guardian.rs new file mode 100644 index 0000000..1797301 --- /dev/null +++ b/werewolves-proto/src/game_test/role/guardian.rs @@ -0,0 +1,203 @@ +use core::num::NonZeroU8; + +use crate::{ + diedto::DiedTo, + error::GameError, + game::{Game, GameSettings, SetupRole}, + game_test::{ + ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log, + }, + message::{ + host::{HostGameMessage, HostNightMessage}, + night::{ActionPromptTitle, ActionResponse}, + }, + role::{PreviousGuardianAction, Role}, +}; + +#[test] +fn guard_kills_guardian_and_attacker() { + init_log(); + let players = gen_players(1..10); + let guardian = players[0].player_id; + let wolf = players[1].player_id; + let wolf2 = players[2].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Werewolf, wolf2); + settings.add_and_assign(SetupRole::Guardian, guardian); + settings.fill_remaining_slots_with_villagers(9); + 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 protect = game.living_villager(); + game.execute().title().guardian(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(protect.player_id()).died_to(), + None + ); + match game.character_by_player_id(guardian).role() { + Role::Guardian { + last_protected: Some(last_protected), + } => assert_eq!( + *last_protected, + PreviousGuardianAction::Protect(protect.identity()) + ), + Role::Guardian { + last_protected: None, + } => panic!("did not record protect"), + _ => panic!("not a guardian?"), + } + + game.execute().title().guardian(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + let killing_wolf_pid = game.village().killing_wolf().unwrap().player_id(); + + game.next_expect_day(); + + match game.character_by_player_id(guardian).role() { + Role::Guardian { + last_protected: Some(last_protected), + } => assert_eq!( + *last_protected, + PreviousGuardianAction::Guard(protect.identity()) + ), + Role::Guardian { + last_protected: None, + } => panic!("did not record protect"), + _ => panic!("not a guardian?"), + } + + assert_eq!( + game.character_by_player_id(guardian).died_to().cloned(), + Some(DiedTo::Wolfpack { + killing_wolf: game.character_by_player_id(killing_wolf_pid).character_id(), + night: NonZeroU8::new(2).unwrap() + }) + ); + + assert_eq!( + game.character_by_player_id(killing_wolf_pid) + .died_to() + .cloned(), + Some(DiedTo::GuardianProtecting { + source: game.character_by_player_id(guardian).character_id(), + protecting: protect.character_id(), + protecting_from: game.character_by_player_id(killing_wolf_pid).character_id(), + protecting_from_cause: Box::new(DiedTo::Wolfpack { + killing_wolf: game.character_by_player_id(killing_wolf_pid).character_id(), + night: NonZeroU8::new(2).unwrap() + }), + night: NonZeroU8::new(2).unwrap(), + }) + ); +} + +#[test] +fn cannot_visit_previous_nights_guard_target() { + init_log(); + let players = gen_players(1..10); + let guardian = players[0].player_id; + let wolf = players[1].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Guardian, guardian); + settings.fill_remaining_slots_with_villagers(9); + 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 protect = game.living_villager(); + game.execute().title().guardian(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(protect.player_id()).died_to(), + None + ); + match game.character_by_player_id(guardian).role() { + Role::Guardian { + last_protected: Some(last_protected), + } => assert_eq!( + *last_protected, + PreviousGuardianAction::Protect(protect.identity()) + ), + Role::Guardian { + last_protected: None, + } => panic!("did not record protect"), + _ => panic!("not a guardian?"), + } + + game.execute().title().guardian(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + let killing_wolf_pid = game.village().killing_wolf().unwrap().player_id(); + game.next_expect_day(); + + match game.character_by_player_id(guardian).role() { + Role::Guardian { + last_protected: Some(last_protected), + } => assert_eq!( + *last_protected, + PreviousGuardianAction::Guard(protect.identity()) + ), + Role::Guardian { + last_protected: None, + } => panic!("did not record protect"), + _ => panic!("not a guardian?"), + } + + assert_eq!( + game.character_by_player_id(guardian).died_to().cloned(), + None, + ); + + assert_eq!( + game.character_by_player_id(killing_wolf_pid) + .died_to() + .cloned(), + None + ); + + game.execute().title().guardian(); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::MarkTarget(protect.character_id()) + ))), + Err(GameError::InvalidTarget) + ); +} diff --git a/werewolves-proto/src/game_test/role/hunter.rs b/werewolves-proto/src/game_test/role/hunter.rs new file mode 100644 index 0000000..a8ed167 --- /dev/null +++ b/werewolves-proto/src/game_test/role/hunter.rs @@ -0,0 +1,77 @@ +use core::num::NonZeroU8; + +use crate::{ + diedto::DiedTo, + error::GameError, + game::{Game, GameSettings, SetupRole}, + game_test::{ + ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log, + }, + message::{ + host::{HostGameMessage, HostNightMessage}, + night::{ActionPromptTitle, ActionResponse}, + }, + role::{PreviousGuardianAction, Role}, +}; + +#[test] +fn can_change_targets() { + init_log(); + let players = gen_players(1..10); + let hunter = players[0].player_id; + let wolf = players[1].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Hunter, hunter); + settings.fill_remaining_slots_with_villagers(9); + 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 hunt_target = game.living_villager(); + game.execute().title().wolf_pack_kill(); + game.mark( + game.living_villager_excl(hunt_target.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(hunt_target.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + match game.character_by_player_id(hunter).role() { + Role::Hunter { + target: Some(target), + } => assert_eq!(*target, hunt_target.character_id()), + Role::Hunter { target: None } => panic!("did not record target"), + _ => panic!("not a guardian?"), + } + + let hunt_target = game.living_villager_excl(hunt_target.player_id()); + + game.execute().title().wolf_pack_kill(); + game.mark( + game.living_villager_excl(hunt_target.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(hunt_target.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + match game.character_by_player_id(hunter).role() { + Role::Hunter { + target: Some(target), + } => assert_eq!(*target, hunt_target.character_id()), + Role::Hunter { target: None } => panic!("did not record target"), + _ => panic!("not a guardian?"), + } +} diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs index f2aa815..890901d 100644 --- a/werewolves-proto/src/game_test/role/mod.rs +++ b/werewolves-proto/src/game_test/role/mod.rs @@ -3,6 +3,8 @@ mod black_knight; mod diseased; mod elder; mod empath; +mod guardian; +mod hunter; mod insomniac; mod lone_wolf; mod mason; diff --git a/werewolves-proto/src/game_test/role/scapegoat.rs b/werewolves-proto/src/game_test/role/scapegoat.rs index a8157c4..9a16e6c 100644 --- a/werewolves-proto/src/game_test/role/scapegoat.rs +++ b/werewolves-proto/src/game_test/role/scapegoat.rs @@ -2,7 +2,7 @@ use core::num::NonZero; use crate::{ diedto::DiedTo, - game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange}, + game::{Game, GameSettings, OrRandom, SetupRole}, game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, message::night::{ActionPrompt, ActionPromptTitle}, player::RoleChange, diff --git a/werewolves-proto/src/message/host.rs b/werewolves-proto/src/message/host.rs index fe983ba..bd99689 100644 --- a/werewolves-proto/src/message/host.rs +++ b/werewolves-proto/src/message/host.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::{ character::CharacterId, error::GameError, - game::{GameOver, GameSettings}, + game::{GameOver, GameSettings, story::GameStory}, message::{ CharacterIdentity, night::{ActionPrompt, ActionResponse, ActionResult}, @@ -21,10 +21,17 @@ pub enum HostMessage { Lobby(HostLobbyMessage), InGame(HostGameMessage), ForceRoleAckFor(CharacterId), - NewLobby, + PostGame(PostGameMessage), Echo(ServerToHostMessage), } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum PostGameMessage { + NewLobby, + NextPage, + PreviousPage, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum HostGameMessage { Day(HostDayMessage), @@ -84,4 +91,8 @@ pub enum ServerToHostMessage { ackd: Box<[CharacterIdentity]>, waiting: Box<[CharacterIdentity]>, }, + Story { + story: GameStory, + page: usize, + }, } diff --git a/werewolves-proto/src/message/ident.rs b/werewolves-proto/src/message/ident.rs index 7b35b4d..33ba897 100644 --- a/werewolves-proto/src/message/ident.rs +++ b/werewolves-proto/src/message/ident.rs @@ -24,14 +24,19 @@ pub struct CharacterIdentity { pub pronouns: Option, pub number: NonZeroU8, } +impl CharacterIdentity { + pub fn into_public(self) -> PublicIdentity { + PublicIdentity { + name: self.name, + pronouns: self.pronouns, + number: Some(self.number), + } + } +} impl From for PublicIdentity { fn from(c: CharacterIdentity) -> Self { - Self { - name: c.name, - pronouns: c.pronouns, - number: Some(c.number), - } + c.into_public() } } diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index df725b8..cba53be 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -8,7 +8,7 @@ use crate::{ diedto::DiedToTitle, error::GameError, message::CharacterIdentity, - role::{Alignment, PreviousGuardianAction, RoleTitle}, + role::{Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleTitle}, }; type Result = core::result::Result; @@ -435,9 +435,9 @@ pub enum ActionResponse { pub enum ActionResult { RoleBlocked, Seer(Alignment), - PowerSeer { powerful: bool }, - Adjudicator { killer: bool }, - Arcanist { same: bool }, + PowerSeer { powerful: Powerful }, + Adjudicator { killer: Killer }, + Arcanist(AlignmentEq), GraveDigger(Option), Mortician(DiedToTitle), Insomniac(Visits), diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index bc68efe..e0cd2f9 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -1,4 +1,8 @@ -use core::num::NonZeroU8; +use core::{ + fmt::Display, + num::NonZeroU8, + ops::{Deref, Not}, +}; use serde::{Deserialize, Serialize}; use werewolves_macros::{ChecksAs, Titles}; @@ -6,107 +10,219 @@ use werewolves_macros::{ChecksAs, Titles}; use crate::{ character::CharacterId, diedto::DiedTo, - game::{DateTime, Village}, + game::{GameTime, Village}, message::CharacterIdentity, }; +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)] +pub enum Killer { + Killer, + #[default] + NotKiller, +} + +impl Killer { + pub const fn killer(&self) -> bool { + matches!(self, Killer::Killer) + } +} + +impl Display for Killer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Killer::Killer => f.write_str("Killer"), + Killer::NotKiller => f.write_str("Not a Killer"), + } + } +} + +impl Not for Killer { + type Output = Killer; + + fn not(self) -> Self::Output { + match self { + Killer::Killer => Killer::NotKiller, + Killer::NotKiller => Killer::Killer, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)] +pub enum Powerful { + Powerful, + #[default] + NotPowerful, +} + +impl Powerful { + pub const fn powerful(&self) -> bool { + matches!(self, Powerful::Powerful) + } +} + +impl Display for Powerful { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Powerful::Powerful => f.write_str("Powerful"), + Powerful::NotPowerful => f.write_str("Not Powerful"), + } + } +} + +impl Not for Powerful { + type Output = Powerful; + + fn not(self) -> Self::Output { + match self { + Powerful::Powerful => Powerful::NotPowerful, + Powerful::NotPowerful => Powerful::Powerful, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub enum AlignmentEq { + Same, + Different, +} +impl Not for AlignmentEq { + type Output = AlignmentEq; + + fn not(self) -> Self::Output { + match self { + AlignmentEq::Same => Self::Different, + AlignmentEq::Different => Self::Same, + } + } +} +impl AlignmentEq { + pub const fn new(same: bool) -> Self { + match same { + true => Self::Same, + false => Self::Different, + } + } + pub const fn same(&self) -> bool { + matches!(self, Self::Same) + } +} + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)] pub enum Role { #[checks(Alignment::Village)] + #[checks(Killer::NotKiller)] + #[checks(Powerful::NotPowerful)] Villager, #[checks(Alignment::Wolves)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] Scapegoat { redeemed: bool }, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Seer, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Arcanist, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Adjudicator, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] PowerSeer, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Mortician, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Beholder, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] MasonLeader { recruits_available: u8, recruits: Box<[CharacterId]>, }, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] Empath { cursed: bool }, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Vindicator, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Diseased, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] BlackKnight { attacked: Option }, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Weightlifter, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::Killer)] #[checks("is_mentor")] PyreMaster { villagers_killed: u8 }, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Gravedigger, #[checks(Alignment::Village)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] #[checks("is_mentor")] #[checks] Hunter { target: Option }, #[checks(Alignment::Village)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] #[checks("is_mentor")] Militia { targeted: Option }, #[checks(Alignment::Wolves)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] #[checks("is_mentor")] MapleWolf { last_kill_on_night: u8 }, #[checks(Alignment::Village)] - #[checks("powerful")] - #[checks("killer")] + #[checks(Powerful::Powerful)] + #[checks(Killer::Killer)] #[checks("is_mentor")] Guardian { last_protected: Option, }, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Protector { last_protected: Option }, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] Apprentice(RoleTitle), #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] #[checks("is_mentor")] Elder { knows_on_night: NonZeroU8, @@ -114,32 +230,33 @@ pub enum Role { lost_protection_night: Option, }, #[checks(Alignment::Village)] - #[checks("powerful")] + #[checks(Powerful::Powerful)] + #[checks(Killer::NotKiller)] Insomniac, #[checks(Alignment::Wolves)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] #[checks("wolf")] Werewolf, #[checks(Alignment::Wolves)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] #[checks("wolf")] AlphaWolf { killed: Option }, #[checks(Alignment::Village)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] #[checks("wolf")] DireWolf, #[checks(Alignment::Wolves)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] #[checks("wolf")] Shapeshifter { shifted_into: Option }, #[checks(Alignment::Wolves)] - #[checks("killer")] - #[checks("powerful")] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] #[checks("wolf")] LoneWolf, } @@ -153,6 +270,41 @@ impl Role { } } + pub const fn killing_wolf_order(&self) -> Option { + Some(match self { + Role::Villager + | Role::Scapegoat { .. } + | Role::Seer + | Role::Arcanist + | Role::Adjudicator + | Role::PowerSeer + | Role::Mortician + | Role::Beholder + | Role::MasonLeader { .. } + | Role::Empath { .. } + | Role::Vindicator + | Role::Diseased + | Role::BlackKnight { .. } + | Role::Weightlifter + | Role::PyreMaster { .. } + | Role::Gravedigger + | Role::Hunter { .. } + | Role::Militia { .. } + | Role::MapleWolf { .. } + | Role::Guardian { .. } + | Role::Protector { .. } + | Role::Apprentice(..) + | Role::Elder { .. } + | Role::Insomniac => return None, + + Role::Werewolf => KillingWolfOrder::Werewolf, + Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf, + Role::DireWolf => KillingWolfOrder::DireWolf, + Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter, + Role::LoneWolf => KillingWolfOrder::LoneWolf, + }) + } + pub const fn wakes_night_zero(&self) -> bool { match self { Role::Insomniac @@ -189,9 +341,9 @@ impl Role { } pub fn wakes(&self, village: &Village) -> bool { - let night_zero = match village.date_time() { - DateTime::Day { number: _ } => return false, - DateTime::Night { number } => number == 0, + let night_zero = match village.time() { + GameTime::Day { number: _ } => return false, + GameTime::Night { number } => number == 0, }; if night_zero { return self.wakes_night_zero(); @@ -205,9 +357,9 @@ impl Role { | Role::BlackKnight { .. } | Role::Villager => false, - Role::LoneWolf => match village.date_time() { - DateTime::Day { number: _ } => return false, - DateTime::Night { number } => NonZeroU8::new(number), + Role::LoneWolf => match village.time() { + GameTime::Day { number: _ } => return false, + GameTime::Night { number } => NonZeroU8::new(number), } .map(|night| village.executions_on_day(night)) .map(|execs| execs.iter().any(|e| e.is_wolf())) @@ -247,8 +399,8 @@ impl Role { .. } => { !woken_for_reveal - && match village.date_time() { - DateTime::Night { number } => number == knows_on_night.get(), + && match village.time() { + GameTime::Night { number } => number == knows_on_night.get(), _ => false, } } @@ -256,12 +408,21 @@ impl Role { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum Alignment { Village, Wolves, } +impl Display for Alignment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Alignment::Village => f.write_str("Village"), + Alignment::Wolves => f.write_str("Wolves"), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, ChecksAs)] pub enum ArcanistCheck { #[checks] @@ -283,3 +444,16 @@ pub enum PreviousGuardianAction { Protect(CharacterIdentity), Guard(CharacterIdentity), } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum KillingWolfOrder { + Werewolf, + AlphaWolf, + Shapeshifter, + Berserker, + Psion, + Bloodletter, + Bloodhound, + DireWolf, + LoneWolf, +} diff --git a/werewolves-server/Cargo.toml b/werewolves-server/Cargo.toml index 649aded..fdc3e8b 100644 --- a/werewolves-server/Cargo.toml +++ b/werewolves-server/Cargo.toml @@ -24,6 +24,8 @@ thiserror = { version = "2" } ciborium = { version = "0.2", optional = true } colored = { version = "3.0" } fast_qr = { version = "0.13", features = ["svg"] } +ron = "0.8" + [features] default = ["cbor"] diff --git a/werewolves-server/src/communication/player.rs b/werewolves-server/src/communication/player.rs index 2f22c46..06854a8 100644 --- a/werewolves-server/src/communication/player.rs +++ b/werewolves-server/src/communication/player.rs @@ -5,22 +5,12 @@ use werewolves_proto::error::GameError; use crate::runner::IdentifiedClientMessage; pub struct PlayerIdComms { - // joined_players: JoinedPlayers, message_recv: Receiver, - // connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>, } impl PlayerIdComms { - pub fn new( - // joined_players: JoinedPlayers, - message_recv: Receiver, - // connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>, - ) -> Self { - Self { - // joined_players, - message_recv, - // connect_recv, - } + pub fn new(message_recv: Receiver) -> Self { + Self { message_recv } } pub async fn recv(&mut self) -> Result { diff --git a/werewolves-server/src/game.rs b/werewolves-server/src/game.rs index da0f469..299f34b 100644 --- a/werewolves-server/src/game.rs +++ b/werewolves-server/src/game.rs @@ -15,7 +15,7 @@ use werewolves_proto::{ game::{Game, GameOver, Village}, message::{ ClientMessage, Identification, ServerMessage, - host::{HostGameMessage, HostMessage, ServerToHostMessage}, + host::{HostGameMessage, HostMessage, PostGameMessage, ServerToHostMessage}, }, player::PlayerId, }; @@ -58,6 +58,7 @@ impl GameRunner { self.joined_players, LobbyComms::new(self.comms, self.connect_recv), ); + lobby.set_settings(self.game.village().settings()); lobby.set_players_in_lobby(self.player_sender); lobby } @@ -268,7 +269,7 @@ impl GameRunner { match message { HostMessage::GetState => self.game.process(HostGameMessage::GetState), HostMessage::InGame(msg) => self.game.process(msg), - HostMessage::Lobby(_) | HostMessage::NewLobby | HostMessage::ForceRoleAckFor(_) => { + HostMessage::Lobby(_) | HostMessage::PostGame(_) | HostMessage::ForceRoleAckFor(_) => { Err(GameError::InvalidMessageForGameState) } HostMessage::Echo(echo) => Ok(echo), @@ -278,6 +279,7 @@ impl GameRunner { pub struct GameEnd { game: Option, + page: Option, result: GameOver, last_error_log: Instant, } @@ -286,6 +288,7 @@ impl GameEnd { pub fn new(game: GameRunner, result: GameOver) -> Self { Self { result, + page: None, game: Some(game), last_error_log: Instant::now() - core::time::Duration::from_secs(60), } @@ -310,32 +313,65 @@ impl GameEnd { } }; - match msg { + self.process(msg) + } + + fn process(&mut self, message: Message) -> Option { + match message { Message::Host(HostMessage::Echo(msg)) => { self.game().unwrap().comms.host().send(msg).log_debug(); } Message::Host(HostMessage::GetState) => { - let result = self.result; - self.game() - .unwrap() - .comms - .host() - .send(ServerToHostMessage::GameOver(result)) - .log_debug() + if let Some(page) = self.page { + let story = self.game().ok()?.game.story(); + self.game() + .ok()? + .comms + .host() + .send(ServerToHostMessage::Story { story, page }) + .log_debug(); + } else { + let result = self.result; + self.game() + .ok()? + .comms + .host() + .send(ServerToHostMessage::GameOver(result)) + .log_debug(); + } } - Message::Host(HostMessage::NewLobby) => { + Message::Host(HostMessage::PostGame(PostGameMessage::PreviousPage)) => { + if let Some(page) = self.page.as_mut() { + if *page == 0 { + self.page = None; + } else { + *page -= 1; + } + } + return self.process(Message::Host(HostMessage::GetState)); + } + Message::Host(HostMessage::PostGame(PostGameMessage::NextPage)) => { + if let Some(page) = self.page.as_mut() { + *page += 1; + } else { + self.page = Some(0); + } + return self.process(Message::Host(HostMessage::GetState)); + } + Message::Host(HostMessage::PostGame(PostGameMessage::NewLobby)) => { self.game() - .unwrap() + .ok()? .comms .host() .send(ServerToHostMessage::Lobby(Box::new([]))) .log_debug(); - let lobby = self.game.take().unwrap().into_lobby(); + let lobby = self.game.take()?.into_lobby(); return Some(lobby); } + Message::Host(_) => self .game() - .unwrap() + .ok()? .comms .host() .send(ServerToHostMessage::Error( @@ -348,7 +384,7 @@ impl GameEnd { }) => { let result = self.result; self.game() - .unwrap() + .ok()? .player_sender .send_if_present(identity.player_id, ServerMessage::GameOver(result)) .log_debug(); diff --git a/werewolves-server/src/lobby.rs b/werewolves-server/src/lobby.rs index c5a4cd7..a5c20ac 100644 --- a/werewolves-server/src/lobby.rs +++ b/werewolves-server/src/lobby.rs @@ -3,7 +3,7 @@ use core::{ ops::{Deref, DerefMut}, }; -use tokio::sync::broadcast::{self, Sender}; +use tokio::sync::broadcast::Sender; use werewolves_proto::{ error::GameError, game::{Game, GameSettings}, @@ -41,6 +41,16 @@ impl Lobby { } } + pub fn set_settings(&mut self, settings: GameSettings) { + self.settings = settings.clone(); + if let Ok(comms) = self.comms() { + comms + .host() + .send(ServerToHostMessage::GameSettings(settings)) + .log_debug(); + } + } + pub fn set_players_in_lobby(&mut self, players_in_lobby: LobbyPlayers) { self.players_in_lobby = players_in_lobby } @@ -175,7 +185,7 @@ impl Lobby { | Message::Host(HostMessage::ForceRoleAckFor(_)) => { return Err(GameError::InvalidMessageForGameState); } - Message::Host(HostMessage::NewLobby) => self + Message::Host(HostMessage::PostGame(_)) => self .comms() .unwrap() .host() diff --git a/werewolves-server/src/saver.rs b/werewolves-server/src/saver.rs index a06fd18..889149e 100644 --- a/werewolves-server/src/saver.rs +++ b/werewolves-server/src/saver.rs @@ -14,8 +14,8 @@ pub trait Saver: Clone + Send + 'static { pub enum FileSaverError { #[error("io error: {0}")] IoError(std::io::Error), - #[error("serialization error")] - SerializationError(#[from] serde_json::Error), + #[error("serialization error: {0}")] + SerializationError(#[from] ron::Error), } impl From for FileSaverError { @@ -39,10 +39,11 @@ impl Saver for FileSaver { type Error = FileSaverError; fn save(&mut self, game: &Game) -> Result { - let name = format!("werewolves_{}.json", chrono::Utc::now().timestamp()); + let name = format!("werewolves_{}.ron", chrono::Utc::now().timestamp()); let path = self.path.join(name.clone()); let mut file = std::fs::File::create_new(path.clone())?; - serde_json::to_writer_pretty(&mut file, &game)?; + // serde_json::to_writer_pretty(&mut file, &game)?; + ron::ser::to_writer_pretty(&mut file, &game.story(), ron::ser::PrettyConfig::new())?; file.flush()?; Ok(path.to_str().map(|s| s.to_string()).unwrap_or(name)) } diff --git a/werewolves/Trunk.toml b/werewolves/Trunk.toml index fa56fef..105927b 100644 --- a/werewolves/Trunk.toml +++ b/werewolves/Trunk.toml @@ -9,5 +9,6 @@ inject_scripts = true # Whether to inject scripts (and module preloads) in offline = false # Run without network access frozen = false # Require Cargo.lock and cache are up to date locked = false # Require Cargo.lock is up to date +# minify = "on_release" # Control minification: can be one of: never, on_release, always minify = "always" # Control minification: can be one of: never, on_release, always no_sri = false # Allow disabling sub-resource integrity (SRI) diff --git a/werewolves/img/gravedigger.svg b/werewolves/img/gravedigger.svg new file mode 100644 index 0000000..a2a5937 --- /dev/null +++ b/werewolves/img/gravedigger.svg @@ -0,0 +1,53 @@ + + + + diff --git a/werewolves/img/heart.svg b/werewolves/img/heart.svg new file mode 100644 index 0000000..4ee9d84 --- /dev/null +++ b/werewolves/img/heart.svg @@ -0,0 +1,98 @@ + + + + diff --git a/werewolves/img/hunter.svg b/werewolves/img/hunter.svg new file mode 100644 index 0000000..efb905b --- /dev/null +++ b/werewolves/img/hunter.svg @@ -0,0 +1,144 @@ + + + + diff --git a/werewolves/img/li.svg b/werewolves/img/li.svg new file mode 100644 index 0000000..b68b40d --- /dev/null +++ b/werewolves/img/li.svg @@ -0,0 +1,56 @@ + + + + diff --git a/werewolves/img/maple-wolf.svg b/werewolves/img/maple-wolf.svg new file mode 100644 index 0000000..1405456 --- /dev/null +++ b/werewolves/img/maple-wolf.svg @@ -0,0 +1,130 @@ + + + + diff --git a/werewolves/img/moon.svg b/werewolves/img/moon.svg new file mode 100644 index 0000000..64b2cf8 --- /dev/null +++ b/werewolves/img/moon.svg @@ -0,0 +1,80 @@ + + + + diff --git a/werewolves/img/power-seer.svg b/werewolves/img/power-seer.svg new file mode 100644 index 0000000..05a9ab5 --- /dev/null +++ b/werewolves/img/power-seer.svg @@ -0,0 +1,65 @@ + + + + diff --git a/werewolves/img/seer.svg b/werewolves/img/seer.svg new file mode 100644 index 0000000..17b248d --- /dev/null +++ b/werewolves/img/seer.svg @@ -0,0 +1,50 @@ + + + + diff --git a/werewolves/img/shield-and-sword.svg b/werewolves/img/shield-and-sword.svg new file mode 100644 index 0000000..5434c7a --- /dev/null +++ b/werewolves/img/shield-and-sword.svg @@ -0,0 +1,74 @@ + + + + diff --git a/werewolves/img/shield.svg b/werewolves/img/shield.svg new file mode 100644 index 0000000..fcdbb54 --- /dev/null +++ b/werewolves/img/shield.svg @@ -0,0 +1,71 @@ + + + + diff --git a/werewolves/img/skull.svg b/werewolves/img/skull.svg new file mode 100644 index 0000000..b0598e9 --- /dev/null +++ b/werewolves/img/skull.svg @@ -0,0 +1,72 @@ + + + + diff --git a/werewolves/index.scss b/werewolves/index.scss index 6681d10..00cb29d 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -18,6 +18,13 @@ $offensive_border: color.change($offensive_color, $alpha: 1.0); $starts_as_villager_color: color.adjust($offensive_color, $hue: 30deg); $starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0); +$wolves_border_faint: color.change($wolves_border, $alpha: 0.3); +$village_border_faint: color.change($village_border, $alpha: 0.3); +$offensive_border_faint: color.change($offensive_border, $alpha: 0.3); +$defensive_border_faint: color.change($defensive_border, $alpha: 0.3); +$intel_border_faint: color.change($intel_border, $alpha: 0.3); +$starts_as_villager_border_faint: color.change($starts_as_villager_border, $alpha: 0.3); + @mixin flexbox() { display: -webkit-box; @@ -350,8 +357,6 @@ button { .role { font-size: 2rem; } - - } h1, @@ -1042,6 +1047,10 @@ input { color: white; background-color: $wolves_border; } + + &.faint { + border: 1px solid $wolves_border_faint; + } } .intel { @@ -1052,6 +1061,10 @@ input { color: white; background-color: $intel_border; } + + &.faint { + border: 1px solid $intel_border_faint; + } } .defensive { @@ -1062,6 +1075,10 @@ input { color: white; background-color: $defensive_border; } + + &.faint { + border: 1px solid $defensive_border_faint; + } } .offensive { @@ -1072,6 +1089,10 @@ input { color: white; background-color: $offensive_border; } + + &.faint { + border: 1px solid $offensive_border_faint; + } } .starts-as-villager { @@ -1082,6 +1103,10 @@ input { color: white; background-color: $starts_as_villager_border; } + + &.faint { + border: 1px solid $starts_as_villager_border_faint; + } } .assignments { @@ -1176,15 +1201,6 @@ input { flex-direction: row; flex-wrap: nowrap; gap: 10px; - - .icon { - width: 32px; - height: 32px; - - &:hover { - filter: contrast(120%) brightness(120%); - } - } } .role { @@ -1202,6 +1218,15 @@ input { } } +.icon { + width: 32px; + height: 32px; + + &:hover { + filter: contrast(120%) brightness(120%); + } +} + .inactive { filter: grayscale(100%) brightness(30%); } @@ -1346,3 +1371,227 @@ input { font-size: 1rem !important; } } + + +.story { + user-select: text; + + .time-period { + .day { + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: center; + + .executed { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 10px; + + &>span { + display: flex; + flex-direction: row; + align-items: baseline; + } + } + } + + .night { + + ul.changes, + ul.choices { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 10px; + + &>li { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + + gap: 10px; + } + + + & span { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: baseline; + gap: 10px; + } + } + } + } +} + +.attribute-span { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: baseline; + align-content: baseline; + justify-content: baseline; + justify-items: baseline; + + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 10px; + + &:has(.killer) { + border: 1px solid rgba(212, 85, 0, 0.5); + } + + &:has(.powerful) { + border: 1px solid rgba(0, 173, 193, 0.5); + } + + &:has(.inactive) { + border: 1px solid rgba(255, 255, 255, 0.3); + } + + img { + vertical-align: sub; + } + + &.execution { + border: 1px solid rgba(255, 255, 255, 0.3); + + } + + &.wolves { + background-color: color.change($wolves_color, $alpha: 0.1); + } + + &.intel { + background-color: color.change($intel_color, $alpha: 0.1); + } + + &.defensive { + background-color: color.change($defensive_color, $alpha: 0.1); + } + + &.offensive { + background-color: color.change($offensive_color, $alpha: 0.1); + } + + &.village { + background-color: color.change($village_color, $alpha: 0.1); + } + + &.starts-as-villager { + background-color: color.change($starts_as_villager_color, $alpha: 0.1); + } +} + +.alignment-eq { + img { + vertical-align: sub; + } + + border: 1px solid $intel_border; + background-color: color.change($intel_color, $alpha: 0.1); + padding-top: 5px; + padding-bottom: 5px; + + padding-left: 10px; + padding-right: 10px; +} + +.character-span { + height: max-content; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: baseline; + align-content: baseline; + justify-content: baseline; + justify-items: baseline; + gap: 5px; + + .role { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: baseline; + + img { + vertical-align: sub; + } + + padding-top: 5px; + padding-bottom: 5px; + + padding-left: 10px; + padding-right: 10px; + height: 100%; + border: none; + gap: 5px; + + &.wolves { + background-color: color.change($wolves_color, $alpha: 0.1); + } + + &.intel { + background-color: color.change($intel_color, $alpha: 0.1); + } + + &.defensive { + background-color: color.change($defensive_color, $alpha: 0.1); + } + + &.offensive { + background-color: color.change($offensive_color, $alpha: 0.1); + } + + &.village { + background-color: color.change($village_color, $alpha: 0.1); + } + } + + padding-right: 10px; + + &.wolves, + &.intel, + &.offensive, + &.defensive, + &.village { + background-color: inherit; + } + + &.wolves { + border: 1px solid $wolves_border_faint; + } + + &.intel { + border: 1px solid $intel_border_faint; + } + + &.defensive { + border: 1px solid $defensive_border_faint; + } + + &.offensive { + border: 1px solid $offensive_border_faint; + } + + &.village { + border: 1px solid $village_border_faint; + } + + .icon { + margin: 0; + } +} + + +.post-game { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + width: 100%; +} diff --git a/werewolves/src/clients/host/host.rs b/werewolves/src/clients/host/host.rs index 5fbb7e4..166efea 100644 --- a/werewolves/src/clients/host/host.rs +++ b/werewolves/src/clients/host/host.rs @@ -11,12 +11,12 @@ use serde::Serialize; use werewolves_proto::{ character::CharacterId, error::GameError, - game::{GameOver, GameSettings}, + game::{GameOver, GameSettings, story::GameStory}, message::{ CharacterIdentity, CharacterState, PlayerState, PublicIdentity, host::{ HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage, - ServerToHostMessage, + PostGameMessage, ServerToHostMessage, }, night::{ActionPrompt, ActionResult}, }, @@ -27,7 +27,7 @@ use yew::{html::Scope, prelude::*}; use crate::{ callback, components::{ - Button, CoverOfDarkness, Lobby, LobbyPlayerAction, RoleReveal, Settings, + Button, CoverOfDarkness, Lobby, LobbyPlayerAction, RoleReveal, Settings, Story, action::{ActionResultView, Prompt}, host::{DaytimePlayerList, Setup}, }, @@ -208,6 +208,10 @@ pub enum HostState { }, Prompt(ActionPrompt, usize), Result(Option, ActionResult), + Story { + story: GameStory, + page: usize, + }, } impl From for HostEvent { @@ -242,6 +246,10 @@ impl From for HostEvent { ServerToHostMessage::WaitingForRoleRevealAcks { ackd, waiting } => { HostEvent::SetState(HostState::RoleReveal { ackd, waiting }) } + ServerToHostMessage::Story { story, page } => { + log::info!("story page: {page}"); + HostEvent::SetState(HostState::Story { story, page }) + } } } } @@ -289,9 +297,24 @@ impl Component for Host { fn view(&self, _ctx: &Context) -> Html { log::trace!("state: {:?}", self.state); let content = match self.state.clone() { + HostState::Story { story, page } => { + let new_lobby_click = crate::callback::send_message( + HostMessage::PostGame(PostGameMessage::NewLobby), + self.send.clone(), + ); + html! { +
+ + +
+ } + } HostState::GameOver { result } => { - let new_lobby = self.big_screen.not().then(|| { - crate::callback::send_message(HostMessage::NewLobby, self.send.clone()) + let cont = self.big_screen.not().then(|| { + crate::callback::send_message( + HostMessage::PostGame(PostGameMessage::NextPage), + self.send.clone(), + ) }); html! { @@ -300,9 +323,9 @@ impl Component for Host { GameOver::VillageWins => "village wins", GameOver::WolvesWin => "wolves win", }} - next={new_lobby} + next={cont} > - {"new lobby"} + {"continue"} } } @@ -449,12 +472,20 @@ impl Component for Host { } }; + let s = _ctx.link().clone(); + let story_on_click = Callback::from(move |_| { + s.send_message(HostEvent::SetState(HostState::Story { + story: crate::clients::host::story_test::test_story(), + page: 0, + })); + }); html! { } }); @@ -468,7 +499,7 @@ impl Component for Host { } } - fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { match msg { HostEvent::QrMode(mode) => { self.qr_mode = mode; @@ -488,7 +519,9 @@ impl Component for Host { players: p, settings: _, } => *p = players.into_iter().collect(), - HostState::Disconnected | HostState::GameOver { result: _ } => { + HostState::Story { .. } + | HostState::Disconnected + | HostState::GameOver { .. } => { let mut send = self.send.clone(); let on_err = self.error_callback.clone(); @@ -531,7 +564,8 @@ impl Component for Host { *s = settings; true } - HostState::Prompt(_, _) + HostState::Story { .. } + | HostState::Prompt(_, _) | HostState::Result(_, _) | HostState::Disconnected | HostState::RoleReveal { diff --git a/werewolves/src/clients/host/story_test.rs b/werewolves/src/clients/host/story_test.rs new file mode 100644 index 0000000..811b82e --- /dev/null +++ b/werewolves/src/clients/host/story_test.rs @@ -0,0 +1,931 @@ +use core::num::NonZeroU8; + +use werewolves_proto::{ + character::{Character, CharacterId}, + diedto::{DiedTo, DiedToTitle}, + game::{self, Game, GameOver, GameSettings, OrRandom, SetupRole, Village, story::GameStory}, + message::{ + CharacterState, Identification, PublicIdentity, + host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, + night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits}, + }, + player::PlayerId, + role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle}, +}; + +trait GameSettingsExt { + fn add_and_assign(&mut self, role: SetupRole, player_id: PlayerId); +} + +impl GameSettingsExt for GameSettings { + fn add_and_assign(&mut self, role: SetupRole, player_id: PlayerId) { + let slot_id = self.new_slot(role.clone().into()); + let mut slot = self.get_slot_by_id(slot_id).unwrap().clone(); + slot.role = role; + slot.assign_to.replace(player_id); + self.update_slot(slot); + } +} + +pub fn test_story() -> GameStory { + let players = (1..32u8) + .filter_map(NonZeroU8::new) + .map(|n| Identification { + player_id: PlayerId::from_u128(n.get() as _), + public: PublicIdentity { + name: format!("Player {n}"), + pronouns: Some("he/him".into()), + number: Some(n), + }, + }) + .collect::>(); + let mut players_iter = players.iter().map(|p| p.player_id); + let ( + werewolf, + dire_wolf, + shapeshifter, + alpha_wolf, + seer, + arcanist, + maple_wolf, + guardian, + vindicator, + adjudicator, + power_seer, + beholder, + gravedigger, + mortician, + insomniac, + empath, + scapegoat, + hunter, + ) = ( + (SetupRole::Werewolf, players_iter.next().unwrap()), + (SetupRole::DireWolf, players_iter.next().unwrap()), + (SetupRole::Shapeshifter, players_iter.next().unwrap()), + (SetupRole::AlphaWolf, players_iter.next().unwrap()), + (SetupRole::Seer, players_iter.next().unwrap()), + (SetupRole::Arcanist, players_iter.next().unwrap()), + (SetupRole::MapleWolf, players_iter.next().unwrap()), + (SetupRole::Guardian, players_iter.next().unwrap()), + (SetupRole::Vindicator, players_iter.next().unwrap()), + (SetupRole::Adjudicator, players_iter.next().unwrap()), + (SetupRole::PowerSeer, players_iter.next().unwrap()), + (SetupRole::Beholder, players_iter.next().unwrap()), + (SetupRole::Gravedigger, players_iter.next().unwrap()), + (SetupRole::Mortician, players_iter.next().unwrap()), + (SetupRole::Insomniac, players_iter.next().unwrap()), + (SetupRole::Empath, players_iter.next().unwrap()), + ( + SetupRole::Scapegoat { + redeemed: OrRandom::Determined(false), + }, + players_iter.next().unwrap(), + ), + (SetupRole::Hunter, players_iter.next().unwrap()), + ); + let mut settings = GameSettings::empty(); + settings.add_and_assign(werewolf.0, werewolf.1); + settings.add_and_assign(dire_wolf.0, dire_wolf.1); + settings.add_and_assign(shapeshifter.0, shapeshifter.1); + settings.add_and_assign(alpha_wolf.0, alpha_wolf.1); + settings.add_and_assign(seer.0, seer.1); + settings.add_and_assign(arcanist.0, arcanist.1); + settings.add_and_assign(maple_wolf.0, maple_wolf.1); + settings.add_and_assign(guardian.0, guardian.1); + settings.add_and_assign(vindicator.0, vindicator.1); + settings.add_and_assign(adjudicator.0, adjudicator.1); + settings.add_and_assign(power_seer.0, power_seer.1); + settings.add_and_assign(beholder.0, beholder.1); + settings.add_and_assign(gravedigger.0, gravedigger.1); + settings.add_and_assign(mortician.0, mortician.1); + settings.add_and_assign(insomniac.0, insomniac.1); + settings.add_and_assign(empath.0, empath.1); + settings.add_and_assign(scapegoat.0, scapegoat.1); + settings.add_and_assign(hunter.0, hunter.1); + settings.fill_remaining_slots_with_villagers(players.len()); + + let ( + werewolf, + dire_wolf, + shapeshifter, + alpha_wolf, + seer, + arcanist, + maple_wolf, + guardian, + vindicator, + adjudicator, + power_seer, + beholder, + gravedigger, + mortician, + insomniac, + empath, + scapegoat, + hunter, + ) = ( + werewolf.1, + dire_wolf.1, + shapeshifter.1, + alpha_wolf.1, + seer.1, + arcanist.1, + maple_wolf.1, + guardian.1, + vindicator.1, + adjudicator.1, + power_seer.1, + beholder.1, + gravedigger.1, + mortician.1, + insomniac.1, + empath.1, + scapegoat.1, + hunter.1, + ); + // let village = Village::new(&players, settings).unwrap(); + let mut game = game::Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + game.next().title().wolves_intro(); + game.r#continue().r#continue(); + + game.next().title().direwolf(); + game.mark(game.character_by_player_id(seer).character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(seer).character_id()); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().role_blocked(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().power_seer(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(arcanist).character_id()); + game.r#continue().role_blocked(); + + game.next_expect_day(); + game.mark_for_execution(game.character_by_player_id(dire_wolf).character_id()); + game.mark_for_execution(game.character_by_player_id(alpha_wolf).character_id()); + + game.execute().title().guardian(); + let protect = game.living_villager(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(protect.character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.response(ActionResponse::Shapeshift).sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(seer).character_id()); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().arcanist(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(seer).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().power_seer(); + + game.next().title().gravedigger(); + game.mark(game.character_by_player_id(dire_wolf).character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf)); + + game.next().title().mortician(); + game.mark(game.character_by_player_id(dire_wolf).character_id()); + assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution); + + game.next().title().empath(); + game.mark(game.living_villager().character_id()); + assert!(!game.r#continue().empath()); + + game.next().title().maple_wolf(); + game.mark( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + game.r#continue().insomniac(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(power_seer).character_id()); + game.r#continue().power_seer(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(protect.player_id()).died_to(), + None + ); + + game.mark_for_execution( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.execute().title().guardian(); + game.mark(protect.character_id()); + game.r#continue().sleep(); + + game.next().title().vindicator(); + game.mark( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(protect.character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.mark(game.character_by_player_id(werewolf).character_id()); + game.r#continue().arcanist(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().power_seer(); + + game.next().title().gravedigger(); + game.mark(game.character_by_player_id(dire_wolf).character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf)); + + game.next().title().mortician(); + game.mark(game.character_by_player_id(dire_wolf).character_id()); + assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution); + + game.next().title().empath(); + game.mark(game.character_by_player_id(scapegoat).character_id()); + assert!(game.r#continue().empath()); + + game.next().title().maple_wolf(); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + game.r#continue().insomniac(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(power_seer).character_id()); + game.r#continue().power_seer(); + + game.next_expect_day(); + game.mark_for_execution( + game.living_villager_excl(protect.player_id()) + .character_id(), + ); + game.execute().title().vindicator(); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.response(ActionResponse::Shapeshift).r#continue(); + + game.next().title().role_change(); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + game.r#continue().arcanist(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().power_seer(); + + game.next().title().gravedigger(); + game.mark(game.character_by_player_id(guardian).character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian)); + + game.next().title().mortician(); + game.mark(game.character_by_player_id(guardian).character_id()); + assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack); + + game.next().title().maple_wolf(); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + game.r#continue().insomniac(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(gravedigger).character_id()); + assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian)); + + game.next_expect_day(); + game.mark_for_execution(game.character_by_player_id(vindicator).character_id()); + game.execute().title().wolf_pack_kill(); + game.mark(game.living_villager().character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.r#continue().seer(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(insomniac).character_id()); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().arcanist(); + + game.next().title().adjudicator(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().adjudicator(); + + game.next().title().power_seer(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().power_seer(); + + game.next().title().gravedigger(); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + assert_eq!(game.r#continue().gravedigger(), None); + + game.next().title().mortician(); + game.mark(game.character_by_player_id(werewolf).character_id()); + assert_eq!( + game.r#continue().mortician(), + DiedToTitle::GuardianProtecting + ); + + game.next().title().maple_wolf(); + game.mark(game.character_by_player_id(hunter).character_id()); + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark(game.character_by_player_id(empath).character_id()); + game.r#continue().sleep(); + + game.next().title().insomniac(); + game.r#continue().insomniac(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(mortician).character_id()); + assert_eq!( + game.r#continue().mortician(), + DiedToTitle::GuardianProtecting + ); + + game.next_expect_game_over(); + + game.story() +} + +#[allow(unused)] +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 maple_wolf(&self); + fn guardian(&self); + fn shapeshifter(&self); + fn alphawolf(&self); + fn direwolf(&self); + fn masons_wake(&self); + fn masons_leader_recruit(&self); + fn beholder(&self); + fn vindicator(&self); + fn pyremaster(&self); + fn empath(&self); + fn adjudicator(&self); + fn lone_wolf(&self); + fn insomniac(&self); + fn power_seer(&self); + fn mortician(&self); +} + +impl ActionPromptTitleExt for ActionPromptTitle { + fn mortician(&self) { + assert_eq!(*self, ActionPromptTitle::Mortician); + } + 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 maple_wolf(&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); + } + fn masons_wake(&self) { + assert_eq!(*self, ActionPromptTitle::MasonsWake) + } + fn masons_leader_recruit(&self) { + assert_eq!(*self, ActionPromptTitle::MasonLeaderRecruit) + } + fn beholder(&self) { + assert_eq!(*self, ActionPromptTitle::Beholder) + } + fn vindicator(&self) { + assert_eq!(*self, ActionPromptTitle::Vindicator) + } + fn pyremaster(&self) { + assert_eq!(*self, ActionPromptTitle::PyreMaster) + } + fn adjudicator(&self) { + assert_eq!(*self, ActionPromptTitle::Adjudicator) + } + fn empath(&self) { + assert_eq!(*self, ActionPromptTitle::Empath) + } + fn lone_wolf(&self) { + assert_eq!(*self, ActionPromptTitle::LoneWolfKill) + } + fn insomniac(&self) { + assert_eq!(*self, ActionPromptTitle::Insomniac) + } + fn power_seer(&self) { + assert_eq!(*self, ActionPromptTitle::PowerSeer) + } +} + +pub trait ActionResultExt { + fn sleep(&self); + fn r#continue(&self); + fn seer(&self) -> Alignment; + fn insomniac(&self) -> Visits; + fn arcanist(&self) -> AlignmentEq; + fn adjudicator(&self) -> Killer; + fn role_blocked(&self); + fn gravedigger(&self) -> Option; + fn power_seer(&self) -> Powerful; + fn mortician(&self) -> DiedToTitle; + fn empath(&self) -> bool; +} + +impl ActionResultExt for ActionResult { + fn empath(&self) -> bool { + match self { + Self::Empath { scapegoat } => *scapegoat, + resp => panic!("expected empath, got {resp:?}"), + } + } + fn mortician(&self) -> DiedToTitle { + match self { + Self::Mortician(role) => *role, + resp => panic!("expected mortician, got {resp:?}"), + } + } + fn gravedigger(&self) -> Option { + match self { + Self::GraveDigger(role) => *role, + resp => panic!("expected gravedigger, got {resp:?}"), + } + } + fn adjudicator(&self) -> Killer { + match self { + Self::Adjudicator { killer } => *killer, + resp => panic!("expected adjudicator, got {resp:?}"), + } + } + fn power_seer(&self) -> Powerful { + match self { + Self::PowerSeer { powerful } => *powerful, + resp => panic!("expected power seer, got {resp:?}"), + } + } + fn sleep(&self) { + assert_eq!(*self, ActionResult::GoBackToSleep) + } + + fn role_blocked(&self) { + assert_eq!(*self, ActionResult::RoleBlocked) + } + + fn r#continue(&self) { + assert_eq!(*self, ActionResult::Continue) + } + + fn seer(&self) -> Alignment { + match self { + ActionResult::Seer(a) => a.clone(), + _ => panic!("expected a seer result"), + } + } + + fn arcanist(&self) -> AlignmentEq { + match self { + ActionResult::Arcanist(same) => *same, + _ => panic!("expected an arcanist result"), + } + } + + fn insomniac(&self) -> Visits { + match self { + ActionResult::Insomniac(v) => v.clone(), + _ => panic!("expected an insomniac result"), + } + } +} + +pub trait AlignmentExt { + fn village(&self); + fn wolves(&self); +} + +impl AlignmentExt for Alignment { + fn village(&self) { + assert_eq!(*self, Alignment::Village) + } + + fn wolves(&self) { + assert_eq!(*self, Alignment::Wolves) + } +} + +pub trait ServerToHostMessageExt { + fn prompt(self) -> ActionPrompt; + fn result(self) -> ActionResult; + fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); +} + +impl ServerToHostMessageExt for ServerToHostMessage { + fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { + match self { + Self::Daytime { + characters, + marked, + day, + .. + } => (characters, marked, day), + resp => panic!("expected daytime, got {resp:?}"), + } + } + + fn prompt(self) -> ActionPrompt { + match self { + Self::ActionPrompt(prompt, _) => prompt, + Self::Daytime { .. } => panic!("{}", "[got daytime]"), + msg => panic!("expected server message <<{msg:?}>> to be an ActionPrompt"), + } + } + + fn result(self) -> ActionResult { + match self { + Self::ActionResult(_, res) => res, + msg => panic!("expected server message <<{msg:?}>> to be an ActionResult"), + } + } +} + +pub trait GameExt { + fn villager_character_ids(&self) -> Box<[CharacterId]>; + fn character_by_player_id(&self, player_id: PlayerId) -> Character; + fn character_by_character_id(&self, character_id: CharacterId) -> 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); + 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; + fn living_villager(&self) -> Character; + #[allow(unused)] + fn get_state(&mut self) -> ServerToHostMessage; + fn next_expect_game_over(&mut self) -> GameOver; +} + +impl GameExt for Game { + fn next_expect_game_over(&mut self) -> GameOver { + match self + .process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + { + ServerToHostMessage::GameOver(outcome) => outcome, + resp => panic!("expected game to be over, got: {resp:?}"), + } + } + fn get_state(&mut self) -> ServerToHostMessage { + self.process(HostGameMessage::GetState).unwrap() + } + fn living_villager(&self) -> Character { + self.village() + .characters() + .into_iter() + .find(|c| c.alive() && matches!(c.role_title(), RoleTitle::Villager)) + .unwrap() + } + + fn living_villager_excl(&self, excl: PlayerId) -> Character { + self.village() + .characters() + .into_iter() + .find(|c| { + c.alive() && matches!(c.role_title(), RoleTitle::Villager) && c.player_id() != excl + }) + .unwrap() + } + + fn villager_character_ids(&self) -> Box<[CharacterId]> { + self.village() + .characters() + .into_iter() + .filter_map(|c| { + (c.alive() && matches!(c.role_title(), RoleTitle::Villager)) + .then_some(c.character_id()) + }) + .collect() + } + + fn character_by_player_id(&self, player_id: PlayerId) -> Character { + self.village() + .character_by_player_id(player_id) + .unwrap() + .clone() + } + + fn character_by_character_id(&self, character_id: CharacterId) -> Character { + self.village() + .character_by_id(character_id) + .unwrap() + .clone() + } + + fn r#continue(&mut self) -> ActionResult { + self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::Continue, + ))) + .unwrap() + .result() + } + + fn mark(&mut self, mark: CharacterId) -> ActionPrompt { + self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::MarkTarget(mark), + ))) + .unwrap() + .prompt() + } + + fn mark_and_check(&mut self, mark: CharacterId) { + let prompt = self.mark(mark); + match prompt { + ActionPrompt::Insomniac { .. } + | ActionPrompt::MasonsWake { .. } + | 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::LoneWolfKill { + marked: Some(marked), + .. + } + | ActionPrompt::Seer { + marked: Some(marked), + .. + } + | ActionPrompt::Adjudicator { + marked: Some(marked), + .. + } + | ActionPrompt::PowerSeer { + marked: Some(marked), + .. + } + | ActionPrompt::Mortician { + marked: Some(marked), + .. + } + | ActionPrompt::Beholder { + marked: Some(marked), + .. + } + | ActionPrompt::MasonLeaderRecruit { + marked: Some(marked), + .. + } + | ActionPrompt::Empath { + marked: Some(marked), + .. + } + | ActionPrompt::Vindicator { + marked: Some(marked), + .. + } + | ActionPrompt::PyreMaster { + 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::Adjudicator { marked: None, .. } + | ActionPrompt::PowerSeer { marked: None, .. } + | ActionPrompt::Mortician { marked: None, .. } + | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::MasonLeaderRecruit { marked: None, .. } + | ActionPrompt::Empath { marked: None, .. } + | ActionPrompt::Vindicator { marked: None, .. } + | ActionPrompt::PyreMaster { 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, .. } + | ActionPrompt::LoneWolfKill { marked: None, .. } => panic!("no mark"), + } + } + + fn next(&mut self) -> ActionPrompt { + self.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + .prompt() + } + + fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { + match self + .process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + { + ServerToHostMessage::Daytime { + characters, + marked, + day, + .. + } => (characters, marked, day), + res => panic!("unexpected response to next_expect_day: {res:?}"), + } + } + + fn response(&mut self, resp: ActionResponse) -> ActionResult { + self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + resp, + ))) + .unwrap() + .result() + } + + fn mark_for_execution( + &mut self, + target: CharacterId, + ) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { + match self + .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( + target, + ))) + .unwrap() + { + ServerToHostMessage::Daytime { + characters, + marked, + day, + .. + } => (characters, marked, day), + res => panic!("unexpected response to mark_for_execution: {res:?}"), + } + } + + fn execute(&mut self) -> ActionPrompt { + assert_eq!( + self.process(HostGameMessage::Day(HostDayMessage::Execute)) + .unwrap() + .prompt(), + ActionPrompt::CoverOfDarkness + ); + self.r#continue().r#continue(); + self.next() + } +} diff --git a/werewolves/src/clients/mod.rs b/werewolves/src/clients/mod.rs index 33f3fb9..b5ff733 100644 --- a/werewolves/src/clients/mod.rs +++ b/werewolves/src/clients/mod.rs @@ -5,9 +5,25 @@ pub mod client { } pub mod host { mod host; + pub mod story_test; pub use host::*; } // mod socket; const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/"; const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/"; + +pub mod story_test { + use yew::prelude::*; + + use crate::components::Story; + #[function_component] + pub fn StoryTest() -> Html { + let story = crate::clients::host::story_test::test_story(); + html! { +
+ +
+ } + } +} diff --git a/werewolves/src/components/action/result.rs b/werewolves/src/components/action/result.rs index 17df4b7..4927ec6 100644 --- a/werewolves/src/components/action/result.rs +++ b/werewolves/src/components/action/result.rs @@ -42,8 +42,8 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { .then(|| html! {}); let body = match &props.result { ActionResult::PowerSeer { powerful } => { - let inactive = powerful.not().then_some("inactive"); - let text = if *powerful { + let inactive = powerful.powerful().not().then_some("inactive"); + let text = if powerful.powerful() { "powerful" } else { "not powerful" @@ -56,7 +56,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { } } ActionResult::Adjudicator { killer } => { - let text = if *killer { + let text = if killer.killer() { "is a killer" } else { "is NOT a killer" @@ -64,7 +64,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { html! { <>

{"your target..."}

- +

{text}

} @@ -124,8 +124,8 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { }} }, - ActionResult::Arcanist { same } => { - let outcome = if *same { + ActionResult::Arcanist(same) => { + let outcome = if same.same() { html! { <>
diff --git a/werewolves/src/components/attributes/align_span.rs b/werewolves/src/components/attributes/align_span.rs new file mode 100644 index 0000000..629eb67 --- /dev/null +++ b/werewolves/src/components/attributes/align_span.rs @@ -0,0 +1,61 @@ +use werewolves_proto::{game::Category, role}; +use yew::prelude::*; + +use crate::components::{AssociatedIcon, Icon, IconSource, IconType}; + +#[derive(Debug, Clone, Copy, PartialEq, Properties)] +pub struct AlignmentSpanProps { + pub alignment: role::Alignment, +} + +#[function_component] +pub fn AlignmentSpan(AlignmentSpanProps { alignment }: &AlignmentSpanProps) -> Html { + let class = match alignment { + role::Alignment::Village => "village", + role::Alignment::Wolves => "wolves", + }; + html! { + +
+ +
+ {alignment.to_string()} +
+ } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct CategorySpanProps { + pub category: Category, + #[prop_or_default] + pub icon: Option, + #[prop_or_default] + pub children: Html, +} + +#[function_component] +pub fn CategorySpan( + CategorySpanProps { + category, + icon, + children, + }: &CategorySpanProps, +) -> Html { + let class = category.class(); + let icon = icon.unwrap_or(match category { + Category::Wolves => IconSource::Wolves, + Category::Villager + | Category::Intel + | Category::Defensive + | Category::Offensive + | Category::StartsAsVillager => IconSource::Village, + }); + html! { + +
+ +
+ {children.clone()} +
+ } +} diff --git a/werewolves/src/components/attributes/comparison.rs b/werewolves/src/components/attributes/comparison.rs new file mode 100644 index 0000000..4d8a1ca --- /dev/null +++ b/werewolves/src/components/attributes/comparison.rs @@ -0,0 +1,35 @@ +use werewolves_proto::role::AlignmentEq; +use yew::prelude::*; + +use crate::components::{Icon, IconSource, IconType}; + +#[derive(Debug, Clone, Copy, PartialEq, Properties)] +pub struct AlignmentComparisonSpanProps { + pub comparison: AlignmentEq, +} + +#[function_component] +pub fn AlignmentComparisonSpan( + AlignmentComparisonSpanProps { comparison }: &AlignmentComparisonSpanProps, +) -> Html { + match comparison { + AlignmentEq::Same => html! { + +
+
+ {"the same"} +
+
+
+ }, + AlignmentEq::Different => html! { + +
+
+ {"different"} +
+
+
+ }, + } +} diff --git a/werewolves/src/components/attributes/death_span.rs b/werewolves/src/components/attributes/death_span.rs new file mode 100644 index 0000000..9120c59 --- /dev/null +++ b/werewolves/src/components/attributes/death_span.rs @@ -0,0 +1,44 @@ +use convert_case::{Case, Casing}; +use werewolves_proto::{ + diedto::{DiedTo, DiedToTitle}, + game::{Category, SetupRoleTitle}, + role, +}; +use yew::prelude::*; + +use crate::components::{AssociatedIcon, Icon, IconSource, IconType, PartialAssociatedIcon}; + +#[derive(Debug, Clone, Copy, PartialEq, Properties)] +pub struct DiedToSpanProps { + pub died_to: DiedToTitle, +} + +#[function_component] +pub fn DiedToSpan(DiedToSpanProps { died_to }: &DiedToSpanProps) -> Html { + let class = match died_to { + DiedToTitle::Execution => "execution", + DiedToTitle::MapleWolfStarved | DiedToTitle::MapleWolf => { + SetupRoleTitle::MapleWolf.category().class() + } + DiedToTitle::Militia => SetupRoleTitle::Militia.category().class(), + DiedToTitle::LoneWolf + | DiedToTitle::AlphaWolf + | DiedToTitle::Shapeshift + | DiedToTitle::Wolfpack => SetupRoleTitle::Werewolf.category().class(), + DiedToTitle::Hunter => SetupRoleTitle::Hunter.category().class(), + DiedToTitle::GuardianProtecting => SetupRoleTitle::Guardian.category().class(), + DiedToTitle::PyreMasterLynchMob | DiedToTitle::PyreMaster => { + SetupRoleTitle::PyreMaster.category().class() + } + DiedToTitle::MasonLeaderRecruitFail => SetupRoleTitle::MasonLeader.category().class(), + }; + let icon = died_to.icon().unwrap_or(IconSource::Skull); + html! { + +
+ +
+ {died_to.to_string().to_case(Case::Title)} +
+ } +} diff --git a/werewolves/src/components/attributes/killer.rs b/werewolves/src/components/attributes/killer.rs new file mode 100644 index 0000000..5bfa21b --- /dev/null +++ b/werewolves/src/components/attributes/killer.rs @@ -0,0 +1,25 @@ +use werewolves_proto::role::{self, Killer}; +use yew::prelude::*; + +use crate::components::{AssociatedIcon, Icon, IconType}; + +#[derive(Debug, Clone, Copy, PartialEq, Properties)] +pub struct KillerSpanProps { + pub killer: Killer, +} + +#[function_component] +pub fn KillerSpan(KillerSpanProps { killer }: &KillerSpanProps) -> Html { + let class = match killer { + Killer::Killer => "killer", + Killer::NotKiller => "inactive", + }; + html! { + +
+ +
+ {killer.to_string()} +
+ } +} diff --git a/werewolves/src/components/attributes/powerful.rs b/werewolves/src/components/attributes/powerful.rs new file mode 100644 index 0000000..86ff429 --- /dev/null +++ b/werewolves/src/components/attributes/powerful.rs @@ -0,0 +1,25 @@ +use werewolves_proto::role::Powerful; +use yew::prelude::*; + +use crate::components::{AssociatedIcon, Icon, IconType}; + +#[derive(Debug, Clone, Copy, PartialEq, Properties)] +pub struct PowerfulSpanProps { + pub powerful: Powerful, +} + +#[function_component] +pub fn PowerfulSpan(PowerfulSpanProps { powerful }: &PowerfulSpanProps) -> Html { + let class = match powerful { + Powerful::Powerful => "powerful", + Powerful::NotPowerful => "inactive", + }; + html! { + +
+ +
+ {powerful.to_string()} +
+ } +} diff --git a/werewolves/src/components/character.rs b/werewolves/src/components/character.rs new file mode 100644 index 0000000..0409d57 --- /dev/null +++ b/werewolves/src/components/character.rs @@ -0,0 +1,44 @@ +use convert_case::{Case, Casing}; +use werewolves_proto::{character::Character, game::SetupRole}; +use yew::prelude::*; + +use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon}; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct CharacterCardProps { + pub char: Character, +} + +#[function_component] +pub fn CharacterCard(CharacterCardProps { char }: &CharacterCardProps) -> Html { + let class = Into::::into(char.role_title()) + .category() + .class(); + let role = char.role_title().to_string().to_case(Case::Title); + let ident = char.identity(); + let pronouns = ident.pronouns.as_ref().map(|p| { + html! { + {"("}{p}{")"} + } + }); + let name = ident.name.clone(); + let source = char.role_title().icon().unwrap_or(if char.is_village() { + IconSource::Village + } else { + IconSource::Wolves + }); + + html! { + + +
+ +
+ {role} +
+ {ident.number.get()} + {name} + {pronouns} +
+ } +} diff --git a/werewolves/src/components/host/daytime.rs b/werewolves/src/components/host/daytime.rs index 672bcac..f4a7f25 100644 --- a/werewolves/src/components/host/daytime.rs +++ b/werewolves/src/components/host/daytime.rs @@ -2,7 +2,7 @@ use core::{num::NonZeroU8, ops::Not}; use werewolves_proto::{ character::CharacterId, - game::DateTime, + game::GameTime, message::{CharacterState, PublicIdentity}, }; use yew::prelude::*; @@ -41,8 +41,8 @@ pub fn DaytimePlayerList( .contains(&c.identity.character_id) .then_some(MarkState::Marked), Some(died_to) => match died_to.date_time() { - DateTime::Day { .. } => Some(MarkState::Dead), - DateTime::Night { number } => { + GameTime::Day { .. } => Some(MarkState::Dead), + GameTime::Night { number } => { if number == day.get() - 1 { Some(MarkState::DiedLastNight) } else { diff --git a/werewolves/src/components/host/setup.rs b/werewolves/src/components/host/setup.rs index 9f043b4..8a34341 100644 --- a/werewolves/src/components/host/setup.rs +++ b/werewolves/src/components/host/setup.rs @@ -124,8 +124,8 @@ pub fn SetupCategory( {count} } }); - let killer_inactive = as_role.killer().not().then_some("inactive"); - let powerful_inactive = as_role.powerful().not().then_some("inactive"); + let killer_inactive = as_role.killer().killer().not().then_some("inactive"); + let powerful_inactive = as_role.powerful().powerful().not().then_some("inactive"); let alignment = match as_role.alignment() { Alignment::Village => "/img/village.svg", Alignment::Wolves => "/img/wolf.svg", diff --git a/werewolves/src/components/icon.rs b/werewolves/src/components/icon.rs index f685234..fac9953 100644 --- a/werewolves/src/components/icon.rs +++ b/werewolves/src/components/icon.rs @@ -1,3 +1,7 @@ +use werewolves_proto::{ + diedto::DiedToTitle, + role::{Alignment, Killer, Powerful, RoleTitle}, +}; use yew::prelude::*; #[derive(Debug, Clone, Copy, PartialEq)] @@ -6,6 +10,16 @@ pub enum IconSource { Wolves, Killer, Powerful, + ListItem, + Skull, + Heart, + Shield, + ShieldAndSword, + Seer, + Hunter, + MapleWolf, + Gravedigger, + PowerSeer, } impl IconSource { @@ -15,20 +29,30 @@ impl IconSource { IconSource::Wolves => "/img/wolf.svg", IconSource::Killer => "/img/killer.svg", IconSource::Powerful => "/img/powerful.svg", + IconSource::ListItem => "/img/li.svg", + IconSource::Skull => "/img/skull.svg", + IconSource::Heart => "/img/heart.svg", + IconSource::Shield => "/img/shield.svg", + IconSource::ShieldAndSword => "/img/shield-and-sword.svg", + IconSource::Seer => "/img/seer.svg", + IconSource::Hunter => "/img/hunter.svg", + IconSource::MapleWolf => "/img/maple-wolf.svg", + IconSource::Gravedigger => "/img/gravedigger.svg", + IconSource::PowerSeer => "/img/power-seer.svg", } } pub const fn class(&self) -> Option<&'static str> { match self { - IconSource::Village | IconSource::Wolves => None, IconSource::Killer => Some("killer"), IconSource::Powerful => Some("powerful"), + _ => None, } } } #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum IconType { - SetupList, + Small, #[default] RoleCheck, } @@ -36,7 +60,7 @@ pub enum IconType { impl IconType { pub const fn class(&self) -> &'static str { match self { - IconType::SetupList => "icon", + IconType::Small => "icon", IconType::RoleCheck => "check-icon", } } @@ -65,3 +89,87 @@ pub fn Icon( /> } } + +pub trait PartialAssociatedIcon { + fn icon(&self) -> Option; +} + +pub trait AssociatedIcon { + fn icon(&self) -> IconSource; +} + +impl AssociatedIcon for Alignment { + fn icon(&self) -> IconSource { + match self { + Alignment::Village => IconSource::Village, + Alignment::Wolves => IconSource::Wolves, + } + } +} + +impl AssociatedIcon for Killer { + fn icon(&self) -> IconSource { + IconSource::Killer + } +} + +impl AssociatedIcon for Powerful { + fn icon(&self) -> IconSource { + IconSource::Powerful + } +} + +impl PartialAssociatedIcon for RoleTitle { + fn icon(&self) -> Option { + Some(match self { + RoleTitle::Scapegoat + | RoleTitle::Arcanist + | RoleTitle::Adjudicator + | RoleTitle::Mortician + | RoleTitle::Beholder + | RoleTitle::MasonLeader + | RoleTitle::Diseased + | RoleTitle::BlackKnight + | RoleTitle::Weightlifter + | RoleTitle::PyreMaster + | RoleTitle::Militia + | RoleTitle::Apprentice + | RoleTitle::Elder + | RoleTitle::Insomniac + | RoleTitle::Werewolf + | RoleTitle::AlphaWolf + | RoleTitle::DireWolf + | RoleTitle::Shapeshifter + | RoleTitle::LoneWolf + | RoleTitle::Villager => return None, + + RoleTitle::PowerSeer => IconSource::PowerSeer, + RoleTitle::Gravedigger => IconSource::Gravedigger, + RoleTitle::MapleWolf => IconSource::MapleWolf, + RoleTitle::Hunter => IconSource::Hunter, + RoleTitle::Empath => IconSource::Heart, + RoleTitle::Seer => IconSource::Seer, + RoleTitle::Guardian => IconSource::ShieldAndSword, + RoleTitle::Vindicator | RoleTitle::Protector => IconSource::Shield, + }) + } +} + +impl PartialAssociatedIcon for DiedToTitle { + fn icon(&self) -> Option { + match self { + DiedToTitle::Execution => Some(IconSource::Skull), + DiedToTitle::MapleWolf | DiedToTitle::MapleWolfStarved => Some(IconSource::MapleWolf), + DiedToTitle::Militia => Some(IconSource::Killer), + DiedToTitle::Wolfpack => None, + DiedToTitle::AlphaWolf => None, + DiedToTitle::Shapeshift => None, + DiedToTitle::Hunter => Some(IconSource::Hunter), + DiedToTitle::GuardianProtecting => Some(IconSource::ShieldAndSword), + DiedToTitle::PyreMaster => None, + DiedToTitle::PyreMasterLynchMob => None, + DiedToTitle::MasonLeaderRecruitFail => None, + DiedToTitle::LoneWolf => None, + } + } +} diff --git a/werewolves/src/components/story.rs b/werewolves/src/components/story.rs new file mode 100644 index 0000000..baec8ac --- /dev/null +++ b/werewolves/src/components/story.rs @@ -0,0 +1,543 @@ +use core::ops::Not; +use std::{collections::HashMap, rc::Rc}; + +use convert_case::{Case, Casing}; +use werewolves_proto::{ + character::{Character, CharacterId}, + game::{ + GameTime, SetupRole, + night::changes::NightChange, + story::{ + DayDetail, GameActions, GameStory, NightChoice, StoryActionPrompt, StoryActionResult, + }, + }, + role::Alignment, +}; +use yew::prelude::*; + +use crate::components::{ + CharacterCard, Icon, IconSource, IconType, PartialAssociatedIcon, + attributes::{ + AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan, + }, +}; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct StoryProps { + pub story: GameStory, +} + +#[function_component] +pub fn Story(StoryProps { story }: &StoryProps) -> Html { + let characters = Rc::new( + story + .starting_village + .characters() + .into_iter() + .map(|c| (c.character_id(), c)) + .collect::>(), + ); + let bits = story + .iter() + .map(|(time, changes)| { + let characters = story + .village_at(match time { + GameTime::Day { .. } => { + time + }, + GameTime::Night { .. } => { + time.previous().unwrap_or(time) + }, + }).ok().flatten() + .map(|v| Rc::new(v.characters().into_iter() + .map(|c| (c.character_id(), c)) + .collect::>())).unwrap_or_else(|| characters.clone()); + let changes = match changes { + GameActions::DayDetails(day_changes) => { + let execute_list = if day_changes.is_empty() { + html! {{"no one was executed"}} + } else { + day_changes + .iter() + .map(|c| match c { + DayDetail::Execute(target) => *target, + }) + .filter_map(|c| story.starting_village.character_by_id(c).ok()) + .map(|c| { + html! { + // + + // + } + }) + .collect::() + }; + + Some(html! { +
+

{"village executed"}

+
+ {execute_list} +
+
+ }) + } + GameActions::NightDetails(details) => details.choices.is_empty().not().then_some({ + let choices = details + .choices + .iter() + .map(|c| { + html! { + + } + }) + .collect::(); + + let changes = details + .changes + .iter() + .map(|c| { + html! { +
  • + +
  • + } + }) + .collect::(); + + html! { +
    + +
      + {choices} +
    + +
      + {changes} +
    +
    + } + }), + }; + + changes + .map(|changes| { + html! { +
    +

    {"on "}{time.to_string()}{"..."}

    + {changes} +
    + } + }) + .unwrap_or_default() + }) + .collect::(); + html! { +
    + {bits} +
    + } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +struct StoryNightChangeProps { + change: NightChange, + characters: Rc>, +} + +#[function_component] +fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html { + match change { + NightChange::RoleChange(character_id, role_title) => characters + .get(character_id) + .map(|char| { + let mut new_char = char.clone(); + let _ = new_char.role_change(*role_title, GameTime::Night { number: 0 }); + + html! { + <> + + {"is now"} + + + } + }) + .unwrap_or_default(), + + NightChange::Kill { target, died_to } => characters + .get(target) + .map(|target| { + html! { + <> + + + {"died to"} + + + } + }) + .unwrap_or_default(), + NightChange::RoleBlock { source, target, .. } => characters + .get(source) + .and_then(|s| characters.get(target).map(|t| (s, t))) + .map(|(source, target)| { + html! { + <> + + {"role blocked"} + + + } + }) + .unwrap_or_default(), + NightChange::Shapeshift { source, into } => characters + .get(source) + .and_then(|s| characters.get(into).map(|i| (s, i))) + .map(|(source, into)| { + html! { + <> + + {"shapeshifted into"} + + + } + }) + .unwrap_or_default(), + + NightChange::ElderReveal { elder } => characters + .get(elder) + .map(|elder| { + html! { + <> + + {"learned they are the Elder"} + + } + }) + .unwrap_or_default(), + NightChange::EmpathFoundScapegoat { empath, scapegoat } => characters + .get(empath) + .and_then(|e| characters.get(scapegoat).map(|s| (e, s))) + .map(|(empath, scapegoat)| { + html! { + <> + + {"found the scapegoat in"} + + {"and took on their curse"} + + } + }) + .unwrap_or_default(), + + NightChange::HunterTarget { .. } + | NightChange::MasonRecruit { .. } + | NightChange::Protection { .. } => html! {}, // sorted in prompt side + } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +struct StoryNightResultProps { + result: StoryActionResult, + characters: Rc>, +} + +#[function_component] +fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html { + match result { + StoryActionResult::RoleBlocked => html! { + {"but was role blocked"} + }, + StoryActionResult::Seer(alignment) => { + html! { + + {"and saw"} + + + } + } + StoryActionResult::PowerSeer { powerful } => { + html! { + + {"and discovered they are"} + + + } + } + StoryActionResult::Adjudicator { killer } => html! { + + {"and saw"} + + + }, + StoryActionResult::Arcanist(same) => html! { + + {"and saw"} + + + }, + StoryActionResult::GraveDigger(None) => html! { + + {"found an empty grave"} + + }, + StoryActionResult::GraveDigger(Some(role_title)) => { + let category = Into::::into(*role_title).category(); + html! { + + {"found the body of a"} + + {role_title.to_string().to_case(Case::Title)} + + + } + } + StoryActionResult::Mortician(died_to_title) => html! { + <> + {"and found the cause of death to be"} + + + }, + StoryActionResult::Insomniac { visits } => { + let visitors = visits + .iter() + .filter_map(|c| characters.get(c)) + .map(|c| { + html! { + + } + }) + .collect::(); + html! { + {visitors} + } + } + + StoryActionResult::Empath { scapegoat: false } => html! { + <> + {"and saw that they are"} + +
    + +
    + {"Not The Scapegoat"} +
    + + }, + StoryActionResult::Empath { scapegoat: true } => html! { + <> + {"and saw that they are"} + +
    + +
    + {"The Scapegoat"} +
    + + }, + } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +struct StoryNightChoiceProps { + choice: NightChoice, + characters: Rc>, +} + +#[function_component] +fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightChoiceProps) -> Html { + let generate = |character_id: &CharacterId, chosen: &CharacterId, action: &'static str| { + characters + .get(character_id) + .and_then(|char| characters.get(chosen).map(|c| (char, c))) + .map(|(char, chosen)| { + html! { + <> + + {action} + + + } + }) + }; + let choice_body = match &choice.prompt { + StoryActionPrompt::Arcanist { + character_id, + chosen: (chosen1, chosen2), + } => characters + .get(character_id) + .and_then(|arcanist| characters.get(chosen1).map(|c| (arcanist, c))) + .and_then(|(arcanist, chosen1)| { + characters + .get(chosen2) + .map(|chosen2| (arcanist, chosen1, chosen2)) + }) + .map(|(arcanist, chosen1, chosen2)| { + html! { + <> + + {"compared"} + + {"and"} + + + } + }), + StoryActionPrompt::MasonsWake { leader, masons } => characters.get(leader).map(|leader| { + let masons = masons + .iter() + .filter_map(|m| characters.get(m)) + .map(|c| { + html! { + + } + }) + .collect::(); + html! { + <> + + {"'s masons"} + {masons} + {"convened in secret"} + + } + }), + StoryActionPrompt::Vindicator { + character_id, + chosen, + } + | StoryActionPrompt::Protector { + character_id, + chosen, + } => generate(character_id, chosen, "protected"), + StoryActionPrompt::Gravedigger { + character_id, + chosen, + } => generate(character_id, chosen, "dug up"), + StoryActionPrompt::Adjudicator { + character_id, + chosen, + } + | StoryActionPrompt::PowerSeer { + character_id, + chosen, + } + | StoryActionPrompt::Empath { + character_id, + chosen, + } + | StoryActionPrompt::Seer { + character_id, + chosen, + } => generate(character_id, chosen, "checked"), + StoryActionPrompt::Hunter { + character_id, + chosen, + } => generate(character_id, chosen, "set a trap for"), + StoryActionPrompt::Militia { + character_id, + chosen, + } => generate(character_id, chosen, "shot"), + StoryActionPrompt::MapleWolf { + character_id, + chosen, + } => characters + .get(character_id) + .and_then(|char| characters.get(chosen).map(|c| (char, c))) + .map(|(char, chosen)| { + html! { + <> + + {"invited"} + + {"for dinner"} + + } + }), + StoryActionPrompt::Guardian { + character_id, + chosen, + guarding, + } => generate( + character_id, + chosen, + if *guarding { "guarded" } else { "protected" }, + ), + StoryActionPrompt::Mortician { + character_id, + chosen, + } => generate(character_id, chosen, "examined"), + StoryActionPrompt::Beholder { + character_id, + chosen, + } => generate(character_id, chosen, "👁️"), + + StoryActionPrompt::MasonLeaderRecruit { + character_id, + chosen, + } => generate(character_id, chosen, "tried recruiting"), + StoryActionPrompt::PyreMaster { + character_id, + chosen, + } => generate(character_id, chosen, "torched"), + StoryActionPrompt::WolfPackKill { chosen } => { + characters.get(chosen).map(|chosen: &Character| { + html! { + <> + + {"attempted a kill on"} + + + } + }) + } + StoryActionPrompt::Shapeshifter { character_id } => { + characters.get(character_id).map(|shifter| { + html! { + <> + + {"decided to shapeshift into the wolf kill target"} + + } + }) + } + StoryActionPrompt::AlphaWolf { + character_id, + chosen, + } => generate(character_id, chosen, "took a stab at"), + StoryActionPrompt::DireWolf { + character_id, + chosen, + } => generate(character_id, chosen, "roleblocked"), + StoryActionPrompt::LoneWolfKill { + character_id, + chosen, + } => generate(character_id, chosen, "sought vengeance from"), + StoryActionPrompt::Insomniac { character_id } => { + characters.get(character_id).map(|insomniac| { + html! { + <> + + {"witnessed visits from"} + + } + }) + } + }; + let result = choice.result.as_ref().map(|result| { + html! { + + } + }); + choice_body + .map(|choice_body| { + html! { +
  • + + {choice_body} + {result} +
  • + } + }) + .unwrap_or_default() +} diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs index 0fc594c..87bb398 100644 --- a/werewolves/src/main.rs +++ b/werewolves/src/main.rs @@ -3,6 +3,9 @@ mod clients; mod storage; mod components { werewolves_macros::include_path!("werewolves/src/components"); + pub mod attributes { + werewolves_macros::include_path!("werewolves/src/components/attributes"); + } pub mod client { werewolves_macros::include_path!("werewolves/src/components/client"); } @@ -48,12 +51,17 @@ fn main() { Callback::from(move |err: Option| cb_clone.send_message(err)); gloo::utils::document().set_title("werewolves"); + if path.starts_with("/host/story") { + let host = yew::Renderer::::with_root(app_element).render(); + return; + } if path.starts_with("/host") { let host = yew::Renderer::::with_root(app_element).render(); - host.send_message(HostEvent::SetErrorCallback(error_callback)); if path.starts_with("/host/big") { host.send_message(HostEvent::SetBigScreenState(true)); + } else { + host.send_message(HostEvent::SetErrorCallback(error_callback)); } } else if path.starts_with("/many-client") { let clients = document.query_selector("clients").unwrap().unwrap();