@8bitscript/random 0.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 8BitScript contributors
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 ADDED
@@ -0,0 +1,69 @@
1
+ # @8bitscript/random
2
+
3
+ Deterministic pseudo-random generators, the same code on every target —
4
+ VIC-20, C64, PET, C128, Atari 8-bit, NES, Commander X16, MEGA65, and web.
5
+ Two, so far, at two import paths, with the same three calls:
6
+
7
+ ```bash
8
+ pnpm add @8bitscript/random
9
+ ```
10
+
11
+ ```
12
+ import { random } from "@8bitscript/random"; // the default: a 16-bit LCG
13
+ // or
14
+ import { table } from "@8bitscript/random/table"; // a precomputed lookup table
15
+
16
+ random.seed(1234);
17
+ let roll: utinyint = random.range(6) + 1; // 1..6
18
+ ```
19
+
20
+ | Call | What it does |
21
+ | --- | --- |
22
+ | `seed(value)` | Replaces the generator's whole state |
23
+ | `next()` | One step of the generator; a byte, 0-255 |
24
+ | `range(bound)` | `next() % bound` — a value from 0 up to (not including) `bound` |
25
+
26
+ `table` adds one more call, since it has no reason not to: `at(index)`
27
+ reads the table directly with no state of its own, for a program that
28
+ already keeps its own running counter (frames elapsed is the usual one).
29
+
30
+ ## Which one
31
+
32
+ | | `@8bitscript/random` | `@8bitscript/random/table` |
33
+ | --- | --- | --- |
34
+ | How `next()` works | `state = state * 25173 + 13849`, return the high byte | `TABLE[index]`, then `index++` |
35
+ | Cost per call | A 16-bit multiply and add | One indexed load |
36
+ | Period | 65536 | 256 |
37
+ | Distribution | Good, not exact | Exact over every 256 consecutive calls — the table is a permutation of 0-255 |
38
+
39
+ Reach for the default generator first. Reach for `/table` where a 6502
40
+ multiply is specifically the thing a piece of code cannot afford — several
41
+ picks in one frame, in a routine already fighting for cycles — and a
42
+ 256-call period is not a problem for what it is picking. The two are
43
+ interchangeable at the call site: switching is one import line.
44
+
45
+ **Deterministic by default, explicitly seeded — never hardware entropy.**
46
+ The root [`AGENTS.md`](../../AGENTS.md) rule is that a program's ordinary
47
+ random numbers must come from a small, seeded, fixed-state generator, and
48
+ hardware entropy (a POKEY register, SID's oscillator 3, timing jitter)
49
+ belongs behind its own separate, explicitly optional import. Both
50
+ generators here are that default. Neither reads a register on any target —
51
+ a program that wants a less predictable seed reaches for a machine's own
52
+ entropy source (`@8bitscript/atari8/random`'s POKEY counter,
53
+ `@8bitscript/c64/random`'s SID voice 3) and hands the byte it reads to
54
+ either one's `seed()`.
55
+
56
+ **Not called until you call it.** A program that never calls `seed()` gets
57
+ the same sequence every run. That is a feature for testing and for
58
+ reproducing a bug from a screenshot, not an oversight — call `seed()` once,
59
+ from whatever the program wants unpredictable (elapsed frames before the
60
+ first key press is the usual shape on a machine with no hardware entropy at
61
+ all), and every run after that differs.
62
+
63
+ **Not cryptographic, on any target.** Public, fixed steps over small state
64
+ are exactly as guessable as they sound, table included. This is for game
65
+ boards, shuffles, and enemy behaviour — nothing a program needs to keep
66
+ secret from the person playing it.
67
+
68
+ See [`src/index.8bs`](src/index.8bs) and [`src/table.8bs`](src/table.8bs)
69
+ for each generator and the reasoning behind it.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@8bitscript/random",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic pseudo-random generators, explicitly seeded, the same code on every target: a 16-bit LCG at the bare import, a precomputed lookup table at ./table. Hardware entropy stays behind its own machine-specific import (@8bitscript/atari8/random, @8bitscript/c64/random).",
5
+ "license": "MIT",
6
+ "8bitscript": {
7
+ "entry": "./src/index.8bs",
8
+ "exports": {
9
+ "./table": "./src/table.8bs"
10
+ }
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "test": "node --test"
20
+ }
21
+ }
package/src/index.8bs ADDED
@@ -0,0 +1,71 @@
1
+ // @8bitscript/random — a small, deterministic pseudo-random generator.
2
+ //
3
+ // import { random } from "@8bitscript/random";
4
+ //
5
+ // random.seed(1234);
6
+ // let roll: utinyint = random.range(6) + 1; // 1..6
7
+ //
8
+ // The root AGENTS.md rule is that "randomness must be deterministic by
9
+ // default, explicitly seeded, with small fixed state" — hardware entropy
10
+ // (a POKEY register, SID's oscillator 3, timing jitter) belongs behind its
11
+ // own separate, explicitly optional import, never silently underneath a
12
+ // program's ordinary random numbers. This package is the deterministic
13
+ // default every target can use: sixteen bits of state, one multiply-and-add
14
+ // step, nothing read from hardware. A program on the one target that does
15
+ // have hardware entropy behind its own import today — POKEY's counter,
16
+ // `@8bitscript/atari8/random` — reads a byte from it once and hands that to
17
+ // `seed()`; this file never reaches for a register itself, on any target,
18
+ // which is what lets it be the same sixteen lines on all nine.
19
+ //
20
+ // This is not the file to reach for if a program ever needs numbers a
21
+ // player cannot predict by reading the source — nothing here is
22
+ // cryptographic, on any target, any more than the hardware sources are.
23
+
24
+ let state: usmallint = 1;
25
+
26
+ export namespace random {
27
+ // Replaces the generator's whole state. A program that never calls this
28
+ // gets the same sequence on every run, starting from `state`'s own
29
+ // initialiser above — a deterministic default, not a random one nobody
30
+ // asked for. 0 is a valid seed: the additive constant below means the
31
+ // next `next()` still moves state away from it, unlike a generator
32
+ // built on multiplication or XOR alone, where an all-zero state is a
33
+ // fixed point.
34
+ function seed(value: usmallint): void {
35
+ state = value;
36
+ }
37
+
38
+ // One step of a 16-bit linear congruential generator: `state = state *
39
+ // A + C`, wrapping the way `usmallint` arithmetic always does, which is
40
+ // exactly the modulus a 16-bit LCG wants (mod 65536). A and C are the
41
+ // constants several 8-bit BASICs used for their own RND — coprime with
42
+ // 65536 and long enough for a game board or a shuffled deck.
43
+ //
44
+ // The result is the *high* byte of the new state, not the low one: the
45
+ // low byte of a LCG this size cycles with a much shorter period (16 for
46
+ // A - 1 divisible by 4, here a period of 4 in the very lowest bit) and
47
+ // fails an obvious pattern check almost immediately, while the high
48
+ // byte carries the full 65536-state period. `state >> 8` costs one
49
+ // instruction on the 6502 and is exact on the web target too — no
50
+ // divide, on either backend.
51
+ function next(): utinyint {
52
+ state = state * 25173 + 13849;
53
+ return state >> 8;
54
+ }
55
+
56
+ // A value from 0 up to (not including) bound — the shape a caller
57
+ // wants when picking among a fixed number of choices: a board cell, a
58
+ // shuffled index, a die face after adding 1. `bound` must be at least
59
+ // 1. The classic modulo-bias caveat applies here as much as it does on
60
+ // any machine: for a `bound` that is not a power of two, the results
61
+ // near 255 are very slightly less likely than the results near 0,
62
+ // because 256 is not an exact multiple of `bound`. That bias is under
63
+ // 1/256 of a percentage point for anything this package is likely to be
64
+ // asked for — a handful of board cells or die faces — and not worth a
65
+ // rejection-sampling loop's extra code on a machine measured in
66
+ // kilobytes; a program that needs an exactly uniform pick from a large
67
+ // bound should say so and this note is where to start.
68
+ function range(bound: utinyint): utinyint {
69
+ return random.next() % bound;
70
+ }
71
+ }
package/src/table.8bs ADDED
@@ -0,0 +1,88 @@
1
+ // @8bitscript/random/table — a precomputed lookup table, not an algorithm:
2
+ // the "random" byte for any position is a read of program data, nothing
3
+ // computed. This is the technique 8-bit code reached for before spending a
4
+ // multiply on randomness was affordable, and it is still the cheapest random
5
+ // byte 8BitScript can produce on any of the nine targets.
6
+ //
7
+ // import { table } from "@8bitscript/random/table";
8
+ //
9
+ // table.seed(37); // any utinyint start point
10
+ // let roll: utinyint = table.range(6) + 1; // 1..6
11
+ //
12
+ // Same shape as the default generator's (`@8bitscript/random`,
13
+ // ../index.8bs): `seed()`, `next()`, `range(bound)`. A program can switch
14
+ // between the two by changing one import line, which is the point of
15
+ // keeping the surface identical rather than making this generator's
16
+ // strengths (or its one real weakness) show up as a different set of calls.
17
+ //
18
+ // **The trade this makes, stated plainly**: `next()` repeats every 256
19
+ // calls, always in the same order from wherever `index` is — a far shorter
20
+ // period than the default generator's 65536 states, and a predictable one:
21
+ // see `index` once and every value after it is known. What it buys back is
22
+ // that `next()` is one indexed load — `TABLE[index]`, `index++` — never a
23
+ // 16-bit multiply and add, on the 6502 or the web. A program that needs
24
+ // more than 256 calls' worth of unpredictability, or that hands `index`'s
25
+ // value to anything a player could see, wants `@8bitscript/random` instead;
26
+ // one that calls this a handful of times per frame, in code where a 6502
27
+ // multiply is the thing being budgeted against, is exactly what this file
28
+ // is for.
29
+ //
30
+ // **The 256 bytes are a permutation, not an arbitrary sequence** — every
31
+ // value 0-255 appears exactly once, shuffled once (a seeded Fisher-Yates,
32
+ // off the source repository, not on any target) and baked in here as
33
+ // `.rodata`. That is a real property this table has and a plain LCG does
34
+ // not: any 256 consecutive calls to `next()`, from any starting `index`,
35
+ // use every possible byte exactly once — perfectly even in the long run
36
+ // by construction, not by chance, at the cost of the short period above.
37
+ const TABLE: array<utinyint, 256> = [
38
+ 184, 152, 234, 42, 232, 225, 236, 202, 129, 123, 183, 74, 187, 15, 249, 166,
39
+ 47, 68, 218, 37, 134, 110, 147, 91, 219, 229, 148, 135, 13, 111, 222, 195,
40
+ 113, 126, 19, 190, 49, 11, 142, 18, 10, 154, 206, 254, 77, 199, 99, 250,
41
+ 231, 59, 233, 72, 107, 196, 26, 252, 235, 238, 100, 138, 139, 189, 178, 118,
42
+ 212, 78, 98, 214, 167, 143, 71, 145, 87, 179, 165, 28, 16, 53, 30, 86,
43
+ 60, 205, 247, 120, 128, 76, 185, 207, 97, 51, 204, 169, 39, 159, 217, 161,
44
+ 43, 57, 4, 75, 141, 3, 251, 84, 155, 198, 23, 239, 61, 27, 90, 14,
45
+ 177, 230, 245, 197, 64, 140, 65, 226, 151, 6, 56, 127, 181, 255, 88, 103,
46
+ 223, 21, 29, 224, 62, 115, 46, 22, 173, 1, 156, 241, 17, 125, 208, 228,
47
+ 122, 132, 54, 70, 69, 105, 149, 101, 200, 112, 243, 168, 80, 66, 182, 248,
48
+ 203, 201, 31, 85, 176, 55, 63, 50, 211, 193, 52, 38, 45, 40, 209, 237,
49
+ 119, 133, 216, 41, 244, 25, 246, 121, 215, 171, 12, 160, 144, 92, 106, 227,
50
+ 35, 130, 8, 108, 158, 162, 36, 48, 174, 34, 117, 67, 153, 186, 194, 32,
51
+ 157, 175, 95, 79, 221, 191, 81, 44, 163, 0, 253, 104, 136, 150, 240, 73,
52
+ 210, 96, 7, 146, 188, 114, 5, 82, 213, 137, 93, 124, 94, 83, 89, 58,
53
+ 116, 170, 180, 2, 242, 131, 192, 102, 20, 164, 109, 172, 220, 24, 9, 33,
54
+ ];
55
+
56
+ let index: utinyint = 0;
57
+
58
+ export namespace table {
59
+ // Replaces the running position, wherever a program wants to start —
60
+ // `index` is a `utinyint`, so any value is in range and wraps at 256
61
+ // for free (256 is exactly `TABLE.length`).
62
+ function seed(value: utinyint): void {
63
+ index = value;
64
+ }
65
+
66
+ // The byte at the current position, then step to the next one.
67
+ function next(): utinyint {
68
+ let value: utinyint = TABLE[index];
69
+ index++;
70
+ return value;
71
+ }
72
+
73
+ // A value from 0 up to (not including) bound. See ../index.8bs for the
74
+ // same modulo-bias note — it applies here too, and for the same reason.
75
+ function range(bound: utinyint): utinyint {
76
+ return table.next() % bound;
77
+ }
78
+
79
+ // The table with no state at all: a program that already keeps its own
80
+ // running counter — frames elapsed since start-up is the usual one —
81
+ // reads a "random" byte by indexing this table with it directly,
82
+ // rather than asking this file to keep a second counter next to it.
83
+ // `at(0)` and repeated `next()` calls from `seed(0)` agree, because
84
+ // this is exactly what `next()` reads before it steps `index`.
85
+ function at(i: utinyint): utinyint {
86
+ return TABLE[i];
87
+ }
88
+ }