You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
3.2 KiB
124 lines
3.2 KiB
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<number, number> = new Map(); |
|
private ladderMap: Map<number, number> = 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 |
|
); |
|
} |
|
}
|
|
|