target split, more prompts, improved kills

This commit is contained in:
emilis 2025-09-28 02:13:34 +01:00
parent e704fdca8b
commit d352cfb1ee
No known key found for this signature in database
17 changed files with 1000 additions and 350 deletions

View File

@ -1,14 +1,16 @@
use core::{fmt::Debug, num::NonZeroU8};
use serde::{Deserialize, Serialize};
use werewolves_macros::Extract;
use crate::{game::DateTime, player::CharacterId};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Extract)]
pub enum DiedTo {
Execution {
day: NonZeroU8,
},
#[extract(source as killer)]
MapleWolf {
source: CharacterId,
night: NonZeroU8,
@ -17,13 +19,17 @@ pub enum DiedTo {
MapleWolfStarved {
night: NonZeroU8,
},
#[extract(killer as killer)]
Militia {
killer: CharacterId,
night: NonZeroU8,
},
#[extract(killing_wolf as killer)]
Wolfpack {
killing_wolf: CharacterId,
night: NonZeroU8,
},
#[extract(killer as killer)]
AlphaWolf {
killer: CharacterId,
night: NonZeroU8,
@ -32,12 +38,17 @@ pub enum DiedTo {
into: CharacterId,
night: NonZeroU8,
},
#[extract(killer as killer)]
Hunter {
killer: CharacterId,
night: NonZeroU8,
},
Guardian {
killer: CharacterId,
#[extract(source as killer)]
GuardianProtecting {
source: CharacterId,
protecting: CharacterId,
protecting_from: CharacterId,
protecting_from_cause: Box<DiedTo>,
night: NonZeroU8,
},
}
@ -47,7 +58,13 @@ impl DiedTo {
match self {
DiedTo::Execution { day } => DateTime::Day { number: *day },
DiedTo::Guardian { killer: _, night }
DiedTo::GuardianProtecting {
source: _,
protecting: _,
protecting_from: _,
protecting_from_cause: _,
night,
}
| DiedTo::MapleWolf {
source: _,
night,
@ -55,7 +72,10 @@ impl DiedTo {
}
| DiedTo::MapleWolfStarved { night }
| DiedTo::Militia { killer: _, night }
| DiedTo::Wolfpack { night }
| DiedTo::Wolfpack {
night,
killing_wolf: _,
}
| DiedTo::AlphaWolf { killer: _, night }
| DiedTo::Shapeshift { into: _, night }
| DiedTo::Hunter { killer: _, night } => DateTime::Night {

View File

@ -69,4 +69,8 @@ pub enum GameError {
GameOngoing,
#[error("needs a role reveal")]
NeedRoleReveal,
#[error("no previous state")]
NoPreviousState,
#[error("invalid original kill for guardian guard")]
GuardianInvalidOriginalKill,
}

View File

@ -0,0 +1,292 @@
use core::{
num::NonZeroU8,
ops::{Deref, Not},
};
use super::Result;
use crate::{
diedto::DiedTo,
error::GameError,
game::{Village, kill::taken::Taken, night::NightChange},
player::{CharacterId, Protection},
};
#[derive(Debug, PartialEq)]
pub enum KillOutcome {
Single(CharacterId, DiedTo),
Guarding {
original_killer: CharacterId,
original_target: CharacterId,
original_kill: DiedTo,
guardian: CharacterId,
night: NonZeroU8,
},
}
impl KillOutcome {
pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
match self {
KillOutcome::Single(character_id, died_to) => Ok(village
.character_by_id_mut(&character_id)
.ok_or(GameError::InvalidTarget)?
.kill(died_to)),
KillOutcome::Guarding {
original_killer,
original_target,
original_kill,
guardian,
night,
} => {
// check if guardian exists before we mutably borrow killer, which would
// prevent us from borrowing village to check after.
village
.character_by_id(&guardian)
.ok_or(GameError::InvalidTarget)?;
village
.character_by_id_mut(&original_killer)
.ok_or(GameError::InvalidTarget)?
.kill(DiedTo::GuardianProtecting {
night,
source: guardian.clone(),
protecting: original_target,
protecting_from: original_killer,
protecting_from_cause: Box::new(original_kill.clone()),
});
village
.character_by_id_mut(&guardian)
.ok_or(GameError::InvalidTarget)?
.kill(original_kill);
Ok(())
}
}
}
}
fn resolve_protection(
killer: CharacterId,
killed_with: &DiedTo,
target: &CharacterId,
protection: &Protection,
night: NonZeroU8,
) -> Option<KillOutcome> {
match protection {
Protection::Guardian {
source,
guarding: true,
} => Some(KillOutcome::Guarding {
original_killer: killer,
guardian: source.clone(),
original_target: target.clone(),
original_kill: killed_with.clone(),
night,
}),
Protection::Guardian {
source: _,
guarding: false,
}
| Protection::Protector { source: _ } => None,
}
}
pub fn resolve_kill(
changes: &mut ChangesLookup<'_>,
target: &CharacterId,
died_to: &DiedTo,
night: u8,
village: &Village,
) -> Result<Option<KillOutcome>> {
if let DiedTo::MapleWolf {
source,
night,
starves_if_fails: true,
} = died_to
&& let Some(protection) = changes.protected_take(target)
{
return Ok(Some(
resolve_protection(source.clone(), died_to, target, &protection, *night).unwrap_or(
KillOutcome::Single(source.clone(), DiedTo::MapleWolfStarved { night: *night }),
),
));
}
if let DiedTo::Wolfpack {
night,
killing_wolf,
} = died_to
&& let Some(ss_source) = changes.shapeshifter()
{
let killing_wolf = village
.character_by_id(killing_wolf)
.ok_or(GameError::InvalidTarget)?;
match changes.protected_take(target) {
Some(protection) => {
return Ok(resolve_protection(
killing_wolf.character_id().clone(),
died_to,
target,
&protection,
*night,
));
}
None => {
// Wolf kill went through -- can kill shifter
return Ok(Some(KillOutcome::Single(
ss_source.clone(),
DiedTo::Shapeshift {
into: target.clone(),
night: *night,
},
)));
}
};
}
let protection = match changes.protected_take(target) {
Some(prot) => prot,
None => return Ok(Some(KillOutcome::Single(target.clone(), died_to.clone()))),
};
match protection.deref() {
Protection::Guardian {
source,
guarding: true,
} => Ok(Some(KillOutcome::Guarding {
original_killer: died_to
.killer()
.ok_or(GameError::GuardianInvalidOriginalKill)?
.clone(),
original_target: target.clone(),
original_kill: died_to.clone(),
guardian: source.clone(),
night: NonZeroU8::new(night).unwrap(),
})),
Protection::Guardian {
source: _,
guarding: false,
}
| Protection::Protector { source: _ } => 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 release<T>(&mut self, taken: Taken<'a, T>) {
self.1.swap_remove(
self.1
.iter()
.enumerate()
.find_map(|(idx, c)| (*c == taken.idx()).then_some(idx))
.unwrap(),
);
}
pub fn protected_take(&mut self, target: &CharacterId) -> Option<Taken<'a, 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(Taken::new(idx, c))
} 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()
})
}
}
mod taken {
use core::ops::Deref;
pub struct Taken<'a, T>(usize, &'a T);
impl<'a, T> Taken<'a, T> {
pub const fn new(idx: usize, item: &'a T) -> Self {
Self(idx, item)
}
pub const fn idx(&self) -> usize {
self.0
}
}
impl<'a, T> Deref for Taken<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.1
}
}
}

View File

@ -1,3 +1,4 @@
mod kill;
mod night;
mod settings;
mod village;
@ -27,12 +28,14 @@ type Result<T> = core::result::Result<T, GameError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Game {
previous: Vec<GameState>,
next: Vec<GameState>,
state: GameState,
}
impl Game {
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
Ok(Self {
next: Vec::new(),
previous: Vec::new(),
state: GameState::Night {
night: Night::new(Village::new(players, settings)?)?,
@ -161,6 +164,24 @@ 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::Night { night }, HostGameMessage::PreviousState) => {
night.previous_state()?;
self.process(HostGameMessage::GetState)
}
}
}

View File

@ -8,7 +8,10 @@ use super::Result;
use crate::{
diedto::DiedTo,
error::GameError,
game::{DateTime, Village},
game::{
DateTime, Village,
kill::{self, ChangesLookup},
},
message::night::{ActionPrompt, ActionResponse, ActionResult},
player::{Character, CharacterId, Protection},
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
@ -19,6 +22,7 @@ pub struct Night {
village: Village,
night: u8,
action_queue: VecDeque<(ActionPrompt, Character)>,
used_actions: Vec<(ActionPrompt, Character)>,
changes: Vec<NightChange>,
night_state: NightState,
}
@ -166,15 +170,62 @@ impl Night {
village,
night_state,
action_queue,
used_actions: Vec::new(),
})
}
pub fn previous_state(&mut self) -> Result<()> {
let (prev_act, prev_char) = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
log::info!("loading previous prompt: {prev_act:?}");
match &self.night_state {
NightState::Active {
current_prompt,
current_char,
current_result: Some(current_result),
} => {
// if let Some(last_change) = self.changes.pop() {
// }
log::info!("removing current result: {current_result:?}");
self.night_state = NightState::Active {
current_prompt: current_prompt.clone(),
current_char: current_char.clone(),
current_result: None,
};
}
NightState::Active {
current_prompt,
current_char,
current_result: None,
} => {
log::info!("pushing current prompt to front of action queue: {current_prompt:?}");
self.action_queue.push_front((
current_prompt.clone(),
self.village.character_by_id(current_char).unwrap().clone(),
));
self.night_state = NightState::Active {
current_prompt: prev_act,
current_char: prev_char.character_id().clone(),
current_result: None,
}
}
NightState::Complete => {
self.night_state = NightState::Active {
current_prompt: prev_act,
current_char: prev_char.character_id().clone(),
current_result: None,
};
}
}
Ok(())
}
pub fn collect_completed(&self) -> Result<Village> {
if !matches!(self.night_state, NightState::Complete) {
return Err(GameError::NotEndOfNight);
}
let mut new_village = self.village.clone();
let changes = ChangesLookup(&self.changes);
let mut changes = ChangesLookup::new(&self.changes);
for change in self.changes.iter() {
match change {
NightChange::RoleChange(character_id, role_title) => new_village
@ -201,108 +252,17 @@ impl Night {
}
}
NightChange::Kill { target, died_to } => {
if let DiedTo::MapleWolf {
source,
night,
starves_if_fails: true,
} = died_to
&& changes.protected(target).is_some()
{
// kill maple first, then act as if they get their kill attempt
new_village
.character_by_id_mut(source)
.unwrap()
.kill(DiedTo::MapleWolfStarved { night: *night });
if let Some(kill) = kill::resolve_kill(
&mut changes,
target,
died_to,
self.night,
&self.village,
)? {
kill.apply_to_village(&mut new_village)?;
}
if let Some(prot) = changes.protected(target) {
match prot {
Protection::Guardian {
source,
guarding: true,
} => {
let kill_source = match died_to {
DiedTo::MapleWolfStarved { night } => {
new_village
.character_by_id_mut(target)
.unwrap()
.kill(DiedTo::MapleWolfStarved { night: *night });
continue;
}
DiedTo::Execution { day: _ } => unreachable!(),
DiedTo::MapleWolf {
source,
night: _,
starves_if_fails: _,
}
| DiedTo::Militia {
killer: source,
night: _,
}
| DiedTo::AlphaWolf {
killer: source,
night: _,
}
| DiedTo::Hunter {
killer: source,
night: _,
} => source.clone(),
DiedTo::Wolfpack { night: _ } => {
if let Some(wolf_to_kill) = new_village
.living_wolf_pack_players()
.into_iter()
.find(|w| matches!(w.role(), Role::Werewolf))
.map(|w| w.character_id().clone())
.or_else(|| {
new_village
.living_wolf_pack_players()
.into_iter()
.next()
.map(|w| w.character_id().clone())
})
{
wolf_to_kill
} else {
// No wolves? Game over?
continue;
}
}
DiedTo::Shapeshift { into: _, night: _ } => target.clone(),
DiedTo::Guardian {
killer: _,
night: _,
} => continue,
};
new_village.character_by_id_mut(&kill_source).unwrap().kill(
DiedTo::Guardian {
killer: source.clone(),
night: NonZeroU8::new(self.night).unwrap(),
},
);
new_village.character_by_id_mut(source).unwrap().kill(
DiedTo::Wolfpack {
night: NonZeroU8::new(self.night).unwrap(),
},
);
continue;
}
Protection::Guardian {
source: _,
guarding: false,
} => continue,
Protection::Protector { source: _ } => continue,
}
}
new_village
.character_by_id_mut(target)
.unwrap()
.kill(DiedTo::Wolfpack {
night: NonZeroU8::new(self.night).unwrap(),
});
}
NightChange::Shapeshift { source } => {
// TODO: shapeshift should probably notify immediately after it happens
if let Some(target) = changes.wolf_pack_kill_target()
&& changes.protected(target).is_none()
{
@ -317,13 +277,15 @@ impl Night {
into: target.clone(),
night: NonZeroU8::new(self.night).unwrap(),
});
let target = new_village.find_by_character_id_mut(target).unwrap();
target
.role_change(
RoleTitle::Werewolf,
DateTime::Night { number: self.night },
)
.unwrap();
// role change pushed in [apply_shapeshift]
// let target = new_village.find_by_character_id_mut(target).unwrap();
// target
// .role_change(
// RoleTitle::Werewolf,
// DateTime::Night { number: self.night },
// )
// .unwrap();
}
}
NightChange::RoleBlock {
@ -347,7 +309,11 @@ impl Night {
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
NightChange::Kill {
target,
died_to: DiedTo::Wolfpack { night: _ },
died_to:
DiedTo::Wolfpack {
night: _,
killing_wolf: _,
},
} => Some(target.clone()),
_ => None,
}) {
@ -361,22 +327,42 @@ impl Night {
// there is protection, so the kill doesn't happen -> no shapeshift
return Ok(ActionResult::GoBackToSleep);
}
if let Some(kill) = self.changes.iter_mut().find(|c| {
// if let Some(kill) = self.changes.iter_mut().find(|c| {
// matches!(
// c,
// NightChange::Kill {
// target: _,
// died_to: DiedTo::Wolfpack { night: _ }
// }
// )
// }) {
// *kill = NightChange::Kill {
// target: source.clone(),
// died_to: DiedTo::Shapeshift {
// into: kill_target.clone(),
// night: NonZeroU8::new(self.night).unwrap(),
// },
// }
// }
if self.changes.iter_mut().any(|c| {
matches!(
c,
NightChange::Kill {
target: _,
died_to: DiedTo::Wolfpack { night: _ }
died_to: DiedTo::Wolfpack {
night: _,
killing_wolf: _
}
}
)
}) {
*kill = NightChange::Kill {
self.changes.push(NightChange::Kill {
target: source.clone(),
died_to: DiedTo::Shapeshift {
into: kill_target.clone(),
night: NonZeroU8::new(self.night).unwrap(),
},
}
});
}
self.changes.push(NightChange::Shapeshift {
source: source.clone(),
@ -780,7 +766,10 @@ impl Night {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: target.clone(),
died_to: DiedTo::Wolfpack { night },
died_to: DiedTo::Wolfpack {
night,
killing_wolf: self.village.killing_wolf_id(),
},
}),
unless: Some(Unless::TargetBlocked(target)),
})
@ -980,6 +969,7 @@ impl Night {
NightState::Complete => return Err(GameError::NightOver),
}
if let Some((prompt, character)) = self.action_queue.pop_front() {
self.used_actions.push((prompt.clone(), character.clone()));
self.night_state = NightState::Active {
current_prompt: prompt,
current_char: character.character_id().clone(),
@ -996,34 +986,3 @@ impl Night {
self.changes.as_slice()
}
}
struct ChangesLookup<'a>(&'a [NightChange]);
impl<'a> ChangesLookup<'a> {
pub fn killed(&self, target: &CharacterId) -> Option<&'a DiedTo> {
self.0.iter().find_map(|c| match c {
NightChange::Kill { target: t, died_to } => (t == target).then_some(died_to),
_ => None,
})
}
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
self.0.iter().find_map(|c| match c {
NightChange::Protection {
target: t,
protection,
} => (t == target).then_some(protection),
_ => None,
})
}
pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> {
self.0.iter().find_map(|c| match c {
NightChange::Kill {
target,
died_to: DiedTo::Wolfpack { night: _ },
} => Some(target),
_ => None,
})
}
}

View File

@ -153,6 +153,19 @@ impl Village {
.collect()
}
pub fn killing_wolf_id(&self) -> CharacterId {
let wolves = self.living_wolf_pack_players();
if let Some(ww) = wolves.iter().find(|w| matches!(w.role(), Role::Werewolf)) {
ww.character_id().clone()
} else if let Some(non_ss_wolf) = wolves.iter().find(|w| {
w.role().wolf() && !matches!(w.role(), Role::Shapeshifter { shifted_into: _ })
}) {
non_ss_wolf.character_id().clone()
} else {
wolves.into_iter().next().unwrap().character_id().clone()
}
}
pub fn living_players(&self) -> Box<[Target]> {
self.characters
.iter()

View File

@ -5,7 +5,7 @@ pub mod night;
use core::{fmt::Display, num::NonZeroU8};
pub use ident::*;
use night::{ActionPrompt, ActionResponse, ActionResult, RoleChange};
use night::{ActionPrompt, ActionResponse, ActionResult};
use serde::{Deserialize, Serialize};
use crate::{

View File

@ -28,6 +28,7 @@ pub enum HostMessage {
pub enum HostGameMessage {
Day(HostDayMessage),
Night(HostNightMessage),
PreviousState,
GetState,
}

View File

@ -2,93 +2,107 @@ use serde::{Deserialize, Serialize};
use werewolves_macros::ChecksAs;
use crate::{
diedto::DiedTo,
message::PublicIdentity,
player::CharacterId,
role::{Alignment, PreviousGuardianAction, Role, RoleTitle},
};
use super::Target;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum ActionType {
Protect = 0,
WolfPackKill = 1,
Direwolf = 2,
Wolf = 3,
Block = 4,
Other = 5,
RoleChange = 6,
}
impl PartialOrd for ActionType {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
(*self as u8).partial_cmp(&(*other as u8))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs)]
pub enum ActionPrompt {
WolvesIntro {
wolves: Box<[(Target, RoleTitle)]>,
},
RoleChange {
new_role: RoleTitle,
},
Seer {
living_players: Box<[Target]>,
},
Protector {
targets: Box<[Target]>,
},
Arcanist {
living_players: Box<[Target]>,
},
Gravedigger {
dead_players: Box<[Target]>,
},
#[checks(ActionType::WolfPackKill)]
WolvesIntro { wolves: Box<[(Target, RoleTitle)]> },
#[checks(ActionType::RoleChange)]
RoleChange { new_role: RoleTitle },
#[checks(ActionType::Other)]
Seer { living_players: Box<[Target]> },
#[checks(ActionType::Protect)]
Protector { targets: Box<[Target]> },
#[checks(ActionType::Other)]
Arcanist { living_players: Box<[Target]> },
#[checks(ActionType::Other)]
Gravedigger { dead_players: Box<[Target]> },
#[checks(ActionType::Other)]
Hunter {
current_target: Option<Target>,
living_players: Box<[Target]>,
},
Militia {
living_players: Box<[Target]>,
},
#[checks(ActionType::Other)]
Militia { living_players: Box<[Target]> },
#[checks(ActionType::Other)]
MapleWolf {
kill_or_die: bool,
living_players: Box<[Target]>,
},
#[checks(ActionType::Protect)]
Guardian {
previous: Option<PreviousGuardianAction>,
living_players: Box<[Target]>,
},
WolfPackKill {
living_villagers: Box<[Target]>,
},
#[checks(ActionType::Wolf)]
WolfPackKill { living_villagers: Box<[Target]> },
#[checks(ActionType::Wolf)]
Shapeshifter,
AlphaWolf {
living_villagers: Box<[Target]>,
},
DireWolf {
living_players: Box<[Target]>,
},
#[checks(ActionType::Wolf)]
AlphaWolf { living_villagers: Box<[Target]> },
#[checks(ActionType::Direwolf)]
DireWolf { living_players: Box<[Target]> },
}
impl PartialOrd for ActionPrompt {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
fn ordering_num(prompt: &ActionPrompt) -> u8 {
match prompt {
ActionPrompt::WolvesIntro { wolves: _ } => 0,
ActionPrompt::Guardian {
living_players: _,
previous: _,
}
| ActionPrompt::Protector { targets: _ } => 1,
ActionPrompt::WolfPackKill {
living_villagers: _,
} => 2,
ActionPrompt::Shapeshifter => 3,
ActionPrompt::AlphaWolf {
living_villagers: _,
} => 4,
ActionPrompt::DireWolf { living_players: _ } => 5,
ActionPrompt::Seer { living_players: _ }
| ActionPrompt::Arcanist { living_players: _ }
| ActionPrompt::Gravedigger { dead_players: _ }
| ActionPrompt::Hunter {
current_target: _,
living_players: _,
}
| ActionPrompt::Militia { living_players: _ }
| ActionPrompt::MapleWolf {
kill_or_die: _,
living_players: _,
}
| ActionPrompt::RoleChange { new_role: _ } => 0xFF,
}
}
ordering_num(self).partial_cmp(&ordering_num(other))
// fn ordering_num(prompt: &ActionPrompt) -> u8 {
// match prompt {
// ActionPrompt::WolvesIntro { wolves: _ } => 0,
// ActionPrompt::Guardian {
// living_players: _,
// previous: _,
// }
// | ActionPrompt::Protector { targets: _ } => 1,
// ActionPrompt::WolfPackKill {
// living_villagers: _,
// } => 2,
// ActionPrompt::Shapeshifter => 3,
// ActionPrompt::AlphaWolf {
// living_villagers: _,
// } => 4,
// ActionPrompt::DireWolf { living_players: _ } => 5,
// ActionPrompt::Seer { living_players: _ }
// | ActionPrompt::Arcanist { living_players: _ }
// | ActionPrompt::Gravedigger { dead_players: _ }
// | ActionPrompt::Hunter {
// current_target: _,
// living_players: _,
// }
// | ActionPrompt::Militia { living_players: _ }
// | ActionPrompt::MapleWolf {
// kill_or_die: _,
// living_players: _,
// }
// | ActionPrompt::RoleChange { new_role: _ } => 0xFF,
// }
// }
// ordering_num(self).partial_cmp(&ordering_num(other))
self.action_type().partial_cmp(&other.action_type())
}
}
@ -121,10 +135,3 @@ pub enum ActionResult {
GoBackToSleep,
WolvesIntroDone,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RoleChange {
Elder(Role),
Apprentice(Role),
Shapeshift(Role),
}

View File

@ -25,5 +25,5 @@ ciborium = { version = "0.2", optional = true }
colored = { version = "3.0" }
[features]
# default = ["cbor"]
default = ["cbor"]
cbor = ["dep:ciborium"]

View File

@ -36,7 +36,7 @@ convert_case = { version = "0.8" }
ciborium = { version = "0.2", optional = true }
[features]
# default = ["cbor"]
default = ["json"]
default = ["cbor"]
# default = ["json"]
cbor = ["dep:ciborium"]
json = ["dep:serde_json"]

View File

@ -836,9 +836,18 @@ clients {
flex-wrap: wrap;
flex-direction: row;
font-size: 2rem;
justify-content: center;
&.margin-20 {
margin-left: 20px;
margin-right: 20px;
justify-content: center;
}
&.margin-5 {
margin-left: 5px;
margin-right: 5px;
}
}
.gap {
@ -1002,7 +1011,7 @@ error {
background-color: $village_color;
border: 3px solid darken($village_color, 20%);
&.on-the-block {
&.marked {
// background-color: brighten($village_color, 100%);
filter: hue-rotate(90deg);
}
@ -1021,3 +1030,25 @@ error {
filter: grayscale(100%);
}
}
.binary {
.button-container {
background-color: $village_color;
border: 3px solid darken($village_color, 20%);
text-align: center;
padding: 0;
margin: 0;
display: flex;
flex: 1 1 0;
button {
font-size: 3rem;
font-weight: bold;
align-self: center;
padding: 20px;
width: 100%;
height: 100%;
margin: 0;
}
}
}

View File

@ -4,7 +4,7 @@ use crate::components::Button;
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct BinaryChoiceProps {
pub on_chosen: Callback<bool>,
pub on_chosen: Option<Callback<bool>>,
#[prop_or_default]
pub children: Html,
}
@ -17,15 +17,19 @@ pub fn BinaryChoice(
}: &BinaryChoiceProps,
) -> Html {
let on_chosen_yes = on_chosen.clone();
let yes = move |_| on_chosen_yes.emit(true);
let yes = on_chosen_yes
.map(|on_chosen| Callback::from(move |_| on_chosen.emit(true)))
.unwrap_or_default();
let on_chosen = on_chosen.clone();
let no = move |_| on_chosen.emit(false);
let no = on_chosen
.map(|on_chosen| Callback::from(move |_| on_chosen.emit(false)))
.unwrap_or_default();
html! {
<div class="column-list">
<div class="column-list binary">
{children.clone()}
<div class="row-list">
<Button on_click={yes}>{"yes"}</Button>
<Button on_click={no}>{"no"}</Button>
<div class="row-list gap">
<Button on_click={yes}>{"Yes"}</Button>
<Button on_click={no}>{"No"}</Button>
</div>
</div>
}

View File

@ -7,12 +7,13 @@ use werewolves_proto::{
night::{ActionPrompt, ActionResponse},
},
player::CharacterId,
role::PreviousGuardianAction,
};
use yew::prelude::*;
use crate::components::{
Identity,
action::{BinaryChoice, SingleTarget, TargetSelection, WolvesIntro},
action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro},
};
#[derive(Debug, Clone, PartialEq, Properties)]
@ -54,7 +55,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
)));
})
.into()
});
html! {
<div>
@ -96,7 +96,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
HostNightMessage::ActionResponse(ActionResponse::Protector(target)),
)));
})
.into()
});
html! {
<div>
@ -108,7 +107,25 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
</div>
}
}
ActionPrompt::Arcanist { living_players } => todo!(),
ActionPrompt::Arcanist { living_players } => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |(t1, t2): (CharacterId, CharacterId)| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Arcanist(t1, t2)),
)));
})
});
html! {
<div>
<TwoTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"arcanist"}
/>
</div>
}
}
ActionPrompt::Gravedigger { dead_players } => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
@ -117,7 +134,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)),
)));
})
.into()
});
html! {
<div>
@ -132,16 +148,112 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
ActionPrompt::Hunter {
current_target,
living_players,
} => todo!(),
ActionPrompt::Militia { living_players } => todo!(),
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Hunter(target)),
)));
})
});
html! {
<SingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"hunter"}
>
<h3>
<b>{"current target: "}</b>{current_target.clone().map(|t| html!{
<Identity ident={t.public} />
}).unwrap_or_else(|| html!{<i>{"none"}</i>})}
</h3>
</SingleTarget>
}
}
ActionPrompt::Militia { living_players } => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: Option<CharacterId>| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Militia(target)),
)));
})
});
html! {
<OptionalSingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"pew pew?"}
/>
}
}
ActionPrompt::MapleWolf {
kill_or_die,
living_players,
} => todo!(),
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: Option<CharacterId>| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::MapleWolf(target)),
)));
})
});
let kill_or_die = kill_or_die.then(|| {
html! {
<em>{"if you fail to eat tonight, you will starve"}</em>
}
});
html! {
<OptionalSingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"nom nom?"}
>
{kill_or_die}
</OptionalSingleTarget>
}
}
ActionPrompt::Guardian {
previous,
living_players,
} => todo!(),
} => {
let last_protect = previous.as_ref().map(|prev| match prev {
PreviousGuardianAction::Protect(target) => {
html! {
<>
<b>{"last night you protected: "}</b>
<Identity ident={target.public.clone()}/>
</>
}
}
PreviousGuardianAction::Guard(target) => html! {
<>
<b>{"last night you guarded: "}</b>
<Identity ident={target.public.clone()}/>
</>
},
});
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then_some({
move |prot| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Guardian(prot)),
)));
}
});
html! {
<SingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"pick someone to protect"}
>
{last_protect}
</SingleTarget>
}
}
ActionPrompt::WolfPackKill { living_villagers } => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
@ -150,25 +262,24 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)),
)));
})
.into()
});
html! {
<div>
<SingleTarget
targets={living_villagers.clone()}
target_selection={on_select}
headline={"wolf pack kill"}
/>
</div>
}
}
ActionPrompt::Shapeshifter => {
let on_complete = props.on_complete.clone();
let on_select = move |shift| {
let on_select = props.big_screen.not().then_some({
move |shift| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)),
)));
};
}
});
html! {
<BinaryChoice on_chosen={on_select}>
<h2>{"shapeshift?"}</h2>
@ -183,10 +294,9 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)),
)));
})
.into()
});
html! {
<SingleTarget
<OptionalSingleTarget
targets={living_villagers.clone()}
target_selection={on_select}
headline={"alpha wolf target"}
@ -201,7 +311,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)),
)));
})
.into()
});
html! {
<SingleTarget

View File

@ -1,67 +1,56 @@
use core::fmt::Debug;
use core::{fmt::Debug, ops::Not};
use werewolves_macros::ChecksAs;
use werewolves_proto::{message::Target, player::CharacterId};
use yew::prelude::*;
use crate::components::Identity;
#[derive(Debug, Clone, PartialEq)]
pub enum TargetSelection {
SingleOptional(Callback<Option<CharacterId>>),
Single(Callback<CharacterId>),
}
impl From<Callback<CharacterId>> for TargetSelection {
fn from(value: Callback<CharacterId>) -> Self {
Self::Single(value)
}
}
impl From<Callback<Option<CharacterId>>> for TargetSelection {
fn from(value: Callback<Option<CharacterId>>) -> Self {
Self::SingleOptional(value)
}
}
impl TargetSelection {
pub const fn button_disabled(&self, selected: &[CharacterId]) -> bool {
match self {
TargetSelection::SingleOptional(_) => selected.len() > 1,
TargetSelection::Single(_) => selected.len() != 1,
}
}
}
use crate::components::{Button, Identity};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct SingleTargetProps {
pub struct TwoTargetProps {
pub targets: Box<[Target]>,
#[prop_or_default]
pub headline: &'static str,
#[prop_or_default]
pub target_selection: Option<TargetSelection>,
pub target_selection: Option<Callback<(CharacterId, CharacterId)>>,
}
pub struct SingleTarget {
selected: Vec<CharacterId>,
#[derive(ChecksAs, Clone)]
enum TwoTargetSelection {
None,
One(CharacterId),
#[checks]
Two(CharacterId, CharacterId),
}
impl Component for SingleTarget {
impl TwoTargetSelection {
fn is_selected(&self, id: &CharacterId) -> bool {
match self {
TwoTargetSelection::None => false,
TwoTargetSelection::One(character_id) => id == character_id,
TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2,
}
}
}
pub struct TwoTarget(TwoTargetSelection);
impl Component for TwoTarget {
type Message = CharacterId;
type Properties = SingleTargetProps;
type Properties = TwoTargetProps;
fn create(_: &Context<Self>) -> Self {
Self {
selected: Vec::new(),
}
Self(TwoTargetSelection::None)
}
fn view(&self, ctx: &Context<Self>) -> Html {
let SingleTargetProps {
headline,
let TwoTargetProps {
targets,
headline,
target_selection,
} = ctx.props();
let target_selection = target_selection.clone();
let scope = ctx.link().clone();
let card_select = Callback::from(move |target| {
@ -73,44 +62,27 @@ impl Component for SingleTarget {
html! {
<TargetCard
target={t.clone()}
selected={self.selected.iter().any(|sel| sel == &t.character_id)}
selected={self.0.is_selected(&t.character_id)}
on_select={card_select.clone()}
/>
}
})
.collect::<Html>();
let headline = if headline.trim().is_empty() {
html!()
} else {
html!(<h2>{headline}</h2>)
};
let on_click =
target_selection
.as_ref()
.and_then(|target_selection| match target_selection {
TargetSelection::SingleOptional(on_click) => {
if self.selected.len() > 1 {
None
} else {
let selected = self.selected.first().cloned();
let on_click = on_click.clone();
Some(Callback::from(move |_| on_click.emit(selected.clone())))
}
}
TargetSelection::Single(on_click) => {
if self.selected.len() != 1 {
None
} else {
let selected = self.selected[0].clone();
let on_click = on_click.clone();
Some(Callback::from(move |_| on_click.emit(selected.clone())))
}
}
});
let headline = headline
.trim()
.is_empty()
.not()
.then(|| html!(<h2>{headline}</h2>));
let submit = target_selection.as_ref().map(|target_selection| {
let disabled = target_selection.button_disabled(&self.selected);
let selected = match &self.0 {
TwoTargetSelection::None | TwoTargetSelection::One(_) => None,
TwoTargetSelection::Two(t1, t2) => Some((t1.clone(), t2.clone())),
};
let target_selection = target_selection.clone();
let disabled = selected.is_none();
let on_click =
selected.map(|(t1, t2)| move |_| target_selection.emit((t1.clone(), t2.clone())));
html! {
<div class="button-container sp-ace">
<button
@ -135,15 +107,220 @@ impl Component for SingleTarget {
}
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
if let Some(idx) = self
.selected
.iter()
.enumerate()
.find_map(|(idx, c)| (c == &msg).then_some(idx))
{
self.selected.swap_remove(idx);
match &self.0 {
TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg),
TwoTargetSelection::One(character_id) => {
if character_id == &msg {
self.0 = TwoTargetSelection::None
} else {
self.selected.push(msg);
self.0 = TwoTargetSelection::Two(character_id.clone(), msg)
}
}
TwoTargetSelection::Two(t1, t2) => {
if &msg == t1 {
self.0 = TwoTargetSelection::One(t2.clone());
} else if &msg == t2 {
self.0 = TwoTargetSelection::One(t1.clone());
} else {
self.0 = TwoTargetSelection::Two(t1.clone(), msg);
}
}
}
true
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct OptionalSingleTargetProps {
pub targets: Box<[Target]>,
#[prop_or_default]
pub headline: &'static str,
#[prop_or_default]
pub target_selection: Option<Callback<Option<CharacterId>>>,
#[prop_or_default]
pub children: Html,
}
pub struct OptionalSingleTarget(Option<CharacterId>);
impl Component for OptionalSingleTarget {
type Message = CharacterId;
type Properties = OptionalSingleTargetProps;
fn create(_: &Context<Self>) -> Self {
Self(None)
}
fn view(&self, ctx: &Context<Self>) -> Html {
let OptionalSingleTargetProps {
targets,
headline,
target_selection,
children,
} = ctx.props();
let target_selection = target_selection.clone();
let scope = ctx.link().clone();
let card_select = Callback::from(move |target| {
scope.send_message(target);
});
let targets = targets
.iter()
.map(|t| {
html! {
<TargetCard
target={t.clone()}
selected={self.0.as_ref().map(|c| c == &t.character_id).unwrap_or_default()}
on_select={card_select.clone()}
/>
}
})
.collect::<Html>();
let headline = headline
.trim()
.is_empty()
.not()
.then(|| html!(<h2>{headline}</h2>));
let submit = target_selection.as_ref().map(|target_selection| {
let target_selection = target_selection.clone();
let sel = self.0.clone();
let on_click = move |_| target_selection.emit(sel.clone());
html! {
<div class="button-container sp-ace">
<button
onclick={on_click}
>
{"submit"}
</button>
</div>
}
});
html! {
<div class="column-list">
{headline}
{children.clone()}
<div class="row-list">
{targets}
</div>
{submit}
</div>
}
}
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
match &self.0 {
Some(t) => {
if t == &msg {
self.0 = None
} else {
self.0 = Some(msg);
}
}
None => self.0 = Some(msg),
}
true
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct SingleTargetProps {
pub targets: Box<[Target]>,
#[prop_or_default]
pub headline: &'static str,
#[prop_or_default]
pub target_selection: Option<Callback<CharacterId>>,
#[prop_or_default]
pub children: Html,
}
pub struct SingleTarget {
selected: Option<CharacterId>,
}
impl Component for SingleTarget {
type Message = CharacterId;
type Properties = SingleTargetProps;
fn create(_: &Context<Self>) -> Self {
Self { selected: None }
}
fn view(&self, ctx: &Context<Self>) -> Html {
let SingleTargetProps {
headline,
targets,
target_selection,
children,
} = ctx.props();
let target_selection = target_selection.clone();
let scope = ctx.link().clone();
let card_select = Callback::from(move |target| {
scope.send_message(target);
});
let targets = targets
.iter()
.map(|t| {
html! {
<TargetCard
target={t.clone()}
selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
on_select={card_select.clone()}
/>
}
})
.collect::<Html>();
let headline = headline
.trim()
.is_empty()
.not()
.then(|| html!(<h2>{headline}</h2>));
let submit = target_selection.as_ref().map(|target_selection| {
let disabled = self.selected.is_none();
let target_selection = target_selection.clone();
let on_click = self
.selected
.clone()
.map(|t| move |_| target_selection.emit(t.clone()));
html! {
<div class="button-container sp-ace">
<button
disabled={disabled}
onclick={on_click}
>
{"submit"}
</button>
</div>
}
});
html! {
<div class="column-list">
{headline}
{children.clone()}
<div class="row-list">
{targets}
</div>
{submit}
</div>
}
}
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
match &self.selected {
Some(current) => {
if current == &msg {
self.selected = None;
} else {
self.selected = Some(msg);
}
}
None => self.selected = Some(msg),
}
true
}
@ -158,17 +335,24 @@ pub struct TargetCardProps {
#[function_component]
fn TargetCard(props: &TargetCardProps) -> Html {
let submenu = {
let button_text = if props.selected { "unpick" } else { "pick" };
let character_id = props.target.character_id.clone();
let on_select = props.on_select.clone();
let target = props.target.character_id.clone();
let on_click = Callback::from(move |_| {
on_select.emit(target.clone());
});
let on_click = Callback::from(move |_| on_select.emit(character_id.clone()));
html! {
<div
class={classes!("target-card", "character", props.selected.then_some("selected"))}
onclick={on_click}
>
<nav class="submenu">
<Button on_click={on_click}>{button_text}</Button>
</nav>
}
};
let marked = props.selected.then_some("marked");
html! {
<div class={"row-list baseline margin-5"}>
<div class={classes!("player", "ident", "column-list", marked)}>
<Identity ident={props.target.public.clone()} />
{submenu}
</div>
</div>
}
}

View File

@ -84,7 +84,7 @@ pub fn DaytimePlayer(
) -> Html {
let dead = died_to.is_some().then_some("dead");
let button_text = if *on_the_block { "unmark" } else { "mark" };
let on_the_block = on_the_block.then_some("on-the-block");
let on_the_block = on_the_block.then_some("marked");
let submenu = died_to.is_none().then_some(()).and_then(|_| {
on_select.as_ref().map(|on_select| {
let character_id = character_id.clone();

View File

@ -469,10 +469,15 @@ impl Component for Host {
HostMessage::Echo(ServerToHostMessage::Error(GameError::NoApprenticeMentor)),
self.send.clone(),
);
let on_prev_click = callback::send_message(
HostMessage::InGame(HostGameMessage::PreviousState),
self.send.clone(),
);
html! {
<nav class="debug-nav">
<nav class="debug-nav" style="z-index: 10;">
<div class="row-list">
<Button on_click={on_error_click}>{"error"}</Button>
<Button on_click={on_prev_click}>{"previous"}</Button>
</div>
</nav>
}