Skip to content

Commit

Permalink
Implement cooldowns
Browse files Browse the repository at this point in the history
  • Loading branch information
konsumlamm committed May 22, 2024
1 parent 275aaae commit eb2c705
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 95 deletions.
14 changes: 7 additions & 7 deletions liberica/src/lib/bindings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// This file has been generated by Specta. DO NOT EDIT.

export type CreateTeamError = "InvalidName" | "NameAlreadyExists"

/**
* Information about a tram station.
*/
Expand All @@ -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 }

10 changes: 7 additions & 3 deletions liberica/src/page/Game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import { useTranslation } from "react-i18next";

export function Game() {
const [ws, setWS] = useState<WebSocketApi>();
const [gs, setGameState] = useState<GameState>({ teams: [], trains: [] });
const [gs, setGameState] = useState<GameState>({
teams: [],
trains: [],
position_cooldown: null,
mr_x_gadget_cooldown: null,
detective_gadget_cooldown: null,
});
const [embarkedTrain, setEmbarkedTrain] = useState<Train>();
const team: Team | undefined = useLocation().state ?? undefined; // this is how Home passes the team
const { t } = useTranslation();
Expand All @@ -29,8 +35,6 @@ export function Game() {
}
}

const [showGadgetMenu, setShowGadgetMenu] = useState(false)

useEffect(() => {
const socket = createWebSocketConnection();

Expand Down
21 changes: 19 additions & 2 deletions robusta/src/gadgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,37 @@ pub enum DetectiveGadget {
#[derive(Debug)]
pub struct GadgetState<T> {
can_be_used: bool,
cooldown: Option<f32>,
used: HashSet<mem::Discriminant<T>>,
}

impl<T> GadgetState<T> {
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<f32> {
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
Expand Down
160 changes: 78 additions & 82 deletions robusta/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};
Expand All @@ -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 {
Expand Down Expand Up @@ -78,8 +79,6 @@ struct AppState {
pub connections: Vec<ClientConnection>,
pub client_id_gen: UniqueIdGen,
pub team_id_gen: UniqueIdGen,
pub mr_x_gadgets: GadgetState<MrXGadget>,
pub detective_gadgets: GadgetState<DetectiveGadget>,
}

impl AppState {
Expand All @@ -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(),
}
}

Expand All @@ -116,7 +113,7 @@ impl AppState {
}
}

type SharedState = std::sync::Arc<tokio::sync::Mutex<AppState>>;
type SharedState = Arc<tokio::sync::Mutex<AppState>>;

async fn handler(ws: WebSocketUpgrade, State(state): State<SharedState>) -> Response {
let (send, rec) = tokio::sync::mpsc::channel(100);
Expand Down Expand Up @@ -368,7 +365,7 @@ fn load_state(send: Sender<InputMessage>) -> SharedState {
}
state.teams = teams;

std::sync::Arc::new(tokio::sync::Mutex::new(state))
Arc::new(tokio::sync::Mutex::new(state))
}

enum SpecialPos {
Expand All @@ -377,7 +374,7 @@ enum SpecialPos {
NotFound,
}

async fn run_game_loop(mut recv: Receiver<InputMessage>, shared_state: SharedState) {
async fn run_game_loop(mut recv: Receiver<InputMessage>, state: SharedState) {
let mut departures = HashMap::new();
let mut log_file = rolling::Builder::new()
.rotation(Rotation::DAILY)
Expand All @@ -387,13 +384,46 @@ async fn run_game_loop(mut recv: Receiver<InputMessage>, 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;

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 mut state = shared_state.lock().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);

let mut state = state.lock().await;
while let Ok(msg) = recv.try_recv() {
match msg {
InputMessage::Client(msg, id) => {
Expand Down Expand Up @@ -434,78 +464,77 @@ async fn run_game_loop(mut recv: Receiver<InputMessage>, 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;
}
}
}
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;
}

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;
}
}
Expand Down Expand Up @@ -539,6 +568,9 @@ async fn run_game_loop(mut recv: Receiver<InputMessage>, 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,
Expand All @@ -558,6 +590,9 @@ async fn run_game_loop(mut recv: Receiver<InputMessage>, 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
Expand All @@ -570,42 +605,3 @@ async fn run_game_loop(mut recv: Receiver<InputMessage>, shared_state: SharedSta
}
}
}

async fn start_game(state: SharedState) -> Sender<SpecialPos> {
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
}
Loading

0 comments on commit eb2c705

Please sign in to comment.