diff --git a/mvp/client/ui/src/messaging/mqttFunctions.ts b/mvp/client/ui/src/messaging/mqttFunctions.ts index 04f5e23..15139f1 100644 --- a/mvp/client/ui/src/messaging/mqttFunctions.ts +++ b/mvp/client/ui/src/messaging/mqttFunctions.ts @@ -23,6 +23,7 @@ export const getClient = async ( { username: connectionDetails.username, password: connectionDetails.password, + protocolVersion: 4, // MQTT 3.1.1 } ); diff --git a/mvp/client/ui/src/pages/HomePage.svelte b/mvp/client/ui/src/pages/HomePage.svelte index ef3f510..fffb3f8 100644 --- a/mvp/client/ui/src/pages/HomePage.svelte +++ b/mvp/client/ui/src/pages/HomePage.svelte @@ -20,6 +20,27 @@ import type { GameSessionDTO } from "src/api/generated"; const updateGameSession = async (newGameSessionDto: GameSessionDTO) => { + // TODO: this is a workaround to prevent the game from updating the game session if it receives an outdated one + // e.g. if an MQTT message arrives after the last POST request is resolved + if ( + $gameSession && + ($gameSession?.is_game_over || + (newGameSessionDto.current_step < $gameSession?.current_step ?? 0)) + ) { + return; + } + + gameOver.set(newGameSessionDto.is_game_over); + gameOverReason.set(newGameSessionDto.game_over_reason ?? null); + + if (newGameSessionDto.is_game_over) { + if (import.meta.env.VITE_DEBUG) { + console.log("Game over. Last known GameSessionDTO:", newGameSessionDto); + } + + $mqttClientUnsubscribe?.(); + } + gameSession.update( ( previousGameSession: GameSessionWithTimeSeries | null, @@ -36,20 +57,6 @@ }; }, ); - checkForGameOver(); - }; - - const checkForGameOver = () => { - if (isUndefinedOrNull($gameSession)) { - return; - } - - gameOver.set($gameSession?.is_game_over ?? false); - gameOverReason.set($gameSession?.game_over_reason ?? null); - - if ($gameSession?.is_game_over) { - $mqttClientUnsubscribe?.(); - } }; @@ -76,8 +83,6 @@ display: flex; flex-direction: column; align-items: center; - padding-left: 2em; - padding-right: 2em; } .title { diff --git a/mvp/server/core/constants.py b/mvp/server/core/constants.py index a77bbe0..94ec81a 100644 --- a/mvp/server/core/constants.py +++ b/mvp/server/core/constants.py @@ -4,7 +4,7 @@ GAME_TICK_INTERVAL = 0.03 # 30ms IDLE_SESSION_TTL_SECONDS = 60 * 30 # 15 minutes SESSION_CLEANUP_INTERVAL_SECONDS = 60 * 60 # 60 minutes -TIMESTEPS_PER_MOVE = 24 # we conceptualize every player move as a full working day, or "8 hours" +TIMESTEPS_PER_MOVE = 24 # "hours" GAME_OVER_MESSAGE_MACHINE_BREAKDOWN = "Machine health has reached 0%" GAME_OVER_MESSAGE_NO_MONEY = "Player ran out of money" diff --git a/mvp/server/core/game/GameSession.py b/mvp/server/core/game/GameSession.py index 6b85e54..d4b0629 100644 --- a/mvp/server/core/game/GameSession.py +++ b/mvp/server/core/game/GameSession.py @@ -83,9 +83,9 @@ async def advance_one_turn(self) -> list[MachineState]: self.current_step += 1 self.machine_state.update_parameters(self.current_step) + # Player earns money for the production at every timestep self.available_funds += math.ceil(REVENUE_PER_DAY / TIMESTEPS_PER_MOVE) - self._log() # Publish state every 2 steps (to reduce the load on the MQTT broker) if self.current_step % 2 == 0: @@ -131,7 +131,3 @@ def purchase_prediction(self, prediction: str) -> bool: self.available_funds -= PREDICTION_MODEL_COST self.available_predictions[prediction] = True return True - - def _log(self, multiple=5) -> None: - if self.current_step % multiple == 0: - print(f"{datetime.now()}: GameSession '{self.id}' - step: {self.current_step} - {self.machine_state}") diff --git a/mvp/server/messaging/mqtt_client.py b/mvp/server/messaging/mqtt_client.py index b20f4d8..9750379 100644 --- a/mvp/server/messaging/mqtt_client.py +++ b/mvp/server/messaging/mqtt_client.py @@ -32,8 +32,12 @@ def __init__(self): if MQTT_USER is None: return - client = paho.Client(client_id="pdmgame_server", userdata=None, protocol=paho.MQTTv5, - callback_api_version=CallbackAPIVersion.VERSION2) + client = paho.Client( + protocol=paho.MQTTv311, + callback_api_version=CallbackAPIVersion.VERSION2, + reconnect_on_failure=False, + clean_session=True + ) client.on_connect = on_connect client.tls_set(tls_version=mqtt.client.ssl.PROTOCOL_TLS) diff --git a/mvp/server/routers/sessions.py b/mvp/server/routers/sessions.py index aeefd10..951aa98 100644 --- a/mvp/server/routers/sessions.py +++ b/mvp/server/routers/sessions.py @@ -36,15 +36,18 @@ def get_session_dependency(session_id: str) -> GameSession: async def cleanup_inactive_sessions(): print(f"{datetime.now()}: Cleaning up sessions...") - for session_id, session in list(sessions.items()): - must_be_dropped = session.is_game_over or session.is_abandoned() - - if session.is_abandoned(): - game_metrics.update_on_game_abandoned(len(sessions) - 1) + sessions_to_drop = [] - if must_be_dropped: + for session_id, session in list(sessions.items()): + is_abandoned = session.is_abandoned() + if session.is_game_over or is_abandoned: print(f"{datetime.now()}: Session '{session_id}' will be dropped") - sessions.pop(session_id) + sessions_to_drop.append((session_id, is_abandoned)) + + for session_id, is_abandoned in sessions_to_drop: + sessions.pop(session_id) + if is_abandoned: + game_metrics.update_on_game_abandoned(len(sessions)) @router.on_event("shutdown") @@ -68,7 +71,7 @@ def publishing_func(game_session: GameSession) -> None: session = GameSession.new_game_session(_id=new_session_id, _state_publish_function=publishing_func) sessions[new_session_id] = session - game_metrics.update_on_game_started(len(sessions)) + game_metrics.update_on_game_started(len(sessions)) return GameSessionDTO.from_session(sessions[new_session_id])