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.", } export class MazeMap { public id:string; public layout:MazeTile[][]; constructor(id:string) { this.id = id; this.layout = []; } 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 validate(): string[] { const errors:string[] = []; if(this.id == null || this.id === "") { errors.push(MazeValidationError.InvalidID); } let hasInvalidTiles = false; let hasNullTiles = 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(hasInvalidTiles) { errors.push(MazeValidationError.InvalidTiles); } if(hasNullTiles) { errors.push(MazeValidationError.NoHolesAllowed); } let hasWrongShape = false; 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); } } return errors; } }