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.
125 lines
3.2 KiB
125 lines
3.2 KiB
2 years ago
|
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
|
||
|
);
|
||
|
}
|
||
|
}
|