mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:30:43 +00:00
record game metadata to gcs
This commit is contained in:
@@ -209,12 +209,19 @@
|
||||
* log stack traces & display them on screen DONE 12/3/2024
|
||||
* add radiation from nuke DONE 12/4/2024
|
||||
* add cities DONE 12/4/2024
|
||||
* write multiplayer games to GCS DONE 12/6/2024
|
||||
* write single player games to GCS
|
||||
* record game winner
|
||||
* record game difficulty
|
||||
* standardize game ids
|
||||
* record commit hash of game
|
||||
* store metadata in BigQuery
|
||||
* replay stored games
|
||||
* max price for units
|
||||
* when player dies, don't remove atom bombs
|
||||
* record and replay games for debugging purposes
|
||||
* add bug report button in game
|
||||
* bugfix: destroyers can't find path to dst and freeze
|
||||
* record single player game stats
|
||||
* alert on attack
|
||||
* alert on unit captured or destroyed
|
||||
* stop requesting lobby when playing game
|
||||
|
||||
+19
-4
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { Difficulty, GameMap, PlayerType, UnitType } from './game/Game';
|
||||
import { Difficulty, GameMap, GameType, PlayerType, UnitType } from './game/Game';
|
||||
|
||||
export type GameID = string
|
||||
export type ClientID = string
|
||||
@@ -41,6 +41,8 @@ export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>
|
||||
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>
|
||||
export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema>
|
||||
|
||||
export type GameRecord = z.infer<typeof GameRecordSchema>
|
||||
|
||||
const PlayerTypeSchema = z.nativeEnum(PlayerType);
|
||||
|
||||
// TODO: create Cell schema
|
||||
@@ -53,7 +55,8 @@ export interface Lobby {
|
||||
|
||||
const GameConfigSchema = z.object({
|
||||
gameMap: z.nativeEnum(GameMap),
|
||||
difficulty: z.nativeEnum(Difficulty)
|
||||
difficulty: z.nativeEnum(Difficulty),
|
||||
gameType: z.nativeEnum(GameType)
|
||||
})
|
||||
|
||||
const EmojiSchema = z.string().refine(
|
||||
@@ -64,6 +67,8 @@ const EmojiSchema = z.string().refine(
|
||||
message: "Must contain at least one emoji character"
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Zod schemas
|
||||
const BaseIntentSchema = z.object({
|
||||
type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'troop_ratio', 'build_unit']),
|
||||
@@ -81,7 +86,6 @@ export const AttackIntentSchema = BaseIntentSchema.extend({
|
||||
targetY: z.number().nullable()
|
||||
});
|
||||
|
||||
|
||||
export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('spawn'),
|
||||
playerID: z.string(),
|
||||
@@ -223,4 +227,15 @@ export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
|
||||
lastTurn: z.number() // The last turn the client saw.
|
||||
})
|
||||
|
||||
export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientIntentMessageSchema, ClientJoinMessageSchema]);
|
||||
export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientIntentMessageSchema, ClientJoinMessageSchema]);
|
||||
|
||||
export const GameRecordSchema = z.object({
|
||||
id: z.string(),
|
||||
gameConfig: GameConfigSchema,
|
||||
startTimestampMS: z.number(),
|
||||
endTimestampMS: z.number(),
|
||||
durationSeconds: z.number(),
|
||||
date: z.string(),
|
||||
usernames: z.array(z.string()),
|
||||
turns: z.array(TurnSchema)
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import DOMPurify from 'dompurify';
|
||||
|
||||
import { Cell, Game, Player, TerraNullius, Tile, Unit } from "./game/Game";
|
||||
import { number } from 'zod';
|
||||
import { GameRecord } from './Schemas';
|
||||
|
||||
export function manhattanDist(c1: Cell, c2: Cell): number {
|
||||
return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y);
|
||||
@@ -222,6 +223,29 @@ export function onlyImages(html: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function ProcessGameRecord(record: GameRecord): GameRecord {
|
||||
const packed: GameRecord = structuredClone(record);
|
||||
packed.turns = []
|
||||
const usernames = new Set<string>()
|
||||
for (const turn of record.turns) {
|
||||
if (turn.intents.length != 0) {
|
||||
packed.turns.push(turn)
|
||||
for (const intent of turn.intents) {
|
||||
if (intent.type == 'spawn') {
|
||||
usernames.add(intent.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
packed.usernames = Array.from(usernames)
|
||||
packed.durationSeconds = Math.floor((record.endTimestampMS - record.startTimestampMS) / 1000)
|
||||
return packed;
|
||||
}
|
||||
|
||||
export function ToBigQuery(record: GameRecord) {
|
||||
|
||||
}
|
||||
|
||||
export function assertNever(x: never): never {
|
||||
throw new Error('Unexpected value: ' + x);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ export enum GameMap {
|
||||
Mena
|
||||
}
|
||||
|
||||
export enum GameType {
|
||||
Singleplayer,
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
export interface UnitInfo {
|
||||
cost: (player: Player) => Gold
|
||||
// Determines if its owner changes when its tile is conquered.
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ClientID, GameConfig, GameID } from "../core/Schemas";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Client } from "./Client";
|
||||
import { GamePhase, GameServer } from "./GameServer";
|
||||
import { Difficulty, GameMap } from "../core/game/Game";
|
||||
import { Difficulty, GameMap, GameType } from "../core/game/Game";
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,17 @@ export class GameManager {
|
||||
|
||||
createPrivateGame(): string {
|
||||
const id = genSmallGameID()
|
||||
this.games.push(new GameServer(id, Date.now(), false, this.config, { gameMap: GameMap.World, difficulty: Difficulty.Medium }))
|
||||
this.games.push(new GameServer(
|
||||
id,
|
||||
Date.now(),
|
||||
false,
|
||||
this.config,
|
||||
{
|
||||
gameMap: GameMap.World,
|
||||
gameType: GameType.Private,
|
||||
difficulty: Difficulty.Medium
|
||||
}
|
||||
))
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -68,7 +78,17 @@ export class GameManager {
|
||||
if (now > this.lastNewLobby + this.config.gameCreationRate()) {
|
||||
this.lastNewLobby = now
|
||||
const id = uuidv4()
|
||||
lobbies.push(new GameServer(id, now, true, this.config, { gameMap: GameMap.World, difficulty: Difficulty.Medium }))
|
||||
lobbies.push(new GameServer(
|
||||
id,
|
||||
now,
|
||||
true,
|
||||
this.config,
|
||||
{
|
||||
gameMap: GameMap.World,
|
||||
gameType: GameType.Public,
|
||||
difficulty: Difficulty.Medium
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
active.filter(g => !g.hasStarted() && g.isPublic).forEach(g => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ClientMessage, ClientMessageSchema, GameConfig, Intent, ServerStartGameMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
|
||||
import { ClientMessage, ClientMessageSchema, GameConfig, GameRecordSchema, Intent, ServerStartGameMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
|
||||
import { Config } from "../core/configuration/Config";
|
||||
import { Client } from "./Client";
|
||||
import WebSocket from 'ws';
|
||||
import { slog } from "./StructuredLog";
|
||||
import { Storage } from '@google-cloud/storage';
|
||||
import { ProcessGameRecord as ProcessRecord } from "../core/Util";
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
@@ -22,6 +23,7 @@ export class GameServer {
|
||||
private intents: Intent[] = []
|
||||
private clients: Client[] = []
|
||||
private _hasStarted = false
|
||||
private _startTime: number = null
|
||||
|
||||
private endTurnIntervalID
|
||||
|
||||
@@ -88,11 +90,13 @@ export class GameServer {
|
||||
}
|
||||
|
||||
public startTime(): number {
|
||||
return this.createdAt + this.config.lobbyLifetime()
|
||||
return this._startTime
|
||||
}
|
||||
|
||||
public start() {
|
||||
this._hasStarted = true
|
||||
this._startTime = Date.now()
|
||||
|
||||
this.clients.forEach(c => {
|
||||
console.log(`game ${this.id} sending start message to ${c.id}`)
|
||||
this.sendStartGameMsg(c.ws, 0)
|
||||
@@ -138,7 +142,7 @@ export class GameServer {
|
||||
// Close all WebSocket connections
|
||||
clearInterval(this.endTurnIntervalID);
|
||||
this.clients.forEach(client => {
|
||||
client.ws.removeAllListeners('message');
|
||||
client.ws.removeAllListeners('message'); // TODO: remove this?
|
||||
if (client.ws.readyState === WebSocket.OPEN) {
|
||||
client.ws.close(1000, "game has ended");
|
||||
}
|
||||
@@ -151,10 +155,14 @@ export class GameServer {
|
||||
const file = bucket.file(this.id);
|
||||
const game = {
|
||||
id: this.id,
|
||||
gameConfig: this.gameConfig,
|
||||
startTimestampMS: this._startTime,
|
||||
endTimestampMS: Date.now(),
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
turns: this.turns
|
||||
}
|
||||
await file.save(JSON.stringify(game), {
|
||||
const processed = ProcessRecord(game)
|
||||
await file.save(JSON.stringify(GameRecordSchema.parse(processed)), {
|
||||
contentType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user