import { Direction, DirectionToVector, MazeMap, MazeTile, } from "./MazeMap"; export 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 interface IStateRepository { loadAllGameStates(): GameState[]; saveAllGameStates(states: GameState[]): boolean; } export interface IMapRepository { loadAllMaps(): MazeMap[]; } export class GameServerOptions { public stateRepo: IStateRepository; public mapRepo: IMapRepository; } 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; private options: GameServerOptions; constructor(options?: GameServerOptions) { if(options != null) { this.options = options; } this.mapList = []; if(options != null && options.mapRepo != null) { this.mapList = options.mapRepo.loadAllMaps(); for (const map of options.mapRepo.loadAllMaps()) { this.addMap(map); } } if(options != null && options.stateRepo != null) { this.states = options.stateRepo.loadAllGameStates(); } else { 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; } this.commitGameStates(); return true; } public endGame(userID: string): boolean { const stateIndex = this.states.findIndex(s => s.userID === userID); if(stateIndex !== -1) { this.states.splice(stateIndex, 1); } this.commitGameStates(); 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; this.commitGameStates(); 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; } private commitGameStates(): void { if(this.options == null || this.options.stateRepo == null || this.options.stateRepo.saveAllGameStates == null){ return; } this.options.stateRepo.saveAllGameStates(this.states); } }