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"
version = "0.1.0"
dependencies = [
"colored",
"log",
"pretty_assertions",
"pretty_env_logger",
"rand",
"serde",
"serde_json",

View File

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

View File

@ -18,6 +18,7 @@ use crate::{
message::{
CharacterState, Identification,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::ActionResponse,
},
player::CharacterId,
};
@ -107,18 +108,15 @@ impl Game {
}
(GameState::Night { night }, HostGameMessage::GetState) => {
if let Some(res) = night.current_result() {
let char = night.current_character().unwrap();
return Ok(ServerToHostMessage::ActionResult(
char.public_identity().clone(),
night
.current_character()
.map(|c| c.public_identity().clone()),
res.clone(),
));
}
if let Some(prompt) = night.current_prompt() {
let char = night.current_character().unwrap();
return Ok(ServerToHostMessage::ActionPrompt(
char.public_identity().clone(),
prompt.clone(),
));
return Ok(ServerToHostMessage::ActionPrompt(prompt.clone()));
}
match night.next() {
Ok(_) => self.process(HostGameMessage::GetState),
@ -140,7 +138,9 @@ impl Game {
HostGameMessage::Night(HostNightMessage::ActionResponse(resp)),
) => match night.received_response(resp.clone()) {
Ok(res) => Ok(ServerToHostMessage::ActionResult(
night.current_character().unwrap().public_identity().clone(),
night
.current_character()
.map(|c| c.public_identity().clone()),
res,
)),
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 {
roles: [
(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::Guardian, NonZeroU8::new(1).unwrap()),
(RoleTitle::Apprentice, NonZeroU8::new(1).unwrap()),
// (RoleTitle::Apprentice, NonZeroU8::new(1).unwrap()),
]
.into_iter()
.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 error;
pub mod game;
#[cfg(test)]
mod game_test;
pub mod message;
pub mod modifier;
pub mod nonzero;

View File

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

View File

@ -21,6 +21,21 @@ pub struct PublicIdentity {
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 {
fn default() -> Self {
Self {

View File

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

View File

@ -6,7 +6,7 @@ use crate::{
diedto::DiedTo,
error::GameError,
game::{DateTime, Village},
message::{Identification, PublicIdentity, Target, night::ActionPrompt},
message::{CharacterIdentity, Identification, PublicIdentity, Target, night::ActionPrompt},
modifier::Modifier,
role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
};
@ -116,6 +116,13 @@ impl Character {
&self.public
}
pub fn character_identity(&self) -> CharacterIdentity {
CharacterIdentity {
character_id: self.character_id.clone(),
public: self.public.clone(),
}
}
pub fn name(&self) -> &str {
&self.public.name
}
@ -213,19 +220,23 @@ impl Character {
| Role::Scapegoat
| Role::Villager => return Ok(None),
Role::Seer => ActionPrompt::Seer {
character_id: self.character_identity(),
living_players: village.living_players_excluding(&self.character_id),
},
Role::Arcanist => ActionPrompt::Arcanist {
character_id: self.character_identity(),
living_players: village.living_players_excluding(&self.character_id),
},
Role::Protector {
last_protected: Some(last_protected),
} => ActionPrompt::Protector {
character_id: self.character_identity(),
targets: village.living_players_excluding(last_protected),
},
Role::Protector {
last_protected: None,
} => ActionPrompt::Protector {
character_id: self.character_identity(),
targets: village.living_players_excluding(&self.character_id),
},
Role::Apprentice(role) => {
@ -243,6 +254,7 @@ impl Character {
DateTime::Night { number } => number + 1 >= current_night,
})
.then(|| ActionPrompt::RoleChange {
character_id: self.character_identity(),
new_role: role.title(),
}));
}
@ -253,49 +265,61 @@ impl Character {
};
return Ok((current_night == knows_on_night.get()).then_some({
ActionPrompt::RoleChange {
character_id: self.character_identity(),
new_role: RoleTitle::Elder,
}
}));
}
Role::Militia { targeted: None } => ActionPrompt::Militia {
character_id: self.character_identity(),
living_players: village.living_players_excluding(&self.character_id),
},
Role::Werewolf => ActionPrompt::WolfPackKill {
living_villagers: village.living_players(),
},
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
character_id: self.character_identity(),
living_villagers: village.living_players_excluding(&self.character_id),
},
Role::DireWolf => ActionPrompt::DireWolf {
character_id: self.character_identity(),
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 {
character_id: self.character_identity(),
dead_players: village.dead_targets(),
},
Role::Hunter { target } => ActionPrompt::Hunter {
character_id: self.character_identity(),
current_target: target.as_ref().and_then(|t| village.target_by_id(t)),
living_players: village.living_players_excluding(&self.character_id),
},
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,
living_players: village.living_players_excluding(&self.character_id),
},
Role::Guardian {
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
} => ActionPrompt::Guardian {
character_id: self.character_identity(),
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
living_players: village.living_players_excluding(&prev_target.character_id),
},
Role::Guardian {
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
} => ActionPrompt::Guardian {
character_id: self.character_identity(),
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
living_players: village.living_players(),
},
Role::Guardian {
last_protected: None,
} => ActionPrompt::Guardian {
character_id: self.character_identity(),
previous: None,
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,
roles_revealed: bool,
joined_players: JoinedPlayers,
// _release_token: InGameToken,
cover_of_darkness: bool,
}
impl GameRunner {
@ -38,7 +36,6 @@ impl GameRunner {
player_sender: LobbyPlayers,
connect_recv: Receiver<(PlayerId, bool)>,
joined_players: JoinedPlayers,
release_token: InGameToken,
) -> Self {
Self {
game,
@ -47,8 +44,6 @@ impl GameRunner {
player_sender,
joined_players,
roles_revealed: false,
// _release_token: release_token,
cover_of_darkness: true,
}
}
@ -203,18 +198,7 @@ impl GameRunner {
if !self.roles_revealed {
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 {
HostMessage::GetState => self.game.process(HostGameMessage::GetState),
HostMessage::InGame(msg) => self.game.process(msg),

View File

@ -190,15 +190,6 @@ impl Lobby {
.iter()
.map(|(id, _)| id.clone())
.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())?;
assert_eq!(game.village().characters().len(), playing_players.len());
@ -210,7 +201,6 @@ impl Lobby {
self.players_in_lobby.clone(),
recv,
self.joined_players.clone(),
release_token,
)));
}
Message::Client(IdentifiedClientMessage {

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ pub struct Connection {
impl Connection {
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 {
match WebSocket::open(url) {
Ok(ws) => break ws,
@ -78,7 +78,7 @@ impl Connection {
}
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 {
log::info!("connecting to {url}");
let mut ws = Self::connect_ws().await.fuse();
@ -511,18 +511,18 @@ impl Component for Client {
true
}
Message::Connect => {
if let Some(player) = self.player.as_ref() {
if let Some(recv) = self.recv.take() {
yew::platform::spawn_local(
Connection {
scope: ctx.link().clone(),
ident: player.clone(),
recv,
}
.run(),
);
return true;
}
if let Some(player) = self.player.as_ref()
&& let Some(recv) = self.recv.take()
{
yew::platform::spawn_local(
Connection {
scope: ctx.link().clone(),
ident: player.clone(),
recv,
}
.run(),
);
return true;
}
while let Err(err) = self.send.try_send(ClientMessage::GetState) {
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";
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 {
match WebSocket::open(url) {
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>) {
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 {
log::info!("connecting to {url}");
let mut ws = connect_ws().await.fuse();
@ -75,13 +75,11 @@ async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
continue 'outer;
}
let mut last_msg = chrono::Local::now();
loop {
let msg = futures::select! {
r = ws.next() => {
match r {
Some(Ok(msg)) => {
last_msg = chrono::Local::now();
msg
},
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 {
Ok(msg) => scope.send_message::<HostEvent>(msg.into()),
Err(err) => {
@ -178,9 +172,8 @@ pub enum HostState {
ackd: Box<[Target]>,
waiting: Box<[Target]>,
},
Prompt(PublicIdentity, ActionPrompt),
Result(PublicIdentity, ActionResult),
CoverOfDarkness,
Prompt(ActionPrompt),
Result(Option<PublicIdentity>, ActionResult),
}
impl From<ServerToHostMessage> for HostEvent {
@ -202,8 +195,8 @@ impl From<ServerToHostMessage> for HostEvent {
ServerToHostMessage::GameOver(game_over) => {
HostEvent::SetState(HostState::GameOver { result: game_over })
}
ServerToHostMessage::ActionPrompt(ident, prompt) => {
HostEvent::SetState(HostState::Prompt(ident, prompt))
ServerToHostMessage::ActionPrompt(prompt) => {
HostEvent::SetState(HostState::Prompt(prompt))
}
ServerToHostMessage::ActionResult(ident, result) => {
HostEvent::SetState(HostState::Result(ident, result))
@ -211,7 +204,6 @@ impl From<ServerToHostMessage> for HostEvent {
ServerToHostMessage::WaitingForRoleRevealAcks { 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}/>
}
}
HostState::Prompt(ident, prompt) => {
HostState::Prompt(prompt) => {
let send = self.send.clone();
let on_complete = Callback::from(move |msg| {
let mut send = send.clone();
@ -418,7 +410,6 @@ impl Component for Host {
prompt={prompt}
big_screen={self.big_screen}
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 on_error_click = callback::send_message(
@ -506,8 +476,7 @@ impl Component for Host {
settings: GameSettings::default(),
}
}
HostState::CoverOfDarkness
| HostState::Prompt(_, _)
HostState::Prompt(_)
| HostState::Result(_, _)
| HostState::Disconnected
| HostState::RoleReveal {
@ -540,8 +509,7 @@ impl Component for Host {
*s = settings;
true
}
HostState::CoverOfDarkness
| HostState::Prompt(_, _)
HostState::Prompt(_)
| HostState::Result(_, _)
| HostState::Disconnected
| HostState::RoleReveal {