fix wolves sleeping inbetween individual wolves

improved identity for prompts
night order improvements
started adding tests for game
This commit is contained in:
emilis 2025-09-30 13:07:59 +01:00
parent d352cfb1ee
commit d90f4ec6fe
No known key found for this signature in database
21 changed files with 1559 additions and 659 deletions

2
Cargo.lock generated
View File

@ -2427,8 +2427,10 @@ dependencies = [
name = "werewolves-proto" name = "werewolves-proto"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"colored",
"log", "log",
"pretty_assertions", "pretty_assertions",
"pretty_env_logger",
"rand", "rand",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -15,3 +15,5 @@ werewolves-macros = { path = "../werewolves-macros" }
[dev-dependencies] [dev-dependencies]
pretty_assertions = { version = "1" } pretty_assertions = { version = "1" }
pretty_env_logger = { version = "0.5" }
colored = { version = "3.0" }

View File

@ -18,6 +18,7 @@ use crate::{
message::{ message::{
CharacterState, Identification, CharacterState, Identification,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::ActionResponse,
}, },
player::CharacterId, player::CharacterId,
}; };
@ -107,18 +108,15 @@ impl Game {
} }
(GameState::Night { night }, HostGameMessage::GetState) => { (GameState::Night { night }, HostGameMessage::GetState) => {
if let Some(res) = night.current_result() { if let Some(res) = night.current_result() {
let char = night.current_character().unwrap();
return Ok(ServerToHostMessage::ActionResult( return Ok(ServerToHostMessage::ActionResult(
char.public_identity().clone(), night
.current_character()
.map(|c| c.public_identity().clone()),
res.clone(), res.clone(),
)); ));
} }
if let Some(prompt) = night.current_prompt() { if let Some(prompt) = night.current_prompt() {
let char = night.current_character().unwrap(); return Ok(ServerToHostMessage::ActionPrompt(prompt.clone()));
return Ok(ServerToHostMessage::ActionPrompt(
char.public_identity().clone(),
prompt.clone(),
));
} }
match night.next() { match night.next() {
Ok(_) => self.process(HostGameMessage::GetState), Ok(_) => self.process(HostGameMessage::GetState),
@ -140,7 +138,9 @@ impl Game {
HostGameMessage::Night(HostNightMessage::ActionResponse(resp)), HostGameMessage::Night(HostNightMessage::ActionResponse(resp)),
) => match night.received_response(resp.clone()) { ) => match night.received_response(resp.clone()) {
Ok(res) => Ok(ServerToHostMessage::ActionResult( Ok(res) => Ok(ServerToHostMessage::ActionResult(
night.current_character().unwrap().public_identity().clone(), night
.current_character()
.map(|c| c.public_identity().clone()),
res, res,
)), )),
Err(GameError::NightNeedsNext) => match night.next() { Err(GameError::NightNeedsNext) => match night.next() {

File diff suppressed because it is too large Load Diff

View File

@ -16,10 +16,10 @@ impl Default for GameSettings {
Self { Self {
roles: [ roles: [
(RoleTitle::Werewolf, NonZeroU8::new(1).unwrap()), (RoleTitle::Werewolf, NonZeroU8::new(1).unwrap()),
(RoleTitle::Seer, NonZeroU8::new(1).unwrap()), // (RoleTitle::Seer, NonZeroU8::new(1).unwrap()),
// (RoleTitle::Militia, NonZeroU8::new(1).unwrap()), // (RoleTitle::Militia, NonZeroU8::new(1).unwrap()),
// (RoleTitle::Guardian, NonZeroU8::new(1).unwrap()), // (RoleTitle::Guardian, NonZeroU8::new(1).unwrap()),
(RoleTitle::Apprentice, NonZeroU8::new(1).unwrap()), // (RoleTitle::Apprentice, NonZeroU8::new(1).unwrap()),
] ]
.into_iter() .into_iter()
.collect(), .collect(),

View File

@ -0,0 +1,614 @@
mod night_order;
use crate::{
error::GameError,
game::{Game, GameSettings},
message::{
CharacterState, Identification, PublicIdentity,
host::{
HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage,
ServerToHostMessageTitle,
},
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
},
player::{CharacterId, PlayerId},
role::RoleTitle,
};
use colored::Colorize;
use core::{num::NonZeroU8, ops::Range};
#[allow(unused)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use std::io::Write;
trait ServerToHostMessageExt {
fn prompt(self) -> ActionPrompt;
fn result(self) -> ActionResult;
}
impl ServerToHostMessageExt for ServerToHostMessage {
fn prompt(self) -> ActionPrompt {
match self {
Self::ActionPrompt(prompt) => prompt,
Self::Daytime {
characters: _,
marked: _,
day: _,
} => panic!("{}", "[got daytime]".bold().red()),
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"),
}
}
}
trait GameExt {
fn next(&mut self) -> ActionPrompt;
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
fn response(&mut self, resp: ActionResponse) -> ActionResult;
fn get_state(&mut self) -> ServerToHostMessage;
fn execute(&mut self) -> ActionPrompt;
fn mark_for_execution(
&mut self,
target: CharacterId,
) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
}
impl GameExt for Game {
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 {
self.process(HostGameMessage::Day(HostDayMessage::Execute))
.unwrap()
.prompt()
}
fn get_state(&mut self) -> ServerToHostMessage {
self.process(HostGameMessage::GetState).unwrap()
}
}
fn init_log() {
let _ = pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Debug)
.format(|f, record| match record.file() {
Some(file) => {
let file = format!(
"[{file}{}]",
record
.line()
.map(|l| format!(":{l}"))
.unwrap_or_else(String::new),
)
.dimmed();
let level = match record.level() {
log::Level::Error => "[err]".red().bold(),
log::Level::Warn => "[warn]".yellow().bold(),
log::Level::Info => "[info]".white().bold(),
log::Level::Debug => "[debug]".dimmed().bold(),
log::Level::Trace => "[trace]".dimmed(),
};
let args = record.args();
let arrow = "".bold().magenta();
writeln!(f, "{file}\n{level} {arrow} {args}")
}
_ => writeln!(f, "[{}] {}", record.level(), record.args()),
})
.is_test(true)
.try_init();
}
fn gen_players(range: Range<u8>) -> Box<[Identification]> {
range
.into_iter()
.map(|num| Identification {
player_id: PlayerId::from_u128(num as _),
public: PublicIdentity {
name: format!("player {num}"),
pronouns: None,
number: NonZeroU8::new(num).unwrap(),
},
})
.collect()
}
#[test]
fn starts_with_wolf_intro() {
let players = gen_players(1..10);
let settings = GameSettings::default();
let mut game = Game::new(&players, settings).unwrap();
let resp = game.process(HostGameMessage::GetState).unwrap();
assert_eq!(
resp,
ServerToHostMessage::ActionPrompt(ActionPrompt::CoverOfDarkness)
)
}
#[test]
fn no_wolf_kill_n1() {
let players = gen_players(1..10);
let mut settings = GameSettings::default();
settings.add(RoleTitle::Shapeshifter).unwrap();
settings.sub(RoleTitle::Werewolf);
settings.add(RoleTitle::Protector).unwrap();
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ })
));
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolvesIntroAck
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep),
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::Daytime {
characters: _,
marked: _,
day: _,
}
));
}
#[test]
fn yes_wolf_kill_n2() {
let players = gen_players(1..10);
let settings = GameSettings::default();
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ })
));
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolvesIntroAck
)))
.unwrap()
.result(),
ActionResult::GoBackToSleep,
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::Daytime {
characters: _,
marked: _,
day: _,
}
));
let execution_target = game
.village()
.characters()
.into_iter()
.find(|v| v.is_village())
.unwrap()
.character_id()
.clone();
match game
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
execution_target.clone(),
)))
.unwrap()
{
ServerToHostMessage::Daytime {
characters: _,
marked,
day: _,
} => assert_eq!(marked.to_vec(), vec![execution_target]),
resp => panic!("unexpected server message: {resp:#?}"),
}
assert_eq!(
game.process(HostGameMessage::Day(HostDayMessage::Execute))
.unwrap(),
ServerToHostMessage::ActionPrompt(ActionPrompt::CoverOfDarkness)
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill {
living_villagers: _
})
));
}
#[test]
fn protect_stops_shapeshift() {
init_log();
let players = gen_players(1..10);
let mut settings = GameSettings::default();
settings.add(RoleTitle::Shapeshifter).unwrap();
settings.sub(RoleTitle::Werewolf);
settings.add(RoleTitle::Protector).unwrap();
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ })
));
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolvesIntroAck
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep),
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::Daytime {
characters: _,
marked: _,
day: _,
}
));
let execution_target = game
.village()
.characters()
.into_iter()
.find(|v| v.is_village() && !matches!(v.role().title(), RoleTitle::Protector))
.unwrap()
.character_id()
.clone();
match game
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
execution_target.clone(),
)))
.unwrap()
{
ServerToHostMessage::Daytime {
characters: _,
marked,
day: _,
} => assert_eq!(marked.to_vec(), vec![execution_target]),
resp => panic!("unexpected server message: {resp:#?}"),
}
assert_eq!(
game.process(HostGameMessage::Day(HostDayMessage::Execute))
.unwrap()
.prompt()
.title(),
ActionPromptTitle::CoverOfDarkness
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap()
.result(),
ActionResult::Continue
);
let (prot_and_wolf_target, prot_char_id) = match game
.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
{
ServerToHostMessage::ActionPrompt(ActionPrompt::Protector {
character_id: prot_char_id,
targets,
}) => (
targets
.into_iter()
.map(|c| game.village().character_by_id(&c.character_id).unwrap())
.find(|c| c.is_village())
.unwrap()
.character_id()
.clone(),
prot_char_id,
),
_ => panic!("first n2 prompt isn't protector"),
};
let target = game
.village()
.character_by_id(&prot_and_wolf_target)
.unwrap()
.clone();
log::info!("target: {target:#?}");
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Protector(prot_and_wolf_target.clone())
)))
.unwrap()
.result(),
ActionResult::GoBackToSleep,
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.prompt()
.title(),
ActionPromptTitle::WolfPackKill
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolfPackKillVote(prot_and_wolf_target.clone())
)))
.unwrap()
.result(),
ActionResult::Continue,
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.prompt()
.title(),
ActionPromptTitle::Shapeshifter,
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Shapeshifter(true)
)))
.unwrap()
.result(),
ActionResult::GoBackToSleep,
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.title(),
ServerToHostMessageTitle::Daytime,
);
let target = game
.village()
.character_by_id(target.character_id())
.unwrap();
assert!(target.is_village());
assert!(target.alive());
let prot = game
.village()
.character_by_id(&prot_char_id.character_id)
.unwrap();
assert!(prot.is_village());
assert!(prot.alive());
assert_eq!(prot.role().title(), RoleTitle::Protector);
}
#[test]
fn wolfpack_kill_all_targets_valid() {
init_log();
let players = gen_players(1..10);
let mut settings = GameSettings::default();
settings.add(RoleTitle::Shapeshifter).unwrap();
settings.sub(RoleTitle::Werewolf);
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ })
));
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolvesIntroAck
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep),
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::Daytime {
characters: _,
marked: _,
day: _,
}
));
let execution_target = game
.village()
.characters()
.into_iter()
.find(|v| v.is_village() && !matches!(v.role().title(), RoleTitle::Protector))
.unwrap()
.character_id()
.clone();
match game
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
execution_target.clone(),
)))
.unwrap()
{
ServerToHostMessage::Daytime {
characters: _,
marked,
day: _,
} => assert_eq!(marked.to_vec(), vec![execution_target]),
resp => panic!("unexpected server message: {resp:#?}"),
}
assert_eq!(
game.process(HostGameMessage::Day(HostDayMessage::Execute))
.unwrap()
.prompt()
.title(),
ActionPromptTitle::CoverOfDarkness
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap()
.result(),
ActionResult::Continue
);
let living_villagers = match game
.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.prompt()
{
ActionPrompt::WolfPackKill { living_villagers } => living_villagers,
_ => panic!("not wolf pack kill"),
};
for (idx, target) in living_villagers.into_iter().enumerate() {
let mut attempt = game.clone();
if let ServerToHostMessage::Error(GameError::InvalidTarget) = attempt
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolfPackKillVote(target.character_id.clone()),
)))
.unwrap()
{
panic!("invalid target {target:?} at index [{idx}]");
}
}
}
#[test]
fn only_1_shapeshift_prompt_if_first_shifts() {
let players = gen_players(1..10);
let mut settings = GameSettings::default();
settings.add(RoleTitle::Shapeshifter).unwrap();
settings.add(RoleTitle::Shapeshifter).unwrap();
settings.sub(RoleTitle::Werewolf);
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.response(ActionResponse::ClearCoverOfDarkness),
ActionResult::Continue
);
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
assert_eq!(
game.response(ActionResponse::WolvesIntroAck),
ActionResult::GoBackToSleep
);
game.next_expect_day();
let target = game
.village()
.characters()
.into_iter()
.find_map(|c| c.is_village().then_some(c.character_id().clone()))
.unwrap();
let (_, marked, _) = game.mark_for_execution(target.clone());
let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]);
assert_eq!(target_list, marked);
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
assert_eq!(
game.response(ActionResponse::ClearCoverOfDarkness),
ActionResult::Continue
);
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
let target = game
.village()
.characters()
.into_iter()
.find_map(|c| (c.is_village() && c.alive()).then_some(c.character_id().clone()))
.unwrap();
assert_eq!(
game.response(ActionResponse::WolfPackKillVote(target)),
ActionResult::Continue,
);
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter);
assert_eq!(
game.response(ActionResponse::Shapeshifter(true)),
ActionResult::Continue,
);
assert_eq!(game.next().title(), ActionPromptTitle::RoleChange);
assert_eq!(
game.response(ActionResponse::RoleChangeAck),
ActionResult::GoBackToSleep
);
game.next_expect_day();
}

