@april-cs-npm/arcade-core 0.0.0-stage → 1.0.1
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/README.md +83 -3
- package/package.json +21 -4
- package/src/hasMoves.js +24 -0
- package/src/index.js +4 -0
- package/src/move.js +65 -0
- package/src/score.js +31 -0
- package/src/spawn.js +31 -0
package/README.md
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @april-cs-npm/arcade-core
|
|
2
|
+
|
|
3
|
+
Pure JavaScript game logic for 2048. No UI, no DOM, no framework — just
|
|
4
|
+
functions that take a board and return a new board, so the same logic can run
|
|
5
|
+
in Node, in tests, or inside any app.
|
|
6
|
+
|
|
7
|
+
Built for the CS5610 arcade project.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @april-cs-npm/arcade-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## The board
|
|
16
|
+
|
|
17
|
+
A board is a 4×4 array of rows. Each cell is a number, where `0` means empty.
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const board = [
|
|
21
|
+
[2, 2, 0, 0],
|
|
22
|
+
[0, 0, 0, 0],
|
|
23
|
+
[4, 0, 0, 0],
|
|
24
|
+
[4, 0, 0, 0],
|
|
25
|
+
];
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A direction is one of: `"left"`, `"right"`, `"up"`, `"down"`.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { move, spawn, hasMoves, score } from '@april-cs-npm/arcade-core';
|
|
34
|
+
|
|
35
|
+
// Slide and merge tiles. Returns a NEW board; the input is never changed.
|
|
36
|
+
const afterMove = move(board, 'left');
|
|
37
|
+
|
|
38
|
+
// How many points that move earned (the sum of tiles created by merges).
|
|
39
|
+
const points = score(board, 'left');
|
|
40
|
+
|
|
41
|
+
// Drop a new 2 or 4 into a random empty cell. Returns a NEW board.
|
|
42
|
+
const afterSpawn = spawn(afterMove);
|
|
43
|
+
|
|
44
|
+
// Is the game still playable? false means game over.
|
|
45
|
+
const canKeepPlaying = hasMoves(afterSpawn);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### `move(board, direction)`
|
|
51
|
+
Slides every tile as far as it can go in `direction`, merging equal neighbours
|
|
52
|
+
once per move, and returns the new board. Does not mutate the input.
|
|
53
|
+
|
|
54
|
+
### `score(board, direction)`
|
|
55
|
+
Returns the points a move in `direction` would earn: the total value of every
|
|
56
|
+
tile created by a merge.
|
|
57
|
+
|
|
58
|
+
### `spawn(board, rng?)`
|
|
59
|
+
Returns a new board with one new tile (a `2` 90% of the time, a `4` 10%) placed
|
|
60
|
+
in a random empty cell. If the board is full, it is returned unchanged.
|
|
61
|
+
|
|
62
|
+
`rng` is optional and defaults to `Math.random`. Pass your own function
|
|
63
|
+
returning a number in `[0, 1)` to make spawning deterministic (useful for tests
|
|
64
|
+
and for seeded games):
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const afterSpawn = spawn(board, () => 0); // always picks the first empty cell
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `hasMoves(board)`
|
|
71
|
+
Returns `true` if any direction would change the board, and `false` when no
|
|
72
|
+
move is possible (game over).
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- Every function is pure: it returns a new board and never changes the one you
|
|
77
|
+
pass in.
|
|
78
|
+
- The package contains no Svelte and no DOM access, so it runs anywhere plain
|
|
79
|
+
JavaScript runs.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@april-cs-npm/arcade-core",
|
|
3
|
-
"version": "
|
|
4
|
-
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
|
|
5
|
+
"description": "npm package for the 2048 logic: move, spawn, hasMoves, score",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"files": ["src"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node test/core.test.js"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/aprilyanggarwood/ying-yang-arcade-core.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "April Yang",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/aprilyanggarwood/ying-yang-arcade-core/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/aprilyanggarwood/ying-yang-arcade-core#readme"
|
|
23
|
+
}
|
package/src/hasMoves.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { move } from './move.js';
|
|
2
|
+
|
|
3
|
+
// true if two boards have the same contents
|
|
4
|
+
function sameBoard(a, b) {
|
|
5
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function hasMoves(board) {
|
|
9
|
+
const directions = ['left', 'right', 'up', 'down'];
|
|
10
|
+
|
|
11
|
+
// if ANY direction changes the board, a move is still possible
|
|
12
|
+
for (const direction of directions) {
|
|
13
|
+
const moved = move(board, direction);
|
|
14
|
+
if (!sameBoard(moved, board)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// none of the four directions changed anything
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export { hasMoves };
|
package/src/index.js
ADDED
package/src/move.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function mergeRow(row) {
|
|
2
|
+
const nums = row.filter((n) => n !== 0);
|
|
3
|
+
|
|
4
|
+
const merged = [];
|
|
5
|
+
let points = 0; // track points earned in this row
|
|
6
|
+
let i = 0;
|
|
7
|
+
while (i < nums.length) {
|
|
8
|
+
if (nums[i] === nums[i + 1]) {
|
|
9
|
+
const sum = nums[i] + nums[i + 1];
|
|
10
|
+
merged.push(sum);
|
|
11
|
+
points += sum; // add the points earned from this merge
|
|
12
|
+
i += 2;
|
|
13
|
+
} else {
|
|
14
|
+
merged.push(nums[i]);
|
|
15
|
+
i += 1;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
while (merged.length < 4) {
|
|
20
|
+
merged.push(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { row: merged, points }; // return both the new row and the points earned
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function slideLeft(board) {
|
|
27
|
+
// for each row, merge it and return the new row (ignore points here)
|
|
28
|
+
return board.map((row) => mergeRow(row).row);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function slideRight(board) {
|
|
33
|
+
// 1. reverse each row
|
|
34
|
+
const reversed = board.map((row) => row.slice().reverse());
|
|
35
|
+
|
|
36
|
+
// 2. slide left (reuse what you already built!)
|
|
37
|
+
const slid = slideLeft(reversed);
|
|
38
|
+
|
|
39
|
+
// 3. reverse each row back
|
|
40
|
+
return slid.map((row) => row.slice().reverse());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function transpose(board) {
|
|
44
|
+
return board[0].map((_, column) =>
|
|
45
|
+
board.map((row) => row[column]),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function move(board, direction) {
|
|
50
|
+
switch (direction) {
|
|
51
|
+
case 'left':
|
|
52
|
+
return slideLeft(board);
|
|
53
|
+
case 'right':
|
|
54
|
+
return slideRight(board);
|
|
55
|
+
case 'up':
|
|
56
|
+
return transpose(slideLeft(transpose(board)));
|
|
57
|
+
case 'down':
|
|
58
|
+
return transpose(slideRight(transpose(board)));
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`Invalid move direction: ${direction}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export { mergeRow, slideLeft, slideRight, transpose, move };
|
package/src/score.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mergeRow, transpose } from './move.js';
|
|
2
|
+
|
|
3
|
+
// Calculate the total score for a given board and direction
|
|
4
|
+
function score(board, direction) {
|
|
5
|
+
let rows;
|
|
6
|
+
switch (direction) {
|
|
7
|
+
case 'left':
|
|
8
|
+
rows = board;
|
|
9
|
+
break;
|
|
10
|
+
case 'right':
|
|
11
|
+
rows = board.map((row) => row.slice().reverse());
|
|
12
|
+
break;
|
|
13
|
+
case 'up':
|
|
14
|
+
rows = transpose(board);
|
|
15
|
+
break;
|
|
16
|
+
case 'down':
|
|
17
|
+
rows = transpose(board).map((row) => row.slice().reverse());
|
|
18
|
+
break;
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`Invalid direction: ${direction}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
total += mergeRow(row).points; // add the points for this row
|
|
26
|
+
}
|
|
27
|
+
return total;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { score };
|
|
31
|
+
|
package/src/spawn.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// rng defaults to Math.random, but a caller can pass their own for testing/seeds
|
|
2
|
+
function spawn(board, rng = Math.random) {
|
|
3
|
+
// 1. find all empty cells, as {row, col} positions
|
|
4
|
+
const empties = [];
|
|
5
|
+
for (let r = 0; r < board.length; r++) {
|
|
6
|
+
for (let c = 0; c < board[r].length; c++) {
|
|
7
|
+
if (board[r][c] === 0) {
|
|
8
|
+
empties.push({ r, c });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 2. if the board is full, there is nowhere to spawn — return it unchanged
|
|
14
|
+
if (empties.length === 0) {
|
|
15
|
+
return board;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 3. pick a random empty cell
|
|
19
|
+
const pick = empties[Math.floor(rng() * empties.length)];
|
|
20
|
+
|
|
21
|
+
// 4. decide the new tile: 90% a 2, 10% a 4 (standard 2048)
|
|
22
|
+
const value = rng() < 0.9 ? 2 : 4;
|
|
23
|
+
|
|
24
|
+
// 5. build a NEW board with that one cell changed (no mutation)
|
|
25
|
+
const next = board.map((row) => row.slice()); // copy every row
|
|
26
|
+
next[pick.r][pick.c] = value;
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export { spawn };
|