added end-game story screen with icons

bugfixes and tests to guardians and hunters
ron serialization for game story saves
This commit is contained in:
emilis 2025-10-12 23:48:52 +01:00
parent 11bc54f996
commit 9f00d2b912
No known key found for this signature in database
55 changed files with 5602 additions and 1011 deletions

26
Cargo.lock generated
View File

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

View File

@ -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<T> = core::result::Result<T, GameError>;
@ -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<KillingWolfOrder> {
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<Guardian<'a>> {
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<GuardianMut<'a>> {
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<DiedTo>;
Guardian, GuardianMut: Option<PreviousGuardianAction>;
);
pub struct BlackKnightKill<'a> {

View File

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

View File

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

View File

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

View File

@ -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<T> = core::result::Result<T, GameError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Game {
previous: Vec<GameState>,
next: Vec<GameState>,
history: GameStory,
state: GameState,
}
impl Game {
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
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<GameOver> {
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<Self> {
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,
},
}
}
}

View File

@ -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<NightChange>),
@ -252,9 +217,9 @@ pub struct Night {
impl Night {
pub fn new(village: Village) -> Result<Self> {
let night = match village.date_time() {
DateTime::Day { number: _ } => return Err(GameError::NotNight),
DateTime::Night { number } => number,
let 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<NightChange> {
fn automatic_changes(&self) -> Vec<NightChange> {
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<Village> {
pub fn collect_changes(&self) -> Result<Box<[NightChange]>> {
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::<Box<[_]>>();
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<ResponseOutcome> {
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
}
}
}

View File

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

View File

@ -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<T> = core::result::Result<T, GameError>;
impl Night {
pub(super) fn process(&self, resp: ActionResponse) -> Result<ResponseOutcome> {
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),
}
}
}

View File

@ -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<T> = core::result::Result<T, GameError>;
#[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<StoryActionResult>,
}
impl NightChoice {
pub fn new(prompt: ActionPrompt, result: ActionResult) -> Option<Self> {
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<RoleTitle>),
Mortician(DiedToTitle),
Insomniac { visits: Box<[CharacterId]> },
Empath { scapegoat: bool },
}
impl StoryActionResult {
pub fn new(result: ActionResult) -> Option<Self> {
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<Self> {
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<GameTime, GameActions>,
}
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<Option<Village>> {
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<Self::Item> {
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,
}
}
}

View File

@ -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::<Box<[_]>>();
if !ww.is_empty() {
return Some(ww[rand::random_range(0..ww.len())]);
}
}
{
let wolves = wolves.collect::<Box<[_]>>();
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::<Box<[_]>>();
wolves.sort_by_key(|w| w.killing_wolf_order());
wolves.first().copied()
}
pub fn wolf_pack_kill(&self) -> Option<ActionPrompt> {
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<Option<GameOver>> {
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<DateTime> {
if self.date_time.is_day() {
pub fn to_day(&mut self) -> Result<GameTime> {
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]> {

View File

@ -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<T> = core::result::Result<T, GameError>;
impl Village {
pub fn with_day_changes(&self, day_changes: &[DayDetail]) -> Result<Self> {
let mut new_village = self.clone();
let executions = day_changes
.iter()
.map(|c| match c {
DayDetail::Execute(c) => *c,
})
.collect::<Box<[_]>>();
new_village.execute(&executions)?;
Ok(new_village)
}
pub fn with_night_changes(&self, all_changes: &[NightChange]) -> Result<Self> {
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::<Box<[_]>>();
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)
}
}

View File

@ -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<RoleTitle>;
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<RoleTitle> {
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::<Box<[_]>>();
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();
}

View File

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

View File

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

View File

@ -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?"),
}
}

View File

@ -3,6 +3,8 @@ mod black_knight;
mod diseased;
mod elder;
mod empath;
mod guardian;
mod hunter;
mod insomniac;
mod lone_wolf;
mod mason;

View File

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

View File

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

View File

@ -24,14 +24,19 @@ pub struct CharacterIdentity {
pub pronouns: Option<String>,
pub number: NonZeroU8,
}
impl CharacterIdentity {
pub fn into_public(self) -> PublicIdentity {
PublicIdentity {
name: self.name,
pronouns: self.pronouns,
number: Some(self.number),
}
}
}
impl From<CharacterIdentity> for PublicIdentity {
fn from(c: CharacterIdentity) -> Self {
Self {
name: c.name,
pronouns: c.pronouns,
number: Some(c.number),
}
c.into_public()
}
}

View File

@ -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<T> = core::result::Result<T, GameError>;
@ -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<RoleTitle>),
Mortician(DiedToTitle),
Insomniac(Visits),

View File

@ -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<DiedTo> },
#[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<CharacterId> },
#[checks(Alignment::Village)]
#[checks("killer")]
#[checks("powerful")]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("is_mentor")]
Militia { targeted: Option<CharacterId> },
#[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<PreviousGuardianAction>,
},
#[checks(Alignment::Village)]
#[checks("powerful")]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
Protector { last_protected: Option<CharacterId> },
#[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<NonZeroU8>,
},
#[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<CharacterId> },
#[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<CharacterId> },
#[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<KillingWolfOrder> {
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,
}

View File

@ -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"]

View File

