Compare commits

10 Commits

2 changed files with 437 additions and 233 deletions

View File

@@ -29,12 +29,15 @@ td.visible {
</fieldset>
<section>
<button id="btnLeft" onclick="btnLeft_onClick()">Left</button>
<button id="btnUp" onclick="btnUp_onClick()">Up</button>
<button id="btnDown" onclick="btnDown_onClick()">Down</button>
<button id="btnRight" onclick="btnRight_onClick()">Right</button>
<button id="btnLeft">Left</button>
<button id="btnUp">Up</button>
<button id="btnDown">Down</button>
<button id="btnRight">Right</button>
</section>
<section>
<button id="btnStart">Start</button>
<button id="btnRedraw">Redraw</button>
</section>
<button onclick="btnGo_onClick();" id="btnGo">Go</button>
<script src="main.js"></script>
</body>
</html>

627
main.js
View File

@@ -1,72 +1,119 @@
const globals = {
grid: null,
tiles: [],
let gameBoard;
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 setup = () => {
document.addEventListener('keydown', ev => {
switch (ev.key) {
case 'w':
case 'ArrowUp':
playMove(gameBoard, Direction.Up);
break;
case 'a':
case 'ArrowLeft':
playMove(gameBoard, Direction.Left);
break;
case 's':
case 'ArrowDown':
playMove(gameBoard, Direction.Down);
break;
case 'd':
case 'ArrowRight':
playMove(gameBoard, Direction.Right);
break;
default:
console.log(ev.key);
break;
}
});
binding = new Binding(['btnStart', 'btnUp', 'btnRight', 'btnDown', 'btnLeft', 'btnRedraw']);
const start = () => {
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);
};
binding.btnStart.on('click', start);
const btnGo_onClick = () => {
globals.grid = makeGrid(4, 4, {value: 0, visible: false});
addRandomTile();
redraw(globals.grid);
// drawGrid(globals.grid, document.getElementById('output'));
// alert("Go");
binding.btnUp.on('click', () => {
playMove(gameBoard, Direction.Up);
});
binding.btnRight.on('click', () => {
playMove(gameBoard, Direction.Right);
});
binding.btnDown.on('click', () => {
playMove(gameBoard, Direction.Down);
});
binding.btnLeft.on('click', () => {
playMove(gameBoard, Direction.Left);
});
binding.btnRedraw.on('click', () => {
redraw(gameBoard);
});
start();
}
const btnUp_onClick = () => {
const grid = globals.grid;
if (canSlideUp(grid)) {
slideUp(grid);
addRandomTile();
redraw(grid)
class ElementBinding {
#element;
constructor (element) {
this.#element = element;
}
on(event, action) {
// TODO(tom): Test multiple bindings.
if (this.#element != null) {
this.#element.addEventListener(event, action);
}
}
}
class Binding {
constructor(elementIds) {
elementIds.forEach(i =>
this[i] = new ElementBinding(document.getElementById(i))
);
}
}
// 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 Up.');
// alert(`Unable to slide ${direction.name}.`);
}
// alert('Up');
checkForEnd(gameBoard);
}
const btnDown_onClick = () => {
const grid = globals.grid;
if (canSlideDown(grid)) {
slideDown(grid);
addRandomTile();
redraw(grid)
} else {
alert('Unable to slide Down.');
}
// alert('Down');
}
const redraw = (gameBoard) => {
const element = document.getElementById('output');
const btnLeft_onClick = () => {
const grid = globals.grid;
if (canSlideLeft(grid)) {
slideLeft(grid);
addRandomTile();
redraw(grid)
} else {
alert('Unable to slide Left.');
}
// alert('Left');
}
const btnRight_onClick = () => {
const grid = globals.grid;
if (canSlideRight(grid)) {
slideRight(grid);
addRandomTile();
redraw(grid)
} else {
alert('Unable to slide Right.');
}
// alert('Right');
}
const drawGrid = (grid, element) => {
// grid is our grid
// element is our div
let text = "<table cellspacing=\"0\">\n";
for (const row of grid.rows) {
for (let rowIndex = 0; rowIndex < gameBoard.numRows; rowIndex++) {
text += " <tr>\n";
for (const tile of row) {
text += " <td title=\"(" + tile.originalLocation.row + ", " + tile.originalLocation.column + ")\"class=\"" + (tile.visible ? "visible " : "hidden ") + "\">" + tile.value + "</td>\n";
for (let columnIndex = 0; columnIndex < gameBoard.numColumns; columnIndex++) {
const tile = gameBoard.getTile(rowIndex, columnIndex);
text += " <td class=\"" + (tile.isVisible ? "visible " : "hidden ") + "\">" + tile.value + "</td>\n";
}
text += " </tr>\n";
}
@@ -74,196 +121,350 @@ const drawGrid = (grid, element) => {
element.innerHTML = text;
}
const makeGrid = (numRows, numColumns, initialValue) => {
const rows = [];
for (rowIndex = 0; rowIndex < numRows; rowIndex++) {
const row = [];
for (columnIndex = 0; columnIndex < numColumns; columnIndex++) {
position = {row: rowIndex, column: columnIndex};
const tile = makeTile(initialValue.value, initialValue.visible, rowIndex, columnIndex);
globals.tiles.push(tile);
row.push(tile);
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;
}
rows.push(row);
}
const grid = {
rows,
numRows,
numColumns,
getTile: (row, column) => {
return rows[row][column];
},
setTile: (row, column, tile) => {
rows[row][column] = tile;
},
swapTiles: (fromRow, fromColumn, toRow, toColumn) => {
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);
},
};
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
*/
const makeTile = (value, visible, row, column) => {
return {
value,
visible,
originalLocation: {
row,
column,
},
equals: (other) => {
return this.value == other.value
&& this.visible == other.visible
&& this.originalLocation.row == other.originalLocation.row
&& this.originalLocation.column == other.originalLocation.column;
class GameBoard {
#grid;
get numColumns() {
return this.#grid.numColumns;
}
};
};
const addRandomTile = () => {
const hiddenTiles = globals.tiles.filter(tile => !tile.visible);
if (hiddenTiles.length <=0) return;
const index = Math.floor(Math.random() * hiddenTiles.length);
const tile = hiddenTiles[index];
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.visible = true;
tile.isVisible = true;
}
const redraw = (grid) => {
const element = document.getElementById('output');
drawGrid(grid, element);
slideInDirection (direction) {
return GameBoard.#slideInDirection_impl(direction, this.#grid);
}
const slideUp = (grid) => {
for (let columnIndex = 0; columnIndex < grid.numColumns; columnIndex++) {
lastIndex = -1;
for (let rowIndex = 0; rowIndex < grid.numRows; rowIndex++) {
const tile = grid.getTile(rowIndex, columnIndex);
if (tile.visible) {
lastIndex++;
if (lastIndex != rowIndex) {
grid.swapTiles(rowIndex, columnIndex, lastIndex, columnIndex);
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);
}
}
const slideLeft = (grid) => {
for (let rowIndex = 0; rowIndex < grid.numRows; rowIndex++) {
lastIndex = -1;
for (let columnIndex = 0; columnIndex < grid.numColumns; columnIndex++) {
const tile = grid.getTile(rowIndex, columnIndex);
if (tile.visible) {
lastIndex++;
if (lastIndex != columnIndex) {
grid.swapTiles(rowIndex, columnIndex, rowIndex, lastIndex);
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;
}
}
const slideDown = (grid) => {
for (let columnIndex = 0; columnIndex < grid.numColumns; columnIndex++) {
lastIndex = grid.numRows;
for (let rowIndex = grid.numRows - 1; rowIndex >= 0; rowIndex -= 1) {
const tile = grid.getTile(rowIndex, columnIndex);
if (tile.visible) {
lastIndex--;
if (lastIndex != rowIndex) {
grid.swapTiles(rowIndex, columnIndex, lastIndex, columnIndex);
}
}
}
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);
}
}
const slideRight = (grid) => {
for (let rowIndex = 0; rowIndex < grid.numRows; rowIndex++) {
lastIndex = grid.numColumns;
for (let columnIndex = grid.numColumns - 1; columnIndex >= 0; columnIndex -= 1) {
const tile = grid.getTile(rowIndex, columnIndex);
if (tile.visible) {
lastIndex--;
if (lastIndex != columnIndex) {
grid.swapTiles(rowIndex, columnIndex, rowIndex, lastIndex);
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} }`;
}
}
const canSlideUp = (grid) => {
canIDoIt = false;
for (let columnIndex = 0; columnIndex < grid.numColumns; columnIndex++) {
lastIndex = -1;
for (let rowIndex = 0; rowIndex < grid.numRows; rowIndex++) {
const tile = grid.getTile(rowIndex, columnIndex);
if (tile.visible) {
lastIndex++;
if (lastIndex != rowIndex) {
canIDoIt = true;
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"} }`;
}
}
return canIDoIt;
}
};
const canSlideLeft = (grid) => {
let canIDoIt = false;
for (let rowIndex = 0; rowIndex < grid.numRows; rowIndex++) {
lastIndex = -1;
for (let columnIndex = 0; columnIndex < grid.numColumns; columnIndex++) {
const tile = grid.getTile(rowIndex, columnIndex);
if (tile.visible) {
lastIndex++;
if (lastIndex != columnIndex) {
canIDoIt = true;
}
}
}
}
return canIDoIt;
}
const canSlideDown = (grid) => {
let canIDoIt = false;
for (let columnIndex = 0; columnIndex < grid.numColumns; columnIndex++) {
lastIndex = grid.numRows;
for (let rowIndex = grid.numRows - 1; rowIndex >= 0; rowIndex -= 1) {
const tile = grid.getTile(rowIndex, columnIndex);
if (tile.visible) {
lastIndex--;
if (lastIndex != rowIndex) {
canIDoIt = true;
}
}
}
}
return canIDoIt;
}
const canSlideRight = (grid) => {
let canIDoIt = false;
for (let rowIndex = 0; rowIndex < grid.numRows; rowIndex++) {
lastIndex = grid.numColumns;
for (let columnIndex = grid.numColumns - 1; columnIndex >= 0; columnIndex -= 1) {
const tile = grid.getTile(rowIndex, columnIndex);
if (tile.visible) {
lastIndex--;
if (lastIndex != columnIndex) {
canIDoIt = true;
}
}
}
}
return canIDoIt;
}
class Settings {
merge;
advance;
testForDone;
getInitialFromIndex;
getInitialToIndex;
getVector;
getLength;
swap;
};
setup();