import { readFile } from "fs/promises"; import { BoardLoadError } from "./errors/board_load_error.js"; interface ISnakeOrLadder { landing: number; destination: number; } export interface IBoardData { size: number; ladders: ISnakeOrLadder[]; snakes: ISnakeOrLadder[]; } export class Board { public size: number; private snakeMap: Map = new Map(); private ladderMap: Map = new Map(); static async loadFromFile( filename: string, encoding: BufferEncoding = "utf-8" ) { let rawData; try { rawData = await readFile(filename, { encoding }); } catch (err) { throw new BoardLoadError(`Unable to read file: ${filename}`); } let jsonData; try { jsonData = JSON.parse(rawData); } catch (err) { throw new BoardLoadError("Invalid JSON"); } if (typeof jsonData !== "object") { throw new BoardLoadError("JSON does not contain board object"); } if ( !("ladders" in jsonData && "snakes" in jsonData && "size" in jsonData) ) { throw new BoardLoadError("Missing properties in board object"); } if (!Number.isInteger(jsonData.size)) { throw new BoardLoadError("Invalid board size"); } const boardData: IBoardData = { size: jsonData.size, snakes: [], ladders: [], }; const props: Array<"snakes" | "ladders"> = ["snakes", "ladders"]; for (const prop of props) { const transportData = jsonData[prop]; if (!Array.isArray(transportData)) { throw new BoardLoadError(`Invalid "${prop}" property in board object`); } for (const transport of transportData) { if ( typeof transport !== "object" || !("landing" in transport) || !Number.isInteger(transport.landing) || !("destination" in transport) || !Number.isInteger(transport.destination) ) { const repr = JSON.stringify(transport); // Remove plural const transportType = prop.replace(/s$/, prop); throw new BoardLoadError(`Invalid ${transportType} entry: ${repr}`); } boardData[prop].push({ landing: transport.landing, destination: transport.destination, }); } } return new Board(boardData); } constructor(boardData: IBoardData) { this.size = boardData.size; for (const transport of boardData.snakes) { if (transport.landing <= transport.destination) { throw new BoardLoadError( "Invalid snake, landing not greater than destination" ); } this.snakeMap.set(transport.landing, transport.destination); } for (const transport of boardData.ladders) { if (transport.landing >= transport.destination) { throw new BoardLoadError( "Invalid ladder, landing not less than destination" ); } this.ladderMap.set(transport.landing, transport.destination); } } tileHasSnake(pos: number) { return this.snakeMap.has(pos); } tileHasLadder(pos: number) { return this.ladderMap.has(pos); } calculateLandingPosition(pos: number) { return Math.min( this.ladderMap.get(pos) ?? this.snakeMap.get(pos) ?? pos, this.size ); } }