game cancelling by host
This commit is contained in:
parent
6850da7555
commit
6b3cce5e37
|
|
@ -732,3 +732,16 @@ dialog .tab-content {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
.dialog-modal:has(.cancel-game-button) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1ch;
|
||||
}
|
||||
|
||||
.scary {
|
||||
color: red;
|
||||
border: 1px solid red;
|
||||
background-color: black;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ pub enum ServerError {
|
|||
InvalidRequest(String),
|
||||
#[error("you're already in an active game: {0}")]
|
||||
AlreadyInActiveGame(GameId),
|
||||
#[error("not your game")]
|
||||
NotYourGame,
|
||||
#[error("this game is already over")]
|
||||
GameAlreadyOver,
|
||||
#[error("{0}")]
|
||||
GameError(#[from] GameError),
|
||||
}
|
||||
|
|
@ -236,11 +240,11 @@ impl axum::response::IntoResponse for ServerError {
|
|||
fn into_response(self) -> axum::response::Response {
|
||||
use axum::{Json, http::StatusCode};
|
||||
|
||||
|
||||
|
||||
(
|
||||
match self {
|
||||
ServerError::AlreadyInActiveGame(_)
|
||||
ServerError::NotYourGame
|
||||
| ServerError::GameAlreadyOver
|
||||
| ServerError::AlreadyInActiveGame(_)
|
||||
| ServerError::GameError(_)
|
||||
| ServerError::InvalidCredentials
|
||||
| ServerError::InvalidRequest(_)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
error::GameError,
|
||||
error::{GameError, ServerError},
|
||||
game::{GameOver, GameSettings, story::GameStory},
|
||||
message::{
|
||||
CharacterIdentity,
|
||||
|
|
@ -39,6 +39,7 @@ pub enum HostMessage {
|
|||
ForceRoleAckFor(CharacterId),
|
||||
PostGame(PostGameMessage),
|
||||
Echo(ServerToHostMessage),
|
||||
CancelGame,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -107,7 +108,7 @@ pub enum ServerToHostMessage {
|
|||
settings: GameSettings,
|
||||
qr_mode: bool,
|
||||
},
|
||||
Error(GameError),
|
||||
Error(ServerError),
|
||||
GameOver(GameOver),
|
||||
WaitingForRoleRevealAcks {
|
||||
ackd: Box<[CharacterIdentity]>,
|
||||
|
|
|
|||
|
|
@ -56,18 +56,22 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
|
||||
pub struct TutorialSettings {
|
||||
pub enabled: bool,
|
||||
pub struct Preferences {
|
||||
pub tutorials_enabled: bool,
|
||||
pub show_cancel_game: bool,
|
||||
}
|
||||
|
||||
impl Default for TutorialSettings {
|
||||
impl Default for Preferences {
|
||||
fn default() -> Self {
|
||||
Self { enabled: true }
|
||||
Self {
|
||||
tutorials_enabled: true,
|
||||
show_cancel_game: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stored for TutorialSettings {
|
||||
const STORAGE_KEY: &str = "tutorial-settings";
|
||||
impl Stored for Preferences {
|
||||
const STORAGE_KEY: &str = "preferences";
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
@ -81,9 +85,9 @@ pub fn App() -> impl IntoView {
|
|||
Effect::new(move || auth_store.init_or_update());
|
||||
Effect::new(move || session_store.init_or_update());
|
||||
|
||||
let (tut_read, tut_write, _) =
|
||||
use_local_storage::<TutorialSettings, JsonSerdeCodec>(TutorialSettings::STORAGE_KEY);
|
||||
provide_context((tut_read, tut_write));
|
||||
let (pref_read, pref_write, _) =
|
||||
use_local_storage::<Preferences, JsonSerdeCodec>(Preferences::STORAGE_KEY);
|
||||
provide_context((pref_read, pref_write));
|
||||
|
||||
let is_logged_in = move || {
|
||||
auth_store
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use werewolves_proto::{
|
|||
player::PlayerId,
|
||||
};
|
||||
|
||||
use crate::app::TutorialSettings;
|
||||
use crate::app::Preferences;
|
||||
|
||||
pub trait Sample {
|
||||
fn sample() -> Self;
|
||||
|
|
@ -51,11 +51,10 @@ impl Sample for Identification {
|
|||
#[component]
|
||||
pub fn TutorialBox(children: Children) -> impl IntoView {
|
||||
let sample_hidden = RwSignal::new(false);
|
||||
let (tut_read, _) =
|
||||
expect_context::<(Signal<TutorialSettings>, WriteSignal<TutorialSettings>)>();
|
||||
let (tut_read, _) = expect_context::<(Signal<Preferences>, WriteSignal<Preferences>)>();
|
||||
|
||||
view! {
|
||||
<div class="tutorial-box" hidden=move || !tut_read.read().enabled>
|
||||
<div class="tutorial-box" hidden=move || !tut_read.read().tutorials_enabled>
|
||||
<button
|
||||
class="hide"
|
||||
hidden=move || sample_hidden.get()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ use werewolves_proto::{
|
|||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
use crate::ConsoleLogError;
|
||||
use crate::app::{Preferences, components::DialogModal};
|
||||
|
||||
#[component]
|
||||
pub fn HostGamePage(
|
||||
message: Signal<Option<Srv2Host>>,
|
||||
|
|
@ -72,5 +76,42 @@ pub fn HostGamePage(
|
|||
}
|
||||
};
|
||||
|
||||
view! { {content} }
|
||||
view! {
|
||||
<CancelGame reply=reply />
|
||||
{content}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn CancelGame(reply: WriteSignal<Option<HostMessage>>) -> impl IntoView {
|
||||
let open = RwSignal::new(false);
|
||||
let prefs = expect_context::<(Signal<Preferences>, WriteSignal<Preferences>)>().0;
|
||||
let cancel = move |_| {
|
||||
open.set(false);
|
||||
reply.set(Some(HostMessage::CancelGame));
|
||||
#[cfg(feature = "hydrate")]
|
||||
gloo::utils::window()
|
||||
.location()
|
||||
.replace("/")
|
||||
.console_log_warn();
|
||||
};
|
||||
let content = move || match prefs.get().show_cancel_game {
|
||||
true => view! {
|
||||
<DialogModal
|
||||
button_class="cancel-game-button".into()
|
||||
text="cancel game".into()
|
||||
open=open
|
||||
>
|
||||
<h1>"this will cancel the game. are you sure?"</h1>
|
||||
<button on:click=cancel class="scary">
|
||||
"i'm sure, cancel the game."
|
||||
</button>
|
||||
</DialogModal>
|
||||
}
|
||||
.into_any(),
|
||||
false => ().into_any(),
|
||||
};
|
||||
view! {
|
||||
{content}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ pub fn PlayerLobby(
|
|||
number.set(Some(nz));
|
||||
} else {
|
||||
e.target().set_value(default.as_str());
|
||||
return;
|
||||
}
|
||||
};
|
||||
let submit = move |ev: SubmitEvent| {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use leptos::{ev::MouseEvent, prelude::*};
|
|||
use reactive_stores::Store;
|
||||
|
||||
use crate::app::{
|
||||
TutorialSettings,
|
||||
Preferences,
|
||||
storage::user::{AuthContext, AuthContextStoreFields},
|
||||
};
|
||||
|
||||
|
|
@ -18,14 +18,35 @@ pub fn UserSettings() -> impl IntoView {
|
|||
};
|
||||
view! { <button on:click=click>"log out"</button> }
|
||||
};
|
||||
let (tut_read, tut_write) =
|
||||
expect_context::<(Signal<TutorialSettings>, WriteSignal<TutorialSettings>)>();
|
||||
let tutorial_toggle_button = move || {
|
||||
match tut_read.read().enabled {
|
||||
true => view! { <button on:click=move |_| tut_write.write().enabled = false>"disable tutorials"</button> }
|
||||
.into_any(),
|
||||
false => view! { <button on:click=move |_| tut_write.write().enabled = true>"enable tutorials"</button> }.into_any(),
|
||||
let (prefs_read, prefs_write) =
|
||||
expect_context::<(Signal<Preferences>, WriteSignal<Preferences>)>();
|
||||
let tutorial_toggle_button = move || match prefs_read.read().tutorials_enabled {
|
||||
true => view! {
|
||||
<button on:click=move |_| {
|
||||
prefs_write.write().tutorials_enabled = false;
|
||||
}>"disable tutorials"</button>
|
||||
}
|
||||
.into_any(),
|
||||
false => view! {
|
||||
<button on:click=move |_| {
|
||||
prefs_write.write().tutorials_enabled = true;
|
||||
}>"enable tutorials"</button>
|
||||
}
|
||||
.into_any(),
|
||||
};
|
||||
let cancel_game_toggle_button = move || match prefs_read.read().show_cancel_game {
|
||||
true => view! {
|
||||
<button on:click=move |_| {
|
||||
prefs_write.write().show_cancel_game = false;
|
||||
}>"disable \"cancel game\" button"</button>
|
||||
}
|
||||
.into_any(),
|
||||
false => view! {
|
||||
<button on:click=move |_| {
|
||||
prefs_write.write().show_cancel_game = true;
|
||||
}>"enable \"cancel game\" button"</button>
|
||||
}
|
||||
.into_any(),
|
||||
};
|
||||
view! {
|
||||
<ul class="user-settings-list">
|
||||
|
|
@ -36,6 +57,7 @@ pub fn UserSettings() -> impl IntoView {
|
|||
<ChangePasswordButton />
|
||||
</li>
|
||||
<li>{tutorial_toggle_button}</li>
|
||||
<li>{cancel_game_toggle_button}</li>
|
||||
<li class="logout">{log_out}</li>
|
||||
</ul>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ impl GameDatabase {
|
|||
from
|
||||
games
|
||||
where
|
||||
id = $1
|
||||
id = $1 and game_status != 'Cancelled'::game_status
|
||||
"#,
|
||||
game.into_uuid(),
|
||||
)
|
||||
|
|
@ -372,6 +372,32 @@ impl GameDatabase {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cancel_game(&self, user: PlayerId, game_id: GameId) -> ServerResult<()> {
|
||||
let game = self.get_game(game_id).await?;
|
||||
if user != game.host {
|
||||
return Err(ServerError::NotYourGame);
|
||||
}
|
||||
if matches!(game.game_state, GameRecordState::GameOver(_)) {
|
||||
return Err(ServerError::GameAlreadyOver);
|
||||
}
|
||||
|
||||
query!(
|
||||
r#"
|
||||
update
|
||||
games
|
||||
set
|
||||
game_status = 'Cancelled'::game_status
|
||||
where
|
||||
id = $1"#,
|
||||
game_id.into_uuid(),
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.into_db_result()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_dead_chat(&self, game_id: GameId) -> ServerResult<Box<[DeadChatMessage]>> {
|
||||
Ok(query!(
|
||||
r#"
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::db::Database;
|
||||
use crate::server::runner::{ClientUpdate, HostOrClientMessage, IdentifiedClientMessage};
|
||||
use werewolves_proto::game::GameId;
|
||||
use chrono::Utc;
|
||||
use werewolves_proto::game::GameId;
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
game::{Game, GameOver},
|
||||
|
|
@ -32,11 +33,13 @@ pub struct GameRunner<'a> {
|
|||
game_id: GameId,
|
||||
game: &'a mut Game,
|
||||
roles_revealed: bool,
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl<'a> GameRunner<'a> {
|
||||
pub fn new(game_id: GameId, game: &'a mut Game) -> Self {
|
||||
pub fn new(game_id: GameId, game: &'a mut Game, db: Database) -> Self {
|
||||
Self {
|
||||
db,
|
||||
game,
|
||||
game_id,
|
||||
roles_revealed: false,
|
||||
|
|
@ -136,6 +139,24 @@ impl<'a> GameRunner<'a> {
|
|||
|
||||
let pre_time = self.game.village().time();
|
||||
let mut is_host_message = false;
|
||||
if let HostMessage::CancelGame = &msg {
|
||||
if let Err(err) = self
|
||||
.db
|
||||
.game()
|
||||
.cancel_game(
|
||||
self.db.game().get_game(self.game_id).await.ok()?.host,
|
||||
self.game_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
super::send_host(self.game_id, ServerToHostMessage::Error(err)).await;
|
||||
return None;
|
||||
}
|
||||
let game_id = self.game_id;
|
||||
tokio::spawn(async move {
|
||||
crate::server::delete_game(game_id).await;
|
||||
});
|
||||
}
|
||||
match self.host_message(msg) {
|
||||
Ok(ServerToHostMessage::DeadChatMessage(msg)) => {
|
||||
super::send_host(self.game_id, ServerToHostMessage::DeadChatMessage(msg)).await;
|
||||
|
|
@ -145,7 +166,7 @@ impl<'a> GameRunner<'a> {
|
|||
super::send_host(self.game_id, resp).await;
|
||||
}
|
||||
Err(err) => {
|
||||
super::send_host(self.game_id, ServerToHostMessage::Error(err)).await;
|
||||
super::send_host(self.game_id, ServerToHostMessage::Error(err.into())).await;
|
||||
}
|
||||
}
|
||||
let messages_for_host = self.game.dead_chats_since(start);
|
||||
|
|
@ -222,6 +243,12 @@ impl<'a> GameRunner<'a> {
|
|||
Err(GameError::GameOngoing)
|
||||
}
|
||||
HostMessage::Echo(echo) => Ok(echo),
|
||||
HostMessage::CancelGame => {
|
||||
log::error!("CancelGame should be handled above");
|
||||
Ok(ServerToHostMessage::Error(
|
||||
GameError::GenericError("CancelGame should be handled above".into()).into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ impl<'a> Lobby<'a> {
|
|||
| Ok(None) => None,
|
||||
Ok(Some(game)) => Some(game),
|
||||
Err((HostOrClientMessage::Host(_), ServerError::GameError(err))) => {
|
||||
super::send_host(self.game_id, ServerToHostMessage::Error(err)).await;
|
||||
super::send_host(self.game_id, ServerToHostMessage::Error(err.into())).await;
|
||||
None
|
||||
}
|
||||
Err((HostOrClientMessage::Host(_), err)) => {
|
||||
|
|
@ -142,11 +142,7 @@ impl<'a> Lobby<'a> {
|
|||
"server error from host request for game({}): {err}",
|
||||
self.game_id
|
||||
);
|
||||
super::send_host(
|
||||
self.game_id,
|
||||
ServerToHostMessage::Error(GameError::GenericError(err.to_string())),
|
||||
)
|
||||
.await;
|
||||
super::send_host(self.game_id, ServerToHostMessage::Error(err)).await;
|
||||
None
|
||||
}
|
||||
Err((
|
||||
|
|
@ -183,6 +179,19 @@ impl<'a> Lobby<'a> {
|
|||
msg: HostOrClientMessage,
|
||||
) -> Result<Option<GameRecord>, ServerError> {
|
||||
match msg {
|
||||
HostOrClientMessage::Host(HostMessage::CancelGame) => {
|
||||
self.db
|
||||
.game()
|
||||
.cancel_game(
|
||||
self.db.game().get_game(self.game_id).await?.host,
|
||||
self.game_id,
|
||||
)
|
||||
.await?;
|
||||
let game_id = self.game_id;
|
||||
tokio::spawn(async move {
|
||||
crate::server::delete_game(game_id).await;
|
||||
});
|
||||
}
|
||||
HostOrClientMessage::Client(IdentifiedClientMessage {
|
||||
update: ClientUpdate::Message(ClientMessage::DeadChat(_)),
|
||||
..
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ pub mod qr;
|
|||
pub mod role_reveal;
|
||||
pub mod runner;
|
||||
|
||||
use core::{
|
||||
fmt::Display,
|
||||
hash::BuildHasherDefault,
|
||||
};
|
||||
use core::{fmt::Display, hash::BuildHasherDefault};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
hash::DefaultHasher,
|
||||
|
|
@ -382,3 +379,19 @@ pub async fn clear_abandoned_game_runners() {
|
|||
"[clear_abandoned_game_runners]".bold()
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn delete_game(game: GameId) {
|
||||
let mut hosts = HOST_CHANNELS.write().await;
|
||||
let mut players = PLAYER_CHANNELS.write().await;
|
||||
if let Some(GameChannels { runner_handle, .. }) = hosts.remove(&game) {
|
||||
runner_handle.abort();
|
||||
}
|
||||
let player_keys = players
|
||||
.keys()
|
||||
.filter(|(gid, _)| *gid == game)
|
||||
.copied()
|
||||
.collect::<Box<_>>();
|
||||
for key in player_keys {
|
||||
players.remove(&key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ async fn next_message(
|
|||
game_id: GameId,
|
||||
client_recv: &mut UnboundedReceiver<IdentifiedClientMessage>,
|
||||
host_recv: &mut UnboundedReceiver<HostMessage>,
|
||||
db: &Database,
|
||||
) -> Option<HostOrClientMessage> {
|
||||
tokio::select! {
|
||||
client_msg = client_recv.recv() => {
|
||||
|
|
@ -53,6 +54,11 @@ async fn next_message(
|
|||
}
|
||||
host_msg = host_recv.recv() => {
|
||||
match host_msg {
|
||||
Some(HostMessage::CancelGame) => {
|
||||
cancel_game(game_id, db).await;
|
||||
log::info!("got game cancellation request for {game_id}");
|
||||
None
|
||||
}
|
||||
Some(msg) => Some(HostOrClientMessage::Host(msg)),
|
||||
None => {
|
||||
log::warn!("host recv for game {game_id} got None; exiting");
|
||||
|
|
@ -63,6 +69,21 @@ async fn next_message(
|
|||
}
|
||||
}
|
||||
|
||||
async fn cancel_game(game_id: GameId, db: &Database) {
|
||||
let game = match db.game().get_game(game_id).await {
|
||||
Ok(g) => g,
|
||||
Err(err) => {
|
||||
log::error!("get game {game_id} for cancellation: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(err) = db.game().cancel_game(game.host, game_id).await {
|
||||
log::error!("error cancelling game: {err}");
|
||||
return;
|
||||
}
|
||||
tokio::spawn(crate::server::delete_game(game_id));
|
||||
}
|
||||
|
||||
async fn add_dummies(game_id: GameId, db: &Database, dummy_count: usize) {
|
||||
let Ok(joined) = db.game().get_joined_players(game_id).await else {
|
||||
return;
|
||||
|
|
@ -107,7 +128,7 @@ pub async fn run_game(
|
|||
if let Some(new_record) = lobby
|
||||
.process(
|
||||
if let Some(msg) =
|
||||
next_message(game_id, &mut client_recv, &mut host_recv).await
|
||||
next_message(game_id, &mut client_recv, &mut host_recv, &db).await
|
||||
{
|
||||
msg
|
||||
} else {
|
||||
|
|
@ -133,7 +154,7 @@ pub async fn run_game(
|
|||
if role_reveal
|
||||
.process(
|
||||
if let Some(msg) =
|
||||
next_message(game_id, &mut client_recv, &mut host_recv).await
|
||||
next_message(game_id, &mut client_recv, &mut host_recv, &db).await
|
||||
{
|
||||
msg
|
||||
} else {
|
||||
|
|
@ -152,13 +173,14 @@ pub async fn run_game(
|
|||
}
|
||||
}
|
||||
GameRecordState::Started(mut current_game) => {
|
||||
let mut runner = GameRunner::new(game_id, &mut current_game);
|
||||
let mut runner = GameRunner::new(game_id, &mut current_game, db.clone());
|
||||
loop {
|
||||
if runner.game_over().is_some()
|
||||
|| runner
|
||||
.process(
|
||||
if let Some(msg) =
|
||||
next_message(game_id, &mut client_recv, &mut host_recv).await
|
||||
next_message(game_id, &mut client_recv, &mut host_recv, &db)
|
||||
.await
|
||||
{
|
||||
msg
|
||||
} else {
|
||||
|
|
@ -184,7 +206,7 @@ pub async fn run_game(
|
|||
story
|
||||
.process(
|
||||
if let Some(msg) =
|
||||
next_message(game_id, &mut client_recv, &mut host_recv).await
|
||||
next_message(game_id, &mut client_recv, &mut host_recv, &db).await
|
||||
{
|
||||
msg
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue