@aminuose/arcade-core 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +78 -2
- package/package.json +11 -4
- package/src/index.js +3 -0
- package/src/mergeRow.js +29 -0
- package/src/move.js +45 -0
- package/src/moves.js +26 -0
- package/src/spawn.js +26 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `move(board, direction)` slides and merges tiles in any direction and returns a new board.
|
|
8
|
+
- `spawn(board, rng?)` places a 2 or 4 in a random empty cell, with an optional `rng` for repeatable results.
|
|
9
|
+
- `hasMoves(board)` reports whether the game can continue.
|
|
10
|
+
- `canMove(board, direction)` reports whether a single direction would change the board.
|
|
11
|
+
- `slide(board, direction)` does the same as `move` and also returns the points earned by the merges.
|
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aminuose/arcade-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pure game logic for 2048. Every function takes a board and returns a new one, with no side effects. The package does not import Svelte, does not touch the DOM, and runs under plain Node or in any browser.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @aminuose/arcade-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { move, spawn, hasMoves, canMove } from '@aminuose/arcade-core';
|
|
15
|
+
|
|
16
|
+
let board = [
|
|
17
|
+
[0, 0, 0, 0],
|
|
18
|
+
[0, 0, 0, 0],
|
|
19
|
+
[0, 0, 0, 0],
|
|
20
|
+
[0, 0, 0, 0],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
board = spawn(spawn(board));
|
|
24
|
+
|
|
25
|
+
if (canMove(board, 'left')) {
|
|
26
|
+
board = spawn(move(board, 'left'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!hasMoves(board)) {
|
|
30
|
+
console.log('Game over');
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Board and directions
|
|
35
|
+
|
|
36
|
+
A board is a 4×4 array of rows. `0` is an empty cell and every other value is a tile.
|
|
37
|
+
Directions are `'left'`, `'right'`, `'up'`, and `'down'`.
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `move(board, direction)`
|
|
42
|
+
|
|
43
|
+
Slides every tile as far as it can go in `direction`, merges equal neighbours once each, and returns the new board. The input board is not modified. Throws if `direction` is not one of the four above.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
move([[2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 'left')[0];
|
|
47
|
+
// [4, 4, 0, 0]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A tile merges at most once per move, so `2 2 2 2` becomes `4 4` and `4 4 8` becomes `8 8`.
|
|
51
|
+
|
|
52
|
+
### `spawn(board, rng?)`
|
|
53
|
+
|
|
54
|
+
Puts a new `2` (90% of the time) or `4` (10%) in a random empty cell and returns the new board. If the board is full, an unchanged copy is returned.
|
|
55
|
+
|
|
56
|
+
`rng` is an optional function that returns a number in `[0, 1)`. It defaults to `Math.random`. It is called once to pick the cell and once to pick the value, so the same sequence of values always produces the same board.
|
|
57
|
+
|
|
58
|
+
### `hasMoves(board)`
|
|
59
|
+
|
|
60
|
+
Returns `true` if any direction would still change the board, and `false` when the game is over.
|
|
61
|
+
|
|
62
|
+
### `canMove(board, direction)`
|
|
63
|
+
|
|
64
|
+
Returns `true` if sliding in `direction` would change the board, and `false` if nothing would happen. A move that changes nothing should not spawn a new tile.
|
|
65
|
+
|
|
66
|
+
### `slide(board, direction)`
|
|
67
|
+
|
|
68
|
+
Does the same work as `move` and also reports the points earned. Returns `{ board, gained }`, where `gained` is the total of every tile created by a merge.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
slide([[2, 2, 4, 4], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 'left').gained;
|
|
72
|
+
// 4 + 8 = 12
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
npm test
|
|
79
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aminuose/arcade-core",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pure game logic for 2048: move, spawn, hasMoves, and canMove.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": "./src/index.js",
|
|
8
|
+
"files": ["src", "CHANGELOG.md"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["2048", "game", "arcade"]
|
|
13
|
+
}
|
package/src/index.js
ADDED
package/src/mergeRow.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slides one row toward index 0 and merges equal neighbours.
|
|
3
|
+
* Each tile merges at most once per call.
|
|
4
|
+
*
|
|
5
|
+
* @param {number[]} row
|
|
6
|
+
* @returns {{ row: number[], gained: number }} the new row and the points earned
|
|
7
|
+
*/
|
|
8
|
+
export function mergeRow(row) {
|
|
9
|
+
const tiles = row.filter((value) => value !== 0);
|
|
10
|
+
const merged = [];
|
|
11
|
+
let gained = 0;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
14
|
+
if (tiles[i] === tiles[i + 1]) {
|
|
15
|
+
const value = tiles[i] * 2;
|
|
16
|
+
merged.push(value);
|
|
17
|
+
gained += value;
|
|
18
|
+
i++;
|
|
19
|
+
} else {
|
|
20
|
+
merged.push(tiles[i]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
while (merged.length < row.length) {
|
|
25
|
+
merged.push(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { row: merged, gained };
|
|
29
|
+
}
|
package/src/move.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mergeRow } from './mergeRow.js';
|
|
2
|
+
|
|
3
|
+
const DIRECTIONS = ['left', 'right', 'up', 'down'];
|
|
4
|
+
|
|
5
|
+
const transpose = (board) => board[0].map((_, col) => board.map((row) => row[col]));
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Same as move, but also reports the points earned by the merges.
|
|
9
|
+
*
|
|
10
|
+
* @param {number[][]} board
|
|
11
|
+
* @param {'left' | 'right' | 'up' | 'down'} direction
|
|
12
|
+
* @returns {{ board: number[][], gained: number }}
|
|
13
|
+
*/
|
|
14
|
+
export function slide(board, direction) {
|
|
15
|
+
if (!DIRECTIONS.includes(direction)) {
|
|
16
|
+
throw new Error(`Unknown direction: ${direction}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const vertical = direction === 'up' || direction === 'down';
|
|
20
|
+
const reversed = direction === 'right' || direction === 'down';
|
|
21
|
+
|
|
22
|
+
let lines = vertical ? transpose(board) : board;
|
|
23
|
+
let gained = 0;
|
|
24
|
+
|
|
25
|
+
lines = lines.map((line) => {
|
|
26
|
+
const input = reversed ? [...line].reverse() : line;
|
|
27
|
+
const result = mergeRow(input);
|
|
28
|
+
gained += result.gained;
|
|
29
|
+
return reversed ? result.row.reverse() : result.row;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return { board: vertical ? transpose(lines) : lines, gained };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Slides every tile as far as it can go in the given direction and merges
|
|
37
|
+
* equal neighbours once each. Returns a new board; the input is not modified.
|
|
38
|
+
*
|
|
39
|
+
* @param {number[][]} board
|
|
40
|
+
* @param {'left' | 'right' | 'up' | 'down'} direction
|
|
41
|
+
* @returns {number[][]}
|
|
42
|
+
*/
|
|
43
|
+
export function move(board, direction) {
|
|
44
|
+
return slide(board, direction).board;
|
|
45
|
+
}
|
package/src/moves.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { move } from './move.js';
|
|
2
|
+
|
|
3
|
+
const DIRECTIONS = ['left', 'right', 'up', 'down'];
|
|
4
|
+
|
|
5
|
+
const sameBoard = (a, b) => a.every((row, r) => row.every((value, c) => value === b[r][c]));
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether sliding in the given direction would change the board.
|
|
9
|
+
*
|
|
10
|
+
* @param {number[][]} board
|
|
11
|
+
* @param {'left' | 'right' | 'up' | 'down'} direction
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export function canMove(board, direction) {
|
|
15
|
+
return !sameBoard(board, move(board, direction));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether any direction would still change the board. False means game over.
|
|
20
|
+
*
|
|
21
|
+
* @param {number[][]} board
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
export function hasMoves(board) {
|
|
25
|
+
return DIRECTIONS.some((direction) => canMove(board, direction));
|
|
26
|
+
}
|
package/src/spawn.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const FOUR_CHANCE = 0.1;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Puts a new 2 (90%) or 4 (10%) in a random empty cell. Returns a new board;
|
|
5
|
+
* a full board is returned as an unchanged copy.
|
|
6
|
+
*
|
|
7
|
+
* @param {number[][]} board
|
|
8
|
+
* @param {() => number} [rng] returns a number in [0, 1), defaults to Math.random
|
|
9
|
+
* @returns {number[][]}
|
|
10
|
+
*/
|
|
11
|
+
export function spawn(board, rng = Math.random) {
|
|
12
|
+
const next = board.map((row) => [...row]);
|
|
13
|
+
const empty = [];
|
|
14
|
+
|
|
15
|
+
next.forEach((row, r) => {
|
|
16
|
+
row.forEach((value, c) => {
|
|
17
|
+
if (value === 0) empty.push([r, c]);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (empty.length === 0) return next;
|
|
22
|
+
|
|
23
|
+
const [r, c] = empty[Math.floor(rng() * empty.length)];
|
|
24
|
+
next[r][c] = rng() < FOUR_CHANCE ? 4 : 2;
|
|
25
|
+
return next;
|
|
26
|
+
}
|