@ -5,22 +5,12 @@ use werewolves_proto::error::GameError;
use crate::runner::IdentifiedClientMessage;
pub struct PlayerIdComms {
// joined_players: JoinedPlayers,
message_recv: Receiver<IdentifiedClientMessage>,
// connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
}
impl PlayerIdComms {
pub fn new(
// joined_players: JoinedPlayers,
message_recv: Receiver<IdentifiedClientMessage>,
// connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
) -> Self {
Self {
// joined_players,
message_recv,
// connect_recv,
}
pub fn new(message_recv: Receiver<IdentifiedClientMessage>) -> Self {
Self { message_recv }
}
pub async fn recv(&mut self) -> Result<IdentifiedClientMessage, GameError> {

View File

@ -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<GameRunner>,
page: Option<usize>,
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<Lobby> {
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();

View File

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

View File

@ -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<std::io::Error> for FileSaverError {
@ -39,10 +39,11 @@ impl Saver for FileSaver {
type Error = FileSaverError;
fn save(&mut self, game: &Game) -> Result<String, Self::Error> {
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))
}

View File

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

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="14.519332mm"
height="14.518607mm"
viewBox="0 0 14.519332 14.518607"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="2"
inkscape:cx="92.75"
inkscape:cy="546.5"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="-3.6500685e-13"
y="0"
width="14.519333"
height="14.518607"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-81.081288,-167.88827)"><path
id="rect172"
style="fill:#7f7f7f;fill-opacity:1;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 94.511966,168.08826 -8.159189,8.15918 -1.539827,-1.53983 -3.324114,3.32339 -0.207549,4.17587 4.176605,-0.20681 3.32375,-3.32376 -1.540192,-1.54019 8.159186,-8.15919 z" /><path
id="path145-7"
style="fill:#a0a0a0;fill-opacity:0.5;stroke:#545454;stroke-width:0.232;stroke-dasharray:none;stroke-opacity:0.7"
d="m 88.300286,169.67427 c -0.48287,0.009 -0.965274,0.0916 -1.420584,0.25373 -1.738714,0.59694 -3.342068,0.9627 -3.03444,4.1274 -0.06576,0.94137 -0.0783,1.89737 0.06408,2.83239 0.07766,0.53633 0.382678,1.09991 0.888836,1.32446 0.446178,0.0891 0.985208,-0.0139 1.31258,0.39223 0.88847,0.68196 0.349966,2.06997 1.028362,1.973 0.4264,-0.0609 0.136174,-0.73886 0.260966,-0.80202 0.192692,-0.0975 0.181336,-0.0608 0.241846,0.18087 0.03243,0.38247 -0.05245,0.69761 0.28112,0.66198 0.550572,-0.0588 0.28691,-0.48129 0.328144,-0.95808 0.0732,-0.0163 0.08707,-0.0125 0.0894,-0.0114 h 5.18e-4 c 0.0023,-0.001 0.01617,-0.005 0.0894,0.0114 0.04123,0.47679 -0.222428,0.89925 0.328144,0.95808 0.333568,0.0356 0.248694,-0.27951 0.28112,-0.66198 0.06051,-0.2417 0.04915,-0.27839 0.241846,-0.18087 0.124792,0.0632 -0.165434,0.74107 0.260966,0.80202 0.678396,0.097 0.139892,-1.29104 1.028362,-1.973 0.327372,-0.40615 0.866402,-0.30311 1.31258,-0.39223 0.506158,-0.22455 0.81169,-0.78813 0.889352,-1.32446 0.14238,-0.93502 0.129318,-1.89102 0.06356,-2.83239 0.307628,-3.1647 -1.295726,-3.53046 -3.03444,-4.1274 -0.45531,-0.16216 -0.937716,-0.24475 -1.420586,-0.25373 -0.0135,0 -0.02664,-1e-4 -0.04031,0 -0.01367,-1e-4 -0.02733,0 -0.04083,0 z m -2.07274,4.18424 c 0.703538,-0.004 1.021816,0.49934 1.094506,0.7059 0.462654,1.11185 -0.373098,1.45665 -1.174088,1.42576 -0.699984,0.0989 -1.258414,0.0508 -1.342554,-1.01286 -0.01001,-0.33209 0.308906,-0.96177 1.096574,-1.08366 0.116648,-0.0235 0.225056,-0.0346 0.325562,-0.0351 z m 4.226614,0 c 0.100506,5.1e-4 0.208914,0.0116 0.325562,0.0351 0.787668,0.12189 1.106584,0.75157 1.096574,1.08366 -0.08414,1.06367 -0.64257,1.11173 -1.342554,1.01286 -0.80099,0.0309 -1.636742,-0.31391 -1.174088,-1.42576 0.07269,-0.20656 0.390968,-0.70943 1.094506,-0.7059 z m -2.113048,2.94194 c 0.215198,0.0216 0.284064,0.38489 0.583426,0.77618 0.276858,0.50031 -0.16476,0.69652 -0.409794,0.48318 -0.146516,-0.11734 -0.171114,-0.12967 -0.173632,-0.13074 -0.0025,0.001 -0.02763,0.0134 -0.17415,0.13074 -0.245034,0.21334 -0.686652,0.0171 -0.409794,-0.48318 0.299362,-0.39129 0.368746,-0.75459 0.583944,-0.77618 z" /></g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

98
werewolves/img/heart.svg Normal file
View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="49.5746mm"
height="46.331978mm"
viewBox="0 0 49.5746 46.331978"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="1.4142136"
inkscape:cx="290.26733"
inkscape:cy="686.60068"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="-1.2367246e-13"
y="0"
width="49.574596"
height="46.331978"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1"><inkscape:path-effect
effect="mirror_symmetry"
start_point="86.254165,185.51628"
end_point="86.385181,235.08332"
center_point="86.319673,210.2998"
id="path-effect149"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="free"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /></defs><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-61.512867,-189.01705)"><g
id="g151"><path
style="fill:#ff2424;fill-opacity:1;stroke:#ff2424;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 86.254166,235.08229 c 0,0 -24.302335,-18.46127 -24.606251,-31.8823 -0.126849,-5.60168 4.153389,-12.70855 9.657292,-13.75832 5.852331,-1.11623 14.123929,1.87077 14.932422,9.82265 z m 0.262022,-6.9e-4 c 0,0 24.204402,-18.58949 24.437362,-32.01193 0.0972,-5.60228 -4.22051,-12.68642 -9.72988,-13.70708 -5.858155,-1.08528 -14.113847,1.94541 -14.880292,9.90145 z"
id="path148"
sodipodi:nodetypes="caacc"
inkscape:path-effect="#path-effect149"
inkscape:original-d="m 86.254166,235.08229 c 0,0 -24.302335,-18.46127 -24.606251,-31.8823 -0.126849,-5.60168 4.153389,-12.70855 9.657292,-13.75832 5.852331,-1.11623 14.123929,1.87077 14.932422,9.82265 z" /><path
sodipodi:type="star"
style="fill:#fff001;fill-opacity:1;stroke:#ff2424;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
id="path150"
inkscape:flatsided="false"
sodipodi:sides="4"
sodipodi:cx="79.886864"
sodipodi:cy="208.97804"
sodipodi:r1="5.2418227"
sodipodi:r2="2.8830025"
sodipodi:arg1="0.607802"
sodipodi:arg2="1.3932002"
inkscape:rounded="0.5"
inkscape:randomized="0"
d="m 84.189903,211.97146 c -1.084141,1.55845 -1.92512,-0.49115 -3.793716,-0.15576 -1.868596,0.33539 -1.944289,2.54952 -3.502742,1.46538 -1.558453,-1.08414 0.491151,-1.92512 0.155762,-3.79371 -0.335389,-1.8686 -2.549524,-1.94429 -1.465383,-3.50275 1.084141,-1.55845 1.925121,0.49116 3.793717,0.15577 1.868596,-0.33539 1.944289,-2.54953 3.502742,-1.46539 1.558452,1.08414 -0.491152,1.92512 -0.155763,3.79372 0.335389,1.8686 2.549524,1.94429 1.465383,3.50274 z"
transform="rotate(28.628814,62.558026,193.09515)" /><path
sodipodi:type="star"
style="fill:#fff001;fill-opacity:1;stroke:#ff2424;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
id="path151"
inkscape:flatsided="false"
sodipodi:sides="4"
sodipodi:cx="68.848625"
sodipodi:cy="200.93324"
sodipodi:r1="6.7481718"
sodipodi:r2="3.3373971"
sodipodi:arg1="0.80500349"
sodipodi:arg2="1.5925996"
inkscape:rounded="0.5"
inkscape:randomized="0"
d="m 73.525842,205.79755 c -1.802133,1.72521 -2.255654,-1.4788 -4.749977,-1.5277 -2.4873,-0.0488 -3.071191,3.13767 -4.791545,1.34061 -1.725212,-1.80213 1.478793,-2.25565 1.527701,-4.74998 0.04877,-2.4873 -3.137671,-3.07119 -1.340613,-4.79154 1.802133,-1.72521 2.255655,1.47879 4.749978,1.5277 2.487299,0.0488 3.07119,-3.13767 4.791545,-1.34061 1.725212,1.80213 -1.478794,2.25565 -1.527702,4.74997 -0.04877,2.4873 3.137671,3.07119 1.340613,4.79155 z" /></g></g></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

144
werewolves/img/hunter.svg Normal file
View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24.399992mm"
height="24.399998mm"
viewBox="0 0 24.399992 24.399998"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="0.5"
inkscape:cx="-77"
inkscape:cy="466"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="0"
y="0"
width="24.399992"
height="24.399998"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1"><inkscape:path-effect
effect="mirror_symmetry"
start_point="50.372741,180.48552"
end_point="50.372741,189.138"
center_point="50.372741,184.81176"
id="path-effect166-2"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="free"
discard_orig_path="false"
fuse_paths="true"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /></defs><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-1.6153444,-96.526492)"><g
id="g179"><g
id="g166-2"
inkscape:path-effect="#path-effect166-2"
style="fill:#ff0505;fill-opacity:0.7;stroke:#c20000;stroke-width:0.4;stroke-dasharray:none;stroke-opacity:0.5"
transform="translate(-36.557573,-76.085222)"
inkscape:export-filename="../../src/werewolves/werewolves/img/hunter.svg"
inkscape:export-xdpi="900.08"
inkscape:export-ydpi="900.08"><path
id="path164-8"
style="fill:#ff0505;fill-opacity:0.7;stroke:#c20000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
d="m 50.373047,183.43359 c -1.534096,0.26874 -1.029204,1.24619 -1.832031,2.63672 -0.65564,1.13559 -1.854319,1.39291 -1.029297,2.41211 0.94012,1.1614 1.512735,0.45231 2.861328,0.33008 1.348593,0.12223 1.921208,0.83132 2.861328,-0.33008 0.825022,-1.0192 -0.373657,-1.27652 -1.029297,-2.41211 -0.802827,-1.39053 -0.297935,-2.36798 -1.832031,-2.63672 z"
inkscape:original-d="m 50.372741,183.43371 c -1.534096,0.26874 -1.028586,1.24549 -1.831413,2.63602 -0.65564,1.13559 -1.854933,1.39253 -1.029911,2.41173 0.94012,1.1614 1.512731,0.45348 2.861324,0.33125 z" /><path
sodipodi:type="star"
style="fill:#ff0505;fill-opacity:0.7;stroke:#c20000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
id="path165-3"
inkscape:flatsided="false"
sodipodi:sides="2"
sodipodi:cx="51.634216"
sodipodi:cy="182.45293"
sodipodi:r1="1.3725731"
sodipodi:r2="0.82291079"
sodipodi:arg1="1.1071487"
sodipodi:arg2="2.677945"
inkscape:rounded="0.5"
inkscape:randomized="0"
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 5.18186,0 c 0.7157,0.35785 0.992016,-0.14395 1.349867,-0.85965 0.35785,-0.7157 0.5935,-1.23783 -0.122201,-1.59568 -0.715701,-0.35785 -0.992016,0.14395 -1.349867,0.85965 -0.35785,0.7157 -0.5935,1.23783 0.122201,1.59568 z"
inkscape:transform-center-x="0.12681959"
inkscape:transform-center-y="0.079724714"
transform="translate(-4.4662386,1.8414355)" /><path
sodipodi:type="star"
style="fill:#ff0505;fill-opacity:0.7;stroke:#c20000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
id="path165-6-8"
inkscape:flatsided="false"
sodipodi:sides="2"
sodipodi:cx="51.634216"
sodipodi:cy="182.45293"
sodipodi:r1="1.3725731"
sodipodi:r2="0.82291079"
sodipodi:arg1="1.1071487"
sodipodi:arg2="2.677945"
inkscape:rounded="0.5"
inkscape:randomized="0"
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 2.374602,-1.10775 c 0.734072,-0.31847 0.527114,-0.85263 0.208644,-1.5867 -0.31847,-0.73407 -0.567138,-1.25013 -1.30121,-0.93166 -0.734072,0.31847 -0.527114,0.85263 -0.208644,1.5867 0.31847,0.73407 0.567138,1.25013 1.30121,0.93166 z"
inkscape:transform-center-x="0.14863452"
inkscape:transform-center-y="0.01863483"
transform="rotate(25.009099,51.670619,176.27381)" /></g><g
id="g178"><path
id="path166-0"
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 13.815426,98.72668 c -5.522776,5e-5 -10.000374,4.47713 -10.000423,9.99991 4.9e-5,5.52277 4.477647,9.99985 10.000423,9.9999 5.522777,-5e-5 9.999857,-4.47713 9.999907,-9.9999 -5e-5,-5.52278 -4.47713,-9.99986 -9.999907,-9.99991 z m 0,1.99988 c 4.418297,-2e-5 8.000047,3.58172 8.000027,8.00003 2e-5,4.4183 -3.58173,8.00004 -8.000027,8.00002 -4.418301,2e-5 -8.000045,-3.58172 -8.000029,-8.00002 -1.6e-5,-4.41831 3.581728,-8.00005 8.000029,-8.00003 z" /><rect
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect167-8-0-4"
width="1.5"
height="6"
x="-109.47657"
y="19.815336"
transform="rotate(-90)"
ry="0" /><rect
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect167-8-0-9-2"
width="1.5"
height="6"
x="-109.47657"
y="1.8153445"
transform="rotate(-90)"
ry="0" /><rect
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect167-8-0-1-2"
width="1.5"
height="6"
x="-14.56517"
y="-120.72649"
transform="scale(-1)"
ry="0" /><rect
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect167-8-0-1-7-2"
width="1.5"
height="6"
x="-14.56517"
y="-102.72649"
transform="scale(-1)"
ry="0" /></g></g></g></svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

56
werewolves/img/li.svg Normal file
View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="21.653456mm"
height="25.003128mm"
viewBox="0 0 21.653456 25.003128"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="4"
inkscape:cx="95.125"
inkscape:cy="747.25"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="-1.7160703e-13"
y="1.0183469e-14"
width="21.653458"
height="25.003126"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-21.652939,-198.29178)"><path
id="path135"
style="fill:#bb07ff;fill-opacity:0.5;stroke-width:0.15"
d="m 21.652942,198.29177 v 3.06235 0.67024 17.53795 0.33693 3.39566 l 21.653459,-12.50157 z m 3.227193,5.59604 11.961027,6.90552 -11.961027,6.90553 z"
inkscape:export-filename="../../src/werewolves/werewolves/img/li.svg"
inkscape:export-xdpi="900.08"
inkscape:export-ydpi="900.08" /><path
d="m 24.88,203.888 v 13.81103 l 11.961027,-6.9055 z"
style="fill:#b906ff;fill-opacity:0.25;stroke-width:0.15"
id="path136" /></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16.688932mm"
height="14.296382mm"
viewBox="0 0 16.688932 14.296382"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="0.5"
inkscape:cx="-77"
inkscape:cy="466"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="0"
y="0"
width="16.688932"
height="14.296382"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1"><inkscape:path-effect
effect="mirror_symmetry"
start_point="88.608019,180.36521"
end_point="88.608019,194.83255"
center_point="88.608019,187.59888"
id="path-effect171"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="free"
discard_orig_path="false"
fuse_paths="true"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="50.372741,180.48552"
end_point="50.372741,189.138"
center_point="50.372741,184.81176"
id="path-effect166-9"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="free"
discard_orig_path="false"
fuse_paths="true"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /></defs><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-66.137764,-154.64787)"><path
id="path168"
style="fill:#dd2e44;fill-opacity:1;stroke:#8c1725;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 88.607422,182.44336 -0.701172,3.80859 -1.953125,-1.31836 1.234375,4.75782 -4.384766,-4.51172 0.972657,2.91797 -3.3125,-1.20899 2.748046,2.58789 -2.310546,0.55664 4.65039,2.92969 -1.697265,1.86914 4.134765,-0.27148 v 1.77929 l 0.619141,-0.004 0.621094,0.004 v -1.77929 l 4.134765,0.27148 -1.697265,-1.86914 4.65039,-2.92969 -2.310547,-0.55664 2.746094,-2.58789 -3.310547,1.20899 0.972656,-2.91797 -4.386718,4.51172 1.234375,-4.75782 -1.953125,1.31836 z"
inkscape:path-effect="#path-effect171"
inkscape:original-d="m 88.655,182.18911 -0.747931,4.06282 -1.953642,-1.31777 1.23441,4.75775 -4.385923,-4.51236 0.973124,2.91846 -3.311647,-1.21027 2.747117,2.58892 -2.310287,0.55711 4.650449,2.92913 -1.697906,1.86965 4.134651,-0.27147 v 1.77805 l 1.207698,-0.007 -0.281059,-1.89723 z"
sodipodi:nodetypes="cccccccccccccccc"
inkscape:label="maple"
inkscape:export-filename="../../src/werewolves/werewolves/img/maple-wolf.svg"
inkscape:export-xdpi="900.08"
inkscape:export-ydpi="900.08"
transform="translate(-14.125194,-27.595579)" /><g
id="g166-0"
inkscape:path-effect="#path-effect166-9"
style="fill:#0f07ff;fill-opacity:0.5;stroke:#05009e;stroke-width:0.47619;stroke-dasharray:none;stroke-opacity:0.5"
transform="matrix(0.84,0,0,0.84,32.089386,7.544321)"><path
id="path164-5"
style="fill:#0f07ff;fill-opacity:0.5;stroke:#05009e;stroke-width:0.47619;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
d="m 50.373047,183.43359 c -1.534096,0.26874 -1.029204,1.24619 -1.832031,2.63672 -0.65564,1.13559 -1.854319,1.39291 -1.029297,2.41211 0.94012,1.1614 1.512735,0.45231 2.861328,0.33008 1.348593,0.12223 1.921208,0.83132 2.861328,-0.33008 0.825022,-1.0192 -0.373657,-1.27652 -1.029297,-2.41211 -0.802827,-1.39053 -0.297935,-2.36798 -1.832031,-2.63672 z"
inkscape:original-d="m 50.372741,183.43371 c -1.534096,0.26874 -1.028586,1.24549 -1.831413,2.63602 -0.65564,1.13559 -1.854933,1.39253 -1.029911,2.41173 0.94012,1.1614 1.512731,0.45348 2.861324,0.33125 z" /><path
sodipodi:type="star"
style="fill:#0f07ff;fill-opacity:0.5;stroke:#05009e;stroke-width:0.47619;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
id="path165-0"
inkscape:flatsided="false"
sodipodi:sides="2"
sodipodi:cx="51.634216"
sodipodi:cy="182.45293"
sodipodi:r1="1.3725731"
sodipodi:r2="0.82291079"
sodipodi:arg1="1.1071487"
sodipodi:arg2="2.677945"
inkscape:rounded="0.5"
inkscape:randomized="0"
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 5.18186,0 c 0.7157,0.35785 0.992016,-0.14395 1.349867,-0.85965 0.35785,-0.7157 0.5935,-1.23783 -0.122201,-1.59568 -0.715701,-0.35785 -0.992016,0.14395 -1.349867,0.85965 -0.35785,0.7157 -0.5935,1.23783 0.122201,1.59568 z"
inkscape:transform-center-x="0.12681959"
inkscape:transform-center-y="0.079724714"
transform="translate(-4.4662386,1.8414355)" /><path
sodipodi:type="star"
style="fill:#0f07ff;fill-opacity:0.5;stroke:#05009e;stroke-width:0.47619;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
id="path165-6-2"
inkscape:flatsided="false"
sodipodi:sides="2"
sodipodi:cx="51.634216"
sodipodi:cy="182.45293"
sodipodi:r1="1.3725731"
sodipodi:r2="0.82291079"
sodipodi:arg1="1.1071487"
sodipodi:arg2="2.677945"
inkscape:rounded="0.5"
inkscape:randomized="0"
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 2.374602,-1.10775 c 0.734072,-0.31847 0.527114,-0.85263 0.208644,-1.5867 -0.31847,-0.73407 -0.567138,-1.25013 -1.30121,-0.93166 -0.734072,0.31847 -0.527114,0.85263 -0.208644,1.5867 0.31847,0.73407 0.567138,1.25013 1.30121,0.93166 z"
inkscape:transform-center-x="0.14863452"
inkscape:transform-center-y="0.01863483"
transform="rotate(25.009099,51.670619,176.27381)" /></g></g></svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

