import { Direction, DirectionToVector, MazeMap, MazeTile, } from "./MazeMap"; class GameState { public userID: string; public mapID: string; public tileID: string; constructor(userID: string, mapID: string, tileID: string) { this.userID = userID; this.mapID = mapID; this.tileID = tileID; } } export class GameServer { // By keeping mapList private, we can ensure, // that the server contains only valid maps. private mapList: MazeMap[]; private states: GameState[]; /** * Keeps track of generated random characters to ensure, * that no two consecutive generated characters are the same. */ private randomCharacter: string; constructor() { this.mapList = []; this.states = []; } public addMap(newMap: MazeMap): boolean { const validationResult = newMap.validate(); if(validationResult.length > 0){ let message = "Cannot add invalid map:"; for (const e of validationResult) { message += "\n\t" + e; } throw new Error(message); } if(-1 === this.mapList.findIndex(m => m.id === newMap.id)) { this.mapList.push(newMap); return true; } else { return false; } } public startGame(userID: string, mapID: string): boolean { this.userIdGuard(userID); const map = this.mapList.find(m => m.id === mapID); if(map == null){ throw new Error(`Map "${mapID}" does not exist.`); } const start = map.getStartTile(); let state = this.states.find(s => s.userID === userID); if(state == null) { state = new GameState(userID, map.id, start.id); this.states.push(state); } else { state.mapID = map.id; state.tileID = start.id; } return true; } public endGame(userID: string): boolean { const stateIndex = this.states.findIndex(s => s.userID === userID); if(stateIndex !== -1) { this.states.splice(stateIndex, 1); } return true; } public navigate(userID: string, direction:Direction): MazeTile { this.userIdGuard(userID); const state = this.states.find(s => s.userID === userID); if(state == null){ return null; } const vec = DirectionToVector(direction); const map = this.mapList.find(m => m.id === state.mapID); let i:number; let j:number; let tile:MazeTile; for (i = 0; i < map.layout.length && tile == null; i++) { const row = map.layout[i]; for (j = 0; j < row.length; j++) { if(row[j].id === state.tileID) { tile = row[j]; break; } } } if(!tile.hasDirection(direction)){ throw new Error("Invalid direction."); } const nextTile = map.layout[i-1+vec[0]][j+vec[1]]; state.tileID = nextTile.id; return nextTile; } public currentPosition(userID: string): MazeTile { this.userIdGuard(userID); const state = this.states.find(s => s.userID === userID); if(state == null){ return null; } const map = this.mapList.find(m => m.id === state.mapID); const tile = map.getTile(state.tileID); return tile; } public listMaps(): string[] { return this.mapList.map(m => m.id); } public createNewUserID(): string { let id = (+new Date()).toString(36); // two prevent equal ids, at the same millisecond, append a random character. id += this.getRandomCharacter(); return id; } /** * Makes sure, that a userID is acceptable. * If not throw the appropriate error. * @param userID supplied userID */ private userIdGuard(userID: string): void { if(userID === undefined || userID === null){ throw new Error("UserID must be supplied!"); } if(userID === "" || /^\s+$/.test(userID)){ throw new Error("UserID must not be empty."); } } /** * Produces a single alphanumeric character, * while ensuring, that no two consecutive outputs are the same. */ private getRandomCharacter(): string { const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let newChar: string; newChar = possible.charAt(Math.floor(Math.random() * possible.length)); while(this.randomCharacter === newChar) { newChar = possible.charAt(Math.floor(Math.random() * possible.length)); } this.randomCharacter = newChar; return newChar; } }