diff --git a/werewolves-proto/src/error.rs b/werewolves-proto/src/error.rs index 3d9f470..1c9a917 100644 --- a/werewolves-proto/src/error.rs +++ b/werewolves-proto/src/error.rs @@ -31,8 +31,6 @@ pub enum GameError { TimedOut, #[error("host channel closed")] HostChannelClosed, - #[error("not all players connected")] - NotAllPlayersConnected, #[error("too few players: got {got} but the settings require at least {need}")] TooFewPlayers { got: u8, need: u8 }, #[error("it's already daytime")] diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index e290f5c..d8b7328 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -98,7 +98,7 @@ impl Night { .partial_cmp(right_prompt) .unwrap_or(core::cmp::Ordering::Equal) }); - let mut action_queue = VecDeque::from(action_queue); + let action_queue = VecDeque::from(action_queue); let (current_prompt, current_char) = if night == 0 { ( ActionPrompt::WolvesIntro { @@ -117,10 +117,18 @@ impl Night { .clone(), ) } else { - action_queue - .pop_front() - .map(|(p, c)| (p, c.character_id().clone())) - .ok_or(GameError::NoNightActions)? + ( + ActionPrompt::WolfPackKill { + living_villagers: village.living_villagers(), + }, + village + .living_wolf_pack_players() + .into_iter() + .next() + .unwrap() + .character_id() + .clone(), + ) }; let night_state = NightState::Active { current_char, @@ -329,7 +337,9 @@ impl Night { } => {} } } - new_village.to_day()?; + if new_village.is_game_over().is_none() { + new_village.to_day()?; + } Ok(new_village) } diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index 1050ff0..2a85098 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -83,17 +83,23 @@ impl Village { .find(|c| c.character_id() == character_id) } - fn wolves_count(&self) -> usize { - self.characters.iter().filter(|c| c.is_wolf()).count() + fn living_wolves_count(&self) -> usize { + self.characters + .iter() + .filter(|c| c.is_wolf() && c.alive()) + .count() } - fn villager_count(&self) -> usize { - self.characters.iter().filter(|c| c.is_village()).count() + fn living_villager_count(&self) -> usize { + self.characters + .iter() + .filter(|c| c.is_village() && c.alive()) + .count() } pub fn is_game_over(&self) -> Option { - let wolves = self.wolves_count(); - let villagers = self.villager_count(); + let wolves = self.living_wolves_count(); + let villagers = self.living_villager_count(); if wolves == 0 { return Some(GameOver::VillageWins); diff --git a/werewolves-server/src/connection.rs b/werewolves-server/src/connection.rs index caa0fba..ad88000 100644 --- a/werewolves-server/src/connection.rs +++ b/werewolves-server/src/connection.rs @@ -1,7 +1,6 @@ use core::num::NonZeroU8; use std::{collections::HashMap, sync::Arc}; -use colored::Colorize; use tokio::{ sync::{ Mutex, @@ -28,10 +27,6 @@ impl ConnectionId { pub const fn player_id(&self) -> &PlayerId { &self.0 } - - pub const fn connect_time(&self) -> &Instant { - &self.1 - } } #[derive(Debug)] @@ -67,10 +62,6 @@ impl JoinedPlayer { pub fn resubscribe_reciever(&self) -> Receiver { self.receiver.resubscribe() } - - pub fn sender(&self) -> Sender { - self.sender.clone() - } } #[derive(Debug, Clone)] @@ -120,16 +111,6 @@ impl JoinedPlayers { } } - pub async fn get_name(&self, player_id: &PlayerId) -> Option { - self.players.lock().await.iter().find_map(|(pid, p)| { - if pid == player_id { - Some(p.name.clone()) - } else { - None - } - }) - } - /// Disconnect the player /// /// Will not disconnect if the player is currently in a game, allowing them to reconnect @@ -153,11 +134,11 @@ impl JoinedPlayers { pub async fn start_game_with(&self, players: &[PlayerId]) -> Result { let mut map = self.players.lock().await; - if !players.iter().all(|p| map.contains_key(p)) { - return Err(GameError::NotAllPlayersConnected); - } + for player in players { - unsafe { map.get_mut(player).unwrap_unchecked() }.in_game = true; + if let Some(player) = map.get_mut(player) { + player.in_game = true; + }; } Ok(InGameToken::new( diff --git a/werewolves-server/src/game.rs b/werewolves-server/src/game.rs index 285eb35..78eb7ac 100644 --- a/werewolves-server/src/game.rs +++ b/werewolves-server/src/game.rs @@ -4,7 +4,7 @@ use crate::{ LogError, communication::{Comms, lobby::LobbyComms}, connection::{InGameToken, JoinedPlayers}, - lobby::{Lobby, PlayerIdSender}, + lobby::{Lobby, LobbyPlayers}, runner::{IdentifiedClientMessage, Message}, }; use tokio::{sync::broadcast::Receiver, time::Instant}; @@ -24,18 +24,18 @@ pub struct GameRunner { game: Game, comms: Comms, connect_recv: Receiver<(PlayerId, bool)>, - player_sender: PlayerIdSender, + player_sender: LobbyPlayers, roles_revealed: bool, joined_players: JoinedPlayers, - _release_token: InGameToken, + // _release_token: InGameToken, cover_of_darkness: bool, } impl GameRunner { - pub const fn new( + pub fn new( game: Game, comms: Comms, - player_sender: PlayerIdSender, + player_sender: LobbyPlayers, connect_recv: Receiver<(PlayerId, bool)>, joined_players: JoinedPlayers, release_token: InGameToken, @@ -47,7 +47,7 @@ impl GameRunner { player_sender, joined_players, roles_revealed: false, - _release_token: release_token, + // _release_token: release_token, cover_of_darkness: true, } } @@ -57,10 +57,13 @@ impl GameRunner { } pub fn into_lobby(self) -> Lobby { - Lobby::new( + // core::mem::drop(self._release_token); + let mut lobby = Lobby::new( self.joined_players, LobbyComms::new(self.comms, self.connect_recv), - ) + ); + lobby.set_players_in_lobby(self.player_sender); + lobby } pub const fn proto_game(&self) -> &Game { @@ -106,7 +109,7 @@ impl GameRunner { .log_err(); }; (update_host)(&acks, &mut self.comms); - let notify_of_role = |player_id: &PlayerId, village: &Village, sender: &PlayerIdSender| { + let notify_of_role = |player_id: &PlayerId, village: &Village, sender: &LobbyPlayers| { if let Some(char) = village.character_by_player_id(player_id) { sender .send_if_present( @@ -245,23 +248,6 @@ impl GameEnd { } } - pub fn end_screen(&mut self) -> Result<()> { - let result = self.result; - for char in self.game()?.game.village().characters() { - self.game()? - .player_sender - .send_if_present(char.player_id(), ServerMessage::GameOver(result)) - .log_debug(); - } - self.game()? - .comms - .host() - .send(ServerToHostMessage::GameOver(result)) - .log_warn(); - - Ok(()) - } - pub async fn next(&mut self) -> Option { let msg = match self.game().unwrap().comms.message().await { Ok(msg) => msg, diff --git a/werewolves-server/src/lobby.rs b/werewolves-server/src/lobby.rs index 0a0e535..3b91e27 100644 --- a/werewolves-server/src/lobby.rs +++ b/werewolves-server/src/lobby.rs @@ -27,7 +27,7 @@ use crate::{ }; pub struct Lobby { - players_in_lobby: PlayerIdSender, + players_in_lobby: LobbyPlayers, settings: GameSettings, joined_players: JoinedPlayers, comms: Option, @@ -39,10 +39,14 @@ impl Lobby { joined_players, comms: Some(comms), settings: GameSettings::default(), - players_in_lobby: PlayerIdSender(Vec::new()), + players_in_lobby: LobbyPlayers(Vec::new()), } } + pub fn set_players_in_lobby(&mut self, players_in_lobby: LobbyPlayers) { + self.players_in_lobby = players_in_lobby + } + const fn comms(&mut self) -> Result<&mut LobbyComms, GameError> { match self.comms.as_mut() { Some(comms) => Ok(comms), @@ -80,7 +84,7 @@ impl Lobby { players.into_boxed_slice() } - async fn send_lobby_info_to_host(&mut self) -> Result<(), GameError> { + pub async fn send_lobby_info_to_host(&mut self) -> Result<(), GameError> { let players = self.get_lobby_player_list().await; self.comms()? .host() @@ -203,7 +207,7 @@ impl Lobby { return Ok(Some(GameRunner::new( game, comms, - self.players_in_lobby.drain(), + self.players_in_lobby.clone(), recv, self.joined_players.clone(), release_token, @@ -300,9 +304,10 @@ impl Lobby { } } -pub struct PlayerIdSender(Vec<(Identification, Sender)>); +#[derive(Clone)] +pub struct LobbyPlayers(Vec<(Identification, Sender)>); -impl Deref for PlayerIdSender { +impl Deref for LobbyPlayers { type Target = Vec<(Identification, Sender)>; fn deref(&self) -> &Self::Target { @@ -310,13 +315,13 @@ impl Deref for PlayerIdSender { } } -impl DerefMut for PlayerIdSender { +impl DerefMut for LobbyPlayers { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl PlayerIdSender { +impl LobbyPlayers { pub fn find(&self, player_id: &PlayerId) -> Option<&Sender> { self.iter() .find_map(|(id, s)| (&id.player_id == player_id).then_some(s)) diff --git a/werewolves-server/src/runner.rs b/werewolves-server/src/runner.rs index 4e602d0..10d1a68 100644 --- a/werewolves-server/src/runner.rs +++ b/werewolves-server/src/runner.rs @@ -1,13 +1,12 @@ use core::time::Duration; -use thiserror::Error; use werewolves_proto::{ - error::GameError, message::{ClientMessage, Identification, host::HostMessage}, player::PlayerId, }; use crate::{ + LogError, communication::lobby::LobbyComms, connection::JoinedPlayers, game::{GameEnd, GameRunner}, @@ -15,14 +14,6 @@ use crate::{ saver::Saver, }; -#[derive(Debug, Error)] -enum GameOrSendError { - #[error("game error: {0}")] - GameError(#[from] GameError), - #[error("send error: {0}")] - SendError(#[from] tokio::sync::mpsc::error::SendError), -} - #[derive(Debug, Clone, PartialEq)] pub struct IdentifiedClientMessage { pub identity: Identification, @@ -76,6 +67,7 @@ pub async fn run_game(joined_players: JoinedPlayers, comms: LobbyComms, mut save RunningState::GameOver(end) => { if let Some(mut new_lobby) = end.next().await { new_lobby.send_lobby_info_to_clients().await; + new_lobby.send_lobby_info_to_host().await.log_debug(); state = RunningState::Lobby(new_lobby) } } diff --git a/werewolves/index.scss b/werewolves/index.scss index 8498298..426d5e9 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -3,6 +3,7 @@ $village_color: rgba(0, 0, 255, 0.7); $connected_color: hsl(120, 68%, 50%); $disconnected_color: hsl(0, 68%, 50%); + html, body { margin: 0; @@ -1015,4 +1016,8 @@ error { background-color: $disconnected_color; border: 3px solid darken($disconnected_color, 20%); } + + &.dead { + filter: grayscale(100%); + } } diff --git a/werewolves/src/callback.rs b/werewolves/src/callback.rs index c50ab3b..c1d6b4c 100644 --- a/werewolves/src/callback.rs +++ b/werewolves/src/callback.rs @@ -2,7 +2,7 @@ use core::fmt::Debug; use std::sync::Arc; use futures::SinkExt; -use yew::{html::Scope, prelude::*}; +use yew::prelude::*; pub fn send_message( msg: T, @@ -19,13 +19,6 @@ pub fn send_message( }) } -pub fn mouse_event(inner: F) -> Callback -where - F: Fn() + 'static, -{ - Callback::from(move |_| (inner)()) -} - pub fn send_fn(msg_fn: F, send: futures::channel::mpsc::Sender) -> Callback

where T: Clone + Debug + 'static, diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index d627369..4df021f 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -12,7 +12,7 @@ use yew::prelude::*; use crate::components::{ Identity, - action::{SingleTarget, WolvesIntro}, + action::{SingleTarget, TargetSelection, WolvesIntro}, }; #[derive(Debug, Clone, PartialEq, Properties)] @@ -48,17 +48,20 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { } ActionPrompt::Seer { living_players } => { let on_complete = props.on_complete.clone(); - let on_select = Callback::from(move |target: CharacterId| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Seer(target)), - ))); + let on_select = props.big_screen.not().then(|| { + Callback::from(move |target: CharacterId| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::Seer(target)), + ))); + }) + .into() }); html! {

{ident}
@@ -85,9 +88,47 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { } } - ActionPrompt::Protector { targets } => todo!(), + ActionPrompt::Protector { targets } => { + let on_complete = props.on_complete.clone(); + let on_select = props.big_screen.not().then(|| { + Callback::from(move |target: CharacterId| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::Protector(target)), + ))); + }) + .into() + }); + html! { +
+ +
+ } + } ActionPrompt::Arcanist { living_players } => todo!(), - ActionPrompt::Gravedigger { dead_players } => todo!(), + ActionPrompt::Gravedigger { dead_players } => { + let on_complete = props.on_complete.clone(); + let on_select = props.big_screen.not().then(|| { + Callback::from(move |target: CharacterId| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)), + ))); + }) + .into() + }); + html! { +
+ +
+ } + } ActionPrompt::Hunter { current_target, living_players, @@ -101,9 +142,62 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { previous, living_players, } => todo!(), - ActionPrompt::WolfPackKill { living_villagers } => todo!(), + ActionPrompt::WolfPackKill { living_villagers } => { + let on_complete = props.on_complete.clone(); + let on_select = props.big_screen.not().then(|| { + Callback::from(move |target: CharacterId| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)), + ))); + }) + .into() + }); + html! { +
+ +
+ } + } ActionPrompt::Shapeshifter => todo!(), - ActionPrompt::AlphaWolf { living_villagers } => todo!(), - ActionPrompt::DireWolf { living_players } => todo!(), + ActionPrompt::AlphaWolf { living_villagers } => { + let on_complete = props.on_complete.clone(); + let on_select = props.big_screen.not().then(|| { + Callback::from(move |target: Option| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)), + ))); + }) + .into() + }); + html! { + + } + } + ActionPrompt::DireWolf { living_players } => { + let on_complete = props.on_complete.clone(); + let on_select = props.big_screen.not().then(|| { + Callback::from(move |target: CharacterId| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)), + ))); + }) + .into() + }); + html! { + + } + } } } diff --git a/werewolves/src/components/action/target.rs b/werewolves/src/components/action/target.rs index e9ab243..4b6d56d 100644 --- a/werewolves/src/components/action/target.rs +++ b/werewolves/src/components/action/target.rs @@ -1,22 +1,48 @@ -use core::ops::Not; +use core::{fmt::Debug, marker::PhantomData, ops::Not}; use werewolves_proto::{message::Target, player::CharacterId}; -use yew::{html::Scope, prelude::*}; +use yew::prelude::*; use crate::components::Identity; +#[derive(Debug, Clone, PartialEq)] +pub enum TargetSelection { + SingleOptional(Callback>), + Single(Callback), +} + +impl From> for TargetSelection { + fn from(value: Callback) -> Self { + Self::Single(value) + } +} + +impl From>> for TargetSelection { + fn from(value: Callback>) -> Self { + Self::SingleOptional(value) + } +} + +impl TargetSelection { + pub const fn button_disabled(&self, selected: &[CharacterId]) -> bool { + match self { + TargetSelection::SingleOptional(_) => selected.len() > 1, + TargetSelection::Single(_) => selected.len() != 1, + } + } +} + #[derive(Debug, Clone, PartialEq, Properties)] pub struct SingleTargetProps { pub targets: Box<[Target]>, - #[prop_or_default] - pub headline: &'static str, - #[prop_or_default] - pub read_only: bool, - pub on_select: Callback, + #[prop_or_default] + pub headline: &'static str, + #[prop_or_default] + pub target_selection: Option, } pub struct SingleTarget { - selected: Option, + selected: Vec, } impl Component for SingleTarget { @@ -25,63 +51,99 @@ impl Component for SingleTarget { type Properties = SingleTargetProps; fn create(_: &Context) -> Self { - Self { selected: None } + Self { + selected: Vec::new(), + } } fn view(&self, ctx: &Context) -> Html { - let SingleTargetProps { read_only, headline, targets, on_select } = ctx.props(); - let scope = ctx.link().clone(); - let card_select = Callback::from(move |target| { - scope.send_message(target); - }); - let targets = targets.iter().map(|t| { - html!{ - - } - }).collect::(); - let headline = if headline.trim().is_empty() { - html!() - } else { - html!(

