First Commit

This commit is contained in:
evanpelle
2024-08-04 19:51:23 -07:00
commit 05f55c490f
53 changed files with 15862 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
import {ClientID} from "../core/Game";
import WebSocket from 'ws';
export class Client {
constructor(public readonly id: ClientID, public readonly ws: WebSocket) { }
}
+66
View File
@@ -0,0 +1,66 @@
import {GameID, LobbyID} from "../core/Game";
import {Client} from "./Client";
import {Lobby} from "./Lobby";
import {GameServer} from "./GameServer";
import {defaultSettings, Settings} from "../core/Settings";
import {generateUniqueID} from "../core/Util";
export class GameManager {
private lastNewLobby: number = 0
private _lobbies: Map<LobbyID, Lobby> = new Map()
private games: Map<GameID, GameServer> = new Map()
constructor(private settings: Settings) { }
public hasLobby(lobbyID: LobbyID): boolean {
return this._lobbies.has(lobbyID)
}
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)
}
startGame(lobby: Lobby) {
const gs = new GameServer(generateUniqueID(), lobby.clients, defaultSettings)
this.games.set(gs.id, gs)
gs.start()
}
tick() {
const now = Date.now()
const active = this.lobbies().filter(l => !l.isExpired(now))
const expired = this.lobbies().filter(l => l.isExpired(now))
this._lobbies = new Map(active.map(lobby => [lobby.id, lobby]));
expired.forEach(lobby => {
const game = new GameServer(generateUniqueID(), lobby.clients, this.settings)
this.games.set(game.id, game)
game.start()
})
if (now > this.lastNewLobby + this.settings.lobbyCreationRate()) {
this.lastNewLobby = now
this.addLobby(new Lobby(generateUniqueID(), this.settings.lobbyLifetime()))
}
}
}
+69
View File
@@ -0,0 +1,69 @@
import {EventBus} from "../core/EventBus";
import {ClientID, GameID} from "../core/Game";
import {ClientMessage, ClientMessageSchema, Intent, ServerStartGameMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn} from "../core/Schemas";
import {Settings} from "../core/Settings";
import {Ticker, TickEvent} from "../core/Ticker";
import {Client} from "./Client";
export class GameServer {
private turns: Turn[] = []
private intents: Intent[] = []
constructor(
public readonly id: GameID,
private clients: Map<ClientID, Client>,
private settings: Settings,
) {
}
public start() {
this.clients.forEach(c => {
c.ws.on('message', (message: string) => {
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
if (clientMsg.type == "intent") {
this.addIntent(clientMsg.intent)
}
})
})
const startGame = JSON.stringify(ServerStartGameMessageSchema.parse(
{
type: "start"
}
))
this.clients.forEach(c => {
c.ws.send(startGame)
})
setInterval(() => this.endTurn(), this.settings.turnIntervalMs());
}
private addIntent(intent: Intent) {
this.intents.push(intent)
}
private endTurn() {
const pastTurn: Turn = {
turnNumber: this.turns.length,
intents: this.intents
}
this.turns.push(pastTurn)
this.intents = []
const msg = JSON.stringify(ServerTurnMessageSchema.parse(
{
type: "turn",
turn: pastTurn
}
))
this.clients.forEach(c => {
c.ws.send(msg)
})
}
private tick(event: TickEvent) {
}
}
+21
View File
@@ -0,0 +1,21 @@
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
}
}
+70
View File
@@ -0,0 +1,70 @@
import express, {json} from 'express';
import http from 'http';
import {WebSocketServer} from 'ws';
import path from 'path';
import {fileURLToPath} from 'url';
import {GameManager} from './GameManager';
import {Client} from './Client';
import {ClientMessage, ClientMessageSchema} from '../core/Schemas';
import {Lobby} from './Lobby';
import {defaultSettings} from '../core/Settings';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({server});
// Serve static files from the 'out' directory
app.use(express.static(path.join(__dirname, '../../out')));
app.use(express.json())
const gm = new GameManager(defaultSettings)
// New GET endpoint to list lobbies
app.get('/lobbies', (req, res) => {
const lobbyList = Array.from(gm.lobbies()).map(lobby => ({
id: lobby.id,
}));
res.json({
lobbies: lobbyList,
});
});
wss.on('connection', (ws) => {
ws.on('message', (message: string) => {
console.log(`got message ${message}`)
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
if (clientMsg.type == "join") {
if (gm.hasLobby(clientMsg.lobbyID)) {
gm.addClientToLobby(new Client(clientMsg.clientID, ws), clientMsg.lobbyID)
}
}
// TODO: send error message
})
});
function runGame() {
setInterval(() => tick(), 1000);
}
function tick() {
gm.tick()
}
const PORT = process.env.PORT || 3000;
console.log(`Server will try to run on http://localhost:${PORT}`);
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
runGame()