80
werewolves/img/moon.svg Normal file
View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="50.149998mm"
height="50.150002mm"
viewBox="0 0 50.149998 50.150002"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="2"
inkscape:cx="92.75"
inkscape:cy="546.5"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="0"
y="0"
width="50.149998"
height="50.150002"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-4.4921871,-117.73386)"><g
id="g172"
transform="translate(-7.14375,-0.396875)"><circle
style="fill:#cfd099;fill-opacity:1;stroke:#ffffff;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
id="path76"
cx="36.710938"
cy="143.20573"
r="25" /><path
id="path99"
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.200209"
d="m 38.581863,146.75538 c -1.916877,1.18904 2.453505,2.42082 0.462124,0.43584 z m 4.389463,2.77679 c 0.137019,2.01253 -0.0156,4.7805 -2.550624,2.73406 -2.861437,0.11654 0.195479,3.17733 -1.523948,3.39965 -0.92667,3.02839 5.356204,3.68705 4.890562,-0.0422 0.53779,-2.01884 0.406862,-4.33848 -0.81599,-6.09151 z m 0.674308,9.56386 c -0.803242,0.7159 1.034521,0.46688 0,0 z" /><path
d="m 51.442339,133.50762 c -5.397597,0.56281 -10.551279,3.63338 -13.382225,8.29965 2.124451,1.1777 3.300948,0.84485 4.632117,-1.20688 2.551245,-1.70683 7.705503,4.70413 6.019673,-0.60498 -3.580177,-0.9319 -4.108073,-5.59673 0.261566,-5.11191 1.037773,0.5368 6.145034,-0.98564 2.468869,-1.37588 z m -2.801896,6.55774 c 0.531865,0.82548 -0.977438,-0.12296 0,0 z"
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
id="path112" /><path
d="m 47.385221,123.15268 c -2.116527,-0.3773 -0.374219,4.06696 0.12442,2.12772 0.315898,-1.44874 1.060285,1.80984 2.221196,0.90171 1.889911,-1.11878 0.27636,-3.33696 -1.527064,-2.87862 -0.275754,-0.0313 -0.551969,-0.0713 -0.818552,-0.15081 z"
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
id="path111" /><path
d="m 37.685038,119.98854 c -3.108408,2.92746 3.876651,0.26308 0.283578,0.27699 l -0.163948,-0.0794 z"
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
id="path110" /><path
id="path109"
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
d="m 32.397523,124.98657 c -1.520733,-2.58364 -1.771355,1.74661 0,0 z m -2.111335,-0.81943 c -0.420973,-0.68681 -0.277666,0.72793 0,0 z m -1.369925,0.2192 c -0.736577,-0.45274 0.113448,0.83066 0,0 z m 0.864925,-2.83663 c -2.403682,-1.73005 -7.217942,3.67933 -4.141043,4.75194 2.481815,0.24594 3.0505,-3.21526 4.141043,-4.75194 z m -2.934509,6.73595 c -0.382718,-1.12289 -0.773242,0.75887 0,0 z m -1.987823,-4.90838 c 0.800465,-0.75074 -0.699944,0.19494 0,0 z m -1.024981,1.17776 c -1.210082,-0.16455 0.156879,1.24643 0,0 z m -0.931645,1.49925 c -1.598736,0.091 -0.357093,1.80583 0,0 z"
inkscape:transform-center-x="-2.2830464"
inkscape:transform-center-y="-0.34255434" /><path
d="m 49.749135,150.49283 c 0.406653,2.6723 -0.700738,5.52316 0.490654,8.07174 1.038538,1.49687 2.463402,-0.25164 3.656675,-0.36492 1.343063,0.19135 2.990671,-0.35794 2.841528,-1.98822 0.208749,-1.45986 0.414602,-3.01659 -0.454517,-4.33018 -1.013182,-1.77262 -2.945795,-3.11543 -5.010966,-3.17524 -0.833932,0.14614 -1.55328,0.92034 -1.523374,1.78682 z m 3.944243,-0.76662 c 0.606103,-0.0492 -0.41019,0.42034 0,0 z m -2.130915,6.41908 c 0.979395,0.87352 -1.618533,0.77939 -0.243726,0.0418 z m 4.670422,-3.31817 c 0.654926,-0.61253 -0.03189,0.78519 0,0 z"
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
id="path101" /><path
id="path97"
style="fill:#80813b;fill-opacity:1;stroke:none;stroke-width:0.231443"
d="m 41.903566,125.93725 c -6.044997,-0.64486 -8.64443,8.42233 -2.19329,10.10398 3.792226,2.96513 10.687212,2.61967 12.734453,-2.19823 2.1165,-7.75207 -9.058032,-2.72396 -10.541163,-7.90575 z m -15.060723,2.99137 c -5.528617,-3.05247 -10.922749,6.92225 -5.197617,9.72036 4.862793,2.09409 10.803366,-4.70252 6.587612,-8.7891 l -0.646271,-0.52979 z m 10.777872,7.1222 c -2.278654,1.80276 2.469375,0.14035 0,0 z m -4.303958,2.37974 c -2.574255,1.2736 2.388024,0.82299 0,0 z m -0.963816,4.17231 c -4.785091,0.19047 3.229165,5.66543 0.667405,0.56979 z m 4.893345,0.43918 c -1.855402,0.88455 1.710607,0.24689 0,0 z m -15.056031,7.1523 c -4.525471,3.17001 0.856774,8.93027 5.149694,7.52189 3.500644,-1.07544 10.261668,1.14637 11.357974,-3.16195 -3.000623,-4.60817 -9.108436,0.13477 -13.410645,-2.25697 -1.192258,-0.41483 -2.282793,-1.13383 -3.097023,-2.10297 z m 16.129285,0.72871 c -1.725064,-2.16335 0.0097,1.68564 0,0 z m -9.224662,9.24983 c -2.494907,3.43877 4.629782,0.13898 0,0 z" /><path
d="m 13.749471,143.49274 c -1.556131,4.09194 5.086128,3.65147 6.733592,6.11814 4.00606,-0.92451 -2.800253,-3.95742 0.489662,-6.1443 2.381473,-1.43043 1.913537,-4.55032 -1.152068,-3.69537 -2.409897,0.40461 -4.454496,1.96416 -6.071186,3.72153 z"
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
id="path1" /></g></g></svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="22.67716mm"
height="10.522518mm"
viewBox="0 0 22.67716 10.522518"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="0.5"
inkscape:cx="-77"
inkscape:cy="466"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="0"
y="1.3155397e-14"
width="22.67716"
height="10.522517"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-86.867671,-166.75493)"><g
id="g176"><g
id="g75-4"
transform="matrix(0.14,0,0,0.14,77.802364,152.21471)"><circle
style="fill:#54ffff;fill-opacity:0.146665;stroke:#00adc1;stroke-width:2.348;stroke-opacity:1"
id="path9-1"
cx="145.74207"
cy="141.43904"
r="25"
inkscape:export-filename="../../src/werewolves/werewolves/img/powerful.svg"
inkscape:export-xdpi="900.08"
inkscape:export-ydpi="900.08" /><path
style="fill:#00adc1;fill-opacity:1;stroke:#00adc1;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 146.7354,116.77348 -12.81832,29.07761 11.35727,-0.99768 -9.76266,20.13334 23.61744,-27.67112 -15.11625,2.07403 12.86097,-20.49482 z"
id="path10-2"
sodipodi:nodetypes="cccccccc" /></g><path
d="m 98.447322,166.75526 c -6.10441,-0.0452 -10.77234,4.43526 -11.57965,5.26015 0.8552,0.87172 5.50621,5.3067 11.57965,5.26169 6.216538,-0.19268 10.307138,-4.38351 11.097508,-5.26118 -0.996,-1.0793 -5.19612,-5.21642 -11.098028,-5.26014 z m -0.24081,1.26091 a 4,4 0 0 1 3.999748,3.99975 4,4 0 0 1 -3.999748,4.00028 4,4 0 0 1 -4.00027,-4.00028 4,4 0 0 1 4.00027,-3.99975 z m -6.69985,2.99982 a 1,1 0 0 1 0.99994,0.99993 1,1 0 0 1 -0.99994,0.99994 1,1 0 0 1 -0.99994,-0.99994 1,1 0 0 1 0.99994,-0.99993 z m 13.400208,0 a 1,1 0 0 1 0.99994,0.99993 1,1 0 0 1 -0.99994,0.99994 1,1 0 0 1 -0.99993,-0.99994 1,1 0 0 1 0.99993,-0.99993 z"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#00adc1;fill-opacity:1;enable-background:accumulate;stop-color:#000000"
id="path124-6" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