View File

@ -0,0 +1,63 @@
use core::num::NonZeroU8;
#[allow(unused)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use crate::{
message::{
CharacterIdentity, PublicIdentity,
night::{ActionPrompt, ActionPromptTitle},
},
player::CharacterId,
};
fn character_identity() -> CharacterIdentity {
CharacterIdentity {
character_id: CharacterId::new(),
public: PublicIdentity {
name: String::new(),
pronouns: None,
number: NonZeroU8::new(1).unwrap(),
},
}
}
#[test]
fn night_order() {
let test_cases: &[(&[ActionPrompt], &[ActionPromptTitle])] = &[(
&[
ActionPrompt::CoverOfDarkness,
ActionPrompt::WolvesIntro {
wolves: Box::new([]),
},
ActionPrompt::Shapeshifter {
character_id: character_identity(),
},
ActionPrompt::WolfPackKill {
living_villagers: Box::new([]),
},
ActionPrompt::Protector {
character_id: character_identity(),
targets: Box::new([]),
},
],
&[
ActionPromptTitle::CoverOfDarkness,
ActionPromptTitle::Protector,
ActionPromptTitle::WolvesIntro,
ActionPromptTitle::WolfPackKill,
ActionPromptTitle::Shapeshifter,
],
)];
for (input, expect) in test_cases {
let mut prompts = input.to_vec();
prompts.sort_by(|left_prompt, right_prompt| {
left_prompt
.partial_cmp(right_prompt)
.unwrap_or(core::cmp::Ordering::Equal)
});
let actual = prompts.into_iter().map(|p| p.title()).collect::<Box<[_]>>();
let actual: &[ActionPromptTitle] = &actual;
assert_eq!(*expect, actual)
}
}

