import { State } from "./State.js"; /** * Represents a game. */ export class Game { /** * The number of chips required for a win. */ static #count = 4; /** * The width of the board. */ static #width = 7; /** * The height of the board. */ static #height = 6; /** * The previous states of the game. * * @type {IState[]} */ #previousStates = []; /** * The state of the game. * * @type {IState} */ #state; /** * Initializes a new instance of the {@link Game `Game`} class. */ constructor() { this.#state = State.create(Game.#width, Game.#height); } /** * Gets the state of the game. */ get state() { return this.#state; } /** * Gets the board of the game. */ get board() { return this.state.board; } /** * Gets the current player. * * @type {CellOwner} */ get currentPlayer() { return this.state.turnCount % 2 === 0 ? "r" : "b"; } /** * Gets the id of the player that is winning. * * @type {CellOwner} */ get winner() { for (let yOffset = 0; yOffset <= 1; yOffset++) { for (let xOffset = (yOffset === 1) ? -1 : 1; xOffset <= 1; xOffset++) { let lowerBound = Math.max(0, xOffset * (Game.#count - 1) * -1); let upperBound = Math.min(Game.#width, Game.#width - (xOffset * (Game.#count - 1))); for (let y = 0; y < (Game.#height - yOffset * (Game.#count - 1)); y++) { for (let x = lowerBound; x < upperBound; x++) { /** * @type {CellOwner[]} */ let tokens = []; for (let i = 0; i < Game.#count; i++) { tokens.push(this.board[y + i * yOffset][x + i * xOffset]); } let player = tokens[0]; if ( player !== "" && tokens.every((token) => token === player)) { return player; } } } } } return null; } /** * Dumps the state of the game. * * @returns {IState} * The JSON string representing the state. */ dump() { return { turnCount: this.state.turnCount, board: this.state.board }; } /** * Loads the game from the specified {@link data `data`}. * * @param {IState} data * The data to load. */ load(data) { this.setState([], data); if (!this.#state) { this.reset(); } } /** * Resets the game. */ reset() { this.setState([], State.create(Game.#width, Game.#height)); } /** * Reverts the last move. */ undo() { this.#state = this.#previousStates.pop(); if (!this.#state) { this.reset(); } } /** * Sets the value located at the specified {@link path `path`} to the specified {@link value `value`}. * * @param {(keyof any)[]} path * The path of the value to set. * * @param {any} value * The value to set. */ setState(path, value) { this.#previousStates.push(this.#state); this.#state = { ...this.#state }; this.#state.turnCount++; if (path.length === 0) { this.#state = value; } else if (path.length === 1) { this.#state = { ...this.#state, [path[0]]: value }; } else { let target = /** @type {any} */ (this.#state); while (path.length > 1) { if (Array.isArray(target[path[0]])) { target[path[0]] = [...target[path[0]]]; } else { target[path[0]] = { ...target[path[0]] }; } target = target[path[0]]; path = path.slice(1); } target[path[0]] = value; } } /** * Adds a chip to the board indicated by the {@link x `x`} and the {@link y `y`} coordinate. * * @param {number} x * The x coordinate to add the chip to. * * @param {number} y * The y coordinate to add the chip to. * * @returns {boolean} * A value indicating whether the chip could be added. */ addChip(x, y) { if (!this.winner) { for (let i = Game.#height - 1; i >= 0; i--) { if (this.board[i][x] === "") { this.setState(["board", i, x], this.currentPlayer); return true; } } } return false; } }