243 lines
5.2 KiB
JavaScript
243 lines
5.2 KiB
JavaScript
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;
|
|
}
|
|
}
|