From 938ec4ff617627f279562e92c7c0cec357c3b425 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Sun, 24 Dec 2023 01:23:47 +0100 Subject: [PATCH 01/15] Start to prototype gadgets --- liberica/src/components/Gadgets.tsx | 43 ++++++++++++++++++++++ liberica/src/lib/bindings.ts | 20 ++++++----- liberica/src/main.tsx | 2 ++ liberica/src/page/Game.tsx | 2 ++ robusta/src/main.rs | 55 +++++++++++++++++++++++++++++ robusta/src/ws_message.rs | 20 +++++++++++ 6 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 liberica/src/components/Gadgets.tsx diff --git a/liberica/src/components/Gadgets.tsx b/liberica/src/components/Gadgets.tsx new file mode 100644 index 0000000..ae2c7e1 --- /dev/null +++ b/liberica/src/components/Gadgets.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { Button } from "./InputElements"; +import { getStops } from "lib/api"; +import { Stop } from "lib/bindings"; + +export function Gadgets() { + const [stops, setStops] = useState([]); + useEffect(() => { + getStops().then(setStops); + }, []); + + return ( +
{ + e.preventDefault(); + alert(e.target); + }} + > +
+ + + + +
+
+ ); +} diff --git a/liberica/src/lib/bindings.ts b/liberica/src/lib/bindings.ts index 6b8a150..0b61c16 100644 --- a/liberica/src/lib/bindings.ts +++ b/liberica/src/lib/bindings.ts @@ -1,25 +1,29 @@ // This file has been generated by Specta. DO NOT EDIT. +export type TeamKind = "MrX" | "Detective" | "Observer" + /** * Information about a tram station. */ export type Stop = { name: string; id: string; lat: number; lon: number } -export type TeamKind = "MrX" | "Detective" | "Observer" +export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } -export type Team = { id: number; name: string; color: string; kind: TeamKind } +export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } -export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { Message: string } +export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney: { image: number[] } } | "NotFound" | "Teleport" | "Shifter" -export type CreateTeamError = "InvalidName" | "NameAlreadyExists" +export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } -export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } +export type GameState = { teams: TeamState[]; trains: Train[] } -export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } +export type CreateTeamError = "InvalidName" | "NameAlreadyExists" + +export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } export type CreateTeam = { name: string; color: string; kind: TeamKind } -export type ClientResponse = { GameState: GameState } +export type Team = { id: number; name: string; color: string; kind: TeamKind } -export type GameState = { teams: TeamState[]; trains: Train[] } +export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" diff --git a/liberica/src/main.tsx b/liberica/src/main.tsx index 56b4d43..9bb1b36 100644 --- a/liberica/src/main.tsx +++ b/liberica/src/main.tsx @@ -3,6 +3,7 @@ import { Game } from "page/Game"; import { Home } from "page/Home"; import { Replay } from "page/Replay"; import { Admin } from "page/Admin"; +import { Gadgets } from "page/Gadgets"; import ReactDOM from "react-dom/client"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import "style/main.css"; @@ -40,6 +41,7 @@ ReactDOM.createRoot(rootElement).render( } /> } /> } /> + } /> , ); diff --git a/liberica/src/page/Game.tsx b/liberica/src/page/Game.tsx index 31e157a..9af3ff3 100644 --- a/liberica/src/page/Game.tsx +++ b/liberica/src/page/Game.tsx @@ -29,6 +29,8 @@ export function Game() { ws?.send({ EmbarkTrain: { train_id: train.line_id } }); } + const [showGadgetMenu, setShowGadgetMenu] = useState(false) + useEffect(() => { const socket = createWebSocketConnection(); diff --git a/robusta/src/main.rs b/robusta/src/main.rs index c514e4a..3be628b 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -40,6 +40,9 @@ const TEAMS_FILE: &str = "teams.json"; /// The name used for the Mr. X team. const MRX: &str = "Mr. X"; +/// The interval between position broadcasts (10 min). +const INTERVAL: Duration = Duration::from_secs(600); + #[derive(Debug)] enum InputMessage { Client(ClientMessage, u32), @@ -94,6 +97,12 @@ impl AppState { self.connections.iter_mut().find(|x| x.id == id) } + fn team_by_client_id(&mut self, id: u32) -> Option<&TeamState> { + self.client(id) + .map(|x| x.team_id) + .and_then(|team_id| self.teams.iter().find(|ts| ts.team.id == team_id)) + } + fn team_mut_by_client_id(&mut self, id: u32) -> Option<&mut TeamState> { self.client(id) .map(|x| x.team_id) @@ -411,6 +420,52 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { team.on_train = None; } } + ClientMessage::MrXGadget(gadget) => { + use ws_message::MrXGadget::*; + if state.team_by_client_id(id).map(|ts| ts.team.kind != TeamKind::MrX).unwrap_or(true) { + warn!("Client {} tried to use MrX Gadget, but is not MrX", id); + continue; + } + match &gadget { + AlternativeFacts { stop_id } => { + // TODO: change next position broadcast + } + Midjourney { image } => { + // TODO: prevent next position boradcast and broadcast image instead + } + NotFound => { + // TODO: prevent next position broadcast + } + Teleport => {} + Shifter => {} + } + for connection in state.connections.iter_mut() { + if connection.send.send(ClientResponse::MrXGadget(gadget.clone())).await.is_err() { + continue; + } + } + } + ClientMessage::DetectiveGadget(gadget) => { + use ws_message::DetectiveGadget::*; + if state.team_by_client_id(id).map(|ts| ts.team.kind != TeamKind::Detective).unwrap_or(true) { + warn!("Client {} tried to use Detective Gadget, but is not Detective", id); + continue; + } + match &gadget { + Stop { stop_id } => { + // TODO: mark stop as blocked for the next 20 mins + }, + OutOfOrder => { + // TODO: immediately broadcast Mr. X position + }, + Shackles => todo!(), + } + for connection in state.connections.iter_mut() { + if connection.send.send(ClientResponse::DetectiveGadget(gadget.clone())).await.is_err() { + continue; + } + } + } } } InputMessage::Server(ServerMessage::Departures(deps)) => { diff --git a/robusta/src/ws_message.rs b/robusta/src/ws_message.rs index a3e4d70..ab52322 100644 --- a/robusta/src/ws_message.rs +++ b/robusta/src/ws_message.rs @@ -7,12 +7,32 @@ pub enum ClientMessage { JoinTeam { team_id: u32 }, EmbarkTrain { train_id: String }, DisembarkTrain, + MrXGadget(MrXGadget), + DetectiveGadget(DetectiveGadget), Message(String), } +#[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] +pub enum MrXGadget { + AlternativeFacts { stop_id: String }, + Midjourney { image: Vec }, + NotFound, + Teleport, + Shifter, +} + +#[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] +pub enum DetectiveGadget { + Stop { stop_id: String }, + OutOfOrder, + Shackles, +} + #[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] pub enum ClientResponse { GameState(GameState), + MrXGadget(MrXGadget), + DetectiveGadget(DetectiveGadget), } #[derive(specta::Type, Default, Clone, Serialize, Deserialize, Debug)] From ec9b5615788d15fbab4c979f49b8e8e1318ed41a Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Sat, 30 Dec 2023 18:07:46 +0100 Subject: [PATCH 02/15] Add Mr. X Gadgets to frontend Determine gadgets based on team Add `Gadget` element --- liberica/src/components/Gadgets.tsx | 104 +++++++++++++++++++--------- liberica/src/main.tsx | 2 +- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/liberica/src/components/Gadgets.tsx b/liberica/src/components/Gadgets.tsx index ae2c7e1..8e642bf 100644 --- a/liberica/src/components/Gadgets.tsx +++ b/liberica/src/components/Gadgets.tsx @@ -1,43 +1,79 @@ -import { useEffect, useState } from "react"; +import { PropsWithChildren, useEffect, useState } from "react"; import { Button } from "./InputElements"; import { getStops } from "lib/api"; -import { Stop } from "lib/bindings"; +import { Stop, Team } from "lib/bindings"; -export function Gadgets() { +function Gadget(props: PropsWithChildren<{ value: string }>) { + return ( + + ); +} + +export function Gadgets({ team }: { team: Team }) { const [stops, setStops] = useState([]); useEffect(() => { getStops().then(setStops); }, []); - return ( -
{ - e.preventDefault(); - alert(e.target); - }} - > -
- - - - -
-
- ); + switch (team.kind) { + case "MrX": + return ( +
{ + e.preventDefault(); + alert(e.target); + }} + > +
+ + Alternative Fakten:{" "} + + + + Midjourney: + + 404 + Teleport + Shifter + +
+
+ ); + case "Detective": + return ( +
{ + e.preventDefault(); + alert(e.target); + }} + > +
+ + Stop:{" "} + + + Out of Order + Fessel + +
+
+ ); + default: + return <>; + } } diff --git a/liberica/src/main.tsx b/liberica/src/main.tsx index 9bb1b36..b50b750 100644 --- a/liberica/src/main.tsx +++ b/liberica/src/main.tsx @@ -41,7 +41,7 @@ ReactDOM.createRoot(rootElement).render( } /> } /> } /> - } /> + } /> , ); From 7e128b8430d1fec3108233df0df043d48e550dec Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Sun, 31 Dec 2023 15:34:20 +0100 Subject: [PATCH 03/15] Separate `Gadgets` element into `MrXGadgets` and `DetectiveGadgets` --- liberica/src/components/Gadgets.tsx | 150 +++++++++++++++++----------- liberica/src/main.tsx | 4 +- 2 files changed, 93 insertions(+), 61 deletions(-) diff --git a/liberica/src/components/Gadgets.tsx b/liberica/src/components/Gadgets.tsx index 8e642bf..50ad638 100644 --- a/liberica/src/components/Gadgets.tsx +++ b/liberica/src/components/Gadgets.tsx @@ -1,79 +1,111 @@ import { PropsWithChildren, useEffect, useState } from "react"; import { Button } from "./InputElements"; import { getStops } from "lib/api"; -import { Stop, Team } from "lib/bindings"; +import { DetectiveGadget, MrXGadget, Stop } from "lib/bindings"; -function Gadget(props: PropsWithChildren<{ value: string }>) { +function Gadget( + props: PropsWithChildren<{ value: string; onSelect?: () => void }>, +) { return ( ); } -export function Gadgets({ team }: { team: Team }) { +export function MrXGadgets() { + const [gadget, setGadget] = useState(); const [stops, setStops] = useState([]); useEffect(() => { getStops().then(setStops); }, []); - switch (team.kind) { - case "MrX": - return ( -
{ - e.preventDefault(); - alert(e.target); + return ( + { + e.preventDefault(); + alert(e.target); + }} + > +
+ + Alternative Fakten:{" "} + + + + Midjourney: + + setGadget("NotFound")}> + 404 + + setGadget("Teleport")}> + Teleport + + setGadget("Shifter")}> + Shifter + + +
+
+ ); +} + +export function DetectiveGadgets() { + const [gadget, setGadget] = useState(); + const [stop, setStop] = useState(""); + const [stops, setStops] = useState([]); + useEffect(() => { + getStops().then(setStops); + }, []); + + return ( +
{ + e.preventDefault(); + alert(gadget); + }} + > +
+ { + if (stop) { + setGadget({ Stop: { stop_id: stop } }); + } else { + setGadget(undefined); + } }} > -
- - Alternative Fakten:{" "} - - - - Midjourney: - - 404 - Teleport - Shifter - -
- - ); - case "Detective": - return ( -
{ - e.preventDefault(); - alert(e.target); - }} + Stop:{" "} + + + setGadget("OutOfOrder")} > -
- - Stop:{" "} - - - Out of Order - Fessel - -
- - ); - default: - return <>; - } + Out of Order +
+ setGadget("Shackles")}> + Fessel + + +
+ + ); } diff --git a/liberica/src/main.tsx b/liberica/src/main.tsx index b50b750..cd6708b 100644 --- a/liberica/src/main.tsx +++ b/liberica/src/main.tsx @@ -3,7 +3,7 @@ import { Game } from "page/Game"; import { Home } from "page/Home"; import { Replay } from "page/Replay"; import { Admin } from "page/Admin"; -import { Gadgets } from "page/Gadgets"; +import { DetectiveGadgets } from "page/Gadgets"; import ReactDOM from "react-dom/client"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import "style/main.css"; @@ -41,7 +41,7 @@ ReactDOM.createRoot(rootElement).render( } /> } /> } /> - } /> + } /> , ); From 2757a72b33d6fe54e4e9077056d5e3b1e8453fb9 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Sun, 31 Dec 2023 15:36:54 +0100 Subject: [PATCH 04/15] Start implementing gadgets timeouts and position broadcasting --- robusta/src/main.rs | 73 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/robusta/src/main.rs b/robusta/src/main.rs index 3be628b..5905a43 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::fs; use std::io::Write; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use axum::{ @@ -40,7 +41,7 @@ const TEAMS_FILE: &str = "teams.json"; /// The name used for the Mr. X team. const MRX: &str = "Mr. X"; -/// The interval between position broadcasts (10 min). +/// The interval between position broadcasts and gadgets uses (10 min). const INTERVAL: Duration = Duration::from_secs(600); #[derive(Debug)] @@ -375,6 +376,47 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { .build("logs") .expect("failed to initialize rolling file appender"); + let can_mr_x_use_gadget = Arc::new(AtomicBool::new(false)); + let can_detective_use_gadget = Arc::new(AtomicBool::new(false)); + enum SpecialPos { + Stop(String), + Image(Vec), + NotFound, + } + let (send_pos, mut recv_pos) = tokio::sync::mpsc::channel(1); + { + let can_mr_x_use_gadget = Arc::clone(&can_mr_x_use_gadget); + let can_detective_use_gadget = Arc::clone(&can_detective_use_gadget); + tokio::spawn(async move { + let mut interval = tokio::time::interval(INTERVAL); + interval.tick().await; + // TODO: broadcast Mr. X start + interval.tick().await; + can_mr_x_use_gadget.store(true, Ordering::SeqCst); + can_detective_use_gadget.store(true, Ordering::SeqCst); + // TODO: broadcast Detective start + loop { + use SpecialPos::*; + + interval.tick().await; + match recv_pos.try_recv() { + Ok(Stop(stop_id)) => { + // TODO: broadcast stop id + } + Ok(Image(image)) => { + // TODO: broadcast image + } + Ok(NotFound) => { + // TODO: broadcast 404 + } + Err(_) => { + // TODO: broadcast Mr. X position + } + } + } + }); + } + // the time for a single frame let mut interval = tokio::time::interval(Duration::from_millis(500)); @@ -426,15 +468,28 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { warn!("Client {} tried to use MrX Gadget, but is not MrX", id); continue; } + match can_mr_x_use_gadget.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) { + Ok(_) => { + let can_mr_x_use_gadget = Arc::clone(&can_mr_x_use_gadget); + tokio::spawn(async move { + tokio::time::sleep(INTERVAL).await; + can_mr_x_use_gadget.store(true, Ordering::SeqCst); + }); + } + Err(_) => continue, + } match &gadget { AlternativeFacts { stop_id } => { - // TODO: change next position broadcast + send_pos.try_send(SpecialPos::Stop(stop_id.clone())); + continue; } Midjourney { image } => { - // TODO: prevent next position boradcast and broadcast image instead + send_pos.try_send(SpecialPos::Image(image.clone())); + continue; } NotFound => { - // TODO: prevent next position broadcast + send_pos.try_send(SpecialPos::NotFound); + continue; } Teleport => {} Shifter => {} @@ -451,6 +506,16 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { warn!("Client {} tried to use Detective Gadget, but is not Detective", id); continue; } + match can_detective_use_gadget.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) { + Ok(_) => { + let can_detective_use_gadget = Arc::clone(&can_detective_use_gadget); + tokio::spawn(async move { + tokio::time::sleep(INTERVAL).await; + can_detective_use_gadget.store(true, Ordering::SeqCst); + }); + } + Err(_) => continue, + } match &gadget { Stop { stop_id } => { // TODO: mark stop as blocked for the next 20 mins From 42abbcfc1f6b9e641061d61cbbdc55911a720da5 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Tue, 21 May 2024 18:45:46 +0200 Subject: [PATCH 05/15] Continue working on gadgets Check if gadget has already been used Use more functions More warnings --- liberica/src/lib/bindings.ts | 20 ++--- liberica/src/main.tsx | 2 +- robusta/src/gadgets.rs | 48 +++++++++++ robusta/src/main.rs | 161 ++++++++++++++++++++--------------- robusta/src/ws_message.rs | 18 +--- 5 files changed, 152 insertions(+), 97 deletions(-) create mode 100644 robusta/src/gadgets.rs diff --git a/liberica/src/lib/bindings.ts b/liberica/src/lib/bindings.ts index 0b61c16..d1699a3 100644 --- a/liberica/src/lib/bindings.ts +++ b/liberica/src/lib/bindings.ts @@ -1,29 +1,29 @@ // This file has been generated by Specta. DO NOT EDIT. -export type TeamKind = "MrX" | "Detective" | "Observer" +export type CreateTeamError = "InvalidName" | "NameAlreadyExists" /** * Information about a tram station. */ export type Stop = { name: string; id: string; lat: number; lon: number } -export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } - -export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } +export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney: { image: number[] } } | "NotFound" | "Teleport" | "Shifter" -export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } - export type GameState = { teams: TeamState[]; trains: Train[] } -export type CreateTeamError = "InvalidName" | "NameAlreadyExists" +export type Team = { id: number; name: string; color: string; kind: TeamKind } -export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } +export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" + +export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } + +export type TeamKind = "MrX" | "Detective" | "Observer" export type CreateTeam = { name: string; color: string; kind: TeamKind } -export type Team = { id: number; name: string; color: string; kind: TeamKind } +export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } -export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" +export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } diff --git a/liberica/src/main.tsx b/liberica/src/main.tsx index cd6708b..7b9a99d 100644 --- a/liberica/src/main.tsx +++ b/liberica/src/main.tsx @@ -3,7 +3,7 @@ import { Game } from "page/Game"; import { Home } from "page/Home"; import { Replay } from "page/Replay"; import { Admin } from "page/Admin"; -import { DetectiveGadgets } from "page/Gadgets"; +import { DetectiveGadgets } from "components/Gadgets"; import ReactDOM from "react-dom/client"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import "style/main.css"; diff --git a/robusta/src/gadgets.rs b/robusta/src/gadgets.rs new file mode 100644 index 0000000..526f1e7 --- /dev/null +++ b/robusta/src/gadgets.rs @@ -0,0 +1,48 @@ +use std::collections::HashSet; +use std::mem; + +use serde::{Deserialize, Serialize}; + +#[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] +pub enum MrXGadget { + AlternativeFacts { stop_id: String }, + Midjourney { image: Vec }, + NotFound, + Teleport, + Shifter, +} + +#[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] +pub enum DetectiveGadget { + Stop { stop_id: String }, + OutOfOrder, + Shackles, +} + +#[derive(Debug)] +pub struct GadgetState { + can_be_used: bool, + used: HashSet>, +} + +impl GadgetState { + pub fn new() -> Self { + Self { + can_be_used: false, + used: HashSet::new(), + } + } + + pub fn try_use(&mut self, gadget: &T) -> bool { + if self.can_be_used && self.used.insert(mem::discriminant(gadget)) { + self.can_be_used = false; + true + } else { + false + } + } + + pub fn allow_use(&mut self) { + self.can_be_used = true; + } +} diff --git a/robusta/src/main.rs b/robusta/src/main.rs index 5905a43..1f7bd5a 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::fs; use std::io::Write; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use axum::{ @@ -27,10 +26,12 @@ use tower_http::{ use tracing::{error, info, warn, Level}; use tracing_appender::rolling::{self, Rotation}; +use crate::gadgets::{GadgetState, DetectiveGadget, MrXGadget}; use crate::kvv::LineDepartures; use crate::unique_id::UniqueIdGen; use crate::ws_message::{ClientMessage, ClientResponse, GameState, Team, TeamKind, TeamState}; +mod gadgets; mod kvv; mod point; mod unique_id; @@ -42,7 +43,7 @@ const TEAMS_FILE: &str = "teams.json"; const MRX: &str = "Mr. X"; /// The interval between position broadcasts and gadgets uses (10 min). -const INTERVAL: Duration = Duration::from_secs(600); +const COOLDOWN: Duration = Duration::from_secs(600); #[derive(Debug)] enum InputMessage { @@ -77,16 +78,20 @@ struct AppState { pub connections: Vec, pub client_id_gen: UniqueIdGen, pub team_id_gen: UniqueIdGen, + pub mr_x_gadgets: GadgetState, + pub detective_gadgets: GadgetState, } impl AppState { - const fn new(game_logic_sender: Sender) -> Self { + fn new(game_logic_sender: Sender) -> Self { Self { teams: Vec::new(), game_logic_sender, connections: Vec::new(), client_id_gen: UniqueIdGen::new(), team_id_gen: UniqueIdGen::new(), + mr_x_gadgets: GadgetState::new(), + detective_gadgets: GadgetState::new(), } } @@ -346,7 +351,7 @@ fn load_state(send: Sender) -> SharedState { .and_then(|x| serde_json::from_str::>(&x).ok()) .unwrap_or_default(); - let mut state = AppState::new(send.clone()); + let mut state = AppState::new(send); let max_id = teams.iter().map(|ts| ts.team.id).max().unwrap_or(0); state.team_id_gen.set_min(max_id + 1); if !teams.iter().any(|ts| ts.team.kind == TeamKind::MrX) { @@ -366,7 +371,13 @@ fn load_state(send: Sender) -> SharedState { Arc::new(tokio::sync::Mutex::new(state)) } -async fn run_game_loop(mut recv: Receiver, state: SharedState) { +enum SpecialPos { + Stop(String), + Image(Vec), + NotFound, +} + +async fn run_game_loop(mut recv: Receiver, shared_state: SharedState) { let mut departures = HashMap::new(); let mut log_file = rolling::Builder::new() .rotation(Rotation::DAILY) @@ -376,46 +387,7 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { .build("logs") .expect("failed to initialize rolling file appender"); - let can_mr_x_use_gadget = Arc::new(AtomicBool::new(false)); - let can_detective_use_gadget = Arc::new(AtomicBool::new(false)); - enum SpecialPos { - Stop(String), - Image(Vec), - NotFound, - } - let (send_pos, mut recv_pos) = tokio::sync::mpsc::channel(1); - { - let can_mr_x_use_gadget = Arc::clone(&can_mr_x_use_gadget); - let can_detective_use_gadget = Arc::clone(&can_detective_use_gadget); - tokio::spawn(async move { - let mut interval = tokio::time::interval(INTERVAL); - interval.tick().await; - // TODO: broadcast Mr. X start - interval.tick().await; - can_mr_x_use_gadget.store(true, Ordering::SeqCst); - can_detective_use_gadget.store(true, Ordering::SeqCst); - // TODO: broadcast Detective start - loop { - use SpecialPos::*; - - interval.tick().await; - match recv_pos.try_recv() { - Ok(Stop(stop_id)) => { - // TODO: broadcast stop id - } - Ok(Image(image)) => { - // TODO: broadcast image - } - Ok(NotFound) => { - // TODO: broadcast 404 - } - Err(_) => { - // TODO: broadcast Mr. X position - } - } - } - }); - } + let send_pos = start_game(Arc::clone(&shared_state)).await; // the time for a single frame let mut interval = tokio::time::interval(Duration::from_millis(500)); @@ -424,7 +396,7 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { interval.tick().await; // handle messages - let mut state = state.lock().await; + let mut state = shared_state.lock().await; while let Ok(msg) = recv.try_recv() { match msg { InputMessage::Client(msg, id) => { @@ -463,32 +435,40 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { } } ClientMessage::MrXGadget(gadget) => { - use ws_message::MrXGadget::*; + use MrXGadget::*; + if state.team_by_client_id(id).map(|ts| ts.team.kind != TeamKind::MrX).unwrap_or(true) { warn!("Client {} tried to use MrX Gadget, but is not MrX", id); continue; } - match can_mr_x_use_gadget.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) { - Ok(_) => { - let can_mr_x_use_gadget = Arc::clone(&can_mr_x_use_gadget); - tokio::spawn(async move { - tokio::time::sleep(INTERVAL).await; - can_mr_x_use_gadget.store(true, Ordering::SeqCst); - }); - } - Err(_) => continue, + if state.mr_x_gadgets.try_use(&gadget) { + let state = Arc::clone(&shared_state); + tokio::spawn(async move { + tokio::time::sleep(COOLDOWN).await; + state.lock().await.mr_x_gadgets.allow_use(); + }); + } else { + warn!("Client {} tried to use MrX Gadget, but is not allowed to", id); + continue; } + match &gadget { AlternativeFacts { stop_id } => { - send_pos.try_send(SpecialPos::Stop(stop_id.clone())); + if send_pos.send(SpecialPos::Stop(stop_id.clone())).await.is_err() { + error!("special position channel closed"); + } continue; } Midjourney { image } => { - send_pos.try_send(SpecialPos::Image(image.clone())); + if send_pos.send(SpecialPos::Image(image.clone())).await.is_err() { + error!("special position channel closed"); + } continue; } NotFound => { - send_pos.try_send(SpecialPos::NotFound); + if send_pos.send(SpecialPos::NotFound).await.is_err() { + error!("special position channel closed"); + } continue; } Teleport => {} @@ -501,21 +481,23 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { } } ClientMessage::DetectiveGadget(gadget) => { - use ws_message::DetectiveGadget::*; + use DetectiveGadget::*; + if state.team_by_client_id(id).map(|ts| ts.team.kind != TeamKind::Detective).unwrap_or(true) { warn!("Client {} tried to use Detective Gadget, but is not Detective", id); continue; } - match can_detective_use_gadget.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) { - Ok(_) => { - let can_detective_use_gadget = Arc::clone(&can_detective_use_gadget); - tokio::spawn(async move { - tokio::time::sleep(INTERVAL).await; - can_detective_use_gadget.store(true, Ordering::SeqCst); - }); - } - Err(_) => continue, + if state.detective_gadgets.try_use(&gadget) { + let state = Arc::clone(&shared_state); + tokio::spawn(async move { + tokio::time::sleep(COOLDOWN).await; + state.lock().await.detective_gadgets.allow_use(); + }); + } else { + warn!("Client {} tried to use Detective Gadget, but is not allowed to", id); + continue; } + match &gadget { Stop { stop_id } => { // TODO: mark stop as blocked for the next 20 mins @@ -523,7 +505,7 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { OutOfOrder => { // TODO: immediately broadcast Mr. X position }, - Shackles => todo!(), + Shackles => {} } for connection in state.connections.iter_mut() { if connection.send.send(ClientResponse::DetectiveGadget(gadget.clone())).await.is_err() { @@ -594,3 +576,42 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { } } } + +async fn start_game(state: SharedState) -> Sender { + let (send_pos, mut recv_pos) = tokio::sync::mpsc::channel(1); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(COOLDOWN); + interval.tick().await; + // TODO: broadcast Mr. X start + interval.tick().await; + { + let mut state = state.lock().await; + state.mr_x_gadgets.allow_use(); + state.detective_gadgets.allow_use(); + } + // TODO: broadcast Detective start + loop { + use SpecialPos::*; + + interval.tick().await; + // broadcast Mr. X position + match recv_pos.try_recv() { + Ok(Stop(stop_id)) => { + // TODO: broadcast stop id + } + Ok(Image(image)) => { + // TODO: broadcast image + } + Ok(NotFound) => { + // TODO: broadcast 404 + } + Err(_) => { + // TODO: broadcast Mr. X position + } + } + } + }); + + send_pos +} diff --git a/robusta/src/ws_message.rs b/robusta/src/ws_message.rs index ab52322..fda9bd3 100644 --- a/robusta/src/ws_message.rs +++ b/robusta/src/ws_message.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::gadgets::{MrXGadget, DetectiveGadget}; + #[derive(specta::Type, Clone, Deserialize, Debug)] pub enum ClientMessage { Position { long: f32, lat: f32 }, @@ -12,22 +14,6 @@ pub enum ClientMessage { Message(String), } -#[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] -pub enum MrXGadget { - AlternativeFacts { stop_id: String }, - Midjourney { image: Vec }, - NotFound, - Teleport, - Shifter, -} - -#[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] -pub enum DetectiveGadget { - Stop { stop_id: String }, - OutOfOrder, - Shackles, -} - #[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] pub enum ClientResponse { GameState(GameState), From e1b9761b0d129dd4b5aee87c38f4060c4722058c Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Wed, 22 May 2024 15:03:40 +0200 Subject: [PATCH 06/15] Implement cooldowns --- liberica/src/lib/bindings.ts | 14 ++-- liberica/src/page/Game.tsx | 10 ++- robusta/src/gadgets.rs | 21 ++++- robusta/src/main.rs | 155 +++++++++++++++++------------------ robusta/src/ws_message.rs | 8 +- 5 files changed, 115 insertions(+), 93 deletions(-) diff --git a/liberica/src/lib/bindings.ts b/liberica/src/lib/bindings.ts index d1699a3..95dc3f6 100644 --- a/liberica/src/lib/bindings.ts +++ b/liberica/src/lib/bindings.ts @@ -1,7 +1,5 @@ // This file has been generated by Specta. DO NOT EDIT. -export type CreateTeamError = "InvalidName" | "NameAlreadyExists" - /** * Information about a tram station. */ @@ -11,19 +9,21 @@ export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney: { image: number[] } } | "NotFound" | "Teleport" | "Shifter" -export type GameState = { teams: TeamState[]; trains: Train[] } +export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown: number | null; detective_gadget_cooldown: number | null; mr_x_gadget_cooldown: number | null } -export type Team = { id: number; name: string; color: string; kind: TeamKind } +export type CreateTeam = { name: string; color: string; kind: TeamKind } export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" -export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } +export type CreateTeamError = "InvalidName" | "NameAlreadyExists" export type TeamKind = "MrX" | "Detective" | "Observer" -export type CreateTeam = { name: string; color: string; kind: TeamKind } - export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } +export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } + export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } +export type Team = { id: number; name: string; color: string; kind: TeamKind } + diff --git a/liberica/src/page/Game.tsx b/liberica/src/page/Game.tsx index 9af3ff3..d839aef 100644 --- a/liberica/src/page/Game.tsx +++ b/liberica/src/page/Game.tsx @@ -10,7 +10,13 @@ import { useTranslation } from "react-i18next"; export function Game() { const [ws, setWS] = useState(); - const [gs, setGameState] = useState({ teams: [], trains: [] }); + const [gs, setGameState] = useState({ + teams: [], + trains: [], + position_cooldown: null, + mr_x_gadget_cooldown: null, + detective_gadget_cooldown: null, + }); const [embarkedTrain, setEmbarkedTrain] = useState(); const team = useLocation().state as Team | undefined; // this is how Home passes the team const { t } = useTranslation(); @@ -29,8 +35,6 @@ export function Game() { ws?.send({ EmbarkTrain: { train_id: train.line_id } }); } - const [showGadgetMenu, setShowGadgetMenu] = useState(false) - useEffect(() => { const socket = createWebSocketConnection(); diff --git a/robusta/src/gadgets.rs b/robusta/src/gadgets.rs index 526f1e7..d2c5304 100644 --- a/robusta/src/gadgets.rs +++ b/robusta/src/gadgets.rs @@ -22,6 +22,7 @@ pub enum DetectiveGadget { #[derive(Debug)] pub struct GadgetState { can_be_used: bool, + cooldown: Option, used: HashSet>, } @@ -29,13 +30,29 @@ impl GadgetState { pub fn new() -> Self { Self { can_be_used: false, + cooldown: None, used: HashSet::new(), } } - pub fn try_use(&mut self, gadget: &T) -> bool { - if self.can_be_used && self.used.insert(mem::discriminant(gadget)) { + pub fn update_time(&mut self, delta: f32) { + if let Some(cooldown) = self.cooldown.as_mut() { + *cooldown -= delta; + if *cooldown < 0.0 { + self.can_be_used = true; + self.cooldown = None; + } + } + } + + pub fn remaining(&self) -> Option { + self.cooldown + } + + pub fn try_use(&mut self, gadget: &T, cooldown: f32) -> bool { + if self.can_be_used && self.cooldown.is_none() && self.used.insert(mem::discriminant(gadget)) { self.can_be_used = false; + self.cooldown = Some(cooldown); true } else { false diff --git a/robusta/src/main.rs b/robusta/src/main.rs index 1f7bd5a..a1d01dd 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -18,6 +18,7 @@ use axum::{ use futures_util::SinkExt; use reqwest::StatusCode; use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::time::Instant; use tower::util::ServiceExt; use tower_http::{ cors::CorsLayer, @@ -26,7 +27,7 @@ use tower_http::{ use tracing::{error, info, warn, Level}; use tracing_appender::rolling::{self, Rotation}; -use crate::gadgets::{GadgetState, DetectiveGadget, MrXGadget}; +use crate::gadgets::{DetectiveGadget, GadgetState, MrXGadget}; use crate::kvv::LineDepartures; use crate::unique_id::UniqueIdGen; use crate::ws_message::{ClientMessage, ClientResponse, GameState, Team, TeamKind, TeamState}; @@ -42,8 +43,8 @@ const TEAMS_FILE: &str = "teams.json"; /// The name used for the Mr. X team. const MRX: &str = "Mr. X"; -/// The interval between position broadcasts and gadgets uses (10 min). -const COOLDOWN: Duration = Duration::from_secs(600); +/// The interval between position broadcasts and gadgets uses in seconds (10 min). +const COOLDOWN: f32 = 600.0; #[derive(Debug)] enum InputMessage { @@ -78,8 +79,6 @@ struct AppState { pub connections: Vec, pub client_id_gen: UniqueIdGen, pub team_id_gen: UniqueIdGen, - pub mr_x_gadgets: GadgetState, - pub detective_gadgets: GadgetState, } impl AppState { @@ -90,8 +89,6 @@ impl AppState { connections: Vec::new(), client_id_gen: UniqueIdGen::new(), team_id_gen: UniqueIdGen::new(), - mr_x_gadgets: GadgetState::new(), - detective_gadgets: GadgetState::new(), } } @@ -377,7 +374,7 @@ enum SpecialPos { NotFound, } -async fn run_game_loop(mut recv: Receiver, shared_state: SharedState) { +async fn run_game_loop(mut recv: Receiver, state: SharedState) { let mut departures = HashMap::new(); let mut log_file = rolling::Builder::new() .rotation(Rotation::DAILY) @@ -387,16 +384,48 @@ async fn run_game_loop(mut recv: Receiver, shared_state: SharedSta .build("logs") .expect("failed to initialize rolling file appender"); - let send_pos = start_game(Arc::clone(&shared_state)).await; + let mut special_pos = None; // the time for a single frame - let mut interval = tokio::time::interval(Duration::from_millis(500)); + let mut interval = tokio::time::interval(Duration::from_millis(100)); + let mut time = Instant::now(); + let mut position_cooldown = None; + let mut mr_x_gadgets = GadgetState::new(); + let mut detective_gadgets = GadgetState::new(); loop { interval.tick().await; + let old_time = time; + time = Instant::now(); + let delta = (time - old_time).as_secs_f32(); + if let Some(cooldown) = position_cooldown.as_mut() { + *cooldown -= delta; + if *cooldown < 0.0 { + position_cooldown = Some(COOLDOWN); + // broadcast Mr. X position + match special_pos { + Some(SpecialPos::Stop(stop_id)) => { + // TODO: broadcast stop id + } + Some(SpecialPos::Image(image)) => { + // TODO: broadcast image + } + Some(SpecialPos::NotFound) => { + // TODO: broadcast 404 + } + None => { + // TODO: broadcast Mr. X position + } + } + special_pos = None; + } + } + mr_x_gadgets.update_time(delta); + detective_gadgets.update_time(delta); + // handle messages - let mut state = shared_state.lock().await; + let mut state = state.lock().await; while let Ok(msg) = recv.try_recv() { match msg { InputMessage::Client(msg, id) => { @@ -437,45 +466,41 @@ async fn run_game_loop(mut recv: Receiver, shared_state: SharedSta ClientMessage::MrXGadget(gadget) => { use MrXGadget::*; - if state.team_by_client_id(id).map(|ts| ts.team.kind != TeamKind::MrX).unwrap_or(true) { + if state + .team_by_client_id(id) + .map(|ts| ts.team.kind != TeamKind::MrX) + .unwrap_or(true) + { warn!("Client {} tried to use MrX Gadget, but is not MrX", id); continue; } - if state.mr_x_gadgets.try_use(&gadget) { - let state = Arc::clone(&shared_state); - tokio::spawn(async move { - tokio::time::sleep(COOLDOWN).await; - state.lock().await.mr_x_gadgets.allow_use(); - }); - } else { + if !mr_x_gadgets.try_use(&gadget, COOLDOWN) { warn!("Client {} tried to use MrX Gadget, but is not allowed to", id); continue; } match &gadget { AlternativeFacts { stop_id } => { - if send_pos.send(SpecialPos::Stop(stop_id.clone())).await.is_err() { - error!("special position channel closed"); - } + special_pos = Some(SpecialPos::Stop(stop_id.clone())); continue; } Midjourney { image } => { - if send_pos.send(SpecialPos::Image(image.clone())).await.is_err() { - error!("special position channel closed"); - } - continue; + special_pos = Some(SpecialPos::Image(image.clone())); } NotFound => { - if send_pos.send(SpecialPos::NotFound).await.is_err() { - error!("special position channel closed"); - } + special_pos = Some(SpecialPos::NotFound); continue; } Teleport => {} Shifter => {} } for connection in state.connections.iter_mut() { - if connection.send.send(ClientResponse::MrXGadget(gadget.clone())).await.is_err() { + if connection + .send + .send(ClientResponse::MrXGadget(gadget.clone())) + .await + .is_err() + { continue; } } @@ -483,17 +508,15 @@ async fn run_game_loop(mut recv: Receiver, shared_state: SharedSta ClientMessage::DetectiveGadget(gadget) => { use DetectiveGadget::*; - if state.team_by_client_id(id).map(|ts| ts.team.kind != TeamKind::Detective).unwrap_or(true) { + if state + .team_by_client_id(id) + .map(|ts| ts.team.kind != TeamKind::Detective) + .unwrap_or(true) + { warn!("Client {} tried to use Detective Gadget, but is not Detective", id); continue; } - if state.detective_gadgets.try_use(&gadget) { - let state = Arc::clone(&shared_state); - tokio::spawn(async move { - tokio::time::sleep(COOLDOWN).await; - state.lock().await.detective_gadgets.allow_use(); - }); - } else { + if !detective_gadgets.try_use(&gadget, COOLDOWN) { warn!("Client {} tried to use Detective Gadget, but is not allowed to", id); continue; } @@ -501,14 +524,19 @@ async fn run_game_loop(mut recv: Receiver, shared_state: SharedSta match &gadget { Stop { stop_id } => { // TODO: mark stop as blocked for the next 20 mins - }, + } OutOfOrder => { // TODO: immediately broadcast Mr. X position - }, + } Shackles => {} } for connection in state.connections.iter_mut() { - if connection.send.send(ClientResponse::DetectiveGadget(gadget.clone())).await.is_err() { + if connection + .send + .send(ClientResponse::DetectiveGadget(gadget.clone())) + .await + .is_err() + { continue; } } @@ -544,6 +572,9 @@ async fn run_game_loop(mut recv: Receiver, shared_state: SharedSta let game_state = GameState { teams: state.teams.clone(), trains, + position_cooldown, + mr_x_gadget_cooldown: mr_x_gadgets.remaining(), + detective_gadget_cooldown: detective_gadgets.remaining(), }; writeln!( log_file, @@ -564,6 +595,9 @@ async fn run_game_loop(mut recv: Receiver, shared_state: SharedSta .cloned() .collect(), trains: game_state.trains.clone(), + position_cooldown: game_state.position_cooldown, + mr_x_gadget_cooldown: game_state.mr_x_gadget_cooldown, + detective_gadget_cooldown: game_state.detective_gadget_cooldown, }; if let Err(err) = connection .send @@ -576,42 +610,3 @@ async fn run_game_loop(mut recv: Receiver, shared_state: SharedSta } } } - -async fn start_game(state: SharedState) -> Sender { - let (send_pos, mut recv_pos) = tokio::sync::mpsc::channel(1); - - tokio::spawn(async move { - let mut interval = tokio::time::interval(COOLDOWN); - interval.tick().await; - // TODO: broadcast Mr. X start - interval.tick().await; - { - let mut state = state.lock().await; - state.mr_x_gadgets.allow_use(); - state.detective_gadgets.allow_use(); - } - // TODO: broadcast Detective start - loop { - use SpecialPos::*; - - interval.tick().await; - // broadcast Mr. X position - match recv_pos.try_recv() { - Ok(Stop(stop_id)) => { - // TODO: broadcast stop id - } - Ok(Image(image)) => { - // TODO: broadcast image - } - Ok(NotFound) => { - // TODO: broadcast 404 - } - Err(_) => { - // TODO: broadcast Mr. X position - } - } - } - }); - - send_pos -} diff --git a/robusta/src/ws_message.rs b/robusta/src/ws_message.rs index fda9bd3..2f0b446 100644 --- a/robusta/src/ws_message.rs +++ b/robusta/src/ws_message.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::gadgets::{MrXGadget, DetectiveGadget}; +use crate::gadgets::{DetectiveGadget, MrXGadget}; #[derive(specta::Type, Clone, Deserialize, Debug)] pub enum ClientMessage { @@ -25,6 +25,12 @@ pub enum ClientResponse { pub struct GameState { pub teams: Vec, pub trains: Vec, + // in seconds + pub position_cooldown: Option, + // in seconds + pub detective_gadget_cooldown: Option, + // in seconds + pub mr_x_gadget_cooldown: Option, } #[derive(specta::Type, Default, Clone, Serialize, Deserialize, Debug)] From 9f1ac5c59f6e0c422dcfd502169856e2b6bd48d1 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Wed, 22 May 2024 17:51:19 +0200 Subject: [PATCH 07/15] Fix TypeScript errors --- liberica/src/lib/bindings.ts | 14 +++++++------- liberica/src/page/Game.tsx | 3 --- robusta/src/ws_message.rs | 3 +++ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/liberica/src/lib/bindings.ts b/liberica/src/lib/bindings.ts index 95dc3f6..d967234 100644 --- a/liberica/src/lib/bindings.ts +++ b/liberica/src/lib/bindings.ts @@ -1,5 +1,7 @@ // This file has been generated by Specta. DO NOT EDIT. +export type CreateTeam = { name: string; color: string; kind: TeamKind } + /** * Information about a tram station. */ @@ -9,21 +11,19 @@ export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney: { image: number[] } } | "NotFound" | "Teleport" | "Shifter" -export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown: number | null; detective_gadget_cooldown: number | null; mr_x_gadget_cooldown: number | null } - -export type CreateTeam = { name: string; color: string; kind: TeamKind } +export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown?: number | null; detective_gadget_cooldown?: number | null; mr_x_gadget_cooldown?: number | null } export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" export type CreateTeamError = "InvalidName" | "NameAlreadyExists" -export type TeamKind = "MrX" | "Detective" | "Observer" +export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } + +export type Team = { id: number; name: string; color: string; kind: TeamKind } export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } -export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } - -export type Team = { id: number; name: string; color: string; kind: TeamKind } +export type TeamKind = "MrX" | "Detective" | "Observer" diff --git a/liberica/src/page/Game.tsx b/liberica/src/page/Game.tsx index d839aef..996ad66 100644 --- a/liberica/src/page/Game.tsx +++ b/liberica/src/page/Game.tsx @@ -13,9 +13,6 @@ export function Game() { const [gs, setGameState] = useState({ teams: [], trains: [], - position_cooldown: null, - mr_x_gadget_cooldown: null, - detective_gadget_cooldown: null, }); const [embarkedTrain, setEmbarkedTrain] = useState(); const team = useLocation().state as Team | undefined; // this is how Home passes the team diff --git a/robusta/src/ws_message.rs b/robusta/src/ws_message.rs index 2f0b446..75b5db8 100644 --- a/robusta/src/ws_message.rs +++ b/robusta/src/ws_message.rs @@ -26,10 +26,13 @@ pub struct GameState { pub teams: Vec, pub trains: Vec, // in seconds + #[specta(optional)] pub position_cooldown: Option, // in seconds + #[specta(optional)] pub detective_gadget_cooldown: Option, // in seconds + #[specta(optional)] pub mr_x_gadget_cooldown: Option, } From 22a30db56046d4a9ee6a835c95959309bb336a99 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Wed, 22 May 2024 23:57:09 +0200 Subject: [PATCH 08/15] Extract logic for running game to a separate function/task --- robusta/src/gadgets.rs | 2 - robusta/src/main.rs | 132 +++++++++++++++++++++++++++-------------- 2 files changed, 86 insertions(+), 48 deletions(-) diff --git a/robusta/src/gadgets.rs b/robusta/src/gadgets.rs index d2c5304..e86f65f 100644 --- a/robusta/src/gadgets.rs +++ b/robusta/src/gadgets.rs @@ -39,7 +39,6 @@ impl GadgetState { if let Some(cooldown) = self.cooldown.as_mut() { *cooldown -= delta; if *cooldown < 0.0 { - self.can_be_used = true; self.cooldown = None; } } @@ -51,7 +50,6 @@ impl GadgetState { pub fn try_use(&mut self, gadget: &T, cooldown: f32) -> bool { if self.can_be_used && self.cooldown.is_none() && self.used.insert(mem::discriminant(gadget)) { - self.can_be_used = false; self.cooldown = Some(cooldown); true } else { diff --git a/robusta/src/main.rs b/robusta/src/main.rs index a1d01dd..500880a 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -384,46 +384,15 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { .build("logs") .expect("failed to initialize rolling file appender"); - let mut special_pos = None; - // the time for a single frame - let mut interval = tokio::time::interval(Duration::from_millis(100)); + let mut interval = tokio::time::interval(Duration::from_millis(500)); + + let running_state = Arc::new(tokio::sync::Mutex::new(RunningState::new())); + start_game(Arc::clone(&running_state)).await; - let mut time = Instant::now(); - let mut position_cooldown = None; - let mut mr_x_gadgets = GadgetState::new(); - let mut detective_gadgets = GadgetState::new(); loop { interval.tick().await; - let old_time = time; - time = Instant::now(); - let delta = (time - old_time).as_secs_f32(); - if let Some(cooldown) = position_cooldown.as_mut() { - *cooldown -= delta; - if *cooldown < 0.0 { - position_cooldown = Some(COOLDOWN); - // broadcast Mr. X position - match special_pos { - Some(SpecialPos::Stop(stop_id)) => { - // TODO: broadcast stop id - } - Some(SpecialPos::Image(image)) => { - // TODO: broadcast image - } - Some(SpecialPos::NotFound) => { - // TODO: broadcast 404 - } - None => { - // TODO: broadcast Mr. X position - } - } - special_pos = None; - } - } - mr_x_gadgets.update_time(delta); - detective_gadgets.update_time(delta); - // handle messages let mut state = state.lock().await; while let Ok(msg) = recv.try_recv() { @@ -474,21 +443,22 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { warn!("Client {} tried to use MrX Gadget, but is not MrX", id); continue; } - if !mr_x_gadgets.try_use(&gadget, COOLDOWN) { + let mut running_state = running_state.lock().await; + if !running_state.mr_x_gadgets.try_use(&gadget, COOLDOWN) { warn!("Client {} tried to use MrX Gadget, but is not allowed to", id); continue; } match &gadget { AlternativeFacts { stop_id } => { - special_pos = Some(SpecialPos::Stop(stop_id.clone())); + running_state.special_pos = Some(SpecialPos::Stop(stop_id.clone())); continue; } Midjourney { image } => { - special_pos = Some(SpecialPos::Image(image.clone())); + running_state.special_pos = Some(SpecialPos::Image(image.clone())); } NotFound => { - special_pos = Some(SpecialPos::NotFound); + running_state.special_pos = Some(SpecialPos::NotFound); continue; } Teleport => {} @@ -516,7 +486,8 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { warn!("Client {} tried to use Detective Gadget, but is not Detective", id); continue; } - if !detective_gadgets.try_use(&gadget, COOLDOWN) { + let mut running_state = running_state.lock().await; + if !running_state.detective_gadgets.try_use(&gadget, COOLDOWN) { warn!("Client {} tried to use Detective Gadget, but is not allowed to", id); continue; } @@ -569,12 +540,15 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { } // log game state - let game_state = GameState { - teams: state.teams.clone(), - trains, - position_cooldown, - mr_x_gadget_cooldown: mr_x_gadgets.remaining(), - detective_gadget_cooldown: detective_gadgets.remaining(), + let game_state = { + let running_state = running_state.lock().await; + GameState { + teams: state.teams.clone(), + trains, + position_cooldown: running_state.position_cooldown, + mr_x_gadget_cooldown: running_state.mr_x_gadgets.remaining(), + detective_gadget_cooldown: running_state.detective_gadgets.remaining(), + } }; writeln!( log_file, @@ -610,3 +584,69 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { } } } + +struct RunningState { + position_cooldown: Option, + mr_x_gadgets: GadgetState, + detective_gadgets: GadgetState, + special_pos: Option, +} + +impl RunningState { + fn new() -> Self { + Self { + position_cooldown: None, + mr_x_gadgets: GadgetState::new(), + detective_gadgets: GadgetState::new(), + special_pos: None, + } + } +} + +async fn start_game(state: Arc>) { + tokio::spawn(async move { + let mut warmup = true; + + let mut time = Instant::now(); + let mut interval = tokio::time::interval(Duration::from_millis(100)); + loop { + interval.tick().await; + let mut state = state.lock().await; + + let old_time = time; + time = Instant::now(); + let delta = (time - old_time).as_secs_f32(); + if let Some(cooldown) = state.position_cooldown.as_mut() { + *cooldown -= delta; + if *cooldown < 0.0 { + state.position_cooldown = Some(COOLDOWN); + if warmup { + // TODO: broadcast Detective start + warmup = false; + state.mr_x_gadgets.allow_use(); + state.detective_gadgets.allow_use(); + } else { + // broadcast Mr. X position + match &state.special_pos { + Some(SpecialPos::Stop(stop_id)) => { + // TODO: broadcast stop id + } + Some(SpecialPos::Image(image)) => { + // TODO: broadcast image + } + Some(SpecialPos::NotFound) => { + // TODO: broadcast 404 + } + None => { + // TODO: broadcast Mr. X position + } + } + state.special_pos = None; + } + } + } + state.mr_x_gadgets.update_time(delta); + state.detective_gadgets.update_time(delta); + } + }); +} From 60937c67ff2eadb12eec2f59b2a2c011157393a9 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Thu, 23 May 2024 16:29:32 +0200 Subject: [PATCH 09/15] Implement Mr. X position messages --- liberica/src/lib/bindings.ts | 20 ++++++++-------- robusta/src/kvv.rs | 4 ++-- robusta/src/main.rs | 44 ++++++++++++++++++++++++------------ robusta/src/ws_message.rs | 9 ++++++++ 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/liberica/src/lib/bindings.ts b/liberica/src/lib/bindings.ts index d967234..26d181b 100644 --- a/liberica/src/lib/bindings.ts +++ b/liberica/src/lib/bindings.ts @@ -1,29 +1,31 @@ // This file has been generated by Specta. DO NOT EDIT. -export type CreateTeam = { name: string; color: string; kind: TeamKind } - /** * Information about a tram station. */ export type Stop = { name: string; id: string; lat: number; lon: number } -export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } - export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney: { image: number[] } } | "NotFound" | "Teleport" | "Shifter" -export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown?: number | null; detective_gadget_cooldown?: number | null; mr_x_gadget_cooldown?: number | null } - export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" -export type CreateTeamError = "InvalidName" | "NameAlreadyExists" +export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } + +export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { MrXPosition: MrXPosition } + +export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown?: number | null; detective_gadget_cooldown?: number | null; mr_x_gadget_cooldown?: number | null } export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } +export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } + +export type MrXPosition = { Stop: string } | { Image: number[] } | "NotFound" + export type Team = { id: number; name: string; color: string; kind: TeamKind } -export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } +export type CreateTeam = { name: string; color: string; kind: TeamKind } -export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } +export type CreateTeamError = "InvalidName" | "NameAlreadyExists" export type TeamKind = "MrX" | "Detective" | "Observer" diff --git a/robusta/src/kvv.rs b/robusta/src/kvv.rs index cefe0ef..2460aaf 100644 --- a/robusta/src/kvv.rs +++ b/robusta/src/kvv.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; use futures_util::future::join_all; use lazy_static::lazy_static; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::OnceLock; @@ -16,7 +16,7 @@ use crate::ws_message::Train; const DEFAULT_WAIT_TIME: Duration = Duration::from_secs(30); /// Information about a tram station. -#[derive(Debug, Serialize, specta::Type, PartialEq)] +#[derive(Debug, Serialize, Deserialize, specta::Type, Clone, PartialEq)] pub struct Stop { /// human readable stop name pub name: String, diff --git a/robusta/src/main.rs b/robusta/src/main.rs index 500880a..65039b0 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -16,6 +16,7 @@ use axum::{ Json, Router, }; use futures_util::SinkExt; +use point::Point; use reqwest::StatusCode; use tokio::sync::mpsc::{Receiver, Sender}; use tokio::time::Instant; @@ -26,6 +27,7 @@ use tower_http::{ }; use tracing::{error, info, warn, Level}; use tracing_appender::rolling::{self, Rotation}; +use ws_message::MrXPosition; use crate::gadgets::{DetectiveGadget, GadgetState, MrXGadget}; use crate::kvv::LineDepartures; @@ -388,7 +390,7 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { let mut interval = tokio::time::interval(Duration::from_millis(500)); let running_state = Arc::new(tokio::sync::Mutex::new(RunningState::new())); - start_game(Arc::clone(&running_state)).await; + start_game(Arc::clone(&state), Arc::clone(&running_state)).await; loop { interval.tick().await; @@ -603,7 +605,7 @@ impl RunningState { } } -async fn start_game(state: Arc>) { +async fn start_game(state: SharedState, running_state: Arc>) { tokio::spawn(async move { let mut warmup = true; @@ -611,42 +613,54 @@ async fn start_game(state: Arc>) { let mut interval = tokio::time::interval(Duration::from_millis(100)); loop { interval.tick().await; - let mut state = state.lock().await; + let mut running_state = running_state.lock().await; let old_time = time; time = Instant::now(); let delta = (time - old_time).as_secs_f32(); - if let Some(cooldown) = state.position_cooldown.as_mut() { + if let Some(cooldown) = running_state.position_cooldown.as_mut() { *cooldown -= delta; if *cooldown < 0.0 { - state.position_cooldown = Some(COOLDOWN); + running_state.position_cooldown = Some(COOLDOWN); if warmup { // TODO: broadcast Detective start warmup = false; - state.mr_x_gadgets.allow_use(); - state.detective_gadgets.allow_use(); + running_state.mr_x_gadgets.allow_use(); + running_state.detective_gadgets.allow_use(); } else { // broadcast Mr. X position - match &state.special_pos { + let mut state = state.lock().await; + let position = match running_state.special_pos.take() { Some(SpecialPos::Stop(stop_id)) => { - // TODO: broadcast stop id + MrXPosition::Stop(stop_id) } Some(SpecialPos::Image(image)) => { - // TODO: broadcast image + MrXPosition::Image(image) } Some(SpecialPos::NotFound) => { - // TODO: broadcast 404 + MrXPosition::NotFound } None => { - // TODO: broadcast Mr. X position + let mr_x = state.teams.iter().find(|ts| ts.team.kind == TeamKind::MrX).expect("no Mr. X"); + let stop = kvv::nearest_stop(Point { latitude: mr_x.lat, longitude: mr_x.long }); + MrXPosition::Stop(stop.id.clone()) + } + }; + for connection in state.connections.iter_mut() { + if let Err(err) = connection + .send + .send(ClientResponse::MrXPosition(position.clone())) + .await + { + error!("failed to send Mr. X position to client {}: {}", connection.id, err); + continue; } } - state.special_pos = None; } } } - state.mr_x_gadgets.update_time(delta); - state.detective_gadgets.update_time(delta); + running_state.mr_x_gadgets.update_time(delta); + running_state.detective_gadgets.update_time(delta); } }); } diff --git a/robusta/src/ws_message.rs b/robusta/src/ws_message.rs index 75b5db8..257f310 100644 --- a/robusta/src/ws_message.rs +++ b/robusta/src/ws_message.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::gadgets::{DetectiveGadget, MrXGadget}; +use crate::kvv::Stop; #[derive(specta::Type, Clone, Deserialize, Debug)] pub enum ClientMessage { @@ -19,6 +20,14 @@ pub enum ClientResponse { GameState(GameState), MrXGadget(MrXGadget), DetectiveGadget(DetectiveGadget), + MrXPosition(MrXPosition), +} + +#[derive(specta::Type, Clone, Serialize, Deserialize, Debug)] +pub enum MrXPosition { + Stop(String), + Image(Vec), + NotFound, } #[derive(specta::Type, Default, Clone, Serialize, Deserialize, Debug)] From b007a9e1d6aaeafc14baed700d890b01483c9708 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Thu, 23 May 2024 17:15:29 +0200 Subject: [PATCH 10/15] Add `broadcast` function & implement detective gadgets --- liberica/src/lib/bindings.ts | 18 +++++----- robusta/src/main.rs | 69 ++++++++++++++++++------------------ robusta/src/ws_message.rs | 2 ++ 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/liberica/src/lib/bindings.ts b/liberica/src/lib/bindings.ts index 26d181b..778940d 100644 --- a/liberica/src/lib/bindings.ts +++ b/liberica/src/lib/bindings.ts @@ -9,23 +9,23 @@ export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" -export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } +export type CreateTeamError = "InvalidName" | "NameAlreadyExists" -export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { MrXPosition: MrXPosition } +export type CreateTeam = { name: string; color: string; kind: TeamKind } -export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown?: number | null; detective_gadget_cooldown?: number | null; mr_x_gadget_cooldown?: number | null } +export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { MrXPosition: MrXPosition } -export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } +export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown?: number | null; detective_gadget_cooldown?: number | null; mr_x_gadget_cooldown?: number | null; blocked_stop?: string | null } export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } -export type MrXPosition = { Stop: string } | { Image: number[] } | "NotFound" +export type TeamKind = "MrX" | "Detective" | "Observer" -export type Team = { id: number; name: string; color: string; kind: TeamKind } +export type MrXPosition = { Stop: string } | { Image: number[] } | "NotFound" -export type CreateTeam = { name: string; color: string; kind: TeamKind } +export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } -export type CreateTeamError = "InvalidName" | "NameAlreadyExists" +export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } -export type TeamKind = "MrX" | "Detective" | "Observer" +export type Team = { id: number; name: string; color: string; kind: TeamKind } diff --git a/robusta/src/main.rs b/robusta/src/main.rs index 65039b0..145d9fc 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -466,16 +466,7 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { Teleport => {} Shifter => {} } - for connection in state.connections.iter_mut() { - if connection - .send - .send(ClientResponse::MrXGadget(gadget.clone())) - .await - .is_err() - { - continue; - } - } + broadcast(&state.connections, ClientResponse::MrXGadget(gadget)).await; } ClientMessage::DetectiveGadget(gadget) => { use DetectiveGadget::*; @@ -488,31 +479,30 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { warn!("Client {} tried to use Detective Gadget, but is not Detective", id); continue; } + let running_state_arc = &running_state; let mut running_state = running_state.lock().await; if !running_state.detective_gadgets.try_use(&gadget, COOLDOWN) { warn!("Client {} tried to use Detective Gadget, but is not allowed to", id); continue; } - match &gadget { + broadcast(&state.connections, ClientResponse::DetectiveGadget(gadget.clone())).await; + match gadget { Stop { stop_id } => { - // TODO: mark stop as blocked for the next 20 mins + running_state.blocked_stop = Some(stop_id); + let running_state = Arc::clone(&running_state_arc); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs_f32(2.0 * COOLDOWN)).await; + running_state.lock().await.blocked_stop = None; + }); } OutOfOrder => { - // TODO: immediately broadcast Mr. X position + let mr_x = state.teams.iter().find(|ts| ts.team.kind == TeamKind::MrX).expect("no Mr. X"); + let stop = kvv::nearest_stop(Point { latitude: mr_x.lat, longitude: mr_x.long }); + broadcast(&state.connections, ClientResponse::MrXPosition(MrXPosition::Stop(stop.id.clone()))).await; } Shackles => {} } - for connection in state.connections.iter_mut() { - if connection - .send - .send(ClientResponse::DetectiveGadget(gadget.clone())) - .await - .is_err() - { - continue; - } - } } } } @@ -550,6 +540,7 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { position_cooldown: running_state.position_cooldown, mr_x_gadget_cooldown: running_state.mr_x_gadgets.remaining(), detective_gadget_cooldown: running_state.detective_gadgets.remaining(), + blocked_stop: running_state.blocked_stop.clone(), } }; writeln!( @@ -574,10 +565,11 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { position_cooldown: game_state.position_cooldown, mr_x_gadget_cooldown: game_state.mr_x_gadget_cooldown, detective_gadget_cooldown: game_state.detective_gadget_cooldown, + blocked_stop: game_state.blocked_stop.clone(), }; if let Err(err) = connection .send - .send(ClientResponse::GameState(game_state.clone())) + .send(ClientResponse::GameState(game_state)) .await { error!("failed to send game state to client {}: {}", connection.id, err); @@ -587,11 +579,26 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { } } +async fn broadcast(connections: &[ClientConnection], message: ClientResponse) { + for connection in connections.iter() { + if let Err(err) = connection.send.send(message.clone()).await { + let message_type = match message { + ClientResponse::GameState(_) => "game state", + ClientResponse::MrXPosition(_) => "Mr. X position", + ClientResponse::MrXGadget(_) => "Mr. X gadget", + ClientResponse::DetectiveGadget(_) => "Detective gadget", + }; + error!("failed to send {} to client {}: {}", message_type, connection.id, err); + } + } +} + struct RunningState { position_cooldown: Option, mr_x_gadgets: GadgetState, detective_gadgets: GadgetState, special_pos: Option, + blocked_stop: Option, } impl RunningState { @@ -601,6 +608,7 @@ impl RunningState { mr_x_gadgets: GadgetState::new(), detective_gadgets: GadgetState::new(), special_pos: None, + blocked_stop: None, } } } @@ -629,7 +637,7 @@ async fn start_game(state: SharedState, running_state: Arc { MrXPosition::Stop(stop_id) @@ -646,16 +654,7 @@ async fn start_game(state: SharedState, running_state: Arc, + #[specta(optional)] + pub blocked_stop: Option, } #[derive(specta::Type, Default, Clone, Serialize, Deserialize, Debug)] From 823ee289196b3e67545f7c46382717972dba66fc Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Fri, 24 May 2024 12:22:12 +0200 Subject: [PATCH 11/15] Format --- robusta/src/main.rs | 48 +++++++++++++++++++++++---------------- robusta/src/ws_message.rs | 1 - 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/robusta/src/main.rs b/robusta/src/main.rs index 145d9fc..9e12b64 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -497,9 +497,20 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { }); } OutOfOrder => { - let mr_x = state.teams.iter().find(|ts| ts.team.kind == TeamKind::MrX).expect("no Mr. X"); - let stop = kvv::nearest_stop(Point { latitude: mr_x.lat, longitude: mr_x.long }); - broadcast(&state.connections, ClientResponse::MrXPosition(MrXPosition::Stop(stop.id.clone()))).await; + let mr_x = state + .teams + .iter() + .find(|ts| ts.team.kind == TeamKind::MrX) + .expect("no Mr. X"); + let stop = kvv::nearest_stop(Point { + latitude: mr_x.lat, + longitude: mr_x.long, + }); + broadcast( + &state.connections, + ClientResponse::MrXPosition(MrXPosition::Stop(stop.id.clone())), + ) + .await; } Shackles => {} } @@ -567,11 +578,7 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { detective_gadget_cooldown: game_state.detective_gadget_cooldown, blocked_stop: game_state.blocked_stop.clone(), }; - if let Err(err) = connection - .send - .send(ClientResponse::GameState(game_state)) - .await - { + if let Err(err) = connection.send.send(ClientResponse::GameState(game_state)).await { error!("failed to send game state to client {}: {}", connection.id, err); continue; } @@ -629,6 +636,7 @@ async fn start_game(state: SharedState, running_state: Arc { - MrXPosition::Stop(stop_id) - } - Some(SpecialPos::Image(image)) => { - MrXPosition::Image(image) - } - Some(SpecialPos::NotFound) => { - MrXPosition::NotFound - } + Some(SpecialPos::Stop(stop_id)) => MrXPosition::Stop(stop_id), + Some(SpecialPos::Image(image)) => MrXPosition::Image(image), + Some(SpecialPos::NotFound) => MrXPosition::NotFound, None => { - let mr_x = state.teams.iter().find(|ts| ts.team.kind == TeamKind::MrX).expect("no Mr. X"); - let stop = kvv::nearest_stop(Point { latitude: mr_x.lat, longitude: mr_x.long }); + let mr_x = state + .teams + .iter() + .find(|ts| ts.team.kind == TeamKind::MrX) + .expect("no Mr. X"); + let stop = kvv::nearest_stop(Point { + latitude: mr_x.lat, + longitude: mr_x.long, + }); MrXPosition::Stop(stop.id.clone()) } }; diff --git a/robusta/src/ws_message.rs b/robusta/src/ws_message.rs index 87547b2..b2437dd 100644 --- a/robusta/src/ws_message.rs +++ b/robusta/src/ws_message.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; use crate::gadgets::{DetectiveGadget, MrXGadget}; -use crate::kvv::Stop; #[derive(specta::Type, Clone, Deserialize, Debug)] pub enum ClientMessage { From 853bd5bd9b7f6551ae10aedc17d144369dc0190d Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Fri, 24 May 2024 15:24:59 +0200 Subject: [PATCH 12/15] Add game start and game end --- liberica/src/lib/bindings.ts | 22 +++++++++++----------- robusta/src/main.rs | 23 +++++++++++++++++++---- robusta/src/ws_message.rs | 3 +++ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/liberica/src/lib/bindings.ts b/liberica/src/lib/bindings.ts index 778940d..6f122c1 100644 --- a/liberica/src/lib/bindings.ts +++ b/liberica/src/lib/bindings.ts @@ -5,27 +5,27 @@ */ export type Stop = { name: string; id: string; lat: number; lon: number } -export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney: { image: number[] } } | "NotFound" | "Teleport" | "Shifter" - -export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" - -export type CreateTeamError = "InvalidName" | "NameAlreadyExists" +export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { MrXPosition: MrXPosition } | "GameStart" | "DetectiveStart" | "GameEnd" -export type CreateTeam = { name: string; color: string; kind: TeamKind } +export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney: { image: number[] } } | "NotFound" | "Teleport" | "Shifter" -export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { MrXPosition: MrXPosition } +export type MrXPosition = { Stop: string } | { Image: number[] } | "NotFound" -export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown?: number | null; detective_gadget_cooldown?: number | null; mr_x_gadget_cooldown?: number | null; blocked_stop?: string | null } +export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } -export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } +export type DetectiveGadget = { Stop: { stop_id: string } } | "OutOfOrder" | "Shackles" export type TeamKind = "MrX" | "Detective" | "Observer" -export type MrXPosition = { Stop: string } | { Image: number[] } | "NotFound" +export type CreateTeam = { name: string; color: string; kind: TeamKind } export type Train = { id: number; long: number; lat: number; line_id: string; line_name: string; direction: string } -export type TeamState = { team: Team; long: number; lat: number; on_train: string | null } +export type ClientMessage = { Position: { long: number; lat: number } } | { SetTeamPosition: { long: number; lat: number } } | { JoinTeam: { team_id: number } } | { EmbarkTrain: { train_id: string } } | "DisembarkTrain" | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { Message: string } export type Team = { id: number; name: string; color: string; kind: TeamKind } +export type GameState = { teams: TeamState[]; trains: Train[]; position_cooldown?: number | null; detective_gadget_cooldown?: number | null; mr_x_gadget_cooldown?: number | null; blocked_stop?: string | null } + +export type CreateTeamError = "InvalidName" | "NameAlreadyExists" + diff --git a/robusta/src/main.rs b/robusta/src/main.rs index 9e12b64..00a7f07 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -389,8 +389,10 @@ async fn run_game_loop(mut recv: Receiver, state: SharedState) { // the time for a single frame let mut interval = tokio::time::interval(Duration::from_millis(500)); - let running_state = Arc::new(tokio::sync::Mutex::new(RunningState::new())); - start_game(Arc::clone(&state), Arc::clone(&running_state)).await; + let mut running_state = RunningState::new(); + start_game(&state.lock().await.connections, &mut running_state).await; + let running_state = Arc::new(tokio::sync::Mutex::new(running_state)); + run_timer_loop(Arc::clone(&state), Arc::clone(&running_state)).await; loop { interval.tick().await; @@ -594,6 +596,9 @@ async fn broadcast(connections: &[ClientConnection], message: ClientResponse) { ClientResponse::MrXPosition(_) => "Mr. X position", ClientResponse::MrXGadget(_) => "Mr. X gadget", ClientResponse::DetectiveGadget(_) => "Detective gadget", + ClientResponse::GameStart => "game start", + ClientResponse::DetectiveStart => "detective start", + ClientResponse::GameEnd => "game end", }; error!("failed to send {} to client {}: {}", message_type, connection.id, err); } @@ -620,7 +625,17 @@ impl RunningState { } } -async fn start_game(state: SharedState, running_state: Arc>) { +async fn start_game(connections: &[ClientConnection], running_state: &mut RunningState) { + broadcast(connections, ClientResponse::GameStart).await; + running_state.position_cooldown = Some(COOLDOWN); +} + +async fn end_game(connections: &[ClientConnection], running_state: &mut RunningState) { + broadcast(connections, ClientResponse::GameEnd).await; + *running_state = RunningState::new(); +} + +async fn run_timer_loop(state: SharedState, running_state: Arc>) { tokio::spawn(async move { let mut warmup = true; @@ -639,7 +654,7 @@ async fn start_game(state: SharedState, running_state: Arc Date: Fri, 24 May 2024 15:34:08 +0200 Subject: [PATCH 13/15] Remove placehodler frontend code --- liberica/src/components/Gadgets.tsx | 111 ---------------------------- liberica/src/main.tsx | 2 - 2 files changed, 113 deletions(-) delete mode 100644 liberica/src/components/Gadgets.tsx diff --git a/liberica/src/components/Gadgets.tsx b/liberica/src/components/Gadgets.tsx deleted file mode 100644 index 50ad638..0000000 --- a/liberica/src/components/Gadgets.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { PropsWithChildren, useEffect, useState } from "react"; -import { Button } from "./InputElements"; -import { getStops } from "lib/api"; -import { DetectiveGadget, MrXGadget, Stop } from "lib/bindings"; - -function Gadget( - props: PropsWithChildren<{ value: string; onSelect?: () => void }>, -) { - return ( - - ); -} - -export function MrXGadgets() { - const [gadget, setGadget] = useState(); - const [stops, setStops] = useState([]); - useEffect(() => { - getStops().then(setStops); - }, []); - - return ( -
{ - e.preventDefault(); - alert(e.target); - }} - > -
- - Alternative Fakten:{" "} - - - - Midjourney: - - setGadget("NotFound")}> - 404 - - setGadget("Teleport")}> - Teleport - - setGadget("Shifter")}> - Shifter - - -
-
- ); -} - -export function DetectiveGadgets() { - const [gadget, setGadget] = useState(); - const [stop, setStop] = useState(""); - const [stops, setStops] = useState([]); - useEffect(() => { - getStops().then(setStops); - }, []); - - return ( -
{ - e.preventDefault(); - alert(gadget); - }} - > -
- { - if (stop) { - setGadget({ Stop: { stop_id: stop } }); - } else { - setGadget(undefined); - } - }} - > - Stop:{" "} - - - setGadget("OutOfOrder")} - > - Out of Order - - setGadget("Shackles")}> - Fessel - - -
-
- ); -} diff --git a/liberica/src/main.tsx b/liberica/src/main.tsx index 7b9a99d..56b4d43 100644 --- a/liberica/src/main.tsx +++ b/liberica/src/main.tsx @@ -3,7 +3,6 @@ import { Game } from "page/Game"; import { Home } from "page/Home"; import { Replay } from "page/Replay"; import { Admin } from "page/Admin"; -import { DetectiveGadgets } from "components/Gadgets"; import ReactDOM from "react-dom/client"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import "style/main.css"; @@ -41,7 +40,6 @@ ReactDOM.createRoot(rootElement).render( } /> } /> } /> - } /> , ); From 9de649d79406a12c7c8d33f988f7e97e0d84bf22 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Fri, 24 May 2024 15:46:23 +0200 Subject: [PATCH 14/15] Format imports --- robusta/src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/robusta/src/main.rs b/robusta/src/main.rs index 00a7f07..cf6ac8a 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -27,12 +27,11 @@ use tower_http::{ }; use tracing::{error, info, warn, Level}; use tracing_appender::rolling::{self, Rotation}; -use ws_message::MrXPosition; use crate::gadgets::{DetectiveGadget, GadgetState, MrXGadget}; use crate::kvv::LineDepartures; use crate::unique_id::UniqueIdGen; -use crate::ws_message::{ClientMessage, ClientResponse, GameState, Team, TeamKind, TeamState}; +use crate::ws_message::{ClientMessage, ClientResponse, GameState, MrXPosition, Team, TeamKind, TeamState}; mod gadgets; mod kvv; From 8c1b926eb3de7c3b42ede0ddc55bc6d4a7e9f807 Mon Sep 17 00:00:00 2001 From: konsumlamm Date: Fri, 24 May 2024 16:05:44 +0200 Subject: [PATCH 15/15] Fix websocket handling --- liberica/src/lib/bindings.ts | 2 +- robusta/src/main.rs | 12 ++++++------ robusta/src/ws_message.rs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/liberica/src/lib/bindings.ts b/liberica/src/lib/bindings.ts index 6f122c1..2d0e7c2 100644 --- a/liberica/src/lib/bindings.ts +++ b/liberica/src/lib/bindings.ts @@ -5,7 +5,7 @@ */ export type Stop = { name: string; id: string; lat: number; lon: number } -export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { MrXPosition: MrXPosition } | "GameStart" | "DetectiveStart" | "GameEnd" +export type ClientResponse = { GameState: GameState } | { MrXGadget: MrXGadget } | { DetectiveGadget: DetectiveGadget } | { MrXPosition: MrXPosition } | { GameStart: null } | { DetectiveStart: null } | { GameEnd: null } export type MrXGadget = { AlternativeFacts: { stop_id: string } } | { Midjourney: { image: number[] } } | "NotFound" | "Teleport" | "Shifter" diff --git a/robusta/src/main.rs b/robusta/src/main.rs index cf6ac8a..a392a22 100644 --- a/robusta/src/main.rs +++ b/robusta/src/main.rs @@ -595,9 +595,9 @@ async fn broadcast(connections: &[ClientConnection], message: ClientResponse) { ClientResponse::MrXPosition(_) => "Mr. X position", ClientResponse::MrXGadget(_) => "Mr. X gadget", ClientResponse::DetectiveGadget(_) => "Detective gadget", - ClientResponse::GameStart => "game start", - ClientResponse::DetectiveStart => "detective start", - ClientResponse::GameEnd => "game end", + ClientResponse::GameStart() => "game start", + ClientResponse::DetectiveStart() => "detective start", + ClientResponse::GameEnd() => "game end", }; error!("failed to send {} to client {}: {}", message_type, connection.id, err); } @@ -625,12 +625,12 @@ impl RunningState { } async fn start_game(connections: &[ClientConnection], running_state: &mut RunningState) { - broadcast(connections, ClientResponse::GameStart).await; + broadcast(connections, ClientResponse::GameStart()).await; running_state.position_cooldown = Some(COOLDOWN); } async fn end_game(connections: &[ClientConnection], running_state: &mut RunningState) { - broadcast(connections, ClientResponse::GameEnd).await; + broadcast(connections, ClientResponse::GameEnd()).await; *running_state = RunningState::new(); } @@ -653,7 +653,7 @@ async fn run_timer_loop(state: SharedState, running_state: Arc