@aminuose/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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 1.0.1
4
+
5
+ No changes to the game logic.
6
+
7
+ - Added `repository`, `bugs`, and `homepage` to `package.json`, so the npm page links to the GitHub repository.
8
+ - Added a Svelte 5 example to the README that shows `slide`, `canMove`, `hasMoves`, and `spawn` working together.
9
+
10
+ ## 1.0.0
11
+
12
+ Initial release.
13
+
14
+ - `move(board, direction)` slides and merges tiles in any direction and returns a new board.
15
+ - `spawn(board, rng?)` places a 2 or 4 in a random empty cell, with an optional `rng` for repeatable results.
16
+ - `hasMoves(board)` reports whether the game can continue.
17
+ - `canMove(board, direction)` reports whether a single direction would change the board.
18
+ - `slide(board, direction)` does the same as `move` and also returns the points earned by the merges.
package/README.md CHANGED
@@ -1,3 +1,116 @@
1
- # Temporary Holding Version
1
+ # @aminuose/arcade-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ## Using it in a Svelte 5 app
35
+
36
+ The package never imports Svelte, so the component below is the only place that knows about it. It keeps the board in `$state`, derives game over from it, and uses `slide` to get the points for each move.
37
+
38
+ ```svelte
39
+ <script>
40
+ import { slide, spawn, canMove, hasMoves } from '@aminuose/arcade-core';
41
+
42
+ const emptyBoard = () => Array.from({ length: 4 }, () => [0, 0, 0, 0]);
43
+ const keys = { ArrowLeft: 'left', ArrowRight: 'right', ArrowUp: 'up', ArrowDown: 'down' };
44
+
45
+ let board = $state(spawn(spawn(emptyBoard())));
46
+ let score = $state(0);
47
+ let gameOver = $derived(!hasMoves(board));
48
+
49
+ function play(direction) {
50
+ if (!canMove(board, direction)) return;
51
+ const result = slide(board, direction);
52
+ board = spawn(result.board);
53
+ score += result.gained;
54
+ }
55
+
56
+ function onkeydown(event) {
57
+ const direction = keys[event.key];
58
+ if (direction) {
59
+ event.preventDefault();
60
+ play(direction);
61
+ }
62
+ }
63
+ </script>
64
+
65
+ <svelte:window {onkeydown} />
66
+
67
+ <p>Score: {score}</p>
68
+ {#if gameOver}<p>Game over</p>{/if}
69
+ ```
70
+
71
+ ## Board and directions
72
+
73
+ A board is a 4×4 array of rows. `0` is an empty cell and every other value is a tile.
74
+ Directions are `'left'`, `'right'`, `'up'`, and `'down'`.
75
+
76
+ ## API
77
+
78
+ ### `move(board, direction)`
79
+
80
+ 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.
81
+
82
+ ```js
83
+ move([[2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 'left')[0];
84
+ // [4, 4, 0, 0]
85
+ ```
86
+
87
+ A tile merges at most once per move, so `2 2 2 2` becomes `4 4` and `4 4 8` becomes `8 8`.
88
+
89
+ ### `spawn(board, rng?)`
90
+
91
+ 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.
92
+
93
+ `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.
94
+
95
+ ### `hasMoves(board)`
96
+
97
+ Returns `true` if any direction would still change the board, and `false` when the game is over.
98
+
99
+ ### `canMove(board, direction)`
100
+
101
+ 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.
102
+
103
+ ### `slide(board, direction)`
104
+
105
+ 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.
106
+
107
+ ```js
108
+ slide([[2, 2, 4, 4], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 'left').gained;
109
+ // 4 + 8 = 12
110
+ ```
111
+
112
+ ## Development
113
+
114
+ ```sh
115
+ npm test
116
+ ```
package/package.json CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "name": "@aminuose/arcade-core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
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
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/AMINUOSE/arcade-core.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/AMINUOSE/arcade-core/issues"
19
+ },
20
+ "homepage": "https://github.com/AMINUOSE/arcade-core#readme"
21
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { move, slide } from './move.js';
2
+ export { spawn } from './spawn.js';
3
+ export { hasMoves, canMove } from './moves.js';
@@ -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
+ }