diff --git a/src/channel_names.ts b/src/consts/channel_names.ts similarity index 100% rename from src/channel_names.ts rename to src/consts/channel_names.ts diff --git a/src/GameCrashManager.ts b/src/game/GameCrashManager.ts similarity index 100% rename from src/GameCrashManager.ts rename to src/game/GameCrashManager.ts diff --git a/src/game/GameManager.ts b/src/game/GameManager.ts new file mode 100644 index 0000000..a604419 --- /dev/null +++ b/src/game/GameManager.ts @@ -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); + } + } +} \ No newline at end of file diff --git a/src/game/gamestore/GameStore.ts b/src/game/gamestore/GameStore.ts new file mode 100644 index 0000000..97d3f8e --- /dev/null +++ b/src/game/gamestore/GameStore.ts @@ -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; +} \ No newline at end of file diff --git a/src/game/gamestore/GameStoreImpl.ts b/src/game/gamestore/GameStoreImpl.ts new file mode 100644 index 0000000..9256485 --- /dev/null +++ b/src/game/gamestore/GameStoreImpl.ts @@ -0,0 +1,16 @@ +import { MancalaGame } from "mancala.js"; +import { GameStore } from "./GameStore"; + +export class GameStoreImpl implements GameStore { + gameStore: Map = new Map() + + 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); + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6696fb6..d414686 100644 --- a/src/index.ts +++ b/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 cors from "cors"; -import { generateKey } from "./key_factory"; -import { MatchMaker } from "./matcmaker"; -import { CommonMancalaGame, MancalaGame } from "mancala.js"; -import fs from "fs"; -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}`); -}); +import { GameManager } from "./game/GameManager"; +import { GameStoreImpl } from "./game/gamestore/GameStoreImpl"; +import { MatchMakerImpl } from "./matchmaker/MatchMakerImpl"; +import { ExpressApp } from "./server/ExpressApp"; +import { WebServer } from "./server/WebServer"; 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 game = gameStore.get(userKey); - if (game) { - rtmt.sendMessage(userKey, channel_on_game_update, game); - } -}); +const expressApp = new ExpressApp(); +const server = new WebServer({expressApp}); -const matchmaker = new MatchMaker(); - -rtmt.listenMessage(channel_new_game, (userKey: string, message: Object) => { - matchmaker.find(userKey); -}); - -const gameStore = new Map(); - -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; -} +rtmt.initWebSocket(server.server, (userKey: string) => gameManager.fireOnPlayerConnected(userKey)); diff --git a/src/matchmaker/MatchMaker.ts b/src/matchmaker/MatchMaker.ts new file mode 100644 index 0000000..bdc84db --- /dev/null +++ b/src/matchmaker/MatchMaker.ts @@ -0,0 +1,6 @@ +export type OnPlayersPaired = (player1Id: string, player2Id: string)=> void; + +export interface MatchMaker { + join(playerId : string): void; + listenOnPlayersPaired(onPlayersPaired: OnPlayersPaired ) : void; +} \ No newline at end of file diff --git a/src/matchmaker/MatchMakerImpl.ts b/src/matchmaker/MatchMakerImpl.ts new file mode 100644 index 0000000..335cffc --- /dev/null +++ b/src/matchmaker/MatchMakerImpl.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/matcmaker.ts b/src/matcmaker.ts deleted file mode 100644 index 0ac5d19..0000000 --- a/src/matcmaker.ts +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/src/server/ExpressApp.ts b/src/server/ExpressApp.ts new file mode 100644 index 0000000..5f57854 --- /dev/null +++ b/src/server/ExpressApp.ts @@ -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; + } +} diff --git a/src/server/WebServer.ts b/src/server/WebServer.ts new file mode 100644 index 0000000..4f782bf --- /dev/null +++ b/src/server/WebServer.ts @@ -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; + } +} + + + + diff --git a/src/key_factory.ts b/src/util/key_factory.ts similarity index 100% rename from src/key_factory.ts rename to src/util/key_factory.ts