mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
better game join logic, create dev and prod configs
This commit is contained in:
@@ -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
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user