const Direction = Object.freeze({ Up: { name: "up" }, Right: { name: "right" }, Down: { name: "down" }, Left: { name: "left" }, get values() { return Object.values(Direction); }, fromName(name) { return this.values.filter(i=>i.name == name)[0] || null; }, }); let gameBoard; const btnStart_onClick = () => { gameBoard = new GameBoard(new Grid(4, 4)); // gameBoard = new GameBoard( // makeDevGrid([ // can't slide down or left // [2,1,3,4], // [3,5,7,9], // [1,2,1,2], // [3,4,5,0], // ]) // ); gameBoard.addRandomTile(); redraw(gameBoard); } const btnRedraw_onClick = () => { redraw(gameBoard); } // TODO(tom): This should take GameBoard instead of Grid or be moved into GameBoard. const playMove = (gameBoard, direction) => { if (gameBoard.slideInDirection(direction)) { gameBoard.addRandomTile(); redraw(gameBoard) } else { // alert(`Unable to slide ${direction.name}.`); } checkForEnd(gameBoard); } const btnUp_onClick = () => { playMove(gameBoard, Direction.Up); } const btnDown_onClick = () => { playMove(gameBoard, Direction.Down); } const btnLeft_onClick = () => { playMove(gameBoard, Direction.Left); } const btnRight_onClick = () => { playMove(gameBoard, Direction.Right); } const redraw = (gameBoard) => { const element = document.getElementById('output'); let text = "\n"; for (let rowIndex = 0; rowIndex < gameBoard.numRows; rowIndex++) { text += " \n"; for (let columnIndex = 0; columnIndex < gameBoard.numColumns; columnIndex++) { const tile = gameBoard.getTile(rowIndex, columnIndex); text += " \n"; } text += " \n"; } text += "
" + tile.value + "
"; element.innerHTML = text; } const checkForEnd = (gameBoard) => { if (gameBoard.gameIsLost()) { alert('A loser is you!'); } } const makeDevGrid = (matrix) => { const numRows = matrix.length; const numColumns = matrix[0].length; const grid = new Grid(numRows, numColumns); for (let rowIndex = 0; rowIndex < numRows; rowIndex++) { const row = matrix[rowIndex]; for (let colIndex = 0; colIndex < numColumns; colIndex++) { const value = row[colIndex]; const tile = grid.getTile(rowIndex, colIndex); tile.value = value; tile.isVisible = value > 0; } } return grid; } /* [0,0,0,0], [0,0,1,0], [0,0,0,0], [2,1,2,2], can't slide down. [0,0,1,0], [0,0,0,0], [0,0,0,0], [3,2,3,0], can't slide down. can't slide left. up() [1,0,1,0], [0,0,3,0], [0,0,0,0], [3,2,0,0], either the 3 doesn't go up or the 1 bounces it back? // fixed */ class GameBoard { #grid; get numColumns() { return this.#grid.numColumns; } get numRows() { return this.#grid.numRows; } constructor(grid) { this.#grid = grid; } getTile(row, column) { return this.#grid.getTile(row, column); } addRandomTile() { const hiddenTiles = this.#grid.tiles.filter(tile => !tile.isVisible); if (hiddenTiles.length <= 0) { return; } const randomIndex = Math.floor(Math.random() * hiddenTiles.length); const tile = hiddenTiles[randomIndex]; // TODO(tom): Make this random of either 1 or 2 and heavily weight 1. tile.value = 1; tile.isVisible = true; } slideInDirection (direction) { return GameBoard.#slideInDirection_impl(direction, this.#grid); } gameIsLost() { return !this.#canSlideInDirection(Direction.Up) && !this.#canSlideInDirection(Direction.Right) && !this.#canSlideInDirection(Direction.Down) && !this.#canSlideInDirection(Direction.Left); } #canSlideInDirection(direction) { const clonedGrid = this.#grid.clone(); return GameBoard.#slideInDirection_impl(direction, clonedGrid); } static #slideInDirection_impl (direction, grid) { const settings = GameBoard.#getSettingsForDirection(direction); const length = settings.getLength(grid); let didChange = false; for (let index = 0; index < length; index++) { const vector = settings.getVector(grid, index); const swap = settings.swap.bind(this, grid, vector, index); const initialFromIndex = settings.getInitialFromIndex(vector.length); const initialToIndex = settings.getInitialToIndex(vector.length); const didChangeThisPass = GameBoard.#slide( initialFromIndex, initialToIndex, vector, settings.advance, swap, settings.merge, settings.testForDone ); didChange = didChange || didChangeThisPass; } return didChange; } static #getSettingsForDirection(direction) { const settings = new Settings(); settings.merge = (from, to) => { to.value++; from.value = 0; from.isVisible = false; }; if (direction === Direction.Up || direction === Direction.Left) { settings.advance = i => i + 1; settings.testForDone = (fromIndex, length) => fromIndex >= length; settings.getInitialFromIndex = () => 1; settings.getInitialToIndex = () => 0; } else if (direction === Direction.Down || direction === Direction.Right) { settings.advance = i => i - 1; settings.testForDone = fromIndex => fromIndex < 0; settings.getInitialFromIndex = (length) => length - 2; settings.getInitialToIndex = (length) => length - 1; } if (direction === Direction.Up || direction === Direction.Down) { settings.getVector = (grid, index) => grid.getColumn(index); settings.swap = (grid, vector, columnIndex, from, to) => { grid.swapTiles(from, columnIndex, to, columnIndex); const tempTile = vector[to]; vector[to] = vector[from]; vector[from] = tempTile; } settings.getLength = grid => grid.numColumns; } else if (direction === Direction.Left || direction === Direction.Right) { settings.getVector = (grid, index) => grid.getRow(index); settings.swap = (grid, vector, rowIndex, from, to) => { grid.swapTiles(rowIndex, from, rowIndex, to); const tempTile = vector[to]; vector[to] = vector[from]; vector[from] = tempTile; }; settings.getLength = grid => grid.numRows; } return settings; } static #slide(fromIndex, toIndex, vector, advance, swap, merge, testForDone) { let didChange = false; let hasMergedPrevious = false; while(!testForDone(fromIndex, vector.length)) { const fromTile = vector[fromIndex]; const toTile = toIndex < vector.length && toIndex >= 0 ? vector[toIndex] : null; if (toTile == null) { // Advance toIndex toIndex = advance(toIndex); hasMergedPrevious = false; continue; } if (fromIndex == toIndex) { // Advance fromIndex fromIndex = advance(fromIndex); hasMergedPrevious = false; continue; } if (!toTile.isVisible) { if (fromTile.isVisible) { swap(fromIndex, toIndex); // Advance fromIndex fromIndex = advance(fromIndex); hasMergedPrevious = false; didChange = true; } else { // Advance fromIndex fromIndex = advance(fromIndex); } } else { if (fromTile.isVisible) { if (fromTile.value == toTile.value) { if (hasMergedPrevious) { // Advance toIndex toIndex = advance(toIndex); hasMergedPrevious = false; didChange = true; } else { // Merge tiles merge(fromTile, toTile); // Advance fromIndex fromIndex = advance(fromIndex); hasMergedPrevious = true; didChange = true; } } else { // Advance toIndex toIndex = advance(toIndex); hasMergedPrevious = false; } } else { fromIndex = advance(fromIndex); } } } return didChange; } }; class Grid { #tiles = []; #rows = []; #numRows; #numColumns; get tiles() { return [...this.#tiles]; } get numRows() { return this.#numRows; } get numColumns() { return this.#numColumns; } constructor(numRows, numColumns) { this.#numRows = numRows; this.#numColumns = numColumns; for (let rowIndex = 0; rowIndex < numRows; rowIndex++) { const row = []; for (let columnIndex = 0; columnIndex < this.numColumns; columnIndex++) { const tile = new Tile(); this.#tiles.push(tile); row.push(tile); } this.#rows.push(row); } } clone() { const newGrid = new Grid(this.numRows, this.numColumns); for (let rowIndex = 0; rowIndex < this.numRows; rowIndex++) { for (let columnIndex = 0; columnIndex < this.numColumns; columnIndex++) { const newTile = newGrid.getTile(rowIndex, columnIndex); const oldTile = this.getTile(rowIndex, columnIndex); newTile.value = oldTile.value; newTile.isVisible = oldTile.isVisible; } } return newGrid; } getColumn (columnIndex) { let vector = []; for (let rowIndex = 0; rowIndex < this.numRows; rowIndex++) { vector.push(this.getTile(rowIndex, columnIndex)); } return vector; } getRow (rowIndex) { let vector = []; for (let columnIndex = 0; columnIndex < this.numColumns; columnIndex++) { vector.push(this.getTile(rowIndex, columnIndex)); } return vector; } getTile (row, column) { if (row >= 0 && row < this.numRows && column >= 0 && column < this.numColumns) { // TODO(tom): Check that this is the right order. return this.#rows[column][row] } return null; } setTile (row, column, tile) { if (row >= 0 && row < this.numRows && column >= 0 && column < this.numColumns) { // TODO(tom): Check that this is the right order. this.#rows[column][row] = tile; } } swapTiles (fromRow, fromColumn, toRow, toColumn) { if ( fromRow >= 0 && fromRow < this.numRows && fromColumn >= 0 && fromColumn < this.numColumns && toRow >= 0 && toRow < this.numRows && toColumn >= 0 && toColumn < this.numColumns) { const fromTile = this.getTile(fromRow, fromColumn); const toTile = this.getTile(toRow, toColumn); this.setTile(fromRow, fromColumn, toTile); this.setTile(toRow, toColumn, fromTile); } } toString (format = t => t.value) { return `[[${format(this.getTile(0, 0))},${format(this.getTile(0, 1))},${format(this.getTile(0, 2))},${format(this.getTile(0, 3))}], [${format(this.getTile(1, 0))},${format(this.getTile(1, 1))},${format(this.getTile(1, 2))},${format(this.getTile(1, 3))}], [${format(this.getTile(2, 0))},${format(this.getTile(2, 1))},${format(this.getTile(2, 2))},${format(this.getTile(2, 3))}], [${format(this.getTile(3, 0))},${format(this.getTile(3, 1))},${format(this.getTile(3, 2))},${format(this.getTile(3, 3))}]]`; } }; class GridPosition { #row; #column; constructor (row, column) { this.#row = row || 0; this.#column = column || 0; } get row() { return this.#row; } get column() { return this.#column; } equals (other) { return other !== null && other !== undefined && this.#row === other.#row && this.#column === other.#column; } toString() { return `{ row: ${this.#row}, column: ${this.#column} }`; } } class Tile { value; isVisible; constructor () { this.value = 0; this.isVisible = false; } equals (other) { return other !== null && other !== undefined && this.value === other.value && this.isVisible === other.isVisible; } toString() { return `{ value: ${this.value}, isVisible: ${this.isVisible ? "true" : "false"} }`; } }; class Settings { merge; advance; testForDone; getInitialFromIndex; getInitialToIndex; getVector; getLength; swap; };