50
werewolves/img/seer.svg Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="22.677168mm"
height="10.522534mm"
viewBox="0 0 22.677168 10.522534"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="22.627417"
inkscape:cx="300.80764"
inkscape:cy="633.52348"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="0"
y="1.2392502e-14"
width="22.677168"
height="10.522533"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-133.55426,-135.5815)"><path
d="m 145.13392,135.58183 c -6.10441,-0.0452 -10.77234,4.43526 -11.57965,5.26015 0.8552,0.87172 5.50621,5.3067 11.57965,5.26169 6.21654,-0.19268 10.30714,-4.38351 11.09751,-5.26118 -0.996,-1.0793 -5.19612,-5.21642 -11.09803,-5.26014 z m -0.24081,1.26091 a 4,4 0 0 1 3.99975,3.99975 4,4 0 0 1 -3.99975,4.00028 4,4 0 0 1 -4.00027,-4.00028 4,4 0 0 1 4.00027,-3.99975 z m -6.69985,2.99982 a 1,1 0 0 1 0.99994,0.99993 1,1 0 0 1 -0.99994,0.99994 1,1 0 0 1 -0.99994,-0.99994 1,1 0 0 1 0.99994,-0.99993 z m 13.40021,0 a 1,1 0 0 1 0.99994,0.99993 1,1 0 0 1 -0.99994,0.99994 1,1 0 0 1 -0.99993,-0.99994 1,1 0 0 1 0.99993,-0.99993 z"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#0f07ff;enable-background:accumulate;stop-color:#000000"
id="path124" /></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="21.913561mm"
height="34.790867mm"
viewBox="0 0 21.913561 34.790867"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="22.627417"
inkscape:cx="300.80764"
inkscape:cy="633.52348"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="0"
y="0"
width="21.913561"
height="34.790867"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-66.647994,-150.10879)"><path
id="path153-4"
style="fill:#675e00;fill-opacity:1;stroke:#957700;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 85.596185,159.31129 c -1.66567,-0.0492 -4.25335,0.61677 -7.991239,1.07073 -3.737893,-0.45396 -6.326587,-1.1194 -7.992277,-1.07022 -2.77615,0.082 -2.990253,2.15033 -1.701188,10.50014 1.120954,7.26088 7.414595,11.26672 9.693465,14.58774 2.278869,-3.32102 8.572509,-7.32686 9.693469,-14.58774 1.28906,-8.34981 1.0739,-10.41872 -1.70223,-10.50065 z"
inkscape:export-filename="../../src/werewolves/werewolves/img/shield-and-sword.svg"
inkscape:export-xdpi="900.08"
inkscape:export-ydpi="900.08" /><path
style="fill:#9d9a00;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 83.045329,161.77646 c -0.283124,0.54265 0.392812,4.5956 0,8.13594 -0.392763,3.5399 -5.097171,11.89492 -5.097171,11.89492 4.172167,-3.05047 7.003337,-7.94259 7.477566,-8.87397 1.558474,-3.06081 1.883779,-10.92635 1.27574,-11.46423 -0.700643,-0.6198 -3.342424,-0.29394 -3.656135,0.30734 z"
id="path156"
sodipodi:nodetypes="sscsss" /><path
id="path4-9-8"
style="fill:#e1e1e1;fill-opacity:1;stroke-width:0.679044"
inkscape:transform-center-x="-0.11920648"
inkscape:transform-center-y="-9.0503731"
d="m 74.576497,155.93257 v 20.3295 l 2.991999,4.80224 2.992334,-4.80224 v -20.3295 z"
sodipodi:nodetypes="cccccc" /><path
id="path4-1-2-1"
style="fill:#000000;fill-opacity:0.504713;stroke-width:0.339521"
inkscape:transform-center-x="-0.11920648"
inkscape:transform-center-y="-9.0503731"
d="m 79.00946,156.45806 h -2.881013 l 1.440049,22.4915 z" /><path
id="path159"
style="fill:#7f6600;fill-opacity:1;stroke-width:0.339521"
inkscape:transform-center-x="-0.11920648"
inkscape:transform-center-y="-9.0503731"
d="m 76.626193,150.59392 v 5.3295 h -2.935667 c -0.178214,0 -0.178214,0.53464 0,0.53464 0.02659,0 7.755941,0 7.755941,0 0.2661,0 0.283695,-0.53464 0,-0.53464 h -2.935333 v -5.3295 c 0,-0.59902 -1.884941,-0.693 -1.884941,0 z"
sodipodi:nodetypes="zcsssscsz" /></g></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

