diff --git a/.gitignore b/.gitignore index db32aed..5bab460 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ .pnp.* node_modules/ **/dist/ +.parcel-cache/ \ No newline at end of file diff --git a/common/package.json b/common/package.json index 69e357e..cd8a448 100644 --- a/common/package.json +++ b/common/package.json @@ -3,5 +3,9 @@ "packageManager": "yarn@4.3.1", "devDependencies": { "typescript": "^5.5.4" - } + }, + "scripts": { + "build": "tsc" + }, + "main": "dist/index.js" } diff --git a/common/src/index.ts b/common/src/index.ts index 09ce2ec..6f6bc59 100644 --- a/common/src/index.ts +++ b/common/src/index.ts @@ -1,3 +1,5 @@ export * from './board/Board.js'; export * from './board/Space.js'; -export * from './board/Property.js'; \ No newline at end of file +export * from './board/Property.js'; + +export * from './protocol/protocol.js'; \ No newline at end of file diff --git a/common/src/protocol/protocol.ts b/common/src/protocol/protocol.ts new file mode 100644 index 0000000..a74e0df --- /dev/null +++ b/common/src/protocol/protocol.ts @@ -0,0 +1,34 @@ +export enum ProtocolOperation { + // Bidirectional + KeepAlive = 'nop', + // Client -> Server + Login = 'login', + // Server -> Client + Init = 'init', +} + +export enum GameState { + Lobby = 'lobby', + InProgress = 'inprogress', + Ended = 'ended', +} + +export interface ProtocolMessage { + op: ProtocolOperation; +} + +export interface LoginMessage extends ProtocolMessage { + op: ProtocolOperation.Login; + username: string; +} + +export interface InitMessage extends ProtocolMessage { + op: ProtocolOperation.Init; + gameState: GameState; + username: string; + players: { + username: string; + money: number; + isBankrupt: boolean; + }; +} \ No newline at end of file diff --git a/package.json b/package.json index ed7ee2f..2e2ed7d 100644 --- a/package.json +++ b/package.json @@ -5,5 +5,12 @@ "server", "webapp", "common" - ] + ], + "scripts": { + "format": "prettier --write **/*.{ts,html,scss}", + "build": "yarn workspaces foreach -p -A run build" + }, + "devDependencies": { + "prettier": "^3.3.3" + } } diff --git a/server/package.json b/server/package.json index f2381af..5e16866 100644 --- a/server/package.json +++ b/server/package.json @@ -3,14 +3,17 @@ "packageManager": "yarn@4.3.1", "devDependencies": { "@types/node": "^22.1.0", + "@types/ws": "^8.5.12", "typescript": "^5.5.4" }, "dependencies": { + "@fastify/cors": "^9.0.1", "@fastify/websocket": "^10.0.1", "fastify": "^4.28.1", "pino": "^9.3.2" }, "scripts": { "build": "tsc" - } + }, + "type": "module" } diff --git a/server/src/game.ts b/server/src/game.ts new file mode 100644 index 0000000..8a8b9fb --- /dev/null +++ b/server/src/game.ts @@ -0,0 +1,45 @@ +import { Player } from "./game/player.js"; +import { GameState, LoginMessage, ProtocolMessage, ProtocolOperation } from "@ntptg/common"; + +export class Game { + private id: string; + private state: GameState; + private players: Player[]; + + constructor(id: string) { + this.id = id; + this.state = GameState.Lobby; + this.players = []; + } + + startGame() { + this.state = GameState.InProgress; + } + + returnToLobby() { + this.state = GameState.Lobby; + } + + addPlayer(player: Player) { + this.players.push(player); + player.on('message', msg => this.handleMessage(player, msg)); + } + + private handleMessage(player: Player, msg: ProtocolMessage) { + switch (msg.op) { + case ProtocolOperation.Login: { + let loginMsg = msg as LoginMessage; + if (!loginMsg.username) return; + + let username = loginMsg.username; + let i = 1; + while (this.players.some(p => p.getUsername() === username)) { + username = loginMsg.username + i++; + } + player.setUsername(username); + + break; + } + } + } +} \ No newline at end of file diff --git a/server/src/game/player.ts b/server/src/game/player.ts new file mode 100644 index 0000000..8e278c9 --- /dev/null +++ b/server/src/game/player.ts @@ -0,0 +1,30 @@ +import EventEmitter from 'events'; +import { WebSocket } from 'ws'; +import { ProtocolMessage } from '@ntptg/common'; + +export interface Player { + on(event: 'message', listener: (msg: ProtocolMessage) => void): this; +} + +export class Player extends EventEmitter { + private username: string | null; + private socket: WebSocket; + money: number; + isBankrupt: boolean; + + constructor(socket: WebSocket) { + super(); + this.username = null; + this.socket = socket; + this.money = 0; + this.isBankrupt = false; + } + + getUsername() { + return this.username; + } + + setUsername(username: string) { + this.username = username; + } +} \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 5809a19..9f0eed6 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,6 +1,69 @@ +import fastifyWebsocket from "@fastify/websocket"; +import fastify from "fastify"; import pino from "pino"; +import { Game } from "./game.js"; +import { randstr } from "./util.js"; +import fastifyCors from "@fastify/cors"; +import { Player } from "./game/player.js"; -let logger = pino({ +const logger = pino({ name: "NTPTG" }); +const games = new Map(); + +const app = fastify({ + logger: logger.child({ module: "fastify" }) +}); + +app.register(fastifyCors, { + origin: true, + methods: ["GET", "POST"] +}); + +app.register(fastifyWebsocket); +app.register(async app => { + app.route({ + method: 'GET', + url: '/api/game/:gameid', + websocket: true, + preValidation: async (req, res) => { + const { gameid } = req.params as { gameid: string }; + if (!games.has(gameid)) { + res.status(400).send({ error: "Game not found" }); + return; + } + }, + handler: async (req, res) => { + res.code(426); + return { error: "This endpoint only accepts WebSocket connections" }; + }, + wsHandler: async (socket, req) => { + const { gameid } = req.params as { gameid: string }; + const game = games.get(gameid)!; + game.addPlayer(new Player(socket)); + } + }); +}); + +app.post('/api/create', async (req, res) => { + if (req.headers['content-type'] !== 'application/json') { + res.status(400); + return { error: "Invalid Content-Type" }; + } + let gameid; + do { + gameid = await randstr(8); + } while (games.has(gameid)); + games.set(gameid, new Game(gameid)); + res.status(201); + return { id: gameid }; +}); + +let port = parseInt(process.env["NTPTG_PORT"]!); +if (isNaN(port)) port = 3000; + +app.listen({ + host: process.env["NTPTG_HOST"] || "127.0.0.1", + port +}); \ No newline at end of file diff --git a/server/src/util.ts b/server/src/util.ts new file mode 100644 index 0000000..660e38d --- /dev/null +++ b/server/src/util.ts @@ -0,0 +1,20 @@ +import * as crypto from 'crypto'; + +export function randint(min: number, max: number): Promise { + return new Promise((res, rej) => crypto.randomBytes(4, (err, buf) => { + if (err) { + rej(err); + return; + } + const random = buf.readUInt32BE(0) / 0x100000000; + res(Math.floor(min + random * (max - min))); + })); +} + +export async function randstr(length: number): Promise { + let buf = Buffer.alloc(length); + for (let i = 0; i < length; i++) { + buf.writeUInt8(await randint(65, 91), i); + } + return buf.toString('ascii'); +} diff --git a/tsconfig.json b/tsconfig.json index 3cf78f9..aa1dbf1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,7 +49,7 @@ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ diff --git a/yarn.lock b/yarn.lock index cf51edf..fc7161f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45,6 +45,16 @@ __metadata: languageName: node linkType: hard +"@fastify/cors@npm:^9.0.1": + version: 9.0.1 + resolution: "@fastify/cors@npm:9.0.1" + dependencies: + fastify-plugin: "npm:^4.0.0" + mnemonist: "npm:0.39.6" + checksum: 10c0/4db9d3d02edbca741c8ed053819bf3b235ecd70e07c640ed91ba0fc1ee2dc8abedbbffeb79ae1a38ccbf59832e414cad90a554ee44227d0811d5a2d062940611 + languageName: node + linkType: hard + "@fastify/error@npm:^3.3.0, @fastify/error@npm:^3.4.0": version: 3.4.1 resolution: "@fastify/error@npm:3.4.1" @@ -240,8 +250,10 @@ __metadata: version: 0.0.0-use.local resolution: "@ntptg/server@workspace:server" dependencies: + "@fastify/cors": "npm:^9.0.1" "@fastify/websocket": "npm:^10.0.1" "@types/node": "npm:^22.1.0" + "@types/ws": "npm:^8.5.12" fastify: "npm:^4.28.1" pino: "npm:^9.3.2" typescript: "npm:^5.5.4" @@ -1247,6 +1259,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:*": + version: 22.2.0 + resolution: "@types/node@npm:22.2.0" + dependencies: + undici-types: "npm:~6.13.0" + checksum: 10c0/c17900b34faecfec204f72970bd658d0c217aaf739c1bf7690c969465b6b26b77a8be1cd9ba735aadbd1dd20b5c3e4f406ec33528bf7c6eec90744886c5d5608 + languageName: node + linkType: hard + "@types/node@npm:^22.1.0": version: 22.1.0 resolution: "@types/node@npm:22.1.0" @@ -1256,6 +1277,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8.5.12": + version: 8.5.12 + resolution: "@types/ws@npm:8.5.12" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/3fd77c9e4e05c24ce42bfc7647f7506b08c40a40fe2aea236ef6d4e96fc7cb4006a81ed1b28ec9c457e177a74a72924f4768b7b4652680b42dfd52bc380e15f9 + languageName: node + linkType: hard + "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -2684,6 +2714,15 @@ __metadata: languageName: node linkType: hard +"mnemonist@npm:0.39.6": + version: 0.39.6 + resolution: "mnemonist@npm:0.39.6" + dependencies: + obliterator: "npm:^2.0.1" + checksum: 10c0/a538945ea547976136ee6e16f224c0a50983143619941f6c4d2c82159e36eb6f8ee93d69d3a1267038fc5b16f88e2d43390023de10dfb145fa15c5e2befa1cdf + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -2835,6 +2874,8 @@ __metadata: "ntptg@workspace:.": version: 0.0.0-use.local resolution: "ntptg@workspace:." + dependencies: + prettier: "npm:^3.3.3" languageName: unknown linkType: soft @@ -2845,6 +2886,13 @@ __metadata: languageName: node linkType: hard +"obliterator@npm:^2.0.1": + version: 2.0.4 + resolution: "obliterator@npm:2.0.4" + checksum: 10c0/ff2c10d4de7d62cd1d588b4d18dfc42f246c9e3a259f60d5716f7f88e5b3a3f79856b3207db96ec9a836a01d0958a21c15afa62a3f4e73a1e0b75f2c2f6bab40 + languageName: node + linkType: hard + "on-exit-leak-free@npm:^2.1.0": version: 2.1.2 resolution: "on-exit-leak-free@npm:2.1.2" @@ -3042,6 +3090,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^3.3.3": + version: 3.3.3 + resolution: "prettier@npm:3.3.3" + bin: + prettier: bin/prettier.cjs + checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26 + languageName: node + linkType: hard + "proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": version: 4.2.0 resolution: "proc-log@npm:4.2.0"