better game join logic, create dev and prod configs

This commit is contained in:
evanpelle
2024-08-17 12:42:18 -07:00
parent ed4201ab8c
commit 0ea670d975
13 changed files with 92 additions and 44 deletions
+7 -4
View File
@@ -19,14 +19,16 @@
* fix server resource leak DONE 8/14/2024
* fix bug where game stops after 10s (websocket disconnection) DONE 8/16/2024
* balance attacks/expansions better DONE 8/16/2024
* delete players when territories too small DONE 8/16/2024
* double attack add troops DONE 8/16/2024
* Have some time for spawning before game starts DONE 8/16/2024
* only send delta turns on connect/reconnect DONE 8/17/2024
* Create separate game config dev vs prod
* improve front page, only one game at a time every 30s
* fix desync
* fix server memory leak
* BUG: boats not going to destination, coast not being recognized
* BUG: boats freeze game on path calculation
* double attack add troops
* Create separate game config dev vs prod
* Have some time for spawning before game starts
* delete players when territories too small
* better algorithm for name render placement
* make boats larger
* have boats not get close to shore
@@ -36,3 +38,4 @@
* fix enemy islands when attacking
* BUG: ocean is considered TerraNullius
* on websocket connect server only send missing turns not all turns
* BUG: fix hotreload (priority queue breaks it)
+12 -10
View File
@@ -1,7 +1,9 @@
import {getConfig} from "../core/configuration/Config";
import {defaultConfig} from "../core/configuration/DefaultConfig";
import {devConfig} from "../core/configuration/DevConfig";
import {TerrainMap} from "../core/Game";
import {PseudoRandom} from "../core/PseudoRandom";
import {GameID, ServerMessage, ServerMessageSchema} from "../core/Schemas";
import {GameID, Lobby, ServerMessage, ServerMessageSchema} from "../core/Schemas";
import {loadTerrainMap} from "../core/TerrainMapLoader";
import {ClientGame, createClientGame} from "./ClientGame";
import {v4 as uuidv4} from 'uuid';
@@ -36,21 +38,21 @@ class Client {
private async fetchAndUpdateLobbies(): Promise<void> {
try {
const data = await this.fetchLobbies();
this.updateLobbiesDisplay(data.lobbies);
const lobbies = await this.fetchLobbies();
this.updateLobbiesDisplay(lobbies);
} catch (error) {
console.error('Error fetching and updating lobbies:', error);
}
}
private updateLobbiesDisplay(lobbies: GameID[]): void {
private updateLobbiesDisplay(lobbies: Lobby[]): void {
if (!this.lobbiesContainer) return;
this.lobbiesContainer.innerHTML = ''; // Clear existing lobbies
lobbies.forEach(lobby => {
const button = document.createElement('button');
button.textContent = `Join Lobby ${lobby}`;
button.textContent = `Join Lobby ${lobby.id} (${Math.floor((lobby.startTime - Date.now()) / 1000)}s)`;
button.onclick = () => this.joinLobby(lobby);
this.lobbiesContainer.appendChild(button);
});
@@ -63,7 +65,7 @@ class Client {
// }
}
async fetchLobbies() {
async fetchLobbies(): Promise<Lobby[]> {
const url = '/lobbies';
try {
const response = await fetch(url);
@@ -71,22 +73,22 @@ class Client {
throw new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`);
}
const data = await response.json();
return data;
return data.lobbies;
} catch (error) {
console.error('Error fetching lobbies:', error);
throw error;
}
}
private async joinLobby(lobbyID: string) {
private async joinLobby(lobby: Lobby) {
clearInterval(this.lobbiesInterval)
this.lobbiesContainer.innerHTML = 'Joining'; // Clear existing lobbies
this.lobbiesContainer.innerHTML = `Joining: ${lobby.id}`; // Clear existing lobbies
this.terrainMap.then((map) => {
if (this.game != null) {
return
}
// TODO make id more random, if two player join same millisecond get same id.
this.game = createClientGame(getUsername(), new PseudoRandom(Date.now()).nextID(), lobbyID, defaultConfig, map)
this.game = createClientGame(getUsername(), new PseudoRandom(Date.now()).nextID(), lobby.id, getConfig(), map)
this.game.join()
})
}
+11 -2
View File
@@ -65,7 +65,8 @@ export class ClientGame {
ClientJoinMessageSchema.parse({
type: "join",
gameID: this.gameID,
clientID: this.id
clientID: this.id,
lastTurn: this.turns.length
})
)
)
@@ -74,7 +75,12 @@ export class ClientGame {
const message: ServerMessage = ServerMessageSchema.parse(JSON.parse(event.data))
if (message.type == "start") {
console.log("starting game!")
this.turns = message.turns
for (const turn of message.turns) {
if (turn.turnNumber < this.turns.length) {
continue
}
this.turns.push(turn)
}
if (!this.isActive) {
this.start()
}
@@ -148,6 +154,9 @@ export class ClientGame {
}
private inputEvent(event: MouseDownEvent) {
if (this.turns.length < this.config.turnsUntilGameStart()) {
return
}
if (!this.isActive) {
return
}
+1 -4
View File
@@ -1,10 +1,7 @@
import {Colord} from "colord";
import {Cell, MutableGame, Game, PlayerEvent, Tile, TileEvent, Player, Execution, BoatEvent} from "../../core/Game";
import {Cell, Game, PlayerEvent, Tile, TileEvent, Player, Execution, BoatEvent} from "../../core/Game";
import {Theme} from "../../core/configuration/Config";
import {DragEvent, ZoomEvent} from "../InputHandler";
import {calculateBoundingBox, placeName} from "../NameBoxCalculator";
import {PseudoRandom} from "../../core/PseudoRandom";
import {BoatAttackExecution} from "../../core/execution/BoatAttackExecution";
import {NameRenderer} from "./NameRenderer";
+2 -2
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warfront</title>
<title>OpenFront</title>
<style>
body {
font-family: Arial, sans-serif;
@@ -67,7 +67,7 @@
</head>
<body>
<h1>Warfront</h1>
<h1>OpenFront (ALPHA)</h1>
<div id="username-container">
<input type="text" id="username" placeholder="Enter your username">
</div>
+7 -2
View File
@@ -21,7 +21,10 @@ export type ServerStartGameMessage = z.infer<typeof ServerStartGameMessageSchema
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>
export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema>
export interface Lobby {
id: string;
startTime: number;
}
// Zod schemas
const BaseIntentSchema = z.object({
@@ -101,7 +104,9 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({
export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
type: z.literal('join'),
clientID: z.string(),
gameID: z.string()
gameID: z.string(),
// The last turn the client saw.
lastTurn: z.number()
})
export const ClientMessageSchema = z.union([ClientIntentMessageSchema, ClientJoinMessageSchema]);
+11
View File
@@ -1,6 +1,17 @@
import {Player, PlayerID, PlayerInfo, TerrainType, TerrainTypes, TerraNullius, Tile} from "../Game";
import {Colord, colord} from "colord";
import {devConfig} from "./DevConfig";
import {defaultConfig} from "./DefaultConfig";
export function getConfig(): Config {
if (process.env.GAME_ENV == 'prod') {
console.log('Using prod config')
return defaultConfig
} else {
console.log('Using dev config')
return devConfig
}
}
export interface Config {
theme(): Theme;
+9 -5
View File
@@ -3,9 +3,11 @@ import {within} from "../Util";
import {Config, PlayerConfig, Theme} from "./Config";
import {pastelTheme} from "./PastelTheme";
export const defaultConfig = new class implements Config {
export class DefaultConfig implements Config {
turnsUntilGameStart(): number {
return 50
return 25
}
numBots(): number {
return 500
@@ -17,10 +19,10 @@ export const defaultConfig = new class implements Config {
return 100
}
gameCreationRate(): number {
return 2 * 1000
return 31.5 * 1000
}
lobbyLifetime(): number {
return 10 * 1000
return 30 * 1000
}
theme(): Theme {return pastelTheme;}
}
@@ -75,4 +77,6 @@ export const defaultPlayerConfig = new class implements PlayerConfig {
return Math.min(player.troops() + toAdd, max)
}
}
}
export const defaultConfig = new DefaultConfig()
+10
View File
@@ -0,0 +1,10 @@
import {DefaultConfig} from "./DefaultConfig";
export const devConfig = new class extends DefaultConfig {
gameCreationRate(): number {
return 2 * 1000
}
lobbyLifetime(): number {
return 10 * 1000
}
}
+2 -2
View File
@@ -20,13 +20,13 @@ export class GameManager {
return this.games.filter(g => g.phase() == phase)
}
addClient(client: Client, gameID: GameID) {
addClient(client: Client, gameID: GameID, lastTurn: number) {
const game = this.games.find(g => g.id == gameID)
if (!game) {
console.log(`game id ${gameID} not found`)
return
}
game.addClient(client)
game.addClient(client, lastTurn)
}
tick() {
+13 -10
View File
@@ -25,10 +25,10 @@ export class GameServer {
constructor(
public readonly id: string,
public readonly createdAt: number,
private settings: Config,
private config: Config,
) { }
public addClient(client: Client) {
public addClient(client: Client, lastTurn: number) {
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)
@@ -46,29 +46,32 @@ export class GameServer {
// In case a client joined the game late and missed the start message.
if (this._hasStarted) {
this.sendStartGameMsg(client.ws)
this.sendStartGameMsg(client.ws, lastTurn)
}
}
public startTime(): number {
return this.createdAt + this.config.lobbyLifetime()
}
public start() {
this._hasStarted = true
this.clients.forEach(c => {
console.log(`game ${this.id} sending start message to ${c.id}`)
this.sendStartGameMsg(c.ws)
this.sendStartGameMsg(c.ws, 0)
})
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.settings.turnIntervalMs());
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs());
}
private addIntent(intent: Intent) {
this.intents.push(intent)
}
private sendStartGameMsg(ws: WebSocket) {
private sendStartGameMsg(ws: WebSocket, lastTurn: number) {
ws.send(JSON.stringify(ServerStartGameMessageSchema.parse(
{
type: "start",
// TODO: this could get large
turns: this.turns
turns: this.turns.slice(lastTurn)
}
)))
}
@@ -106,10 +109,10 @@ export class GameServer {
}
phase(): GamePhase {
if (Date.now() - this.createdAt < this.settings.lobbyLifetime()) {
if (Date.now() - this.createdAt < this.config.lobbyLifetime()) {
return GamePhase.Lobby
}
if (Date.now() - this.createdAt < this.settings.lobbyLifetime() + this.gameDuration) {
if (Date.now() - this.createdAt < this.config.lobbyLifetime() + this.gameDuration) {
return GamePhase.Active
}
return GamePhase.Finished
+4 -3
View File
@@ -8,6 +8,7 @@ import {Client} from './Client';
import {ClientMessage, ClientMessageSchema} from '../core/Schemas';
import {defaultConfig} from '../core/configuration/DefaultConfig';
import {GamePhase} from './GameServer';
import {getConfig} from '../core/configuration/Config';
@@ -23,12 +24,12 @@ const wss = new WebSocketServer({server});
app.use(express.static(path.join(__dirname, '../../out')));
app.use(express.json())
const gm = new GameManager(defaultConfig)
const gm = new GameManager(getConfig())
// New GET endpoint to list lobbies
app.get('/lobbies', (req, res) => {
res.json({
lobbies: gm.gamesByPhase(GamePhase.Lobby).map(g => g.id),
lobbies: gm.gamesByPhase(GamePhase.Lobby).map(g => ({id: g.id, startTime: g.startTime()})),
});
});
@@ -39,7 +40,7 @@ wss.on('connection', (ws) => {
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
if (clientMsg.type == "join") {
console.log('got join request')
gm.addClient(new Client(clientMsg.clientID, ws), clientMsg.gameID)
gm.addClient(new Client(clientMsg.clientID, ws), clientMsg.gameID, clientMsg.lastTurn)
}
// TODO: send error message
})
+3
View File
@@ -42,6 +42,9 @@ export default (env, argv) => {
new webpack.DefinePlugin({
'process.env.WEBSOCKET_URL': JSON.stringify(isProduction ? '' : 'localhost:3000')
}),
new webpack.DefinePlugin({
'process.env.GAME_ENV': JSON.stringify(isProduction ? 'prod' : 'dev')
}),
],
devServer: isProduction ? {} : {
static: {