diff --git a/src/js/Components.js b/src/js/Components.js index ae91a9b..f3c672f 100644 --- a/src/js/Components.js +++ b/src/js/Components.js @@ -4,6 +4,7 @@ const saveGameKey = "connect-force-save"; const newGameClass = "new-game"; const saveGameClass = "save-game"; const loadGameClass = "load-game"; +const undoClass = "undo-game"; /** * Gets a component which represents the specified {@link game `game`}. @@ -35,6 +36,10 @@ export function App(game) { game.load(JSON.parse(localStorage.getItem(saveGameKey))); } + else if (target.classList.contains(undoClass)) + { + game.undo(); + } else { for (let y = 0; y < game.board.length; y++) @@ -49,7 +54,6 @@ export function App(game) { if (game.addChip(x, y)) { - game.state.turnCount++; game.draw(); } } @@ -141,7 +145,8 @@ export function MenuBar() { className: "menu-bar" }, [Button, ["New Game", { className: newGameClass }]], [Button, ["Save Game", { className: saveGameClass }]], - [Button, ["Load Game", { className: loadGameClass }]] + [Button, ["Load Game", { className: loadGameClass }]], + [Button, ["Undo Last Move", { className: undoClass }]] ]; } diff --git a/src/js/Game.js b/src/js/Game.js index 6758026..85cc48a 100644 --- a/src/js/Game.js +++ b/src/js/Game.js @@ -22,6 +22,13 @@ export class Game */ static #height = 6; + /** + * The previous states of the game. + * + * @type {IState[]} + */ + #previousStates = []; + /** * The state of the game. * @@ -140,7 +147,7 @@ export class Game */ load(data) { - this.#state = data; + this.setState([], data); this.draw(); } @@ -157,10 +164,27 @@ export class Game */ reset() { - this.#state = State.create(Game.#width, Game.#height); + this.setState([], State.create(Game.#width, Game.#height)); this.draw(); } + /** + * Reverts the last move. + */ + undo() + { + this.#state = this.#previousStates.pop(); + + if (!this.#state) + { + this.reset(); + } + else + { + this.draw(); + } + } + /** * Replaces the content of the board with the current state. */ @@ -171,6 +195,55 @@ export class Game SuiWeb.render([App, this], container); } + /** + * 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. * @@ -191,7 +264,7 @@ export class Game { if (this.board[i][x] === "") { - this.board[i][x] = this.currentPlayer; + this.setState(["board", i, x], this.currentPlayer); return true; } }