Files
base/main.js

418 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 = new Grid(4, 4, new Tile(0, 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 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 = new Grid(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 {
#grid_;
constructor() {
}
};
class Grid {
#tiles_ = [];
#rows_ = [];
#numRows_;
#numColumns_;
get tiles() { return [...this.#tiles_]; }
get numRows() { return this.#numRows_; }
get numColumns() { return this.#numColumns_; }
constructor(numRows, numColumns, initialValue) {
this.#numRows_ = numRows;
this.#numColumns_ = numColumns;
for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
const row = [];
for (let columnIndex = 0; columnIndex < this.numColumns; columnIndex++) {
const position = new GridPosition(rowIndex, columnIndex);
const tile = new Tile(initialValue.value, initialValue.isVisible, position);
this.#tiles_.push(tile);
row.push(tile);
}
this.#rows_.push(row);
}
}
clone() {
const newGrid = new Grid(this.numRows, this.numColumns, new Tile(0, false, new GridPosition(0, 0)));
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);
// TODO(tom): Set the originalPosition of newTile to be the same as oldTile.
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 `[[${t(this.getTile(0, 0))},${t(this.getTile(0, 1))},${t(this.getTile(0, 2))},${t(this.getTile(0, 3))}],
[${t(this.getTile(1, 0))},${t(this.getTile(1, 1))},${t(this.getTile(1, 2))},${t(this.getTile(1, 3))}],
[${t(this.getTile(2, 0))},${t(this.getTile(2, 1))},${t(this.getTile(2, 2))},${t(this.getTile(2, 3))}],
[${t(this.getTile(3, 0))},${t(this.getTile(3, 1))},${t(this.getTile(3, 2))},${t(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 {
#originalPosition_;
value;
isVisible;
get originalPosition() {
return {...this.#originalPosition_};
}
constructor (value, isVisible, originalPosition = new GridPosition(0, 0)) {
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"} }`;
}
};