71
werewolves/img/shield.svg Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="21.913561mm"
height="26.091125mm"
viewBox="0 0 21.913561 26.091125"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="2"
inkscape:cx="307.25"
inkscape:cy="795"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="4.4822163e-22"
y="0"
width="21.913561"
height="26.091125"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-69.386984,-204.0026)"><g
id="g156"><path
id="path153-4"
style="fill:#fff001;fill-opacity:1;stroke:#daaf00;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 106.9933,181.0502 c -1.66567,-0.0492 -4.25335,0.61677 -7.991239,1.07073 -3.737893,-0.45396 -6.326587,-1.1194 -7.992277,-1.07022 -2.77615,0.082 -2.990253,2.15033 -1.701188,10.50014 1.120954,7.26088 7.414595,11.26672 9.693465,14.58774 2.278869,-3.32102 8.572509,-7.32686 9.693469,-14.58774 1.28906,-8.34981 1.0739,-10.41872 -1.70223,-10.50065 z"
transform="translate(-18.658128,23.45515)" /><path
style="fill:#fffbae;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 85.724999,207.43333 c -0.283124,0.54265 0.921927,4.69525 0,8.13594 -0.92193,3.44068 -5.097171,11.89492 -5.097171,11.89492 4.172167,-3.05047 7.003337,-7.94259 7.477566,-8.87397 1.558474,-3.06081 1.883779,-10.92635 1.27574,-11.46423 -0.700643,-0.6198 -3.342424,-0.29394 -3.656135,0.30734 z"
id="path156"
sodipodi:nodetypes="sscsss" /><path
sodipodi:type="star"
style="fill:#7f6500;fill-opacity:1;stroke:#daaf00;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path155-0"
inkscape:flatsided="false"
sodipodi:sides="6"
sodipodi:cx="99.020317"
sodipodi:cy="191.42604"
sodipodi:r1="7.1881213"
sodipodi:r2="4.5083508"
sodipodi:arg1="1.906205"
sodipodi:arg2="1.8672678"
inkscape:rounded="0.5"
inkscape:randomized="0"
d="m 96.65431,198.21361 c -1.309919,0.3028 1.927067,-1.45786 1.048904,-2.4759 -2.111403,-2.44772 -3.538875,-0.60338 -5.744108,-2.96691 -0.917188,-0.98302 2.22608,0.93996 2.668647,-0.32957 1.064081,-3.05239 -1.2469,-3.36644 -0.30264,-6.458 0.392731,-1.28582 0.299014,2.39782 1.619743,2.14633 3.175484,-0.60467 2.291975,-2.76307 5.441464,-3.49109 1.30992,-0.30279 -1.927062,1.45787 -1.0489,2.4759 2.1114,2.44772 3.53888,0.60338 5.74411,2.96691 0.91719,0.98302 -2.22608,-0.93996 -2.66865,0.32957 -1.06408,3.05239 1.2469,3.36645 0.30264,6.458 -0.39273,1.28582 -0.29901,-2.39782 -1.61974,-2.14633 -3.175485,0.60467 -2.291977,2.76307 -5.44147,3.49109 z"
transform="translate(-18.678228,23.718064)" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

