Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/sensor timeseries #10

Merged
merged 5 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion mvp/client/ui/src/components/GameOver.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<script lang="ts">
import { gameOverReason, gameSession } from "src/stores/stores";

const gameSessionWithoutStateSnapshots = {
...$gameSession,
machineStateSnapshots: undefined,
};
</script>

<h3>Game Over</h3>
<pre>{JSON.stringify($gameSession, null, 2)}</pre>
<pre>{JSON.stringify(gameSessionWithoutStateSnapshots, null, 2)}</pre>
<p>{$gameOverReason}</p>
20 changes: 12 additions & 8 deletions mvp/client/ui/src/components/MachineData.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { isUndefinedOrNull } from "src/shared/utils";
import { PlayerActionsService } from "src/api/generated";
import { isNotUndefinedNorNull } from "src/shared/utils";
import { PlayerActionsService, type GameSessionDTO } from "src/api/generated";
import {
dayInProgress,
gameOver,
Expand All @@ -11,6 +11,8 @@
} from "src/stores/stores";
import Sensor from "src/components/Sensor.svelte";

export let updateGameSession: (newGameSessionDto: GameSessionDTO) => void;

$: {
sensorPurchaseButtonDisabled.set(
$dayInProgress ||
Expand All @@ -31,12 +33,12 @@
}

try {
const result =
const newGameSessionDto =
await PlayerActionsService.purchaseSensorPlayerActionsPurchasesSensorsPost(
sensorName,
$gameSession?.id!,
);
gameSession.set(result);
updateGameSession(newGameSessionDto);
} catch (error: any) {
if (error.status === 400) {
alert("Not enough funds to buy the sensor!");
Expand All @@ -52,12 +54,12 @@
}

try {
const result =
const newGameSessionDto =
await PlayerActionsService.purchasePredictionPlayerActionsPurchasesPredictionModelsPost(
"predicted_rul",
$gameSession?.id!,
);
gameSession.set(result);
updateGameSession(newGameSessionDto);
} catch (error: any) {
if (error.status === 400) {
alert("Not enough funds to buy the prediction model!");
Expand All @@ -68,7 +70,7 @@
};
</script>

{#if !isUndefinedOrNull($gameSession)}
{#if isNotUndefinedNorNull($gameSession)}
<div class="machine-data">
<h3>Operational Parameters</h3>
{#each Object.entries($gameSession?.machine_state?.operational_parameters ?? {}) as [parameter, value]}
Expand All @@ -85,7 +87,9 @@
? `${$gameSession.machine_state?.predicted_rul} steps`
: "???"}
<span
hidden={!isUndefinedOrNull($gameSession?.machine_state?.predicted_rul)}
hidden={isNotUndefinedNorNull(
$gameSession?.machine_state?.predicted_rul,
)}
>
<button
disabled={$predictionPurchaseButtonDisabled}
Expand Down
4 changes: 3 additions & 1 deletion mvp/client/ui/src/components/Sensor.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import { formatNumber } from "src/shared/utils";

export let parameter: string;
export let value: number | null;
export let sensorPurchaseButtonDisabled: boolean;
Expand All @@ -14,7 +16,7 @@
</script>

<p>
{formatParameterName(parameter)}: {value ?? "???"}
{formatParameterName(parameter)}: {formatNumber(value) ?? "???"}
<span hidden={value != null}>
<button
disabled={sensorPurchaseButtonDisabled}
Expand Down
33 changes: 21 additions & 12 deletions mvp/client/ui/src/components/SessionData.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<script lang="ts">
import { PlayerActionsService, SessionsService } from "src/api/generated";
import { isUndefinedOrNull } from "src/shared/utils";
import {
PlayerActionsService,
SessionsService,
type GameSessionDTO,
} from "src/api/generated";
import {
formatNumber,
isNotUndefinedNorNull,
isUndefinedOrNull,
} from "src/shared/utils";
import {
dayInProgress,
gameOver,
Expand All @@ -11,7 +19,8 @@
} from "src/stores/stores";

export let maintenanceCost: number;
export let fetchExistingSession: () => Promise<void>;
export let pollGameSession: () => Promise<void>;
export let updateGameSession: (newGameSessionDto: GameSessionDTO) => void;

$: {
maintenanceButtonDisabled.set(
Expand All @@ -26,20 +35,20 @@
if (isUndefinedOrNull($gameSession) || $gameOver) {
return;
}
// TODO: migrate this polling strategy to a websocket connection
// TODO: migrate this polling strategy to websockets / MQTT
// start fetching machine health every second while the day is advancing
const intervalId = setInterval(fetchExistingSession, 500);
const intervalId = setInterval(pollGameSession, 100);
dayInProgress.set(true);

try {
let result = await SessionsService.advanceSessionsTurnsPut(
let newGameSessionDto = await SessionsService.advanceSessionsTurnsPut(
$gameSession?.id!,
);
gameSession.set(result);
updateGameSession(newGameSessionDto);
} catch (error) {
console.error("Error advancing day:", error);
} finally {
await fetchExistingSession();
await pollGameSession();
// stop fetching machine health until the player advances to next day again
clearInterval(intervalId);
dayInProgress.set(false);
Expand All @@ -53,11 +62,11 @@
}

try {
const result =
const newGameSessionDto =
await PlayerActionsService.doMaintenancePlayerActionsMaintenanceInterventionsPost(
$gameSession!.id,
);
gameSession.set(result);
updateGameSession(newGameSessionDto);
performedMaintenanceInThisTurn.set(true);
} catch (error: any) {
if (error.status === 400) {
Expand All @@ -69,11 +78,11 @@
};
</script>

{#if !isUndefinedOrNull($gameSession)}
{#if isNotUndefinedNorNull($gameSession)}
<div class="session-data">
<h3>Game Session Details</h3>
<p>Current Step: {$gameSession?.current_step}</p>
<p>Available Funds: {$gameSession?.available_funds}</p>
<p>Available Funds: {formatNumber($gameSession?.available_funds)}</p>
<div class="session-commands">
<button on:click={advanceToNextDay} disabled={$dayInProgress}>
Advance to next day
Expand Down
35 changes: 26 additions & 9 deletions mvp/client/ui/src/pages/HomePage.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { SessionsService } from "../api/generated";
import { isUndefinedOrNull } from "src/shared/utils";
import { SessionsService, type GameSessionDTO } from "../api/generated";
import { isNotUndefinedNorNull, isUndefinedOrNull } from "src/shared/utils";
import GameOver from "src/components/GameOver.svelte";
import MachineView from "src/components/MachineView.svelte";
import SessionData from "src/components/SessionData.svelte";
Expand All @@ -14,23 +14,39 @@

const startSession = async () => {
try {
const result = await SessionsService.createSessionSessionsPost();
gameSession.set(result);
const newGameSessionDto =
await SessionsService.createSessionSessionsPost();
updateGameSession(newGameSessionDto);
} catch (error) {
console.error("Error fetching session:", error);
}
};

const fetchExistingSession = async () => {
const updateGameSession = (newGameSessionDto: GameSessionDTO) => {
gameSession.update((previousGameSession) => {
if (isNotUndefinedNorNull(previousGameSession)) {
previousGameSession!.machineStateSnapshots[
newGameSessionDto.current_step
] = newGameSessionDto.machine_state!;
}

return {
...newGameSessionDto,
machineStateSnapshots: previousGameSession?.machineStateSnapshots ?? {},
};
});
};

const pollGameSession = async () => {
if (isUndefinedOrNull($gameSession) || $gameOver) {
return;
}

try {
let result = await SessionsService.getSessionSessionsGet(
let newGameSessionDto = await SessionsService.getSessionSessionsGet(
$gameSession?.id!,
);
gameSession.set(result);
updateGameSession(newGameSessionDto);
checkForGameOver();
} catch (error) {
console.error("Error fetching session:", error);
Expand Down Expand Up @@ -61,9 +77,10 @@
<div class="game-data">
<SessionData
maintenanceCost={$globalSettings?.maintenance_cost ?? 0}
{fetchExistingSession}
{pollGameSession}
{updateGameSession}
/>
<MachineData />
<MachineData {updateGameSession} />
</div>
{/if}
</div>
Expand Down
7 changes: 7 additions & 0 deletions mvp/client/ui/src/shared/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { GameSessionDTO, MachineStateDTO } from "src/api/generated";

export type MachineStateSnapshotDict = { [key: number]: MachineStateDTO }

export interface GameSessionWithStateSnapshots extends GameSessionDTO {
machineStateSnapshots: MachineStateSnapshotDict
}
10 changes: 9 additions & 1 deletion mvp/client/ui/src/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export const isUndefinedOrNull = (value: any): boolean => {
return value === undefined || value === null;
}
}

export const isNotUndefinedNorNull = (value: any): boolean => {
return !isUndefinedOrNull(value);
}

export const formatNumber = (number: number | undefined | null) => {
return number?.toFixed(2);
};
5 changes: 3 additions & 2 deletions mvp/client/ui/src/stores/stores.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { writable } from 'svelte/store';
import type { GameParametersDTO, GameSessionDTO } from 'src/api/generated';
import type { GameParametersDTO } from 'src/api/generated';
import type { GameSessionWithStateSnapshots } from 'src/shared/types';

export const globalSettings = writable<GameParametersDTO | null>(null);
export const gameSession = writable<GameSessionDTO | null>(null);
export const gameSession = writable<GameSessionWithStateSnapshots | null>(null);
export const gameOver = writable(false);
export const gameOverReason = writable<string | null>(null);
export const maintenanceButtonDisabled = writable(false);
Expand Down
2 changes: 1 addition & 1 deletion mvp/server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async def http_exception_handler(_, exc):


@app.on_event("shutdown")
async def cleanup_sessions():
async def clear_all_sessions():
sessions.clear()


Expand Down
1 change: 1 addition & 0 deletions mvp/server/core/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Gameplay
DEFAULT_SESSION_ID = 'test'
GAME_TICK_INTERVAL = 0.5
IDLE_SESSION_TTL_SECONDS = 60 * 30 # 30 minutes
TIMESTEPS_PER_MOVE = 8 # we conceptualize every player move as a full working day, or "8 hours"
GAME_OVER_MESSAGE_MACHINE_BREAKDOWN = "Machine health has reached 0%"
GAME_OVER_MESSAGE_NO_MONEY = "Player ran out of money"
Expand Down
19 changes: 18 additions & 1 deletion mvp/server/core/game/GameSession.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import datetime
from typing import Callable

from pydantic import BaseModel
Expand All @@ -16,7 +17,9 @@ class GameSession(BaseModel):
is_game_over: bool = False
available_sensors: dict[str, bool] = None
available_predictions: dict[str, bool] = None

last_updated: datetime.datetime = None
# TODO: do something with these states. May be useful for prediction functionality.
machine_state_history: list[tuple[int, MachineState]] = []
# TODO: Update this function in-game, to simulate a change in the model (an "upgrade" for the player)
rul_predictor: Callable[[int], int | None] = default_rul_prediction_fn

Expand All @@ -31,8 +34,13 @@ def new_game_session(_id: str):
session.available_sensors = {sensor: False for sensor in session.machine_state.get_purchasable_sensors()}
session.available_predictions = {prediction: False for prediction in
session.machine_state.get_purchasable_predictions()}
session.last_updated = datetime.datetime.now()

return session

def is_abandoned(self):
return (datetime.datetime.now() - self.last_updated).total_seconds() > IDLE_SESSION_TTL_SECONDS

def check_if_game_over(self):
self.is_game_over = True

Expand Down Expand Up @@ -73,6 +81,15 @@ async def advance_one_turn(self) -> list[MachineState]:
collected_machine_states_during_turn
)

self.last_updated = datetime.datetime.now()

self.machine_state_history.extend(
zip(
range(self.current_step - TIMESTEPS_PER_MOVE, self.current_step),
collected_machine_states_during_turn
)
)

return collected_machine_states_during_turn

def do_maintenance(self) -> bool:
Expand Down
7 changes: 7 additions & 0 deletions mvp/server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ cachetools==5.3.2
certifi==2023.11.17
charset-normalizer==3.3.2
click==8.1.7
click-spinner==0.1.10
colorama==0.4.6
croniter==1.4.1
dnspython==2.4.2
email-validator==2.1.0.post1
execnet==2.0.2
fastapi==0.104.1
fastapi-utilities==0.2.0
gitdb==4.0.11
GitPython==3.1.40
greenlet==3.0.3
h11==0.14.0
httpcore==1.0.2
httptools==0.6.1
Expand Down Expand Up @@ -43,6 +48,7 @@ pydeck==0.8.1b0
Pygments==2.17.2
pytest==7.4.3
pytest-asyncio==0.23.2
pytest-xdist==3.5.0
python-dateutil==2.8.2
python-dotenv==1.0.0
python-multipart==0.0.6
Expand All @@ -55,6 +61,7 @@ rpds-py==0.13.2
six==1.16.0
smmap==5.0.1
sniffio==1.3.0
SQLAlchemy==2.0.27
starlette==0.27.0
streamlit==1.29.0
tenacity==8.2.3
Expand Down
Loading
Loading