websocket reconnect on failure

This commit is contained in:
evanpelle
2024-08-16 11:06:01 -07:00
parent d062d1ac79
commit 332c2cfcac
13 changed files with 159 additions and 200 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import {ClientID} from "../core/Game";
import WebSocket from 'ws';
import {ClientID} from '../core/Schemas';
export class Client {
+31 -51
View File
@@ -1,73 +1,53 @@
import {GameID, LobbyID} from "../core/Game";
import {Client} from "./Client";
import {Lobby} from "./Lobby";
import {GameServer} from "./GameServer";
import {GamePhase, GameServer} from "./GameServer";
import {Config} from "../core/configuration/Config";
import {defaultConfig} from "../core/configuration/DefaultConfig";
import {PseudoRandom} from "../core/PseudoRandom";
import WebSocket from 'ws';
import {ClientID, GameID} from "../core/Schemas";
import {Client} from "./Client";
export class GameManager {
private lastNewLobby: number = 0
private _lobbies: Map<LobbyID, Lobby> = new Map()
private games: Map<GameID, GameServer> = new Map()
private games: GameServer[] = []
private random = new PseudoRandom(123)
constructor(private settings: Config) { }
constructor(private config: Config) { }
public hasLobby(lobbyID: LobbyID): boolean {
return this._lobbies.has(lobbyID)
gamesByPhase(phase: GamePhase): GameServer[] {
return this.games.filter(g => g.phase() == phase)
}
public addClientToLobby(client: Client, lobbyID: LobbyID) {
this._lobbies.get(lobbyID).addClient(client)
}
addLobby(lobby: Lobby) {
this._lobbies.set(lobby.id, lobby)
}
lobby(id: LobbyID): Lobby {
return this._lobbies.get(id)
}
lobbies(): Lobby[] {
return Array.from(this._lobbies.values())
}
addGame(game: GameServer) {
this.games.set(game.id, game)
addClient(client: Client, gameID: GameID) {
const game = this.games.find(g => g.id == gameID)
if (!game) {
console.log(`game id ${gameID} not found`)
return
}
game.addClient(client)
}
tick() {
const lobbies = this.gamesByPhase(GamePhase.Lobby)
const active = this.gamesByPhase(GamePhase.Active)
const finished = this.gamesByPhase(GamePhase.Finished)
const now = Date.now()
const active = this.lobbies().filter(l => !l.isExpired(now - 2000))
const expired = this.lobbies().filter(l => l.isExpired(now - 2000))
this._lobbies = new Map(active.map(lobby => [lobby.id, lobby]));
expired.forEach(lobby => {
const game = new GameServer(lobby.id, now, lobby.clients, this.settings)
this.games.set(game.id, game)
game.start()
})
if (now > this.lastNewLobby + this.settings.lobbyCreationRate()) {
if (now > this.lastNewLobby + this.config.gameCreationRate()) {
this.lastNewLobby = now
this.addLobby(new Lobby(this.random.nextID(), this.settings.lobbyLifetime()))
const id = this.random.nextID()
console.log(`creating game ${id}`)
lobbies.push(new GameServer(id, now, this.config))
}
const activeGames: Map<GameID, GameServer> = new Map()
for (const [id, game] of this.games) {
if (game.isActive()) {
activeGames.set(id, game)
} else {
game.endGame()
}
}
//this.games = activeGames
active.filter(g => !g.hasStarted()).forEach(g => {
g.start()
})
finished.forEach(g => {
g.endGame()
})
this.games = [...lobbies, ...active]
}
}
+52 -27
View File
@@ -1,50 +1,64 @@
import {EventBus} from "../core/EventBus";
import {ClientID, GameID} from "../core/Game";
import {ClientMessage, ClientMessageSchema, Intent, ServerStartGameMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn} from "../core/Schemas";
import {Config} from "../core/configuration/Config";
import {Ticker, TickEvent} from "../core/Ticker";
import {Client} from "./Client";
export enum GamePhase {
Lobby = 'LOBBY',
Active = 'ACTIVE',
Finished = 'FINISHED'
}
export class GameServer {
private gameDuration = 5 * 60 * 1000 // TODO!!! fix this
private turns: Turn[] = []
private intents: Intent[] = []
private lastUpdate = 0;
private clients: Client[] = []
private _hasStarted = false
constructor(
public readonly id: GameID,
private startTime: number,
private clients: Map<ClientID, Client>,
public readonly id: string,
public readonly createdAt: number,
private settings: Config,
) {
this.lastUpdate = Date.now()
) { }
public addClient(client: Client) {
console.log(`game ${this.id} adding client ${client.id}`)
// Remove stale client if this is a reconnect
this.clients = this.clients.filter(c => c.id != client.id)
this.clients.push(client)
client.ws.on('message', (message: string) => {
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
if (clientMsg.type == "intent") {
if (clientMsg.gameID == this.id) {
this.addIntent(clientMsg.intent)
} else {
console.warn(`client ${clientMsg.clientID} sent to wrong game`)
}
}
})
}
public start() {
this.clients.forEach(c => {
c.ws.on('message', (message: string) => {
this.lastUpdate = Date.now()
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
if (clientMsg.type == "intent") {
if (clientMsg.gameID == this.id) {
this.addIntent(clientMsg.intent)
} else {
console.warn(`client ${clientMsg.clientID} sent to wrong game`)
}
}
})
})
this._hasStarted = true
const startGame = JSON.stringify(ServerStartGameMessageSchema.parse(
{
type: "start"
}
))
this.clients.forEach(c => {
console.log(`game ${this.id} sending start message to ${c.id}`)
c.ws.send(startGame)
})
setInterval(() => this.endTurn(), this.settings.turnIntervalMs());
// setInterval(() => {
// this.clients.forEach(c => {
// c.ws.close(1011, 'Intentional error for testing');
// })
// }, 1000)
}
private addIntent(intent: Intent) {
@@ -69,10 +83,7 @@ export class GameServer {
this.clients.forEach(c => {
c.ws.send(msg)
})
}
public isActive(): boolean {
return Date.now() - this.lastUpdate < 1000 * 60 * 5 // 5 minutes
}
endGame() {
@@ -85,4 +96,18 @@ export class GameServer {
});
}
phase(): GamePhase {
if (Date.now() - this.createdAt < this.settings.lobbyLifetime()) {
return GamePhase.Lobby
}
if (Date.now() - this.createdAt < this.settings.lobbyLifetime() + this.gameDuration) {
return GamePhase.Active
}
return GamePhase.Finished
}
hasStarted(): boolean {
return this._hasStarted
}
}
-21
View File
@@ -1,21 +0,0 @@
import {ClientID} from "../core/Game";
import {Client} from "./Client";
export class Lobby {
public clients: Map<ClientID, Client> = new Map()
private startGameTs: number
constructor(public readonly id: string, durationMs: number) {
this.startGameTs = Date.now() + durationMs
}
public addClient(client: Client) {
this.clients.set(client.id, client)
}
public isExpired(now: number): boolean {
return now > this.startGameTs
}
}
+3 -12
View File
@@ -6,8 +6,8 @@ import {fileURLToPath} from 'url';
import {GameManager} from './GameManager';
import {Client} from './Client';
import {ClientMessage, ClientMessageSchema} from '../core/Schemas';
import {Lobby} from './Lobby';
import {defaultConfig} from '../core/configuration/DefaultConfig';
import {GamePhase} from './GameServer';
@@ -27,12 +27,8 @@ const gm = new GameManager(defaultConfig)
// New GET endpoint to list lobbies
app.get('/lobbies', (req, res) => {
const lobbyList = Array.from(gm.lobbies()).filter(l => !l.isExpired(Date.now())).map(lobby => ({
id: lobby.id,
}));
res.json({
lobbies: lobbyList,
lobbies: gm.gamesByPhase(GamePhase.Lobby).map(g => g.id),
});
});
@@ -43,12 +39,7 @@ wss.on('connection', (ws) => {
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
if (clientMsg.type == "join") {
console.log('got join request')
if (gm.hasLobby(clientMsg.lobbyID)) {
console.log('client joining lobby')
gm.addClientToLobby(new Client(clientMsg.clientID, ws), clientMsg.lobbyID)
} else {
console.log('lobby not found')
}
gm.addClient(new Client(clientMsg.clientID, ws), clientMsg.gameID)
}
// TODO: send error message
})