407 lines
13 KiB
JavaScript
407 lines
13 KiB
JavaScript
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; },
|
|
});
|
|
const globals = {
|
|
grid: null,
|
|
};
|
|
|
|
const btnStart_onClick = () => {
|
|
globals.grid = makeGrid(4, 4, {value: 0, isVisible: false});
|
|
addRandomTile();
|
|
// globals.grid = makeDevGrid([ // can't slide down or left
|
|
// [2,1,3,4],
|
|
// [3,5,7,9],
|
|
// [1,2,1,2],
|
|
// [3,4,5,0],
|
|
// ]);
|
|
redraw(globals.grid);
|
|
}
|
|
|
|
const btnRedraw_onClick = () => {
|
|
redraw(globals.grid);
|
|
}
|
|
|
|
const getSettingsForDirection = (direction) => {
|
|
let settings = {
|
|
merge: (from, to) => {
|
|
to.value++;
|
|
from.value = 0;
|
|
from.isVisible = false;
|
|
}
|
|
};
|
|
|
|
if (direction === Direction.Up || direction === Direction.Left) {
|
|
settings = {
|
|
...settings,
|
|
advance: i => i + 1,
|
|
testForDone: (fromIndex, length) => fromIndex >= length,
|
|
getInitialFromIndex: () => 1,
|
|
getInitialToIndex: () => 0
|
|
};
|
|
} else if (direction === Direction.Down || direction === Direction.Right) {
|
|
settings = {
|
|
...settings,
|
|
advance: i => i - 1,
|
|
testForDone: fromIndex => fromIndex < 0,
|
|
getInitialFromIndex: (length) => length - 2,
|
|
getInitialToIndex: (length) => length - 1
|
|
};
|
|
}
|
|
|
|
if (direction === Direction.Up || direction === Direction.Down) {
|
|
settings = {
|
|
...settings,
|
|
getVector: (grid, index) => grid.getColumn(index),
|
|
swap: (grid, vector, columnIndex, from, to) => {
|
|
grid.swapTiles(from, columnIndex, to, columnIndex);
|
|
const tempTile = vector[to];
|
|
vector[to] = vector[from];
|
|
vector[from] = tempTile;
|
|
},
|
|
getLength: (grid) => grid.numColumns
|
|
}
|
|
} else if (direction === Direction.Left || direction === Direction.Right) {
|
|
settings = {
|
|
...settings,
|
|
getVector: (grid, index) => grid.getRow(index),
|
|
swap: (grid, vector, rowIndex, from, to) => {
|
|
grid.swapTiles(rowIndex, from, rowIndex, to);
|
|
const tempTile = vector[to];
|
|
vector[to] = vector[from];
|
|
vector[from] = tempTile;
|
|
},
|
|
getLength: (grid) => grid.numRows
|
|
}
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
const slideInDirection = (grid, direction) => {
|
|
const settings = 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 = slide(
|
|
initialFromIndex,
|
|
initialToIndex,
|
|
vector,
|
|
settings.advance,
|
|
swap,
|
|
settings.merge,
|
|
settings.testForDone
|
|
);
|
|
didChange = didChange || didChangeThisPass;
|
|
}
|
|
return didChange;
|
|
}
|
|
|
|
const playMove = (grid, direction) => {
|
|
if (slideInDirection(grid, direction)) {
|
|
addRandomTile();
|
|
redraw(grid)
|
|
} else {
|
|
alert(`Unable to slide ${direction.name}.`);
|
|
}
|
|
checkForEnd(grid);
|
|
}
|
|
|
|
const btnUp_onClick = () => {
|
|
playMove(globals.grid, Direction.Up);
|
|
}
|
|
|
|
const btnDown_onClick = () => {
|
|
playMove(globals.grid, Direction.Down);
|
|
}
|
|
|
|
const btnLeft_onClick = () => {
|
|
playMove(globals.grid, Direction.Left);
|
|
}
|
|
|
|
const btnRight_onClick = () => {
|
|
playMove(globals.grid, Direction.Right);
|
|
}
|
|
|
|
const drawGrid = (grid, element) => {
|
|
// grid is our grid
|
|
// element is our div
|
|
let text = "<table cellspacing=\"0\">\n";
|
|
for (let rowIndex = 0; rowIndex < grid.numRows; rowIndex++) {
|
|
text += " <tr>\n";
|
|
for (let columnIndex = 0; columnIndex < grid.numColumns; columnIndex++) {
|
|
const tile = grid.getTile(rowIndex, columnIndex);
|
|
text += " <td title=\"(" + tile.originalPosition.row + ", " + tile.originalPosition.column + ")\"class=\"" + (tile.isVisible ? "visible " : "hidden ") + "\">" + tile.value + "</td>\n";
|
|
}
|
|
text += " </tr>\n";
|
|
}
|
|
text += "</table>";
|
|
element.innerHTML = text;
|
|
}
|
|
|
|
const makeGrid = (numRows, numColumns, initialValue) => {
|
|
const tiles = [];
|
|
const rows = [];
|
|
for (rowIndex = 0; rowIndex < numRows; rowIndex++) {
|
|
const row = [];
|
|
for (columnIndex = 0; columnIndex < numColumns; columnIndex++) {
|
|
position = {row: rowIndex, column: columnIndex};
|
|
const tile = new Tile(initialValue.value, initialValue.isVisible, new GridPosition(rowIndex, columnIndex));
|
|
tiles.push(tile);
|
|
row.push(tile);
|
|
}
|
|
rows.push(row);
|
|
}
|
|
const grid = {
|
|
rows,
|
|
numRows,
|
|
numColumns,
|
|
tiles,
|
|
clone: () => {
|
|
const newGrid = makeGrid(numRows, numColumns, new Tile(0, false, new GridPosition(0, 0)));
|
|
for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
|
|
for (let columnIndex = 0; columnIndex < numColumns; columnIndex++) {
|
|
const newTile = newGrid.getTile(rowIndex, columnIndex);
|
|
const oldTile = grid.getTile(rowIndex, columnIndex);
|
|
newTile.originalPosition.column = oldTile.originalPosition.column;
|
|
newTile.originalPosition.row = oldTile.originalPosition.row;
|
|
newTile.value = oldTile.value;
|
|
newTile.isVisible = oldTile.isVisible;
|
|
}
|
|
}
|
|
return newGrid;
|
|
},
|
|
getColumn: (index) => {
|
|
let vector = [];
|
|
for (let rowIndex = 0; rowIndex < grid.numRows; rowIndex++) {
|
|
vector.push(grid.getTile(rowIndex, index));
|
|
}
|
|
return vector;
|
|
},
|
|
getRow: (index) => {
|
|
let vector = [];
|
|
for (let columnIndex = 0; columnIndex < grid.numColumns; columnIndex++) {
|
|
vector.push(grid.getTile(index, columnIndex));
|
|
}
|
|
return vector;
|
|
},
|
|
getTile: (row, column) => {
|
|
if (row >= 0 && row < numRows && column >= 0 && column < numColumns) {
|
|
return rows[column][row];
|
|
}
|
|
return null;
|
|
},
|
|
setTile: (row, column, tile) => {
|
|
if (row >= 0 && row < numRows && column >= 0 && column < numColumns) {
|
|
rows[column][row] = tile;
|
|
}
|
|
},
|
|
swapTiles: (fromRow, fromColumn, toRow, toColumn) => {
|
|
if (
|
|
fromRow >= 0 && fromRow < numRows
|
|
&& fromColumn >= 0 && fromColumn < numColumns
|
|
&& toRow >= 0 && toRow < numRows
|
|
&& toColumn >= 0 && toColumn < numColumns) {
|
|
fromTile = grid.getTile(fromRow, fromColumn);
|
|
toTile = grid.getTile(toRow, toColumn);
|
|
// maybe set previous locations on the tiles
|
|
grid.setTile(fromRow, fromColumn, toTile);
|
|
grid.setTile(toRow, toColumn, fromTile);
|
|
}
|
|
},
|
|
toString: (mapFn = i => i.value) => {
|
|
return "[[" + mapFn(grid.getTile(0, 0)) + "," + mapFn(grid.getTile(0, 1)) + "," + mapFn(grid.getTile(0, 2)) + "," + mapFn(grid.getTile(0, 3)) + "],\n"
|
|
+ " [" + mapFn(grid.getTile(1, 0)) + "," + mapFn(grid.getTile(1, 1)) + "," + mapFn(grid.getTile(1, 2)) + "," + mapFn(grid.getTile(1, 3)) + "],\n"
|
|
+ " [" + mapFn(grid.getTile(2, 0)) + "," + mapFn(grid.getTile(2, 1)) + "," + mapFn(grid.getTile(2, 2)) + "," + mapFn(grid.getTile(2, 3)) + "],\n"
|
|
+ " [" + mapFn(grid.getTile(3, 0)) + "," + mapFn(grid.getTile(3, 1)) + "," + mapFn(grid.getTile(3, 2)) + "," + mapFn(grid.getTile(3, 3)) + "]]";
|
|
}
|
|
};
|
|
return grid;
|
|
}
|
|
|
|
const addRandomTile = () => {
|
|
const hiddenTiles = globals.grid.tiles.filter(tile => !tile.isVisible);
|
|
if (hiddenTiles.length <=0) return;
|
|
const index = Math.floor(Math.random() * hiddenTiles.length);
|
|
const tile = hiddenTiles[index];
|
|
tile.value = 1;
|
|
tile.isVisible = true;
|
|
}
|
|
|
|
const redraw = (grid) => {
|
|
const element = document.getElementById('output');
|
|
drawGrid(grid, element);
|
|
}
|
|
|
|
const slide = (fromIndex, toIndex, vector, advance, swap, merge, testForDone) => {
|
|
let didIDoIt = 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;
|
|
didIDoIt = 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;
|
|
didIDoIt = true;
|
|
} else {
|
|
// Merge tiles
|
|
merge(fromTile, toTile);
|
|
// Advance fromIndex
|
|
fromIndex = advance(fromIndex);
|
|
hasMergedPrevious = true;
|
|
didIDoIt = true;
|
|
}
|
|
} else {
|
|
// Advance toIndex
|
|
toIndex = advance(toIndex);
|
|
hasMergedPrevious = false;
|
|
}
|
|
} else {
|
|
fromIndex = advance(fromIndex);
|
|
}
|
|
}
|
|
}
|
|
return didIDoIt;
|
|
}
|
|
|
|
const canSlideInDirection = (grid, direction) => {
|
|
const clonedGrid = grid.clone();
|
|
return slideInDirection(clonedGrid, direction);
|
|
}
|
|
|
|
const gameIsLost = (grid) => {
|
|
return !canSlideInDirection(grid, Direction.Up)
|
|
&& !canSlideInDirection(grid, Direction.Right)
|
|
&& !canSlideInDirection(grid, Direction.Down)
|
|
&& !canSlideInDirection(grid, Direction.Left);
|
|
}
|
|
|
|
const checkForEnd = (grid) => {
|
|
if (gameIsLost(grid)) {
|
|
alert('A loser is you!');
|
|
}
|
|
}
|
|
|
|
const makeDevGrid = (matrix) => {
|
|
const numRows = matrix.length;
|
|
const numColumns = matrix[0].length;
|
|
const initialValue = new Tile(0, false, new GridPosition(0, 0));
|
|
const grid = makeGrid(numRows, numColumns, initialValue);
|
|
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 {
|
|
|
|
};
|
|
|
|
class Grid {
|
|
|
|
};
|
|
|
|
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 {
|
|
#originalPosition_;
|
|
value;
|
|
isVisible;
|
|
get originalPosition() {
|
|
return {...this.#originalPosition_};
|
|
}
|
|
constructor (value, isVisible, originalPosition) {
|
|
this.value = value;
|
|
this.isVisible = isVisible;
|
|
this.originalPosition_ = {...originalPosition};
|
|
}
|
|
equals (other) {
|
|
return other !== null && other !== undefined
|
|
&& this.value === other.value
|
|
&& this.isVisible === other.isVisible
|
|
&& this.#originalPosition_.row === other.#originalPosition_.row
|
|
&& this.#originalPosition_.column === other.#originalPosition_.column;
|
|
}
|
|
toString() {
|
|
return `{ value: ${this.value}, originalPosition: { row: ${this.#originalPosition_.row}, column: ${this.#originalPosition_.column} }, isVisible: ${this.isVisible ? "true" : "false"} }`;
|
|
}
|
|
};
|