Compare commits
12 Commits
main
...
6e23a1358b
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e23a1358b | |||
| d2c8566f9c | |||
| 8056f17923 | |||
| 4b3934999c | |||
| 18e6686e59 | |||
| 0129e43b8e | |||
| 12557bfdbe | |||
| fa41124102 | |||
| cb914f53df | |||
| ff7a473e2e | |||
| b96684f2ea | |||
| 3294002c80 |
43
index.html
Normal file
43
index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Grid Demo</title>
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style type="text/css">
|
||||||
|
table {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
td.hidden {
|
||||||
|
background-color: #f88;
|
||||||
|
}
|
||||||
|
td.visible {
|
||||||
|
background-color: #8f8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<fieldset>
|
||||||
|
<legend title="asdf">Grid</legend>
|
||||||
|
<div id="output"></div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
470
main.js
Normal file
470
main.js
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ${direction.name}.`);
|
||||||
|
}
|
||||||
|
checkForEnd(gameBoard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const redraw = (gameBoard) => {
|
||||||
|
const element = document.getElementById('output');
|
||||||
|
|
||||||
|
let text = "<table cellspacing=\"0\">\n";
|
||||||
|
for (let rowIndex = 0; rowIndex < gameBoard.numRows; rowIndex++) {
|
||||||
|
text += " <tr>\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";
|
||||||
|
}
|
||||||
|
text += "</table>";
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
setup();
|
||||||
Reference in New Issue
Block a user