commit
c2bb66fa88
119
src/game/GameManager.ts
Normal file
119
src/game/GameManager.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { CommonMancalaGame, MancalaGame } from "mancala.js";
|
||||||
|
import { RTMT } from "../rtmt/rtmt";
|
||||||
|
import { GameStore } from "./gamestore/GameStore";
|
||||||
|
import { generateKey } from "../util/key_factory";
|
||||||
|
import {
|
||||||
|
channel_game_move,
|
||||||
|
channel_leave_game,
|
||||||
|
channel_new_game,
|
||||||
|
channel_on_game_crashed,
|
||||||
|
channel_on_game_start,
|
||||||
|
channel_on_game_update,
|
||||||
|
channel_on_game_user_leave
|
||||||
|
} from "../consts/channel_names";
|
||||||
|
import { GameMove } from "../models/GameMove";
|
||||||
|
import { GameCrashManager } from "./GameCrashManager";
|
||||||
|
import { MatchMaker } from "../matchmaker/MatchMaker";
|
||||||
|
|
||||||
|
export class GameManager {
|
||||||
|
gameStore: GameStore;
|
||||||
|
rtmt: RTMT;
|
||||||
|
matchMaker: MatchMaker;
|
||||||
|
|
||||||
|
constructor(params: { gameStore: GameStore, rtmt: RTMT, matchMaker: MatchMaker }) {
|
||||||
|
this.gameStore = params.gameStore;
|
||||||
|
this.rtmt = params.rtmt;
|
||||||
|
this.matchMaker = params.matchMaker;
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize() {
|
||||||
|
this.listenOnPlayersPaired();
|
||||||
|
this.listenRtmtMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private listenRtmtMessages() {
|
||||||
|
this.rtmt.listenMessage(channel_new_game, (userKey: string, message: Object) => {
|
||||||
|
this.matchMaker.join(userKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rtmt.listenMessage(channel_game_move, (userKey: string, message: Object) => {
|
||||||
|
this.onGameMove(userKey, message as GameMove);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rtmt.listenMessage(channel_leave_game, (userKey: string, message: Object) => {
|
||||||
|
this.onPlayerLeave(userKey)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onGameMove(userKey: string, gameMove: GameMove) {
|
||||||
|
const game = this.gameStore.get(userKey);
|
||||||
|
if (game) {
|
||||||
|
try {
|
||||||
|
game.moveByPlayerPit(userKey, gameMove.index);
|
||||||
|
this.rtmt.sendMessage(game.player1Id, channel_on_game_update, game);
|
||||||
|
this.rtmt.sendMessage(game.player2Id, channel_on_game_update, game);
|
||||||
|
if (game.state == "ended") {
|
||||||
|
this.deleteGame(game);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
this.onGameError(game, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Game not found!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onGameError(game: MancalaGame, error: any) {
|
||||||
|
console.error(error);
|
||||||
|
const crashFileName = GameCrashManager.logGameCrash(error, game);
|
||||||
|
console.info(`Game crash saved to file : ${crashFileName}`);
|
||||||
|
this.rtmt.sendMessage(game.player1Id, channel_on_game_crashed, error);
|
||||||
|
this.rtmt.sendMessage(game.player2Id, channel_on_game_crashed, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPlayerLeave(userKey: string) {
|
||||||
|
const game = this.gameStore.get(userKey);
|
||||||
|
if (game) {
|
||||||
|
this.deleteGame(game);
|
||||||
|
this.rtmt.sendMessage(game.player1Id, channel_on_game_user_leave, userKey);
|
||||||
|
this.rtmt.sendMessage(game.player2Id, channel_on_game_user_leave, userKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private listenOnPlayersPaired() {
|
||||||
|
this.matchMaker.listenOnPlayersPaired((player1Id: string, player2Id: string) => {
|
||||||
|
const game = this.createMancalaGame(player1Id, player2Id);
|
||||||
|
this.startGame(game);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public fireOnPlayerConnected(playerId: string) {
|
||||||
|
const game = this.gameStore.get(playerId);
|
||||||
|
if (game) {
|
||||||
|
this.rtmt.sendMessage(playerId, channel_on_game_update, game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public createMancalaGame(userKey1: string, userKey2: string) {
|
||||||
|
const game = new CommonMancalaGame(generateKey(), userKey1, userKey2);
|
||||||
|
const random = Math.random();
|
||||||
|
game.turnPlayerId = random > 0.5 ? userKey1 : userKey2;
|
||||||
|
|
||||||
|
this.gameStore.set(userKey1, game);
|
||||||
|
this.gameStore.set(userKey2, game);
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public startGame(game: MancalaGame) {
|
||||||
|
this.rtmt.sendMessage(game.player1Id, channel_on_game_start, game);
|
||||||
|
this.rtmt.sendMessage(game.player2Id, channel_on_game_start, game);
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteGame(game: MancalaGame) {
|
||||||
|
if (game) {
|
||||||
|
this.gameStore.remove(game.player1Id);
|
||||||
|
this.gameStore.remove(game.player2Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/game/gamestore/GameStore.ts
Normal file
7
src/game/gamestore/GameStore.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { MancalaGame } from "mancala.js";
|
||||||
|
|
||||||
|
export interface GameStore {
|
||||||
|
get(id: string): MancalaGame | undefined;
|
||||||
|
set(id: string, game: MancalaGame): void;
|
||||||
|
remove(id: string): void;
|
||||||
|
}
|
||||||
16
src/game/gamestore/GameStoreImpl.ts
Normal file
16
src/game/gamestore/GameStoreImpl.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { MancalaGame } from "mancala.js";
|
||||||
|
import { GameStore } from "./GameStore";
|
||||||
|
|
||||||
|
export class GameStoreImpl implements GameStore {
|
||||||
|
gameStore: Map<string, MancalaGame> = new Map<string, MancalaGame>()
|
||||||
|
|
||||||
|
get(id: string): MancalaGame | undefined {
|
||||||
|
return this.gameStore.get(id);
|
||||||
|
}
|
||||||
|
set(id: string, game: MancalaGame): void {
|
||||||
|
this.gameStore.set(id, game);
|
||||||
|
}
|
||||||
|
remove(id: string): void {
|
||||||
|
this.gameStore.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/index.ts
128
src/index.ts
@ -1,121 +1,17 @@
|
|||||||
import express, { Request, Response } from "express";
|
|
||||||
import * as http from "http";
|
|
||||||
import { RTMTWS } from "./rtmt/rtmt_websocket";
|
import { RTMTWS } from "./rtmt/rtmt_websocket";
|
||||||
import cors from "cors";
|
import { GameManager } from "./game/GameManager";
|
||||||
import { generateKey } from "./key_factory";
|
import { GameStoreImpl } from "./game/gamestore/GameStoreImpl";
|
||||||
import { MatchMaker } from "./matcmaker";
|
import { MatchMakerImpl } from "./matchmaker/MatchMakerImpl";
|
||||||
import { CommonMancalaGame, MancalaGame } from "mancala.js";
|
import { ExpressApp } from "./server/ExpressApp";
|
||||||
import fs from "fs";
|
import { WebServer } from "./server/WebServer";
|
||||||
import {
|
|
||||||
channel_game_move,
|
|
||||||
channel_leave_game,
|
|
||||||
channel_new_game,
|
|
||||||
channel_on_game_crashed,
|
|
||||||
channel_on_game_start,
|
|
||||||
channel_on_game_update,
|
|
||||||
channel_on_game_user_leave,
|
|
||||||
} from "./channel_names";
|
|
||||||
import morgan from "morgan";
|
|
||||||
import { GameMove } from "./models/GameMove";
|
|
||||||
import { GameCrashManager } from "./GameCrashManager";
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(cors());
|
|
||||||
app.use(
|
|
||||||
morgan("common", {
|
|
||||||
stream: fs.createWriteStream("./access.log", { flags: "a" }),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use(morgan("dev"));
|
|
||||||
|
|
||||||
const server = http.createServer(app);
|
|
||||||
|
|
||||||
app.get("/", (req: Request, res: Response) => {
|
|
||||||
res.send("Server up and running!");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/register/", (req: Request, res: Response) => {
|
|
||||||
res.send(generateKey());
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = process.env.PORT || 5000;
|
|
||||||
|
|
||||||
server.listen(port, () => {
|
|
||||||
console.log(`Server started on port ${port}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const rtmt = new RTMTWS();
|
const rtmt = new RTMTWS();
|
||||||
|
const gameStore = new GameStoreImpl();
|
||||||
|
const matchMaker = new MatchMakerImpl();
|
||||||
|
const gameManager = new GameManager({ rtmt, gameStore, matchMaker })
|
||||||
|
|
||||||
rtmt.initWebSocket(server, (userKey: string) => {
|
const expressApp = new ExpressApp();
|
||||||
const game = gameStore.get(userKey);
|
const server = new WebServer({expressApp});
|
||||||
if (game) {
|
|
||||||
rtmt.sendMessage(userKey, channel_on_game_update, game);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const matchmaker = new MatchMaker();
|
rtmt.initWebSocket(server.server, (userKey: string) => gameManager.fireOnPlayerConnected(userKey));
|
||||||
|
|
||||||
rtmt.listenMessage(channel_new_game, (userKey: string, message: Object) => {
|
|
||||||
matchmaker.find(userKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
const gameStore = new Map<string, MancalaGame>();
|
|
||||||
|
|
||||||
matchmaker.onPlayersPaired = (userKey1: string, userKey2: string) => {
|
|
||||||
const game = createMancalaGame(userKey1, userKey2);
|
|
||||||
|
|
||||||
rtmt.sendMessage(userKey1, channel_on_game_start, game);
|
|
||||||
rtmt.sendMessage(userKey2, channel_on_game_start, game);
|
|
||||||
};
|
|
||||||
|
|
||||||
rtmt.listenMessage(channel_game_move, (userKey: string, message: Object) => {
|
|
||||||
const gameMove: GameMove = message as GameMove;
|
|
||||||
|
|
||||||
const game = gameStore.get(userKey);
|
|
||||||
if (game) {
|
|
||||||
try {
|
|
||||||
game.moveByPlayerPit(userKey, gameMove.index);
|
|
||||||
rtmt.sendMessage(game.player1Id, channel_on_game_update, game);
|
|
||||||
rtmt.sendMessage(game.player2Id, channel_on_game_update, game);
|
|
||||||
if (game.state == "ended") {
|
|
||||||
gameStore.delete(game.player1Id);
|
|
||||||
gameStore.delete(game.player2Id);
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err);
|
|
||||||
const crashFileName = GameCrashManager.logGameCrash(err, game);
|
|
||||||
console.info(`Game crash saved to file : ${crashFileName}`);
|
|
||||||
rtmt.sendMessage(game.player1Id, channel_on_game_crashed, err);
|
|
||||||
rtmt.sendMessage(game.player2Id, channel_on_game_crashed, err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("Game not found!");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
rtmt.listenMessage(channel_leave_game, (userKey: string, message: Object) => {
|
|
||||||
const game = gameStore.get(userKey);
|
|
||||||
if (game) {
|
|
||||||
deleteGame(game);
|
|
||||||
rtmt.sendMessage(game.player1Id, channel_on_game_user_leave, userKey);
|
|
||||||
rtmt.sendMessage(game.player2Id, channel_on_game_user_leave, userKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteGame = (game: MancalaGame) => {
|
|
||||||
if (game) {
|
|
||||||
gameStore.delete(game.player1Id);
|
|
||||||
gameStore.delete(game.player2Id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function createMancalaGame(userKey1: string, userKey2: string) {
|
|
||||||
const game = new CommonMancalaGame(generateKey(), userKey1, userKey2);
|
|
||||||
const random = Math.random();
|
|
||||||
game.turnPlayerId = random > 0.5 ? userKey1 : userKey2;
|
|
||||||
|
|
||||||
gameStore.set(userKey1, game);
|
|
||||||
gameStore.set(userKey2, game);
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|||||||
6
src/matchmaker/MatchMaker.ts
Normal file
6
src/matchmaker/MatchMaker.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type OnPlayersPaired = (player1Id: string, player2Id: string)=> void;
|
||||||
|
|
||||||
|
export interface MatchMaker {
|
||||||
|
join(playerId : string): void;
|
||||||
|
listenOnPlayersPaired(onPlayersPaired: OnPlayersPaired ) : void;
|
||||||
|
}
|
||||||
26
src/matchmaker/MatchMakerImpl.ts
Normal file
26
src/matchmaker/MatchMakerImpl.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { MatchMaker, OnPlayersPaired } from "./MatchMaker";
|
||||||
|
|
||||||
|
export class MatchMakerImpl implements MatchMaker {
|
||||||
|
private waitingUserKey : string | undefined
|
||||||
|
|
||||||
|
private onPlayersPaired: OnPlayersPaired | undefined = undefined;
|
||||||
|
|
||||||
|
private fireOnPlayerPaired(player1Id: string, player2Id: string) : void {
|
||||||
|
this.onPlayersPaired?.(player1Id, player2Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public join(playerId : string): void {
|
||||||
|
if(this.waitingUserKey === playerId) return;
|
||||||
|
if(this.waitingUserKey){
|
||||||
|
const user1 = this.waitingUserKey as string
|
||||||
|
this.waitingUserKey = undefined
|
||||||
|
this.fireOnPlayerPaired(user1, playerId)
|
||||||
|
}else{
|
||||||
|
this.waitingUserKey = playerId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public listenOnPlayersPaired(onPlayersPaired: OnPlayersPaired ) : void {
|
||||||
|
this.onPlayersPaired = onPlayersPaired;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
// todo : use queue
|
|
||||||
export class MatchMaker {
|
|
||||||
private waitingUserKey : string | undefined
|
|
||||||
|
|
||||||
public onPlayersPaired!: (userKey1: string, userKey2: string) => void
|
|
||||||
|
|
||||||
public find(userKey : string) : void{
|
|
||||||
if(this.waitingUserKey === userKey) return;
|
|
||||||
if(this.waitingUserKey){
|
|
||||||
const user1 = this.waitingUserKey as string
|
|
||||||
this.waitingUserKey = undefined
|
|
||||||
this.fireOnPlayerPaired(user1, userKey)
|
|
||||||
}else{
|
|
||||||
this.waitingUserKey = userKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fireOnPlayerPaired(userKey1 : string, userKey2 : string) : void {
|
|
||||||
this.onPlayersPaired!!(userKey1, userKey2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
src/server/ExpressApp.ts
Normal file
27
src/server/ExpressApp.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import cors from "cors";
|
||||||
|
import express from "express";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import morgan from "morgan";
|
||||||
|
import fs from "fs";
|
||||||
|
import { generateKey } from "../util/key_factory";
|
||||||
|
|
||||||
|
export class ExpressApp {
|
||||||
|
private createAccessLogMiddleware() {
|
||||||
|
return morgan("common", {
|
||||||
|
stream: fs.createWriteStream("./access.log", { flags: "a" }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public createExpressApp(): Express.Application {
|
||||||
|
const app = express();
|
||||||
|
app.use(cors());
|
||||||
|
app.use(this.createAccessLogMiddleware());
|
||||||
|
app.use(morgan("dev"));
|
||||||
|
app.get("/", (req: Request, res: Response) => {
|
||||||
|
res.send("Server up and running!");
|
||||||
|
});
|
||||||
|
app.get("/register/", (req: Request, res: Response) => {
|
||||||
|
res.send(generateKey());
|
||||||
|
});
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/server/WebServer.ts
Normal file
21
src/server/WebServer.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import * as http from "http";
|
||||||
|
|
||||||
|
export class WebServer {
|
||||||
|
server: http.Server;
|
||||||
|
constructor(props: { expressApp: Express.Application }) {
|
||||||
|
this.server = this.createWebServer(props.expressApp);
|
||||||
|
}
|
||||||
|
public createWebServer(expressApp: Express.Application): http.Server {
|
||||||
|
const server = http.createServer(expressApp);
|
||||||
|
const port = process.env.PORT || 5000;
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`Server started on port ${port}`);
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user