View File

@ -7,6 +7,8 @@ use thiserror::Error;
pub mod diedto; pub mod diedto;
pub mod error; pub mod error;
pub mod game; pub mod game;
#[cfg(test)]
mod game_test;
pub mod message; pub mod message;
pub mod modifier; pub mod modifier;
pub mod nonzero; pub mod nonzero;

View File

@ -1,6 +1,7 @@
use core::num::NonZeroU8; use core::num::NonZeroU8;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use werewolves_macros::Extract;
use crate::{ use crate::{
error::GameError, error::GameError,
@ -44,6 +45,12 @@ pub enum HostDayMessage {
MarkForExecution(CharacterId), MarkForExecution(CharacterId),
} }
impl From<HostDayMessage> for HostGameMessage {
fn from(value: HostDayMessage) -> Self {
HostGameMessage::Day(value)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum HostLobbyMessage { pub enum HostLobbyMessage {
GetState, GetState,
@ -54,6 +61,7 @@ pub enum HostLobbyMessage {
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(test, derive(werewolves_macros::Titles))]
pub enum ServerToHostMessage { pub enum ServerToHostMessage {
Disconnect, Disconnect,
Daytime { Daytime {
@ -61,8 +69,8 @@ pub enum ServerToHostMessage {
marked: Box<[CharacterId]>, marked: Box<[CharacterId]>,
day: NonZeroU8, day: NonZeroU8,
}, },
ActionPrompt(PublicIdentity, ActionPrompt), ActionPrompt(ActionPrompt),
ActionResult(PublicIdentity, ActionResult), ActionResult(Option<PublicIdentity>, ActionResult),
Lobby(Box<[PlayerState]>), Lobby(Box<[PlayerState]>),
GameSettings(GameSettings), GameSettings(GameSettings),
Error(GameError), Error(GameError),
@ -71,5 +79,4 @@ pub enum ServerToHostMessage {
ackd: Box<[Target]>, ackd: Box<[Target]>,
waiting: Box<[Target]>, waiting: Box<[Target]>,
}, },
CoverOfDarkness,
} }

View File

@ -21,6 +21,21 @@ pub struct PublicIdentity {
pub number: NonZeroU8, pub number: NonZeroU8,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CharacterIdentity {
pub character_id: CharacterId,
pub public: PublicIdentity,
}
impl CharacterIdentity {
pub const fn new(character_id: CharacterId, public: PublicIdentity) -> Self {
Self {
character_id,
public,
}
}
}
impl Default for PublicIdentity { impl Default for PublicIdentity {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@ -1,107 +1,125 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use werewolves_macros::ChecksAs; use werewolves_macros::{ChecksAs, Titles};
use crate::{ use crate::{
diedto::DiedTo, message::CharacterIdentity,
message::PublicIdentity,
player::CharacterId, player::CharacterId,
role::{Alignment, PreviousGuardianAction, Role, RoleTitle}, role::{Alignment, PreviousGuardianAction, RoleTitle},
}; };
use super::Target; use super::Target;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
pub enum ActionType { pub enum ActionType {
Protect = 0, Cover,
WolfPackKill = 1, WolvesIntro,
Direwolf = 2, Protect,
Wolf = 3, WolfPackKill,
Block = 4, Direwolf,
Other = 5, OtherWolf,
RoleChange = 6, Block,
Other,
RoleChange,
} }
impl PartialOrd for ActionType { impl ActionType {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { const fn is_wolfy(&self) -> bool {
(*self as u8).partial_cmp(&(*other as u8)) matches!(
self,
ActionType::Direwolf
| ActionType::OtherWolf
| ActionType::WolfPackKill
| ActionType::WolvesIntro
)
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles)]
pub enum ActionPrompt { pub enum ActionPrompt {
#[checks(ActionType::Cover)]
CoverOfDarkness,
#[checks(ActionType::WolfPackKill)] #[checks(ActionType::WolfPackKill)]
#[checks]
WolvesIntro { wolves: Box<[(Target, RoleTitle)]> }, WolvesIntro { wolves: Box<[(Target, RoleTitle)]> },
#[checks(ActionType::RoleChange)] #[checks(ActionType::RoleChange)]
RoleChange { new_role: RoleTitle }, RoleChange {
character_id: CharacterIdentity,
new_role: RoleTitle,
},
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Seer { living_players: Box<[Target]> }, Seer {
character_id: CharacterIdentity,
living_players: Box<[Target]>,
},
#[checks(ActionType::Protect)] #[checks(ActionType::Protect)]
Protector { targets: Box<[Target]> }, Protector {
character_id: CharacterIdentity,
targets: Box<[Target]>,
},
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Arcanist { living_players: Box<[Target]> }, Arcanist {
character_id: CharacterIdentity,
living_players: Box<[Target]>,
},
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Gravedigger { dead_players: Box<[Target]> }, Gravedigger {
character_id: CharacterIdentity,
dead_players: Box<[Target]>,
},
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Hunter { Hunter {
character_id: CharacterIdentity,
current_target: Option<Target>, current_target: Option<Target>,
living_players: Box<[Target]>, living_players: Box<[Target]>,
}, },
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Militia { living_players: Box<[Target]> }, Militia {
character_id: CharacterIdentity,
living_players: Box<[Target]>,
},
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
MapleWolf { MapleWolf {
character_id: CharacterIdentity,
kill_or_die: bool, kill_or_die: bool,
living_players: Box<[Target]>, living_players: Box<[Target]>,
}, },
#[checks(ActionType::Protect)] #[checks(ActionType::Protect)]
Guardian { Guardian {
character_id: CharacterIdentity,
previous: Option<PreviousGuardianAction>, previous: Option<PreviousGuardianAction>,
living_players: Box<[Target]>, living_players: Box<[Target]>,
}, },
#[checks(ActionType::Wolf)] #[checks(ActionType::WolfPackKill)]
WolfPackKill { living_villagers: Box<[Target]> }, WolfPackKill { living_villagers: Box<[Target]> },
#[checks(ActionType::Wolf)] #[checks(ActionType::OtherWolf)]
Shapeshifter, Shapeshifter { character_id: CharacterIdentity },
#[checks(ActionType::Wolf)] #[checks(ActionType::OtherWolf)]
AlphaWolf { living_villagers: Box<[Target]> }, AlphaWolf {
character_id: CharacterIdentity,
living_villagers: Box<[Target]>,
},
#[checks(ActionType::Direwolf)] #[checks(ActionType::Direwolf)]
DireWolf { living_players: Box<[Target]> }, DireWolf {
character_id: CharacterIdentity,
living_players: Box<[Target]>,
},
}
impl ActionPrompt {
pub const fn is_wolfy(&self) -> bool {
self.action_type().is_wolfy()
|| match self {
ActionPrompt::RoleChange {
character_id: _,
new_role,
} => new_role.wolf(),
_ => false,
}
}
} }
impl PartialOrd for ActionPrompt { impl PartialOrd for ActionPrompt {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { 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))
self.action_type().partial_cmp(&other.action_type()) self.action_type().partial_cmp(&other.action_type())
} }
} }
@ -124,6 +142,7 @@ pub enum ActionResponse {
#[checks] #[checks]
RoleChangeAck, RoleChangeAck,
WolvesIntroAck, WolvesIntroAck,
ClearCoverOfDarkness,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -133,5 +152,5 @@ pub enum ActionResult {
Arcanist { same: bool }, Arcanist { same: bool },
GraveDigger(Option<RoleTitle>), GraveDigger(Option<RoleTitle>),
GoBackToSleep, GoBackToSleep,
WolvesIntroDone, Continue,
} }

View File

@ -6,7 +6,7 @@ use crate::{
diedto::DiedTo, diedto::DiedTo,
error::GameError, error::GameError,
game::{DateTime, Village}, game::{DateTime, Village},
message::{Identification, PublicIdentity, Target, night::ActionPrompt}, message::{CharacterIdentity, Identification, PublicIdentity, Target, night::ActionPrompt},
modifier::Modifier, modifier::Modifier,
role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
}; };
@ -116,6 +116,13 @@ impl Character {
&self.public &self.public
} }
pub fn character_identity(&self) -> CharacterIdentity {
CharacterIdentity {
character_id: self.character_id.clone(),
public: self.public.clone(),
}
}
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.public.name &self.public.name
} }
@ -213,19 +220,23 @@ impl Character {
| Role::Scapegoat | Role::Scapegoat
| Role::Villager => return Ok(None), | Role::Villager => return Ok(None),
Role::Seer => ActionPrompt::Seer { Role::Seer => ActionPrompt::Seer {
character_id: self.character_identity(),
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(&self.character_id),
}, },
Role::Arcanist => ActionPrompt::Arcanist { Role::Arcanist => ActionPrompt::Arcanist {
character_id: self.character_identity(),
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(&self.character_id),
}, },
Role::Protector { Role::Protector {
last_protected: Some(last_protected), last_protected: Some(last_protected),
} => ActionPrompt::Protector { } => ActionPrompt::Protector {
character_id: self.character_identity(),
targets: village.living_players_excluding(last_protected), targets: village.living_players_excluding(last_protected),
}, },
Role::Protector { Role::Protector {
last_protected: None, last_protected: None,
} => ActionPrompt::Protector { } => ActionPrompt::Protector {
character_id: self.character_identity(),
targets: village.living_players_excluding(&self.character_id), targets: village.living_players_excluding(&self.character_id),
}, },
Role::Apprentice(role) => { Role::Apprentice(role) => {
@ -243,6 +254,7 @@ impl Character {
DateTime::Night { number } => number + 1 >= current_night, DateTime::Night { number } => number + 1 >= current_night,
}) })
.then(|| ActionPrompt::RoleChange { .then(|| ActionPrompt::RoleChange {
character_id: self.character_identity(),
new_role: role.title(), new_role: role.title(),
})); }));
} }
@ -253,49 +265,61 @@ impl Character {
}; };
return Ok((current_night == knows_on_night.get()).then_some({ return Ok((current_night == knows_on_night.get()).then_some({
ActionPrompt::RoleChange { ActionPrompt::RoleChange {
character_id: self.character_identity(),
new_role: RoleTitle::Elder, new_role: RoleTitle::Elder,
} }
})); }));
} }
Role::Militia { targeted: None } => ActionPrompt::Militia { Role::Militia { targeted: None } => ActionPrompt::Militia {
character_id: self.character_identity(),
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(&self.character_id),
}, },
Role::Werewolf => ActionPrompt::WolfPackKill { Role::Werewolf => ActionPrompt::WolfPackKill {
living_villagers: village.living_players(), living_villagers: village.living_players(),
}, },
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf { Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
character_id: self.character_identity(),
living_villagers: village.living_players_excluding(&self.character_id), living_villagers: village.living_players_excluding(&self.character_id),
}, },
Role::DireWolf => ActionPrompt::DireWolf { Role::DireWolf => ActionPrompt::DireWolf {
character_id: self.character_identity(),
living_players: village.living_players(), living_players: village.living_players(),
}, },
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter, Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
character_id: self.character_identity(),
},
Role::Gravedigger => ActionPrompt::Gravedigger { Role::Gravedigger => ActionPrompt::Gravedigger {
character_id: self.character_identity(),
dead_players: village.dead_targets(), dead_players: village.dead_targets(),
}, },
Role::Hunter { target } => ActionPrompt::Hunter { Role::Hunter { target } => ActionPrompt::Hunter {
character_id: self.character_identity(),
current_target: target.as_ref().and_then(|t| village.target_by_id(t)), current_target: target.as_ref().and_then(|t| village.target_by_id(t)),
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(&self.character_id),
}, },
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf { Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
character_id: self.character_identity(),
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night, kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(&self.character_id),
}, },
Role::Guardian { Role::Guardian {
last_protected: Some(PreviousGuardianAction::Guard(prev_target)), last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
} => ActionPrompt::Guardian { } => ActionPrompt::Guardian {
character_id: self.character_identity(),
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())), previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
living_players: village.living_players_excluding(&prev_target.character_id), living_players: village.living_players_excluding(&prev_target.character_id),
}, },
Role::Guardian { Role::Guardian {
last_protected: Some(PreviousGuardianAction::Protect(prev_target)), last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
} => ActionPrompt::Guardian { } => ActionPrompt::Guardian {
character_id: self.character_identity(),
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())), previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
living_players: village.living_players(), living_players: village.living_players(),
}, },
Role::Guardian { Role::Guardian {
last_protected: None, last_protected: None,
} => ActionPrompt::Guardian { } => ActionPrompt::Guardian {
character_id: self.character_identity(),
previous: None, previous: None,
living_players: village.living_players(), living_players: village.living_players(),
}, },