72
werewolves/img/skull.svg Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="45.428978mm"
height="54.734951mm"
viewBox="0 0 45.428978 54.734951"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="1.4142136"
inkscape:cx="204.00031"
inkscape:cy="749.53319"
inkscape:window-width="1918"
inkscape:window-height="1061"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer4"><inkscape:page
x="0"
y="0"
width="45.428978"
height="54.734951"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1"><inkscape:path-effect
effect="mirror_symmetry"
start_point="145.93901,200.00018"
end_point="145.93901,257.23118"
center_point="145.93901,228.61568"
id="path-effect146"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="free"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /></defs><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-102.64477,-201.30981)"><path
d="m 145.93901,249.87364 c 0,0 -0.0303,-0.0367 -0.44909,0.0563 -0.20617,2.38398 1.11335,4.49732 -1.63951,4.79143 -1.66784,0.17819 -1.24406,-1.39832 -1.40619,-3.31064 -0.30255,-1.20853 -0.24531,-1.39312 -1.20877,-0.90551 -0.62396,0.31579 0.82573,3.70676 -1.30627,4.01151 -3.39198,0.48485 -0.69804,-6.45466 -5.14039,-9.8645 -1.63686,-2.03072 -4.33202,-1.51761 -6.56291,-1.96319 -2.53079,-1.12277 -4.05741,-3.94017 -4.44572,-6.62182 -0.7119,-4.67507 -0.64763,-9.45558 -0.31885,-14.16244 -1.53814,-15.82349 6.47812,-17.65127 15.17169,-20.63595 2.27655,-0.8108 4.68806,-1.22375 7.10241,-1.26865 0.0675,0 0.13527,2e-5 0.2036,5.1e-4 v 35.63039 c -1.07599,0.10793 -1.42136,1.92498 -2.91817,3.88142 -1.38429,2.50152 0.8238,3.48208 2.04897,2.41535 0.78196,-0.62623 0.8692,-0.65371 0.8692,-0.65371 z m -17.67696,-23.3593 c 0.4207,5.31835 3.21309,5.55909 6.71301,5.06471 4.00495,0.15448 8.18296,-1.56893 5.86969,-7.12815 -0.41537,-1.18035 -2.43304,-4.29435 -7.09899,-3.3542 -3.93834,0.60943 -5.53376,3.75721 -5.48371,5.41764 z m 17.67696,23.3593 c 0,0 0.0303,-0.0367 0.44909,0.0563 0.20617,2.38398 -1.11335,4.49732 1.63951,4.79143 1.66784,0.17819 1.24406,-1.39832 1.40619,-3.31064 0.30255,-1.20853 0.24531,-1.39312 1.20877,-0.90551 0.62396,0.31579 -0.82573,3.70676 1.30627,4.01151 3.39198,0.48485 0.69804,-6.45466 5.14039,-9.8645 1.63686,-2.03072 4.33202,-1.51761 6.56291,-1.96319 2.53079,-1.12277 4.05741,-3.94017 4.44572,-6.62182 0.7119,-4.67507 0.64763,-9.45558 0.31885,-14.16244 1.53814,-15.82349 -6.47812,-17.65127 -15.17169,-20.63595 -2.27655,-0.8108 -4.68806,-1.22375 -7.10241,-1.26865 -0.0675,0 -0.13527,2e-5 -0.2036,5.1e-4 v 35.63039 c 1.07599,0.10793 1.42136,1.92498 2.91817,3.88142 1.38429,2.50152 -0.8238,3.48208 -2.04897,2.41535 -0.78196,-0.62623 -0.8692,-0.65371 -0.8692,-0.65371 z m 17.67696,-23.3593 c -0.4207,5.31835 -3.21309,5.55909 -6.71301,5.06471 -4.00495,0.15448 -8.18296,-1.56893 -5.86969,-7.12815 0.41537,-1.18035 2.43304,-4.29435 7.09899,-3.3542 3.93834,0.60943 5.53376,3.75721 5.48371,5.41764 z"
style="fill:#a0a0a0;fill-opacity:1;stroke-width:0.161404"
id="path145"
inkscape:original-d="m 145.93901,249.87364 c 0,0 -0.0303,-0.0367 -0.44909,0.0563 -0.20617,2.38398 1.11335,4.49732 -1.63951,4.79143 -1.66784,0.17819 -1.24406,-1.39832 -1.40619,-3.31064 -0.30255,-1.20853 -0.24531,-1.39312 -1.20877,-0.90551 -0.62396,0.31579 0.82573,3.70676 -1.30627,4.01151 -3.39198,0.48485 -0.69804,-6.45466 -5.14039,-9.8645 -1.63686,-2.03072 -4.33202,-1.51761 -6.56291,-1.96319 -2.53079,-1.12277 -4.05741,-3.94017 -4.44572,-6.62182 -0.7119,-4.67507 -0.64763,-9.45558 -0.31885,-14.16244 -1.53814,-15.82349 6.47812,-17.65127 15.17169,-20.63595 2.27655,-0.8108 4.68806,-1.22375 7.10241,-1.26865 0.0675,0 0.13527,2e-5 0.2036,5.1e-4 v 35.63039 c -1.07599,0.10793 -1.42136,1.92498 -2.91817,3.88142 -1.38429,2.50152 0.8238,3.48208 2.04897,2.41535 0.78196,-0.62623 0.8692,-0.65371 0.8692,-0.65371 z m -17.67696,-23.3593 c 0.4207,5.31835 3.21309,5.55909 6.71301,5.06471 4.00495,0.15448 8.18296,-1.56893 5.86969,-7.12815 -0.41537,-1.18035 -2.43304,-4.29435 -7.09899,-3.3542 -3.93834,0.60943 -5.53376,3.75721 -5.48371,5.41764 z"
inkscape:path-effect="#path-effect146"
transform="translate(-20.579754,1.3096207)"
sodipodi:nodetypes="ccscssccccccccccccccccc"
inkscape:export-filename="../../src/werewolves/werewolves/img/skull;.svg"
inkscape:export-xdpi="900.08"
inkscape:export-ydpi="900.08" /></g></svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

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

