export enum Direction { none = 0, top = 1 << 0, left = 1 << 1, bottom = 1 << 2, right = 1 << 3, } export function OppositeDirection(dir:Direction): Direction { switch (dir) { case Direction.top: return Direction.bottom; case Direction.bottom: return Direction.top; case Direction.right: return Direction.left; case Direction.left: return Direction.right; case Direction.none: return Direction.none; default: throw new Error(`"${dir}" does not have a recognized opposite.`); } } function DirectionToVector(dir:Direction): number[] { switch (dir) { case Direction.top: return [-1, 0]; case Direction.left: return [0, -1]; case Direction.bottom: return [1, 0]; case Direction.right: return [0, 1]; default: return [0, 0]; } } export enum TileValidationError { InvalidID = "Id must not be null or empty.", NoPathways = "Tile must have at least one pathway.", } export class MazeTile { public paths:Direction[]; public id:string; constructor(id:string) { this.id = id; this.paths = []; } public getRepresentation(): number { let output = Direction.none; for (const dir of this.paths) { output |= dir; } return output; } public validate(): TileValidationError[] { const errors: TileValidationError[] = []; if(this.id == null || this.id === "") { errors.push(TileValidationError.InvalidID); } if(this.paths.length === 0) { errors.push(TileValidationError.NoPathways); } return errors; } public hasDirection(dir:Direction): boolean { for (const d of this.paths) { if(d === dir) { return true; } } return false; } } export enum MazeValidationError { InvalidID = "Id must not be null or empty.", InvalidTiles = "Maze contains invalid tiles.", PathWithoutDestination = "Pathways must have a corresponding destination tile.", WrongShape = "Maze must be shaped like a rectangle.", NoHolesAllowed = "Maze has null-tiles.", TileIdsNotUnique = "Some tiles have the same id.", StartPositionNotOnMap = "The starting tile has to be on the map.", InvalidStartPosition = "The starting tile needs to have pathways.", } export class MazeMap { public id:string; public layout:MazeTile[][]; public startPosition: number[]; constructor(id:string) { this.id = id; this.layout = []; this.startPosition = [0, 0]; } public getRepresentation(): number[][] { const output:number[][] = []; for (const row of this.layout) { const newRow:number[] = []; for (const cell of row) { newRow.push(cell.getRepresentation()); } output.push(newRow); } return output; } public getTile(tileID: string): MazeTile { for (const row of this.layout) { for (const tile of row) { if(tile.id === tileID){ return tile; } } } return null; } public getStartTile(): MazeTile { return this.layout[this.startPosition[0]][this.startPosition[1]]; } public validate(): MazeValidationError[] { const errors:MazeValidationError[] = []; if(this.id == null || this.id === "") { errors.push(MazeValidationError.InvalidID); } let hasInvalidTiles = false; let hasNullTiles = false; let hasIDDuplicates = false; for (const row of this.layout) { if(row == null) { hasNullTiles = true; continue; } for (const tile of row) { if(tile == null) { hasNullTiles = true; continue; } if(tile.validate().length > 0) { hasInvalidTiles = true; } if(!hasIDDuplicates){ for (const row2 of this.layout) { if(row2 == null){ continue; } for (const tile2 of row2) { if(tile2 == null || tile2 === tile) { continue; } if(tile2.id === tile.id) { hasIDDuplicates = true; } } } } } } if(hasInvalidTiles) { errors.push(MazeValidationError.InvalidTiles); } if(hasNullTiles) { errors.push(MazeValidationError.NoHolesAllowed); } if(hasIDDuplicates) { errors.push(MazeValidationError.TileIdsNotUnique); } let hasWrongShape = false; if(this.layout.length === 0) { errors.push(MazeValidationError.WrongShape); hasWrongShape = true; } else { for (const row of this.layout) { if(row == null || row.length !== this.layout[0].length) { errors.push(MazeValidationError.WrongShape); hasWrongShape = true; break; } } } // Only do the complex path validation if there are no violations of shape requirements. if(!hasNullTiles && !hasWrongShape) { let hasPathsWithoutDestination = false; for(let i=0;i<this.layout.length && !hasPathsWithoutDestination;i++) { for(let j=0;j<this.layout[i].length && !hasPathsWithoutDestination;j++) { for(let k=0;k<this.layout[i][j].paths.length && !hasPathsWithoutDestination;k++) { const dir = this.layout[i][j].paths[k]; if(dir === Direction.none) { continue; } const [x, y] = DirectionToVector(dir); const opposite = OppositeDirection(dir); if(i+x < 0 || i+x >= this.layout.length || j+y < 0 || j+y >= this.layout[i+x].length) { hasPathsWithoutDestination = true; break; } if(!this.layout[i+x][j+y].hasDirection(opposite)) { hasPathsWithoutDestination = true; break; } } } } if(hasPathsWithoutDestination) { errors.push(MazeValidationError.PathWithoutDestination); } } if(this.layout != null){ if(this.startPosition[0] < 0 || this.startPosition[0] >= this.layout.length) { errors.push(MazeValidationError.StartPositionNotOnMap); } else if(this.layout.length>0 && (this.startPosition[1] < 0 || this.startPosition[1] >= this.layout[0].length)) { errors.push(MazeValidationError.StartPositionNotOnMap); } else if(this.layout.length > 0 && this.layout[this.startPosition[0]][this.startPosition[1]] != null) { const start = this.layout[this.startPosition[0]][this.startPosition[1]]; let hasPaths = false; for (const dir of start.paths) { if(dir !== Direction.none){ hasPaths = true; break; } } if(!hasPaths) { errors.push(MazeValidationError.InvalidStartPosition); } } } return errors; } }