ugly-game 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/dist/dmath.d.ts +14 -0
- package/dist/dmath.js +57 -0
- package/dist/gpuShadow.d.ts +53 -0
- package/dist/gpuShadow.js +315 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/dist/input/ControlHints.d.ts +29 -0
- package/dist/input/ControlHints.js +112 -0
- package/dist/input/TouchControls.d.ts +16 -0
- package/dist/input/TouchControls.js +33 -0
- package/dist/input/browserBackend.d.ts +40 -0
- package/dist/input/browserBackend.js +205 -0
- package/dist/input/resolve.d.ts +98 -0
- package/dist/input/resolve.js +141 -0
- package/dist/mat4.d.ts +17 -0
- package/dist/mat4.js +99 -0
- package/dist/moat.d.ts +64 -0
- package/dist/moat.js +76 -0
- package/dist/noise.d.ts +35 -0
- package/dist/noise.js +91 -0
- package/package.json +10 -0
package/dist/moat.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** The only thing the harness needs from a game: a deterministic sim it can clone, hash, and step. */
|
|
2
|
+
export interface MoatSim<S, I> {
|
|
3
|
+
clone(s: S): S;
|
|
4
|
+
hash(s: S): string;
|
|
5
|
+
step(s: S, input: I): void;
|
|
6
|
+
}
|
|
7
|
+
/** Run a sim over an input stream and record the per-step content hash (index 0 = the initial state). */
|
|
8
|
+
export declare function runTrace<S, I>(sim: MoatSim<S, I>, init: S, inputs: I[]): string[];
|
|
9
|
+
export interface Equivalence {
|
|
10
|
+
inSync: boolean;
|
|
11
|
+
firstDivergentStep: number;
|
|
12
|
+
steps: number;
|
|
13
|
+
}
|
|
14
|
+
/** Compare a reference trace to a candidate trace; report the first divergent step (or in-sync). */
|
|
15
|
+
export declare function equivalence(reference: string[], candidate: string[]): Equivalence;
|
|
16
|
+
/** The oracle check: a reference runner and a candidate runner (which may use DIFFERENT code paths —
|
|
17
|
+
* brute vs culled, full-load vs streamed, perfect-knowledge vs predicted) must agree tick-for-tick. */
|
|
18
|
+
export declare function oracleCheck(reference: () => string[], candidate: () => string[]): Equivalence;
|
|
19
|
+
/** Re-run a sim to a given step and return the state there — for localizing WHICH field diverged (the
|
|
20
|
+
* game supplies a `locate(refState, candState)`, e.g. the factory's "tile (9,7) content A=1 B=0"). */
|
|
21
|
+
export declare function stateAt<S, I>(sim: MoatSim<S, I>, init: S, inputs: I[], step: number): S;
|
|
22
|
+
export interface SweepResult<P, V extends string> {
|
|
23
|
+
results: {
|
|
24
|
+
perturbation: P;
|
|
25
|
+
verdict: V;
|
|
26
|
+
detail?: unknown;
|
|
27
|
+
}[];
|
|
28
|
+
counts: Record<string, number>;
|
|
29
|
+
flagged: {
|
|
30
|
+
perturbation: P;
|
|
31
|
+
verdict: V;
|
|
32
|
+
detail?: unknown;
|
|
33
|
+
}[];
|
|
34
|
+
total: number;
|
|
35
|
+
clean: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Apply each perturbation, replay + classify (the game's `run`), and flag the ones `bad` says are bugs. */
|
|
38
|
+
export declare function perturbationSweep<P, V extends string>(perturbations: P[], run: (p: P, i: number) => {
|
|
39
|
+
verdict: V;
|
|
40
|
+
detail?: unknown;
|
|
41
|
+
}, bad: (v: V) => boolean): SweepResult<P, V>;
|
|
42
|
+
export type MoatCheck = {
|
|
43
|
+
kind: 'equivalence';
|
|
44
|
+
name: string;
|
|
45
|
+
axis: string;
|
|
46
|
+
expectSync?: boolean;
|
|
47
|
+
run: () => Equivalence;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'sweep';
|
|
50
|
+
name: string;
|
|
51
|
+
axis: string;
|
|
52
|
+
run: () => SweepResult<unknown, string>;
|
|
53
|
+
};
|
|
54
|
+
export interface MoatOutcome {
|
|
55
|
+
name: string;
|
|
56
|
+
axis: string;
|
|
57
|
+
kind: 'equivalence' | 'sweep';
|
|
58
|
+
pass: boolean;
|
|
59
|
+
summary: string;
|
|
60
|
+
}
|
|
61
|
+
/** Run one moat check and reduce it to a pass/fail + one-line summary — the same shape for every axis. */
|
|
62
|
+
export declare function runMoat(check: MoatCheck): MoatOutcome;
|
|
63
|
+
/** Run a whole suite of moats — the productized thesis: one harness, every axis. */
|
|
64
|
+
export declare function runMoatSuite(checks: MoatCheck[]): MoatOutcome[];
|
package/dist/moat.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// THE MOAT HARNESS — the AI-debuggable-game thesis, productized. Across the factory, FPS, physics, and
|
|
3
|
+
// open-world slices we hand-wrote six bespoke "explorers/oracles" that each find real bugs in a
|
|
4
|
+
// deterministic game. They are all one of TWO shapes, and this module is those two shapes made reusable,
|
|
5
|
+
// so a NEW game inherits the moat instead of re-deriving it:
|
|
6
|
+
//
|
|
7
|
+
// (1) ORACLE EQUIVALENCE — an OPTIMIZED run (culled / parallel / streamed / predicted+rolled-back /
|
|
8
|
+
// lockstep-client-B) must be BYTE-IDENTICAL to a GROUND-TRUTH run (brute / serial / fully-loaded /
|
|
9
|
+
// perfect-knowledge / lockstep-client-A). Compare per-step hashes; a divergence is localized to
|
|
10
|
+
// the exact step (and, with a game `locate`, the exact field). Covers factory culling/parallel/
|
|
11
|
+
// lockstep-desync, FPS rollback netcode, and open-world streaming invariance.
|
|
12
|
+
//
|
|
13
|
+
// (2) PERTURBATION SWEEP — take a deterministic base world, apply each of many PLAUSIBLE perturbations
|
|
14
|
+
// (remove a belt, stand the player on each tile, aim at each target, spawn a bot at each cell,
|
|
15
|
+
// inject a level defect), DETERMINISTICALLY replay, and CLASSIFY the outcome; flag the bad ones.
|
|
16
|
+
// Covers the factory deadlock hunt, FPS reachability, AI spawn-reachability, KCC traversal, seams.
|
|
17
|
+
//
|
|
18
|
+
// Every verdict is a reproducible replay, not a screenshot dispute — because the sim under test is a
|
|
19
|
+
// pure function of its inputs. This file has ZERO game knowledge; it only needs a deterministic sim.
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// ═══════════════════════ Pattern 1 · ORACLE EQUIVALENCE ═══════════════════════
|
|
22
|
+
/** Run a sim over an input stream and record the per-step content hash (index 0 = the initial state). */
|
|
23
|
+
export function runTrace(sim, init, inputs) {
|
|
24
|
+
const s = sim.clone(init);
|
|
25
|
+
const out = [sim.hash(s)];
|
|
26
|
+
for (const inp of inputs) {
|
|
27
|
+
sim.step(s, inp);
|
|
28
|
+
out.push(sim.hash(s));
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/** Compare a reference trace to a candidate trace; report the first divergent step (or in-sync). */
|
|
33
|
+
export function equivalence(reference, candidate) {
|
|
34
|
+
const n = Math.min(reference.length, candidate.length);
|
|
35
|
+
for (let i = 0; i < n; i++)
|
|
36
|
+
if (reference[i] !== candidate[i])
|
|
37
|
+
return { inSync: false, firstDivergentStep: i, steps: n };
|
|
38
|
+
return { inSync: reference.length === candidate.length, firstDivergentStep: -1, steps: n };
|
|
39
|
+
}
|
|
40
|
+
/** The oracle check: a reference runner and a candidate runner (which may use DIFFERENT code paths —
|
|
41
|
+
* brute vs culled, full-load vs streamed, perfect-knowledge vs predicted) must agree tick-for-tick. */
|
|
42
|
+
export function oracleCheck(reference, candidate) {
|
|
43
|
+
return equivalence(reference(), candidate());
|
|
44
|
+
}
|
|
45
|
+
/** Re-run a sim to a given step and return the state there — for localizing WHICH field diverged (the
|
|
46
|
+
* game supplies a `locate(refState, candState)`, e.g. the factory's "tile (9,7) content A=1 B=0"). */
|
|
47
|
+
export function stateAt(sim, init, inputs, step) {
|
|
48
|
+
const s = sim.clone(init);
|
|
49
|
+
for (let i = 0; i < step && i < inputs.length; i++)
|
|
50
|
+
sim.step(s, inputs[i]);
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
/** Apply each perturbation, replay + classify (the game's `run`), and flag the ones `bad` says are bugs. */
|
|
54
|
+
export function perturbationSweep(perturbations, run, bad) {
|
|
55
|
+
const results = perturbations.map((p, i) => { const r = run(p, i); return { perturbation: p, verdict: r.verdict, detail: r.detail }; });
|
|
56
|
+
const counts = {};
|
|
57
|
+
for (const r of results)
|
|
58
|
+
counts[r.verdict] = (counts[r.verdict] ?? 0) + 1;
|
|
59
|
+
const flagged = results.filter((r) => bad(r.verdict));
|
|
60
|
+
return { results, counts, flagged, total: results.length, clean: flagged.length === 0 };
|
|
61
|
+
}
|
|
62
|
+
/** Run one moat check and reduce it to a pass/fail + one-line summary — the same shape for every axis. */
|
|
63
|
+
export function runMoat(check) {
|
|
64
|
+
if (check.kind === 'equivalence') {
|
|
65
|
+
const e = check.run();
|
|
66
|
+
const expectSync = check.expectSync ?? true;
|
|
67
|
+
const pass = e.inSync === expectSync;
|
|
68
|
+
const summary = e.inSync ? `byte-identical to the oracle over ${e.steps} steps` : `diverges from the oracle @ step ${e.firstDivergentStep}`;
|
|
69
|
+
return { name: check.name, axis: check.axis, kind: 'equivalence', pass, summary };
|
|
70
|
+
}
|
|
71
|
+
const s = check.run();
|
|
72
|
+
const counts = Object.entries(s.counts).map(([k, v]) => `${v} ${k}`).join(', ');
|
|
73
|
+
return { name: check.name, axis: check.axis, kind: 'sweep', pass: s.clean, summary: s.clean ? `all ${s.total} clean (${counts})` : `${s.flagged.length}/${s.total} flagged (${counts})` };
|
|
74
|
+
}
|
|
75
|
+
/** Run a whole suite of moats — the productized thesis: one harness, every axis. */
|
|
76
|
+
export function runMoatSuite(checks) { return checks.map(runMoat); }
|
package/dist/noise.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Deterministic 0..1 at an integer lattice point (hashed, engine-portable). */
|
|
2
|
+
export declare function hashLattice(seed: number, ix: number, iz: number): number;
|
|
3
|
+
/** One octave of bilinear value-noise over a lattice of period F, normalized to [0,1]. */
|
|
4
|
+
export declare function octave01(seed: number, x: number, z: number, F: number): number;
|
|
5
|
+
/** One octave of bilinear value-noise, amplitude amp (matches world.ts's original `octave`). */
|
|
6
|
+
export declare function octave(seed: number, x: number, z: number, F: number, amp: number): number;
|
|
7
|
+
export interface FbmCfg {
|
|
8
|
+
octaves: number;
|
|
9
|
+
F: number;
|
|
10
|
+
amp: number;
|
|
11
|
+
lacunarity?: number;
|
|
12
|
+
gain?: number;
|
|
13
|
+
seedStep?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Fractal Brownian motion: sum of octaves at increasing frequency (×lacunarity) and decreasing
|
|
16
|
+
* amplitude (×gain). Output ∈ [0, Σ amp_i]. Each octave uses a distinct derived seed. */
|
|
17
|
+
export declare function fbm2(seed: number, x: number, z: number, cfg: FbmCfg): number;
|
|
18
|
+
/** Ridged noise — sharp crests where the underlying octave is mid-valued. Output ∈ [0, amp]. */
|
|
19
|
+
export declare function ridged2(seed: number, x: number, z: number, F: number, amp: number): number;
|
|
20
|
+
/** Billow noise — puffy lobes (dunes/clouds); complement of ridged. Output ∈ [0, amp]. */
|
|
21
|
+
export declare function billow2(seed: number, x: number, z: number, F: number, amp: number): number;
|
|
22
|
+
/** Domain warp: displace the sample point by two decorrelated octaves. Per-axis displacement ∈
|
|
23
|
+
* [-warpAmp, warpAmp], so a downstream field's slope grows by at most warpAmp/warpF (bounded). */
|
|
24
|
+
export declare function domainWarp2(seed: number, x: number, z: number, warpF: number, warpAmp: number): [number, number];
|
|
25
|
+
export interface Worley {
|
|
26
|
+
f1: number;
|
|
27
|
+
f2: number;
|
|
28
|
+
cellId: number;
|
|
29
|
+
}
|
|
30
|
+
/** Worley/cellular noise: distance to nearest (f1) and 2nd-nearest (f2) feature point, plus a stable
|
|
31
|
+
* id of the nearest cell. f2−f1 is a natural crack/edge field (rivers, region borders). */
|
|
32
|
+
export declare function worley2(seed: number, x: number, z: number, F: number): Worley;
|
|
33
|
+
/** Voronoi region label ∈ [0, regions): which of N regions the nearest feature cell maps to.
|
|
34
|
+
* Piecewise-constant → gives biome regions with organic (jittered) borders. */
|
|
35
|
+
export declare function voronoiRegion(seed: number, x: number, z: number, F: number, regions: number): number;
|
package/dist/noise.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// NOISE — the deterministic terrain PLATFORM primitives (generic, no world/biome assumptions).
|
|
3
|
+
// Every function is a pure function of (seed, coords) using ONLY IEEE-exact ops (+ - * / sqrt via
|
|
4
|
+
// dmath) so terrain is bit-identical across engines — the moat. These compose (in terrainCompose.ts)
|
|
5
|
+
// into rich per-biome terrain that stays WALKABLE: the load-bearing property here is that value-noise
|
|
6
|
+
// has a BOUNDED per-unit slope (≤ amp/F), which is what lets composed height respect the step budget.
|
|
7
|
+
// Building blocks: value-noise octaves, FBM, ridged/billow variants, domain warp, and worley/voronoi
|
|
8
|
+
// cellular fields (used for biome regions and river/crack edges). See design/moonshot-open-world.md.
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
import { dhypot } from './dmath';
|
|
11
|
+
/** Deterministic 0..1 at an integer lattice point (hashed, engine-portable). */
|
|
12
|
+
export function hashLattice(seed, ix, iz) {
|
|
13
|
+
let n = (Math.imul(ix, 73856093) ^ Math.imul(iz, 19349663) ^ Math.imul(seed, 83492791)) >>> 0;
|
|
14
|
+
n = Math.imul(n ^ (n >>> 13), 1274126177) >>> 0;
|
|
15
|
+
return (n % 100000) / 100000;
|
|
16
|
+
}
|
|
17
|
+
/** One octave of bilinear value-noise over a lattice of period F, normalized to [0,1]. */
|
|
18
|
+
export function octave01(seed, x, z, F) {
|
|
19
|
+
const x0 = Math.floor(x / F), z0 = Math.floor(z / F), fx = x / F - x0, fz = z / F - z0;
|
|
20
|
+
const a = hashLattice(seed, x0, z0), b = hashLattice(seed, x0 + 1, z0), c = hashLattice(seed, x0, z0 + 1), d = hashLattice(seed, x0 + 1, z0 + 1);
|
|
21
|
+
const ab = a + (b - a) * fx, cd = c + (d - c) * fx;
|
|
22
|
+
return ab + (cd - ab) * fz;
|
|
23
|
+
}
|
|
24
|
+
/** One octave of bilinear value-noise, amplitude amp (matches world.ts's original `octave`). */
|
|
25
|
+
export function octave(seed, x, z, F, amp) {
|
|
26
|
+
return octave01(seed, x, z, F) * amp;
|
|
27
|
+
}
|
|
28
|
+
/** Fractal Brownian motion: sum of octaves at increasing frequency (×lacunarity) and decreasing
|
|
29
|
+
* amplitude (×gain). Output ∈ [0, Σ amp_i]. Each octave uses a distinct derived seed. */
|
|
30
|
+
export function fbm2(seed, x, z, cfg) {
|
|
31
|
+
const lac = cfg.lacunarity ?? 2, gain = cfg.gain ?? 0.5, step = cfg.seedStep ?? 1013;
|
|
32
|
+
let sum = 0, F = cfg.F, amp = cfg.amp, s = seed;
|
|
33
|
+
for (let i = 0; i < cfg.octaves; i++) {
|
|
34
|
+
sum += octave01(s, x, z, F) * amp;
|
|
35
|
+
F /= lac;
|
|
36
|
+
amp *= gain;
|
|
37
|
+
s = (s * 6364136223 + step) | 0;
|
|
38
|
+
}
|
|
39
|
+
return sum;
|
|
40
|
+
}
|
|
41
|
+
/** Ridged noise — sharp crests where the underlying octave is mid-valued. Output ∈ [0, amp]. */
|
|
42
|
+
export function ridged2(seed, x, z, F, amp) {
|
|
43
|
+
return (1 - Math.abs(2 * octave01(seed, x, z, F) - 1)) * amp;
|
|
44
|
+
}
|
|
45
|
+
/** Billow noise — puffy lobes (dunes/clouds); complement of ridged. Output ∈ [0, amp]. */
|
|
46
|
+
export function billow2(seed, x, z, F, amp) {
|
|
47
|
+
return Math.abs(2 * octave01(seed, x, z, F) - 1) * amp;
|
|
48
|
+
}
|
|
49
|
+
/** Domain warp: displace the sample point by two decorrelated octaves. Per-axis displacement ∈
|
|
50
|
+
* [-warpAmp, warpAmp], so a downstream field's slope grows by at most warpAmp/warpF (bounded). */
|
|
51
|
+
export function domainWarp2(seed, x, z, warpF, warpAmp) {
|
|
52
|
+
const dx = (2 * octave01(seed ^ 0x1b56, x, z, warpF) - 1) * warpAmp;
|
|
53
|
+
const dz = (2 * octave01(seed ^ 0x7f2a, x, z, warpF) - 1) * warpAmp;
|
|
54
|
+
return [x + dx, z + dz];
|
|
55
|
+
}
|
|
56
|
+
/** Nearest & second-nearest jittered feature cell to (x,z) at period F. Shared by worley2 + regions. */
|
|
57
|
+
function nearest(seed, x, z, F) {
|
|
58
|
+
const cx = Math.floor(x / F), cz = Math.floor(z / F);
|
|
59
|
+
let f1 = Infinity, f2 = Infinity, ncx = cx, ncz = cz;
|
|
60
|
+
for (let i = -1; i <= 1; i++)
|
|
61
|
+
for (let j = -1; j <= 1; j++) {
|
|
62
|
+
const gcx = cx + i, gcz = cz + j;
|
|
63
|
+
const fxp = (gcx + hashLattice(seed ^ 0x2c1b, gcx, gcz)) * F; // jittered feature point
|
|
64
|
+
const fzp = (gcz + hashLattice(seed ^ 0x51af, gcx, gcz)) * F;
|
|
65
|
+
const d = dhypot(x - fxp, z - fzp);
|
|
66
|
+
if (d < f1) {
|
|
67
|
+
f2 = f1;
|
|
68
|
+
f1 = d;
|
|
69
|
+
ncx = gcx;
|
|
70
|
+
ncz = gcz;
|
|
71
|
+
}
|
|
72
|
+
else if (d < f2) {
|
|
73
|
+
f2 = d;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { ncx, ncz, f1, f2 };
|
|
77
|
+
}
|
|
78
|
+
/** Worley/cellular noise: distance to nearest (f1) and 2nd-nearest (f2) feature point, plus a stable
|
|
79
|
+
* id of the nearest cell. f2−f1 is a natural crack/edge field (rivers, region borders). */
|
|
80
|
+
export function worley2(seed, x, z, F) {
|
|
81
|
+
const n = nearest(seed, x, z, F);
|
|
82
|
+
const cellId = (hashLattice(seed ^ 0x9e37, n.ncx, n.ncz) * 1e6) | 0;
|
|
83
|
+
return { f1: n.f1, f2: n.f2, cellId };
|
|
84
|
+
}
|
|
85
|
+
/** Voronoi region label ∈ [0, regions): which of N regions the nearest feature cell maps to.
|
|
86
|
+
* Piecewise-constant → gives biome regions with organic (jittered) borders. */
|
|
87
|
+
export function voronoiRegion(seed, x, z, F, regions) {
|
|
88
|
+
const n = nearest(seed, x, z, F);
|
|
89
|
+
const h = (Math.imul(n.ncx | 0, 374761393) ^ Math.imul(n.ncz | 0, 668265263) ^ Math.imul(seed, 2246822519)) >>> 0;
|
|
90
|
+
return h % regions;
|
|
91
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ugly-game",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
|
|
6
|
+
"files": ["dist"],
|
|
7
|
+
"scripts": { "build": "tsc -p tsconfig.json", "prepublishOnly": "tsc -p tsconfig.json" },
|
|
8
|
+
"peerDependencies": { "react": ">=18" },
|
|
9
|
+
"devDependencies": { "typescript": "^5.6.0", "@types/react": "^19.0.0", "@webgpu/types": "^0.1.40" }
|
|
10
|
+
}
|