View File

@ -1,19 +0,0 @@
[Unit]
Description=blog
After=network.target
[Service]
Type=simple
User=blog
Group=blog
WorkingDirectory=/home/blog
Environment=RUST_LOG=info
Environment=PORT=3024
ExecStart=/home/blog/blog-server
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,19 @@
[Unit]
Description=werewolves
After=network.target
[Service]
Type=simple
User=werewolf
Group=werewolf
WorkingDirectory=/home/werewolf
Environment=RUST_LOG=info
Environment=PORT=3028
ExecStart=/home/werewolf/werewolves-server
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -27,8 +27,6 @@ pub struct GameRunner {
player_sender: LobbyPlayers, player_sender: LobbyPlayers,
roles_revealed: bool, roles_revealed: bool,
joined_players: JoinedPlayers, joined_players: JoinedPlayers,
// _release_token: InGameToken,
cover_of_darkness: bool,
} }
impl GameRunner { impl GameRunner {
@ -38,7 +36,6 @@ impl GameRunner {
player_sender: LobbyPlayers, player_sender: LobbyPlayers,
connect_recv: Receiver<(PlayerId, bool)>, connect_recv: Receiver<(PlayerId, bool)>,
joined_players: JoinedPlayers, joined_players: JoinedPlayers,
release_token: InGameToken,
) -> Self { ) -> Self {
Self { Self {
game, game,
@ -47,8 +44,6 @@ impl GameRunner {
player_sender, player_sender,
joined_players, joined_players,
roles_revealed: false, roles_revealed: false,
// _release_token: release_token,
cover_of_darkness: true,
} }
} }
@ -203,18 +198,7 @@ impl GameRunner {
if !self.roles_revealed { if !self.roles_revealed {
return Err(GameError::NeedRoleReveal); return Err(GameError::NeedRoleReveal);
} }
if self.cover_of_darkness {
match &message {
HostMessage::GetState | HostMessage::InGame(HostGameMessage::GetState) => {
return Ok(ServerToHostMessage::CoverOfDarkness);
}
HostMessage::InGame(HostGameMessage::Night(HostNightMessage::Next)) => {
self.cover_of_darkness = false;
return self.host_message(HostMessage::GetState);
}
_ => return Err(GameError::InvalidMessageForGameState),
};
}
match message { match message {
HostMessage::GetState => self.game.process(HostGameMessage::GetState), HostMessage::GetState => self.game.process(HostGameMessage::GetState),
HostMessage::InGame(msg) => self.game.process(msg), HostMessage::InGame(msg) => self.game.process(msg),

View File

@ -190,15 +190,6 @@ impl Lobby {
.iter() .iter()
.map(|(id, _)| id.clone()) .map(|(id, _)| id.clone())
.collect::<Box<[_]>>(); .collect::<Box<[_]>>();
let release_token = self
.joined_players
.start_game_with(
&playing_players
.iter()
.map(|id| id.player_id.clone())
.collect::<Box<[_]>>(),
)
.await?;
let game = Game::new(&playing_players, self.settings.clone())?; let game = Game::new(&playing_players, self.settings.clone())?;
assert_eq!(game.village().characters().len(), playing_players.len()); assert_eq!(game.village().characters().len(), playing_players.len());
@ -210,7 +201,6 @@ impl Lobby {
self.players_in_lobby.clone(), self.players_in_lobby.clone(),
recv, recv,
self.joined_players.clone(), self.joined_players.clone(),
release_token,
))); )));
} }
Message::Client(IdentifiedClientMessage { Message::Client(IdentifiedClientMessage {

View File

@ -108,14 +108,14 @@ async fn main() {
let jp_clone = joined_players.clone(); let jp_clone = joined_players.clone();
let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR)) let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR));
.canonicalize()
.expect("canonicalizing path");
if let Err(err) = std::fs::create_dir(&path) if let Err(err) = std::fs::create_dir(&path)
&& !matches!(err.kind(), std::io::ErrorKind::AlreadyExists) && !matches!(err.kind(), std::io::ErrorKind::AlreadyExists)
{ {
panic!("creating save dir at [{path:?}]: {err}") panic!("creating save dir at [{path:?}]: {err}")
} }
// Check if we can write to the path // Check if we can write to the path
{ {
let test_file_path = path.join(".test"); let test_file_path = path.join(".test");
@ -125,7 +125,7 @@ async fn main() {
std::fs::remove_file(&test_file_path).log_err(); std::fs::remove_file(&test_file_path).log_err();
} }
let saver = FileSaver::new(path); let saver = FileSaver::new(path.canonicalize().expect("canonicalizing path"));
tokio::spawn(async move { tokio::spawn(async move {
crate::runner::run_game(jp_clone, lobby_comms, saver).await; crate::runner::run_game(jp_clone, lobby_comms, saver).await;
panic!("game over"); panic!("game over");

View File

@ -1,8 +1,8 @@
use core::{num::NonZeroU8, ops::Not}; use core::ops::Not;
use werewolves_proto::{ use werewolves_proto::{
message::{ message::{
PublicIdentity, Target, PublicIdentity,
host::{HostGameMessage, HostMessage, HostNightMessage}, host::{HostGameMessage, HostMessage, HostNightMessage},
night::{ActionPrompt, ActionResponse}, night::{ActionPrompt, ActionResponse},
}, },
@ -12,42 +12,59 @@ use werewolves_proto::{
use yew::prelude::*; use yew::prelude::*;
use crate::components::{ use crate::components::{
Identity, CoverOfDarkness, Identity,
action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro}, action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro},
}; };
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct ActionPromptProps { pub struct ActionPromptProps {
pub prompt: ActionPrompt, pub prompt: ActionPrompt,
pub ident: PublicIdentity,
#[prop_or_default] #[prop_or_default]
pub big_screen: bool, pub big_screen: bool,
pub on_complete: Callback<HostMessage>, pub on_complete: Callback<HostMessage>,
} }
fn identity_html(ident: Option<&PublicIdentity>) -> Option<Html> {
ident.map(|ident| {
html! {
<Identity ident={ident.clone()}/>
}
})
}
#[function_component] #[function_component]
pub fn Prompt(props: &ActionPromptProps) -> Html { pub fn Prompt(props: &ActionPromptProps) -> Html {
let ident = props
.big_screen
.not()
.then(|| html! {<Identity ident={props.ident.clone()}/>});
match &props.prompt { match &props.prompt {
ActionPrompt::CoverOfDarkness => {
let on_complete = props.on_complete.clone();
let next = props.big_screen.not().then(|| {
Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::Next,
)))
})
});
return html! {
<CoverOfDarkness next={next} />
};
}
ActionPrompt::WolvesIntro { wolves } => { ActionPrompt::WolvesIntro { wolves } => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_complete = Callback::from(move |_| { let on_complete = Callback::from(move |_| {
on_complete.emit(HostMessage::InGame( on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
werewolves_proto::message::host::HostGameMessage::Night( HostNightMessage::ActionResponse(
werewolves_proto::message::host::HostNightMessage::ActionResponse(
werewolves_proto::message::night::ActionResponse::WolvesIntroAck, werewolves_proto::message::night::ActionResponse::WolvesIntroAck,
), ),
), )))
))
}); });
html! { html! {
<WolvesIntro big_screen={props.big_screen} on_complete={on_complete} wolves={wolves.clone()}/> <WolvesIntro big_screen={props.big_screen} on_complete={on_complete} wolves={wolves.clone()}/>
} }
} }
ActionPrompt::Seer { living_players } => { ActionPrompt::Seer {
character_id,
living_players,
} => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| { let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| { Callback::from(move |target: CharacterId| {
@ -58,7 +75,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{ident} {identity_html(props.big_screen.then_some(&character_id.public))}
<SingleTarget <SingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -67,7 +84,10 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
</div> </div>
} }
} }
ActionPrompt::RoleChange { new_role } => { ActionPrompt::RoleChange {
character_id,
new_role,
} => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_click = Callback::from(move |_| { let on_click = Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night( on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
@ -81,14 +101,17 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{ident} {identity_html(props.big_screen.then_some(&character_id.public))}
<h2>{"your role has changed"}</h2> <h2>{"your role has changed"}</h2>
<p>{new_role.to_string()}</p> <p>{new_role.to_string()}</p>
{cont} {cont}
</div> </div>
} }
} }
ActionPrompt::Protector { targets } => { ActionPrompt::Protector {
character_id,
targets,
} => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| { let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| { Callback::from(move |target: CharacterId| {
@ -99,6 +122,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props.big_screen.then_some(&character_id.public))}
<SingleTarget <SingleTarget
targets={targets.clone()} targets={targets.clone()}
target_selection={on_select} target_selection={on_select}
@ -107,7 +131,10 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
</div> </div>
} }
} }
ActionPrompt::Arcanist { living_players } => { ActionPrompt::Arcanist {
character_id,
living_players,
} => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| { let on_select = props.big_screen.not().then(|| {
Callback::from(move |(t1, t2): (CharacterId, CharacterId)| { Callback::from(move |(t1, t2): (CharacterId, CharacterId)| {
@ -118,6 +145,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props.big_screen.then_some(&character_id.public))}
<TwoTarget <TwoTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -126,7 +154,10 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
</div> </div>
} }
} }
ActionPrompt::Gravedigger { dead_players } => { ActionPrompt::Gravedigger {
character_id,
dead_players,
} => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| { let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| { Callback::from(move |target: CharacterId| {
@ -137,6 +168,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props.big_screen.then_some(&character_id.public))}
<SingleTarget <SingleTarget
targets={dead_players.clone()} targets={dead_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -146,6 +178,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
} }
} }
ActionPrompt::Hunter { ActionPrompt::Hunter {
character_id,
current_target, current_target,
living_players, living_players,
} => { } => {
@ -158,6 +191,8 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}) })
}); });
html! { html! {
<div>
{identity_html(props.big_screen.then_some(&character_id.public))}
<SingleTarget <SingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -169,9 +204,13 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}).unwrap_or_else(|| html!{<i>{"none"}</i>})} }).unwrap_or_else(|| html!{<i>{"none"}</i>})}
</h3> </h3>
</SingleTarget> </SingleTarget>
</div>
} }
} }
ActionPrompt::Militia { living_players } => { ActionPrompt::Militia {
character_id,
living_players,
} => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| { let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: Option<CharacterId>| { Callback::from(move |target: Option<CharacterId>| {
@ -181,14 +220,18 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}) })
}); });
html! { html! {
<div>
{identity_html(props.big_screen.then_some(&character_id.public))}
<OptionalSingleTarget <OptionalSingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
headline={"pew pew?"} headline={"pew pew?"}
/> />
</div>
} }
} }
ActionPrompt::MapleWolf { ActionPrompt::MapleWolf {
character_id: _,
kill_or_die, kill_or_die,
living_players, living_players,
} => { } => {
@ -206,6 +249,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
} }
}); });
html! { html! {
<div>
<OptionalSingleTarget <OptionalSingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -213,9 +257,11 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
> >
{kill_or_die} {kill_or_die}
</OptionalSingleTarget> </OptionalSingleTarget>
</div>
} }
} }
ActionPrompt::Guardian { ActionPrompt::Guardian {
character_id: _,
previous, previous,
living_players, living_players,
} => { } => {
@ -271,7 +317,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
/> />
} }
} }
ActionPrompt::Shapeshifter => { ActionPrompt::Shapeshifter { character_id: _ } => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then_some({ let on_select = props.big_screen.not().then_some({
move |shift| { move |shift| {
@ -286,7 +332,10 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
</BinaryChoice> </BinaryChoice>
} }
} }
ActionPrompt::AlphaWolf { living_villagers } => { ActionPrompt::AlphaWolf {
character_id: _,
living_villagers,
} => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| { let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: Option<CharacterId>| { Callback::from(move |target: Option<CharacterId>| {
@ -303,7 +352,10 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
/> />
} }
} }
ActionPrompt::DireWolf { living_players } => { ActionPrompt::DireWolf {
character_id: _,
living_players,
} => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| { let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| { Callback::from(move |target: CharacterId| {

View File

@ -16,7 +16,8 @@ use crate::components::{CoverOfDarkness, Identity};
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct ActionResultProps { pub struct ActionResultProps {
pub result: ActionResult, pub result: ActionResult,
pub ident: PublicIdentity, #[prop_or_default]
pub ident: Option<PublicIdentity>,
#[prop_or_default] #[prop_or_default]
pub big_screen: bool, pub big_screen: bool,
pub on_complete: Callback<HostMessage>, pub on_complete: Callback<HostMessage>,
@ -24,10 +25,12 @@ pub struct ActionResultProps {
#[function_component] #[function_component]
pub fn ActionResultView(props: &ActionResultProps) -> Html { pub fn ActionResultView(props: &ActionResultProps) -> Html {
let ident = props let ident = props.ident.as_ref().and_then(|ident| {
props
.big_screen .big_screen
.not() .not()
.then(|| html! {<Identity ident={props.ident.clone()}/>}); .then(|| html! {<Identity ident={ident.clone()}/>})
});
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_complete = Callback::from(move |_| { let on_complete = Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night( on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
@ -98,18 +101,10 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
</CoverOfDarkness> </CoverOfDarkness>
} }
} }
ActionResult::WolvesIntroDone => { ActionResult::Continue => {
let on_complete = props.on_complete.clone(); props.on_complete.emit(HostMessage::GetState);
let next = props.big_screen.not().then(|| {
Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::Next,
)))
})
});
html! { html! {
<CoverOfDarkness message={"wolves go to sleep"} next={next}/> <CoverOfDarkness />
} }
} }
} }

View File

@ -50,7 +50,7 @@ pub struct Connection {
impl Connection { impl Connection {
async fn connect_ws() -> WebSocket { async fn connect_ws() -> WebSocket {
let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); let url = option_env!("LOCAL").map(|_| DEBUG_URL).unwrap_or(LIVE_URL);
loop { loop {
match WebSocket::open(url) { match WebSocket::open(url) {
Ok(ws) => break ws, Ok(ws) => break ws,
@ -78,7 +78,7 @@ impl Connection {
} }
async fn run(mut self) { async fn run(mut self) {
let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); let url = option_env!("LOCAL").map(|_| DEBUG_URL).unwrap_or(LIVE_URL);
'outer: loop { 'outer: loop {
log::info!("connecting to {url}"); log::info!("connecting to {url}");
let mut ws = Self::connect_ws().await.fuse(); let mut ws = Self::connect_ws().await.fuse();
@ -511,8 +511,9 @@ impl Component for Client {
true true
} }
Message::Connect => { Message::Connect => {
if let Some(player) = self.player.as_ref() { if let Some(player) = self.player.as_ref()
if let Some(recv) = self.recv.take() { && let Some(recv) = self.recv.take()
{
yew::platform::spawn_local( yew::platform::spawn_local(
Connection { Connection {
scope: ctx.link().clone(), scope: ctx.link().clone(),
@ -523,7 +524,6 @@ impl Component for Client {
); );
return true; return true;
} }
}
while let Err(err) = self.send.try_send(ClientMessage::GetState) { while let Err(err) = self.send.try_send(ClientMessage::GetState) {
log::error!("send IsThereALobby: {err}") log::error!("send IsThereALobby: {err}")
} }

View File

@ -36,7 +36,7 @@ const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/host";
const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/host"; const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/host";
async fn connect_ws() -> WebSocket { async fn connect_ws() -> WebSocket {
let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); let url = option_env!("LOCAL").map(|_| DEBUG_URL).unwrap_or(LIVE_URL);
loop { loop {
match WebSocket::open(url) { match WebSocket::open(url) {
Ok(ws) => break ws, Ok(ws) => break ws,
@ -64,7 +64,7 @@ fn encode_message(msg: &impl Serialize) -> websocket::Message {
} }
async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) { async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); let url = option_env!("LOCAL").map(|_| DEBUG_URL).unwrap_or(LIVE_URL);
'outer: loop { 'outer: loop {
log::info!("connecting to {url}"); log::info!("connecting to {url}");
let mut ws = connect_ws().await.fuse(); let mut ws = connect_ws().await.fuse();
@ -75,13 +75,11 @@ async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
continue 'outer; continue 'outer;
} }
let mut last_msg = chrono::Local::now();
loop { loop {
let msg = futures::select! { let msg = futures::select! {
r = ws.next() => { r = ws.next() => {
match r { match r {
Some(Ok(msg)) => { Some(Ok(msg)) => {
last_msg = chrono::Local::now();
msg msg
}, },
Some(Err(err)) => { Some(Err(err)) => {
@ -137,10 +135,6 @@ async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
} }
} }
}; };
let took = chrono::Local::now() - last_msg;
if took.num_milliseconds() >= 100 {
log::warn!("took {took}")
}
match parse { match parse {
Ok(msg) => scope.send_message::<HostEvent>(msg.into()), Ok(msg) => scope.send_message::<HostEvent>(msg.into()),
Err(err) => { Err(err) => {
@ -178,9 +172,8 @@ pub enum HostState {
ackd: Box<[Target]>, ackd: Box<[Target]>,
waiting: Box<[Target]>, waiting: Box<[Target]>,
}, },
Prompt(PublicIdentity, ActionPrompt), Prompt(ActionPrompt),
Result(PublicIdentity, ActionResult), Result(Option<PublicIdentity>, ActionResult),
CoverOfDarkness,
} }
impl From<ServerToHostMessage> for HostEvent { impl From<ServerToHostMessage> for HostEvent {
@ -202,8 +195,8 @@ impl From<ServerToHostMessage> for HostEvent {
ServerToHostMessage::GameOver(game_over) => { ServerToHostMessage::GameOver(game_over) => {
HostEvent::SetState(HostState::GameOver { result: game_over }) HostEvent::SetState(HostState::GameOver { result: game_over })
} }
ServerToHostMessage::ActionPrompt(ident, prompt) => { ServerToHostMessage::ActionPrompt(prompt) => {
HostEvent::SetState(HostState::Prompt(ident, prompt)) HostEvent::SetState(HostState::Prompt(prompt))
} }
ServerToHostMessage::ActionResult(ident, result) => { ServerToHostMessage::ActionResult(ident, result) => {
HostEvent::SetState(HostState::Result(ident, result)) HostEvent::SetState(HostState::Result(ident, result))
@ -211,7 +204,6 @@ impl From<ServerToHostMessage> for HostEvent {
ServerToHostMessage::WaitingForRoleRevealAcks { ackd, waiting } => { ServerToHostMessage::WaitingForRoleRevealAcks { ackd, waiting } => {
HostEvent::SetState(HostState::RoleReveal { ackd, waiting }) HostEvent::SetState(HostState::RoleReveal { ackd, waiting })
} }
ServerToHostMessage::CoverOfDarkness => HostEvent::SetState(HostState::CoverOfDarkness),
} }
} }
} }
@ -402,7 +394,7 @@ impl Component for Host {
<RoleReveal ackd={ackd} waiting={waiting} on_force_ready={on_force_ready}/> <RoleReveal ackd={ackd} waiting={waiting} on_force_ready={on_force_ready}/>
} }
} }
HostState::Prompt(ident, prompt) => { HostState::Prompt(prompt) => {
let send = self.send.clone(); let send = self.send.clone();
let on_complete = Callback::from(move |msg| { let on_complete = Callback::from(move |msg| {
let mut send = send.clone(); let mut send = send.clone();
@ -418,7 +410,6 @@ impl Component for Host {
prompt={prompt} prompt={prompt}
big_screen={self.big_screen} big_screen={self.big_screen}
on_complete={on_complete} on_complete={on_complete}
ident={ident}
/> />
} }
} }
@ -442,27 +433,6 @@ impl Component for Host {
/> />
} }
} }
HostState::CoverOfDarkness => {
let next = self.big_screen.not().then(|| {
let send = self.send.clone();
Callback::from(move |_| {
let mut send = send.clone();
yew::platform::spawn_local(async move {
if let Err(err) = send
.send(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::Next,
)))
.await
{
log::error!("sending action result response: {err}")
}
});
})
});
return html! {
<CoverOfDarkness next={next} />
};
}
}; };
let debug_nav = self.debug.then(|| { let debug_nav = self.debug.then(|| {
let on_error_click = callback::send_message( let on_error_click = callback::send_message(
@ -506,8 +476,7 @@ impl Component for Host {
settings: GameSettings::default(), settings: GameSettings::default(),
} }
} }
HostState::CoverOfDarkness HostState::Prompt(_)
| HostState::Prompt(_, _)
| HostState::Result(_, _) | HostState::Result(_, _)
| HostState::Disconnected | HostState::Disconnected
| HostState::RoleReveal { | HostState::RoleReveal {
@ -540,8 +509,7 @@ impl Component for Host {
*s = settings; *s = settings;
true true
} }
HostState::CoverOfDarkness HostState::Prompt(_)
| HostState::Prompt(_, _)
| HostState::Result(_, _) | HostState::Result(_, _)
| HostState::Disconnected | HostState::Disconnected
| HostState::RoleReveal { | HostState::RoleReveal {