View File

@ -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<CharacterIdentity>, ActionResult),
Story {
story: GameStory,
page: usize,
},
}
impl From<ServerToHostMessage> for HostEvent {
@ -242,6 +246,10 @@ impl From<ServerToHostMessage> 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<Self>) -> 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! {
<div class="post-game">
<Story story={story} />
<Button on_click={new_lobby_click}>{"new lobby"}</Button>
</div>
}
}
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"}
</CoverOfDarkness>
}
}
@ -449,12 +472,20 @@ impl Component for Host {
<Button on_click={to_big}>{"big screen 📺"}</Button>
}
};
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! {
<nav class="debug-nav" style="z-index: 3;">
<Button on_click={on_error_click}>{"error"}</Button>
<Button on_click={on_prev_click}>{"previous"}</Button>
{screen}
<Button on_click={client_click}>{"client"}</Button>
<Button on_click={story_on_click}>{"story"}</Button>
</nav>
}
});
@ -468,7 +499,7 @@ impl Component for Host {
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
fn update(&mut self, _ctx: &Context<Self>, 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 {

View File

@ -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::<Box<[_]>>();
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<RoleTitle>;
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<RoleTitle> {
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()
}
}

View File

@ -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! {
<div class="content">
<Story story={story}/>
</div>
}
}
}

View File

@ -42,8 +42,8 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
.then(|| html! {<Button on_click={on_complete}>{"continue"}</Button>});
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! {
<>
<h1>{"your target..."}</h1>
<Icon source={IconSource::Killer} inactive={!*killer}/>
<Icon source={IconSource::Killer} inactive={killer.killer().not()}/>
<h2>{text}</h2>
</>
}
@ -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! {
<>
<div class="arcanist-result">

View File

@ -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! {
<span class={classes!("attribute-span", "faint", class)}>
<div>
<Icon source={alignment.icon()} icon_type={IconType::Small}/>
</div>
{alignment.to_string()}
</span>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct CategorySpanProps {
pub category: Category,
#[prop_or_default]
pub icon: Option<IconSource>,
#[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! {
<span class={classes!("attribute-span", "faint", class)}>
<div>
<Icon source={icon} icon_type={IconType::Small}/>
</div>
{children.clone()}
</span>
}
}

View File

@ -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! {
<span class="alignment-eq">
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
{"the same"}
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
</span>
},
AlignmentEq::Different => html! {
<span class="alignment-eq">
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
{"different"}
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
</span>
},
}
}

View File

@ -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! {
<span class={classes!("attribute-span", "faint", class)}>
<div>
<Icon source={icon} icon_type={IconType::Small}/>
</div>
{died_to.to_string().to_case(Case::Title)}
</span>
}
}

View File

@ -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! {
<span class={classes!("attribute-span", "faint")}>
<div class={classes!(class)}>
<Icon source={killer.icon()} icon_type={IconType::Small}/>
</div>
{killer.to_string()}
</span>
}
}

View File

@ -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! {
<span class={classes!("attribute-span", "faint")}>
<div class={classes!(class)}>
<Icon source={powerful.icon()} icon_type={IconType::Small}/>
</div>
{powerful.to_string()}
</span>
}
}

View File

@ -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::<SetupRole>::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! {
<span class="pronouns">{"("}{p}{")"}</span>
}
});
let name = ident.name.clone();
let source = char.role_title().icon().unwrap_or(if char.is_village() {
IconSource::Village
} else {
IconSource::Wolves
});
html! {
<span class={classes!("character-span", class)}>
<span class={classes!("role", class)}>
<div>
<Icon source={source} icon_type={IconType::Small}/>
</div>
{role}
</span>
<span class={classes!("number")}><b>{ident.number.get()}</b></span>
<span class="name">{name}</span>
{pronouns}
</span>
}
}

View File

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

View File

@ -124,8 +124,8 @@ pub fn SetupCategory(
<span class="count">{count}</span>
}
});
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",

View File

@ -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<IconSource>;
}
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<IconSource> {
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<IconSource> {
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,
}
}
}

View File

