Compare commits
12 Commits
starter/jq
...
6e23a1358b
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e23a1358b | |||
| d2c8566f9c | |||
| 8056f17923 | |||
| 4b3934999c | |||
| 18e6686e59 | |||
| 0129e43b8e | |||
| 12557bfdbe | |||
| fa41124102 | |||
| cb914f53df | |||
| ff7a473e2e | |||
| b96684f2ea | |||
| 3294002c80 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
|
||||
39
base.css
39
base.css
@@ -1,39 +0,0 @@
|
||||
body {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.box {
|
||||
border: 4px solid #28e;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.vertical-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.vertical-stack > * {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vertical-stack > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
border: 0.5px solid #558;
|
||||
background: hsl(228, 100%, 90%);
|
||||
background: linear-gradient(150deg, hsl(228, 100%, 95%) 0%, hsl(228, 100%, 80%) 100%);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: small;
|
||||
font-family: "Dosis", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
44
index.html
44
index.html
@@ -1,29 +1,43 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>My Demo</title>
|
||||
<title>Grid Demo</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta charset="utf-8">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Dosis:wght@200..800&display=swap" rel="stylesheet">
|
||||
<link type="text/css" rel="stylesheet" href="reset.css" />
|
||||
<link type="text/css" rel="stylesheet" href="base.css" />
|
||||
<link type="text/css" rel="stylesheet" href="styles.css" />
|
||||
<script src="node_modules/jquery/dist/jquery.js"></script>
|
||||
<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>
|
||||
<div class="vertical-stack">
|
||||
<fieldset id="demo" class="box">
|
||||
<legend title="asdf">My Demo</legend>
|
||||
<fieldset>
|
||||
<legend title="asdf">Grid</legend>
|
||||
<div id="output"></div>
|
||||
</fieldset>
|
||||
|
||||
<section class="box">
|
||||
<button id="btnGo" class="action-button">Go</button>
|
||||
<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>
|
||||
</div>
|
||||
<footer class="footer">copyright © 2024 <a href="https://sfcoder.me">Tom Hicks</a> licensed under <a href="https://opensource.org/license/gpl-2-0">GPL v2.0 or later</a></footer>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
475
main.js
475
main.js
@@ -1,5 +1,470 @@
|
||||
(()=>{
|
||||
jQuery(() => {
|
||||
console.log('Ready');
|
||||
})
|
||||
})();
|
||||
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();
|
||||
11
package.json
11
package.json
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "demo",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "Tom Hicks <headhunter3@gmail.com>",
|
||||
"license": "GLPv2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jquery": "^3.7.1"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
jquery@^3.7.1:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
|
||||
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==
|
||||
Reference in New Issue
Block a user