@zixial3/arcade-core 0.0.0-stage → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zixiao Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,93 @@
1
- # Temporary Holding Version
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.
1
+ # @zixial3/arcade-core
2
+
3
+ Pure game logic for **2048**: no framework, no DOM, no globals. Works in Node and in the browser.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @zixial3/arcade-core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ A board is a square array of numbers where `0` means empty.
14
+
15
+ ```js
16
+ import {
17
+ move,
18
+ spawn,
19
+ hasMoves,
20
+ canMove,
21
+ startBoard,
22
+ } from "@zixial3/arcade-core";
23
+
24
+ let board = startBoard(); // empty 4×4 board with two starting tiles
25
+
26
+ if (canMove(board, "left")) {
27
+ board = move(board, "left"); // slide and merge
28
+ board = spawn(board); // add a 2 or 4 in a random empty cell
29
+ }
30
+
31
+ if (!hasMoves(board)) console.log("game over");
32
+ ```
33
+
34
+ Every function returns a **new** board and never mutates its input.
35
+
36
+ ## API
37
+
38
+ | Function | Returns |
39
+ | ------------------------------------- | ----------------------------------------------------------------------------------------------- |
40
+ | `move(board, direction)` | New board after sliding every tile towards `direction` and merging equal neighbours once. |
41
+ | `spawn(board, rng?)` | New board with a 2 (90%) or 4 (10%) in a random empty cell. `rng` defaults to `Math.random`. |
42
+ | `hasMoves(board)` | `true` if any direction would change the board; `false` means game over. |
43
+ | `canMove(board, direction)` | `true` if moving in `direction` would change the board. |
44
+ | `moveWithDetails(board, direction)` | `{ board, moves, merges, points, changed }`: where every tile went, for animations and scoring. |
45
+ | `startBoard(rng?, size?)` | Empty board (default 4×4) with two starting tiles. |
46
+ | `createBoard(size?)` | Empty board. |
47
+ | `createRng(seed)` | Seeded random number generator to pass as `rng`. Its `state` resumes the same sequence. |
48
+ | `emptyCells(board)`, `maxTile(board)` | Helpers. |
49
+
50
+ `direction` is `'up' | 'down' | 'left' | 'right'`.
51
+
52
+ ### Merging rules
53
+
54
+ Each tile merges at most once per move:
55
+
56
+ ```js
57
+ move([[2, 2, 2, 2], ...], 'left'); // → [[4, 4, 0, 0], ...]
58
+ move([[4, 4, 8, 0], ...], 'left'); // → [[8, 8, 0, 0], ...]
59
+ ```
60
+
61
+ ### Scoring
62
+
63
+ `moveWithDetails` reports `points`, the sum of every tile created by a merge:
64
+
65
+ ```js
66
+ const { board, points } = moveWithDetails([[2, 2, 4, 4], ...], 'left');
67
+ // board[0] → [4, 8, 0, 0], points → 12
68
+ ```
69
+
70
+ ### Seeded games
71
+
72
+ ```js
73
+ import { createRng, startBoard, spawn } from "@zixial3/arcade-core";
74
+
75
+ const rng = createRng(1234);
76
+ let board = startBoard(rng); // same starting tiles for seed 1234, every time
77
+ board = spawn(move(board, "up"), rng); // same spawn for the same move
78
+
79
+ const saved = rng.state; // save this…
80
+ const resumed = createRng(saved); // …and pick up exactly where you left off
81
+ ```
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ npm install
87
+ npm test # vitest
88
+ npm run build # TypeScript → dist/
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pure game logic for 2048.
3
+ *
4
+ * A board is a square grid of numbers where 0 means "empty". Every function
5
+ * returns a new board and never mutates its input, touches the DOM, or reads
6
+ * global state. Randomness is injected through an `rng` argument.
7
+ */
8
+ export type Board = number[][];
9
+ export type Direction = 'up' | 'down' | 'left' | 'right';
10
+ export declare const DIRECTIONS: readonly Direction[];
11
+ /** A function returning a float in [0, 1), like `Math.random`. */
12
+ export type Rng = () => number;
13
+ export interface Position {
14
+ row: number;
15
+ col: number;
16
+ }
17
+ /** Where one tile travelled during a move. */
18
+ export interface TileMove {
19
+ from: Position;
20
+ to: Position;
21
+ }
22
+ /** A cell where two tiles combined into one. */
23
+ export interface Merge {
24
+ at: Position;
25
+ value: number;
26
+ }
27
+ export interface MoveResult {
28
+ board: Board;
29
+ /** One entry for every tile that was on the board before the move. */
30
+ moves: TileMove[];
31
+ merges: Merge[];
32
+ /** Sum of every tile created by a merge. */
33
+ points: number;
34
+ changed: boolean;
35
+ }
36
+ /** Creates an empty `size` × `size` board. */
37
+ export declare function createBoard(size?: number): Board;
38
+ /**
39
+ * Slides every tile towards `direction`, merging equal neighbours once each,
40
+ * and reports where every tile went.
41
+ */
42
+ export declare function moveWithDetails(board: Board, direction: Direction): MoveResult;
43
+ /** Slides and merges every tile in `direction` and returns the new board. */
44
+ export declare function move(board: Board, direction: Direction): Board;
45
+ /** True if moving in `direction` would change the board. */
46
+ export declare function canMove(board: Board, direction: Direction): boolean;
47
+ /** True if any direction would still change the board; false means game over. */
48
+ export declare function hasMoves(board: Board): boolean;
49
+ /** Positions of every empty cell, in reading order. */
50
+ export declare function emptyCells(board: Board): Position[];
51
+ /**
52
+ * Puts a 2 (90%) or a 4 (10%) in a random empty cell and returns the new board.
53
+ * A full board is returned unchanged (as a copy).
54
+ */
55
+ export declare function spawn(board: Board, rng?: Rng): Board;
56
+ /** The largest tile on the board. */
57
+ export declare function maxTile(board: Board): number;
58
+ /** An empty board with two starting tiles. */
59
+ export declare function startBoard(rng?: Rng, size?: number): Board;
60
+ export { createRng, type SeededRng } from './rng.js';
package/dist/index.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Pure game logic for 2048.
3
+ *
4
+ * A board is a square grid of numbers where 0 means "empty". Every function
5
+ * returns a new board and never mutates its input, touches the DOM, or reads
6
+ * global state. Randomness is injected through an `rng` argument.
7
+ */
8
+ export const DIRECTIONS = ['up', 'down', 'left', 'right'];
9
+ /** Creates an empty `size` × `size` board. */
10
+ export function createBoard(size = 4) {
11
+ return Array.from({ length: size }, () => Array(size).fill(0));
12
+ }
13
+ /**
14
+ * Cells of each line, ordered from the edge the tiles slide towards.
15
+ * Moving left, row 0 is read left to right; moving down, column 0 is read
16
+ * bottom to top; and so on.
17
+ */
18
+ function linesFor(size, direction) {
19
+ const indices = [...Array(size).keys()];
20
+ const reversed = [...indices].reverse();
21
+ switch (direction) {
22
+ case 'left':
23
+ return indices.map((row) => indices.map((col) => ({ row, col })));
24
+ case 'right':
25
+ return indices.map((row) => reversed.map((col) => ({ row, col })));
26
+ case 'up':
27
+ return indices.map((col) => indices.map((row) => ({ row, col })));
28
+ case 'down':
29
+ return indices.map((col) => reversed.map((row) => ({ row, col })));
30
+ }
31
+ }
32
+ /**
33
+ * Slides every tile towards `direction`, merging equal neighbours once each,
34
+ * and reports where every tile went.
35
+ */
36
+ export function moveWithDetails(board, direction) {
37
+ const next = createBoard(board.length);
38
+ const moves = [];
39
+ const merges = [];
40
+ let points = 0;
41
+ for (const line of linesFor(board.length, direction)) {
42
+ let write = 0;
43
+ // Index in `line` of the last placed tile that may still merge.
44
+ let mergeable = -1;
45
+ for (const from of line) {
46
+ const value = board[from.row][from.col];
47
+ if (value === 0)
48
+ continue;
49
+ const previous = mergeable >= 0 ? line[mergeable] : undefined;
50
+ if (previous && next[previous.row][previous.col] === value) {
51
+ const merged = value * 2;
52
+ next[previous.row][previous.col] = merged;
53
+ moves.push({ from, to: previous });
54
+ merges.push({ at: previous, value: merged });
55
+ points += merged;
56
+ mergeable = -1;
57
+ }
58
+ else {
59
+ const to = line[write];
60
+ next[to.row][to.col] = value;
61
+ moves.push({ from, to });
62
+ mergeable = write;
63
+ write += 1;
64
+ }
65
+ }
66
+ }
67
+ const changed = moves.some(({ from, to }) => from.row !== to.row || from.col !== to.col);
68
+ return { board: next, moves, merges, points, changed };
69
+ }
70
+ /** Slides and merges every tile in `direction` and returns the new board. */
71
+ export function move(board, direction) {
72
+ return moveWithDetails(board, direction).board;
73
+ }
74
+ /** True if moving in `direction` would change the board. */
75
+ export function canMove(board, direction) {
76
+ return moveWithDetails(board, direction).changed;
77
+ }
78
+ /** True if any direction would still change the board; false means game over. */
79
+ export function hasMoves(board) {
80
+ return DIRECTIONS.some((direction) => canMove(board, direction));
81
+ }
82
+ /** Positions of every empty cell, in reading order. */
83
+ export function emptyCells(board) {
84
+ const cells = [];
85
+ board.forEach((row, r) => row.forEach((value, c) => {
86
+ if (value === 0)
87
+ cells.push({ row: r, col: c });
88
+ }));
89
+ return cells;
90
+ }
91
+ /**
92
+ * Puts a 2 (90%) or a 4 (10%) in a random empty cell and returns the new board.
93
+ * A full board is returned unchanged (as a copy).
94
+ */
95
+ export function spawn(board, rng = Math.random) {
96
+ const next = board.map((row) => [...row]);
97
+ const cells = emptyCells(board);
98
+ if (cells.length === 0)
99
+ return next;
100
+ const cell = cells[Math.floor(rng() * cells.length)];
101
+ next[cell.row][cell.col] = rng() < 0.9 ? 2 : 4;
102
+ return next;
103
+ }
104
+ /** The largest tile on the board. */
105
+ export function maxTile(board) {
106
+ return Math.max(0, ...board.flat());
107
+ }
108
+ /** An empty board with two starting tiles. */
109
+ export function startBoard(rng = Math.random, size = 4) {
110
+ return spawn(spawn(createBoard(size), rng), rng);
111
+ }
112
+ export { createRng } from './rng.js';
package/dist/rng.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Rng } from './index.js';
2
+ /** A seeded `Rng` whose current position can be saved and resumed. */
3
+ export interface SeededRng extends Rng {
4
+ /** Pass this to `createRng` to continue the exact same sequence later. */
5
+ readonly state: number;
6
+ }
7
+ /**
8
+ * Creates a deterministic random number generator (mulberry32).
9
+ * The same seed always produces the same sequence, so the same moves
10
+ * always spawn tiles in the same places.
11
+ */
12
+ export declare function createRng(seed: number): SeededRng;
package/dist/rng.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Creates a deterministic random number generator (mulberry32).
3
+ * The same seed always produces the same sequence, so the same moves
4
+ * always spawn tiles in the same places.
5
+ */
6
+ export function createRng(seed) {
7
+ let state = seed >>> 0;
8
+ const next = () => {
9
+ state = (state + 0x6d2b79f5) >>> 0;
10
+ let t = state;
11
+ t = Math.imul(t ^ (t >>> 15), t | 1);
12
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
13
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
14
+ };
15
+ return Object.defineProperty(next, 'state', { get: () => state });
16
+ }
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
- {
2
- "name": "@zixial3/arcade-core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@zixial3/arcade-core",
3
+ "version": "1.1.0",
4
+ "description": "Pure, framework-free game logic for 2048.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "test": "vitest run",
21
+ "prepublishOnly": "npm run build && npm test",
22
+ "prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
23
+ },
24
+ "keywords": [
25
+ "2048",
26
+ "game",
27
+ "puzzle"
28
+ ],
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^7.0.2",
35
+ "vitest": "^5.1.0"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/zixial3/arcade-core.git"
40
+ },
41
+ "homepage": "https://github.com/zixial3/arcade-core#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/zixial3/arcade-core/issues"
44
+ },
45
+ "author": "Zixiao Liu"
46
+ }