@ -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::<HashMap<CharacterId, Character>>(),
);
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::<HashMap<CharacterId, Character>>())).unwrap_or_else(|| characters.clone());
let changes = match changes {
GameActions::DayDetails(day_changes) => {
let execute_list = if day_changes.is_empty() {
html! {<span>{"no one was executed"}</span>}
} 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! {
// <span>
<CharacterCard char={c.clone()}/>
// </span>
}
})
.collect::<Html>()
};
Some(html! {
<div class="day">
<h3>{"village executed"}</h3>
<div class="executed">
{execute_list}
</div>
</div>
})
}
GameActions::NightDetails(details) => details.choices.is_empty().not().then_some({
let choices = details
.choices
.iter()
.map(|c| {
html! {
<StoryNightChoice choice={c.clone()} characters={characters.clone()}/>
}
})
.collect::<Html>();
let changes = details
.changes
.iter()
.map(|c| {
html! {
<li>
<StoryNightChange change={c.clone()} characters={characters.clone()}/>
</li>
}
})
.collect::<Html>();
html! {
<div class="night">
<label>{"choices"}</label>
<ul class="choices">
{choices}
</ul>
<label>{"changes"}</label>
<ul class="changes">
{changes}
</ul>
</div>
}
}),
};
changes
.map(|changes| {
html! {
<div class="time-period">
<h1>{"on "}{time.to_string()}{"..."}</h1>
{changes}
</div>
}
})
.unwrap_or_default()
})
.collect::<Html>();
html! {
<div class="story">
{bits}
</div>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
struct StoryNightChangeProps {
change: NightChange,
characters: Rc<HashMap<CharacterId, Character>>,
}
#[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! {
<>
<CharacterCard char={char.clone()}/>
{"is now"}
<CharacterCard char={new_char.clone()}/>
</>
}
})
.unwrap_or_default(),
NightChange::Kill { target, died_to } => characters
.get(target)
.map(|target| {
html! {
<>
<Icon source={IconSource::Skull} icon_type={IconType::Small}/>
<CharacterCard char={target.clone()}/>
{"died to"}
<DiedToSpan died_to={died_to.title()}/>
</>
}
})
.unwrap_or_default(),
NightChange::RoleBlock { source, target, .. } => characters
.get(source)
.and_then(|s| characters.get(target).map(|t| (s, t)))
.map(|(source, target)| {
html! {
<>
<CharacterCard char={source.clone()}/>
{"role blocked"}
<CharacterCard char={target.clone()}/>
</>
}
})
.unwrap_or_default(),
NightChange::Shapeshift { source, into } => characters
.get(source)
.and_then(|s| characters.get(into).map(|i| (s, i)))
.map(|(source, into)| {
html! {
<>
<CharacterCard char={source.clone()}/>
{"shapeshifted into"}
<CharacterCard char={into.clone()}/>
</>
}
})
.unwrap_or_default(),
NightChange::ElderReveal { elder } => characters
.get(elder)
.map(|elder| {
html! {
<>
<CharacterCard char={elder.clone()}/>
{"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! {
<>
<CharacterCard char={empath.clone()}/>
{"found the scapegoat in"}
<CharacterCard char={scapegoat.clone()}/>
{"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<HashMap<CharacterId, Character>>,
}
#[function_component]
fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
match result {
StoryActionResult::RoleBlocked => html! {
<span>{"but was role blocked"}</span>
},
StoryActionResult::Seer(alignment) => {
html! {
<span>
<span>{"and saw"}</span>
<AlignmentSpan alignment={*alignment}/>
</span>
}
}
StoryActionResult::PowerSeer { powerful } => {
html! {
<span>
<span>{"and discovered they are"}</span>
<PowerfulSpan powerful={*powerful}/>
</span>
}
}
StoryActionResult::Adjudicator { killer } => html! {
<span>
<span>{"and saw"}</span>
<KillerSpan killer={*killer}/>
</span>
},
StoryActionResult::Arcanist(same) => html! {
<span>
<span>{"and saw"}</span>
<AlignmentComparisonSpan comparison={*same}/>
</span>
},
StoryActionResult::GraveDigger(None) => html! {
<span>
{"found an empty grave"}
</span>
},
StoryActionResult::GraveDigger(Some(role_title)) => {
let category = Into::<SetupRole>::into(*role_title).category();
html! {
<span>
<span>{"found the body of a"}</span>
<CategorySpan category={category} icon={role_title.icon()}>
{role_title.to_string().to_case(Case::Title)}
</CategorySpan>
</span>
}
}
StoryActionResult::Mortician(died_to_title) => html! {
<>
{"and found the cause of death to be"}
<DiedToSpan died_to={*died_to_title}/>
</>
},
StoryActionResult::Insomniac { visits } => {
let visitors = visits
.iter()
.filter_map(|c| characters.get(c))
.map(|c| {
html! {
<CharacterCard char={c.clone()}/>
}
})
.collect::<Html>();
html! {
{visitors}
}
}
StoryActionResult::Empath { scapegoat: false } => html! {
<>
{"and saw that they are"}
<span class="attribute-span faint">
<div class="inactive">
<Icon source={IconSource::Heart} icon_type={IconType::Small}/>
</div>
{"Not The Scapegoat"}
</span>
</>
},
StoryActionResult::Empath { scapegoat: true } => html! {
<>
{"and saw that they are"}
<span class="attribute-span faint wolves">
<div>
<Icon source={IconSource::Heart} icon_type={IconType::Small}/>
</div>
{"The Scapegoat"}
</span>
</>
},
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
struct StoryNightChoiceProps {
choice: NightChoice,
characters: Rc<HashMap<CharacterId, Character>>,
}
#[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! {
<>
<CharacterCard char={char.clone()}/>
<span>{action}</span>
<CharacterCard char={chosen.clone()}/>
</>
}
})
};
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! {
<>
<CharacterCard char={arcanist.clone()}/>
<span>{"compared"}</span>
<CharacterCard char={chosen1.clone()}/>
<span>{"and"}</span>
<CharacterCard char={chosen2.clone()}/>
</>
}
}),
StoryActionPrompt::MasonsWake { leader, masons } => characters.get(leader).map(|leader| {
let masons = masons
.iter()
.filter_map(|m| characters.get(m))
.map(|c| {
html! {
<CharacterCard char={c.clone()}/>
}
})
.collect::<Html>();
html! {
<>
<CharacterCard char={leader.clone()}/>
<span>{"'s masons"}</span>
{masons}
<span>{"convened in secret"}</span>
</>
}
}),
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! {
<>
<CharacterCard char={char.clone()}/>
<span>{"invited"}</span>
<CharacterCard char={chosen.clone()}/>
<span>{"for dinner"}</span>
</>
}
}),
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! {
<>
<AlignmentSpan alignment={Alignment::Wolves}/>
<span>{"attempted a kill on"}</span>
<CharacterCard char={chosen.clone()} />
</>
}
})
}
StoryActionPrompt::Shapeshifter { character_id } => {
characters.get(character_id).map(|shifter| {
html! {
<>
<CharacterCard char={shifter.clone()} />
<span>{"decided to shapeshift into the wolf kill target"}</span>
</>
}
})
}
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! {
<>
<CharacterCard char={insomniac.clone()} />
<span>{"witnessed visits from"}</span>
</>
}
})
}
};
let result = choice.result.as_ref().map(|result| {
html! {
<StoryNightResult result={result.clone()} characters={characters.clone()}/>
}
});
choice_body
.map(|choice_body| {
html! {
<li>
<Icon source={IconSource::ListItem} icon_type={IconType::Small}/>
{choice_body}
{result}
</li>
}
})
.unwrap_or_default()
}

View File

@ -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<WerewolfError>| cb_clone.send_message(err));
gloo::utils::document().set_title("werewolves");
if path.starts_with("/host/story") {
let host = yew::Renderer::<clients::story_test::StoryTest>::with_root(app_element).render();
return;
}
if path.starts_with("/host") {
let host = yew::Renderer::<Host>::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();