@april-cs-npm/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/README.md CHANGED
@@ -1,3 +1,83 @@
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
+ # @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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
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
+ }
@@ -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
@@ -0,0 +1,4 @@
1
+ export { move } from './move.js';
2
+ export { spawn } from './spawn.js';
3
+ export { hasMoves } from './hasMoves.js';
4
+ export { score } from './score.js';
package/src/move.js ADDED
@@ -0,0 +1,64 @@
1
+ function mergeRow(row) {
2
+ // STEP 1: keep only the non-zero numbers
3
+ const nums = row.filter((n) => n !== 0);
4
+
5
+ // STEP 2: merge equal neighbours once
6
+ const merged = [];
7
+ let i = 0;
8
+ while (i < nums.length) {
9
+ if (nums[i] === nums[i + 1]) {
10
+ merged.push(nums[i] + nums[i + 1]);
11
+ i += 2; // skip both so they can't merge again
12
+ } else {
13
+ merged.push(nums[i]);
14
+ i += 1;
15
+ }
16
+ }
17
+
18
+ // STEP 3: pad with zeros until it's 4 long again
19
+ while (merged.length < 4) {
20
+ merged.push(0);
21
+ }
22
+
23
+ return merged;
24
+ }
25
+
26
+ function slideLeft(board) {
27
+ return board.map((row) => mergeRow(row));
28
+ }
29
+
30
+
31
+ function slideRight(board) {
32
+ // 1. reverse each row
33
+ const reversed = board.map((row) => row.slice().reverse());
34
+
35
+ // 2. slide left (reuse what you already built!)
36
+ const slid = slideLeft(reversed);
37
+
38
+ // 3. reverse each row back
39
+ return slid.map((row) => row.slice().reverse());
40
+ }
41
+
42
+ function transpose(board) {
43
+ return board[0].map((_, column) =>
44
+ board.map((row) => row[column]),
45
+ );
46
+ }
47
+
48
+ function move(board, direction) {
49
+ switch (direction) {
50
+ case 'left':
51
+ return slideLeft(board);
52
+ case 'right':
53
+ return slideRight(board);
54
+ case 'up':
55
+ return transpose(slideLeft(transpose(board)));
56
+ case 'down':
57
+ return transpose(slideRight(transpose(board)));
58
+ default:
59
+ throw new Error(`Invalid move direction: ${direction}`);
60
+ }
61
+ }
62
+
63
+
64
+ export { mergeRow, slideLeft, slideRight, transpose, move };
package/src/score.js ADDED
@@ -0,0 +1,50 @@
1
+ import { transpose } from './move.js';
2
+
3
+ // points earned by merges in a single row when it slides left
4
+ function rowScore(row) {
5
+ const nums = row.filter((n) => n !== 0);
6
+ let points = 0;
7
+ let i = 0;
8
+ while (i < nums.length) {
9
+ if (nums[i] === nums[i + 1]) {
10
+ points += nums[i] + nums[i + 1]; // a merge happened: add the NEW tile's value
11
+ i += 2;
12
+ } else {
13
+ i += 1;
14
+ }
15
+ }
16
+ return points;
17
+ }
18
+
19
+ // total points a move would earn in the given direction
20
+ function score(board, direction) {
21
+ // reshape the board so `direction` points LEFT, the same way move() does,
22
+ // then sum rowScore across all rows.
23
+ let rows;
24
+ switch (direction) {
25
+ case 'left':
26
+ rows = board;
27
+ break;
28
+ case 'right':
29
+ rows = board.map((row) => row.slice().reverse());
30
+ break;
31
+ case 'up':
32
+ rows = transpose(board);
33
+ break;
34
+ case 'down':
35
+ rows = transpose(board).map((row) => row.slice().reverse());
36
+ break;
37
+ default:
38
+ throw new Error(`Invalid direction: ${direction}`);
39
+ }
40
+
41
+ // add up the score from every row
42
+ let total = 0;
43
+ for (const row of rows) {
44
+ total += rowScore(row);
45
+ }
46
+ return total;
47
+ }
48
+
49
+
50
+ export { score };
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 };