{headline}

) - }; - - let on_select = on_select.clone(); - let on_click = if let Some(target) = self.selected.clone() { - Callback::from(move |_| on_select.emit(target.clone())) - } else { - Callback::from(|_| ()) - }; + let SingleTargetProps { + headline, + targets, + target_selection, + } = ctx.props(); + let target_selection = target_selection.clone(); + let scope = ctx.link().clone(); + let card_select = Callback::from(move |target| { + scope.send_message(target); + }); + let targets = targets + .iter() + .map(|t| { + html! { + + } + }) + .collect::(); + let headline = if headline.trim().is_empty() { + html!() + } else { + html!(

{headline}

) + }; - let submit = read_only.not().then(|| html!{ -
- -
- }); - - html!{ -
- {headline} -
- {targets} -
- {submit} -
- } + let on_click = + target_selection + .as_ref() + .and_then(|target_selection| match target_selection { + TargetSelection::SingleOptional(on_click) => { + if self.selected.len() > 1 { + None + } else { + let selected = self.selected.iter().next().cloned(); + let on_click = on_click.clone(); + Some(Callback::from(move |_| on_click.emit(selected.clone()))) + } + } + TargetSelection::Single(on_click) => { + if self.selected.len() != 1 { + None + } else { + let selected = self.selected[0].clone(); + let on_click = on_click.clone(); + Some(Callback::from(move |_| on_click.emit(selected.clone()))) + } + } + }); + + let submit = target_selection.as_ref().map(|target_selection| { + let disabled = target_selection.button_disabled(&self.selected); + html! { +
+ +
+ } + }); + + html! { +
+ {headline} +
+ {targets} +
+ {submit} +
+ } } fn update(&mut self, _: &Context, msg: Self::Message) -> bool { - if let Some(selected) = self.selected.as_ref() { - if selected == &msg { - self.selected = None; - } else { - self.selected = Some(msg); - } + if let Some(idx) = self + .selected + .iter() + .enumerate() + .find_map(|(idx, c)| (c == &msg).then_some(idx)) + { + self.selected.swap_remove(idx); } else { - self.selected = Some(msg); + self.selected.push(msg); } true } @@ -96,17 +158,17 @@ pub struct TargetCardProps { #[function_component] fn TargetCard(props: &TargetCardProps) -> Html { - let on_select = props.on_select.clone(); - let target = props.target.character_id.clone(); - let on_click = Callback::from(move |_| { - on_select.emit(target.clone()); - }); + let on_select = props.on_select.clone(); + let target = props.target.character_id.clone(); + let on_click = Callback::from(move |_| { + on_select.emit(target.clone()); + }); html! { -
- -
- } +
+ +
+ } } diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs index 1c723c5..c94585c 100644 --- a/werewolves/src/main.rs +++ b/werewolves/src/main.rs @@ -14,7 +14,7 @@ mod callback; use core::num::NonZeroU8; use pages::{Client, ErrorComponent, Host, WerewolfError}; -use web_sys::{Element, HtmlElement, Url, wasm_bindgen::JsCast}; +use web_sys::Url; use werewolves_proto::{ message::{Identification, PublicIdentity}, player::PlayerId, diff --git a/werewolves/src/pages/client.rs b/werewolves/src/pages/client.rs index 6ae71a0..34a6995 100644 --- a/werewolves/src/pages/client.rs +++ b/werewolves/src/pages/client.rs @@ -18,7 +18,7 @@ use werewolves_proto::{ Target, night::{ActionPrompt, ActionResponse, ActionResult}, }, - player::{Character, CharacterId, PlayerId}, + player::PlayerId, role::RoleTitle, }; use yew::{html::Scope, prelude::*}; diff --git a/werewolves/src/pages/host.rs b/werewolves/src/pages/host.rs index abcc71d..d0c09a4 100644 --- a/werewolves/src/pages/host.rs +++ b/werewolves/src/pages/host.rs @@ -261,22 +261,20 @@ impl Component for Host { log::info!("state: {:?}", self.state); let content = match self.state.clone() { HostState::GameOver { result } => { - let send = self.send.clone(); - let new_lobby = Callback::from(move |_| { - let send = send.clone(); - yew::platform::spawn_local(async move { - if let Err(err) = send.clone().send(HostMessage::NewLobby).await { - log::error!("send new lobby: {err}"); - } - }); + let new_lobby = self.big_screen.not().then(|| { + crate::callback::send_message(HostMessage::NewLobby, self.send.clone()) }); + html! { -
-

{format!("game over: {result:?}")}

-
- -
-
+ "village wins", + GameOver::WolvesWin => "wolves win", + }} + next={new_lobby} + > + {"new lobby"} + } } HostState::Disconnected => html! { @@ -497,6 +495,12 @@ impl Component for Host { players: p, settings: _, } => *p = players, + HostState::GameOver { result: _ } => { + self.state = HostState::Lobby { + players, + settings: GameSettings::default(), + } + } HostState::CoverOfDarkness | HostState::Prompt(_, _) | HostState::Result(_, _) @@ -505,7 +509,6 @@ impl Component for Host { ackd: _, waiting: _, } - | HostState::GameOver { result: _ } | HostState::Day { characters: _, day: _,