add some http and websocket protocol shit
This commit is contained in:
parent
3341770b6e
commit
c96d18dd70
12 changed files with 272 additions and 6 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,3 +13,4 @@
|
|||
.pnp.*
|
||||
node_modules/
|
||||
**/dist/
|
||||
.parcel-cache/
|
|
@ -3,5 +3,9 @@
|
|||
"packageManager": "yarn@4.3.1",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"main": "dist/index.js"
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export * from './board/Board.js';
|
||||
export * from './board/Space.js';
|
||||
export * from './board/Property.js';
|
||||
|
||||
export * from './protocol/protocol.js';
|
34
common/src/protocol/protocol.ts
Normal file
34
common/src/protocol/protocol.ts
Normal file
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
45
server/src/game.ts
Normal file
45
server/src/game.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
server/src/game/player.ts
Normal file
30
server/src/game/player.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<string, Game>();
|
||||
|
||||
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
|
||||
});
|
20
server/src/util.ts
Normal file
20
server/src/util.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import * as crypto from 'crypto';
|
||||
|
||||
export function randint(min: number, max: number): Promise<number> {
|
||||
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<string> {
|
||||
let buf = Buffer.alloc(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
buf.writeUInt8(await randint(65, 91), i);
|
||||
}
|
||||
return buf.toString('ascii');
|
||||
}
|
|
@ -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. */
|
||||
|
|
57
yarn.lock
57
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"
|
||||
|
|
Loading…
Reference in a new issue