@netforge/engine 0.0.0-stage → 0.2.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.
Files changed (245) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +100 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +215 -0
  76. package/dist/net/protocol.js +10 -0
  77. package/dist/net/room-client.d.ts +65 -0
  78. package/dist/net/room-client.js +156 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms-full.txt +460 -0
  244. package/llms.txt +183 -0
  245. package/package.json +104 -3
@@ -0,0 +1,216 @@
1
+ /**
2
+ * dungeon(...): a procedural dungeon floor from a seed
3
+ * ("dungeon layouts can be generated from a seed on both sides").
4
+ *
5
+ * Rooms sit on a grid of cells, grown from the start room as a random
6
+ * tree, with a few extra doors that make loops. Neighbouring rooms are
7
+ * joined by straight corridors. The result is the `field` a dungeon game
8
+ * passes to game({ field }) (walls and floors as boxes, the start room at
9
+ * [0, 0] where players spawn) and the rooms with their depth from the
10
+ * start, so a system can wake encounters room by room.
11
+ *
12
+ * Pure and deterministic: the same spec gives the same dungeon on the room
13
+ * server, in the client and in the playtest, with nothing downloaded.
14
+ */
15
+ import { z } from "zod";
16
+ import { GameDefinitionError } from "./schemas.js";
17
+ export const dungeonSchema = z
18
+ .object({
19
+ /** Any whole number: a different seed is a different dungeon. */
20
+ seed: z.number().int().min(0).max(0xffffffff),
21
+ /** Rooms on the floor, the start and the boss room included. */
22
+ rooms: z.number().int().min(3, "a dungeon has at least 3 rooms").max(20, "20 rooms is the cap (a run should end in about 10 minutes)"),
23
+ /** Grid cell size in metres: room spacing. Default 24. */
24
+ cell: z.number().int().min(16).max(32).default(24),
25
+ /** Room side in metres, [smallest, largest]. Default [12, 18]. Kept at least 4 m under the cell. */
26
+ size: z.tuple([z.number().int().min(8).max(28), z.number().int().min(8).max(28)]).default([12, 18]),
27
+ /** Corridor width in metres (even). Default 4. */
28
+ corridor: z.number().int().min(2).max(8).default(4),
29
+ /** Extra doors beyond the tree, for loops. Default 2. */
30
+ loops: z.number().int().min(0).max(8).default(2),
31
+ })
32
+ .strict();
33
+ /** mulberry32: a small seeded PRNG. */
34
+ function prng(seed) {
35
+ let a = seed >>> 0;
36
+ return () => {
37
+ a = (a + 0x6d2b79f5) >>> 0;
38
+ let t = a;
39
+ t = Math.imul(t ^ (t >>> 15), t | 1);
40
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
41
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
42
+ };
43
+ }
44
+ const evenIn = (rnd, lo, hi) => {
45
+ const a = Math.ceil(lo / 2);
46
+ const b = Math.floor(hi / 2);
47
+ return 2 * (a + Math.floor(rnd() * (b - a + 1)));
48
+ };
49
+ /** Build a dungeon floor from a spec. Throws GameDefinitionError with readable lines on a bad spec. */
50
+ export function dungeon(spec) {
51
+ const parsed = dungeonSchema.safeParse(spec);
52
+ if (!parsed.success)
53
+ throw new GameDefinitionError("dungeon(...)", parsed.error.issues.map((i) => `${i.path.join(".") || "spec"}: ${i.message}`));
54
+ const s = parsed.data;
55
+ const [lo, hi] = s.size[0] <= s.size[1] ? s.size : [s.size[1], s.size[0]];
56
+ if (hi > s.cell - 4)
57
+ throw new GameDefinitionError("dungeon(...)", [`size ${hi} m does not fit a ${s.cell} m cell with walls between rooms: keep size under cell - 4`]);
58
+ if (s.corridor > lo - 2)
59
+ throw new GameDefinitionError("dungeon(...)", [`a ${s.corridor} m corridor is wider than the smallest room's wall allows: keep corridor at most size[0] - 2`]);
60
+ const rnd = prng(s.seed);
61
+ // A square grid big enough to grow in; the start is its middle cell, so the floor stays near [0, 0].
62
+ const g = Math.max(3, Math.ceil(Math.sqrt(s.rooms)) + 2);
63
+ const mid = Math.floor(g / 2);
64
+ const key = (i, j) => j * g + i;
65
+ const cells = [[mid, mid]];
66
+ const index = new Map([[key(mid, mid), 0]]);
67
+ const links = [new Set()];
68
+ const DIRS = [
69
+ [1, 0],
70
+ [-1, 0],
71
+ [0, 1],
72
+ [0, -1],
73
+ ];
74
+ let guard = 0;
75
+ while (cells.length < s.rooms && guard++ < 10_000) {
76
+ // Grow from a random room, leaning toward the newest so the floor reaches out rather than balling up.
77
+ const from = rnd() < 0.5 ? cells.length - 1 : Math.floor(rnd() * cells.length);
78
+ const [ci, cj] = cells[from];
79
+ const [di, dj] = DIRS[Math.floor(rnd() * 4)];
80
+ const ni = ci + di;
81
+ const nj = cj + dj;
82
+ if (ni < 0 || nj < 0 || ni >= g || nj >= g || index.has(key(ni, nj)))
83
+ continue;
84
+ const id = cells.length;
85
+ cells.push([ni, nj]);
86
+ index.set(key(ni, nj), id);
87
+ links.push(new Set([from]));
88
+ links[from]?.add(id);
89
+ }
90
+ // Loops: a few extra doors between neighbours that are not joined yet.
91
+ for (let k = 0, tries = 0; k < s.loops && tries < 200; tries++) {
92
+ const a = Math.floor(rnd() * cells.length);
93
+ const [ai, aj] = cells[a];
94
+ const [di, dj] = DIRS[Math.floor(rnd() * 4)];
95
+ const b = index.get(key(ai + di, aj + dj));
96
+ if (b === undefined || links[a]?.has(b))
97
+ continue;
98
+ links[a]?.add(b);
99
+ links[b]?.add(a);
100
+ k++;
101
+ }
102
+ // Depth from the start (breadth first).
103
+ const depth = new Array(cells.length).fill(-1);
104
+ depth[0] = 0;
105
+ const queue = [0];
106
+ for (let h = 0; h < queue.length; h++) {
107
+ const a = queue[h];
108
+ for (const b of links[a] ?? []) {
109
+ if (depth[b] !== -1)
110
+ continue;
111
+ depth[b] = (depth[a] ?? 0) + 1;
112
+ queue.push(b);
113
+ }
114
+ }
115
+ // Rooms: centred in their cells (so corridors run straight), even sizes on whole metres.
116
+ const rooms = cells.map(([i, j], id) => {
117
+ const w = id === 0 ? hi : evenIn(rnd, lo, hi);
118
+ const d = id === 0 ? hi : evenIn(rnd, lo, hi);
119
+ return { id, x: (i - mid) * s.cell, z: (j - mid) * s.cell, hx: w / 2, hz: d / 2, depth: depth[id] ?? 0, links: [...(links[id] ?? [])].sort((a, b) => a - b) };
120
+ });
121
+ const floors = rooms.map((r) => [r.x, r.z, r.hx, r.hz]);
122
+ const half = s.corridor / 2;
123
+ for (const a of rooms) {
124
+ for (const bi of a.links) {
125
+ if (bi < a.id)
126
+ continue;
127
+ const b = rooms[bi];
128
+ if (a.z === b.z) {
129
+ const [l, r] = a.x < b.x ? [a, b] : [b, a];
130
+ const x0 = l.x + l.hx;
131
+ const x1 = r.x - r.hx;
132
+ floors.push([(x0 + x1) / 2, a.z, (x1 - x0) / 2 + 0.5, half]);
133
+ }
134
+ else {
135
+ const [l, r] = a.z < b.z ? [a, b] : [b, a];
136
+ const z0 = l.z + l.hz;
137
+ const z1 = r.z - r.hz;
138
+ floors.push([a.x, (z0 + z1) / 2, half, (z1 - z0) / 2 + 0.5]);
139
+ }
140
+ }
141
+ }
142
+ // Walls: every 1 m cell next to the floor that is not floor, merged into boxes.
143
+ let ext = 0;
144
+ for (const [x, z, hx, hz] of floors)
145
+ ext = Math.max(ext, Math.abs(x) + hx, Math.abs(z) + hz);
146
+ const n = Math.ceil(ext) + 2;
147
+ const size = 2 * n;
148
+ const floor = new Uint8Array(size * size);
149
+ for (const [x, z, hx, hz] of floors) {
150
+ for (let j = Math.floor(z - hz + n); j < Math.ceil(z + hz + n); j++) {
151
+ for (let i = Math.floor(x - hx + n); i < Math.ceil(x + hx + n); i++) {
152
+ if (i >= 0 && j >= 0 && i < size && j < size)
153
+ floor[j * size + i] = 1;
154
+ }
155
+ }
156
+ }
157
+ const wall = new Uint8Array(size * size);
158
+ for (let j = 0; j < size; j++) {
159
+ for (let i = 0; i < size; i++) {
160
+ if (floor[j * size + i] === 1)
161
+ continue;
162
+ let near = false;
163
+ for (let dj = -1; dj <= 1 && !near; dj++) {
164
+ for (let di = -1; di <= 1; di++) {
165
+ const a = i + di;
166
+ const b = j + dj;
167
+ if (a >= 0 && b >= 0 && a < size && b < size && floor[b * size + a] === 1) {
168
+ near = true;
169
+ break;
170
+ }
171
+ }
172
+ }
173
+ if (near)
174
+ wall[j * size + i] = 1;
175
+ }
176
+ }
177
+ // Merge: maximal runs along x per row, then runs with the same ends stacked down the rows.
178
+ const walls = [];
179
+ let open = new Map();
180
+ for (let j = 0; j <= size; j++) {
181
+ const next = new Map();
182
+ for (let i = 0; j < size && i < size; i++) {
183
+ if (wall[j * size + i] !== 1)
184
+ continue;
185
+ let i1 = i;
186
+ while (i1 + 1 < size && wall[j * size + i1 + 1] === 1)
187
+ i1++;
188
+ const k = `${i}:${i1}`;
189
+ const prev = open.get(k);
190
+ next.set(k, prev ? [prev[0], prev[1], prev[2]] : [i, i1, j]);
191
+ if (prev)
192
+ open.delete(k);
193
+ i = i1;
194
+ }
195
+ for (const [i0, i1, j0] of open.values())
196
+ walls.push([(i0 + i1 + 1) / 2 - n, (j0 + j) / 2 - n, (i1 - i0 + 1) / 2, (j - j0) / 2]);
197
+ open = next;
198
+ }
199
+ const halfSize = Math.max(20, Math.min(120, n + 1));
200
+ let boss = rooms[0];
201
+ for (const r of rooms)
202
+ if (r.depth > boss.depth || (r.depth === boss.depth && r.id > boss.id))
203
+ boss = r;
204
+ return {
205
+ field: { halfSize, pillars: [], walls, floors },
206
+ rooms,
207
+ start: rooms[0],
208
+ boss,
209
+ roomAt(x, z) {
210
+ for (const r of rooms)
211
+ if (Math.abs(x - r.x) <= r.hx && Math.abs(z - r.z) <= r.hz)
212
+ return r.id;
213
+ return -1;
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Headless test harness: run a game on the server sim
3
+ * with bot players and report what happened.
4
+ *
5
+ * When a game module has been declared (game(), prefabs, weapons, systems,
6
+ * earn rules), it runs that game exactly as the room server does: compiled
7
+ * content in engine-sim, the systems on the GameRunner, earn rules counted
8
+ * per player. With no game declared it runs the week-2 engine-sim arena
9
+ * (the built-in chaser spawner) with scripted bots.
10
+ */
11
+ export interface HarnessAssertion {
12
+ readonly name: string;
13
+ readonly check: (report: HarnessReport) => boolean;
14
+ }
15
+ export interface HarnessOptions {
16
+ readonly ticks: number;
17
+ readonly bots: number;
18
+ /** Arena: enemies the spawner keeps alive. Game: hold at least this many alive on top of the waves (stress). */
19
+ readonly enemies?: number;
20
+ readonly seed?: number;
21
+ readonly tickRate?: number;
22
+ readonly assert?: readonly HarnessAssertion[];
23
+ /**
24
+ * Who plays (survivor-style games): "basic" circles and never misses a
25
+ * beat (the default, a stress player); "human" reacts 200 ms late, kites
26
+ * in wide arcs, takes a second per card (the fun metrics' player).
27
+ */
28
+ readonly bot?: "basic" | "human";
29
+ /** Human bots' card picks: "random" (default) or "greedy". */
30
+ readonly picks?: "random" | "greedy";
31
+ /** With `enemies`: hold a mix of every non-boss enemy type (all behaviours on) instead of type 0. */
32
+ readonly enemyMix?: boolean;
33
+ }
34
+ /**
35
+ * How fun the opening and the run felt to the bots, measured (GAME-DESIGN.md).
36
+ * Horde games (survivor-like) are measured over the first run; arena games
37
+ * over every life. Fields that do not apply to the genre are null.
38
+ */
39
+ export interface FunMetrics {
40
+ readonly genre: "horde" | "arena" | "race";
41
+ /** Seconds until an enemy first came within 3 m of a player (horde) or a player first took damage (arena). */
42
+ readonly firstThreatSeconds: number | null;
43
+ /** Seconds until the first reward: the first level-up (horde) or the first kill (arena). */
44
+ readonly firstRewardSeconds: number | null;
45
+ /** Horde: highest player level at 1:00, 3:00, 6:00 and 9:00 of the first run (null when the run ended first). */
46
+ readonly levelAt: {
47
+ readonly "60": number | null;
48
+ readonly "180": number | null;
49
+ readonly "360": number | null;
50
+ readonly "540": number | null;
51
+ };
52
+ /** Horde: times a player went down before 1:00. */
53
+ readonly deathsBeforeMinute: number | null;
54
+ /** Horde: the most enemies on one player's screen at 0:30. */
55
+ readonly onScreenAt30: number | null;
56
+ /** Horde: contact hits each player took in the first minute. */
57
+ readonly contactHitsFirstMinute: number | null;
58
+ /** Horde: share of downs after 3:00 that a telegraphed attack caused (null with no such downs). */
59
+ readonly telegraphedDeathShare: number | null;
60
+ /** Horde: share of enemy spawns that appeared on a player's screen (should be 0). */
61
+ readonly spawnsOnScreen: number | null;
62
+ /** Horde: how long the first run lasted (the whole playtest when it never ended), s. */
63
+ readonly survivalSeconds: number | null;
64
+ /** Arena: median seconds from a (re)spawn to the first damage taken (spawn camping when short). */
65
+ readonly spawnToDamageSeconds: number | null;
66
+ /** Arena: share of deaths within 3 s of spawning. */
67
+ readonly spawnDeathShare: number | null;
68
+ /** Arena: best player's kills over the mean (1 is even; above 3 one player stomps). */
69
+ readonly killSpread: number | null;
70
+ /** Arena: times the lead changed hands, per round. */
71
+ readonly leadChanges: number | null;
72
+ /** Horde: bosses killed in the first run, with seconds from spawn to kill. */
73
+ readonly bossKills: readonly {
74
+ readonly prefab: string;
75
+ readonly seconds: number;
76
+ }[];
77
+ /** Horde: what last hit each player that went down in the first run ("contact", "charge", "blast", ...). */
78
+ readonly downsBySource: Readonly<Record<string, number>>;
79
+ /** Horde: seconds until a player first took damage of any kind (null if never). */
80
+ readonly firstHitSeconds?: number | null;
81
+ /** Horde: lowest health any player was left on by a hit before 2:00 (null if nobody was hit). */
82
+ readonly lowestHealthBy120?: number | null;
83
+ /** Horde: seconds until the first elite and the first boss (mini or final) appeared. */
84
+ readonly firstEliteSeconds?: number | null;
85
+ readonly firstBossSeconds?: number | null;
86
+ /** Horde: share of player time in the first run spent downed or out (waiting, not playing). */
87
+ readonly downTimeShare?: number | null;
88
+ /** Race: median seconds from the start signal to a playtest player's first finish (null: nobody finished). */
89
+ readonly finishSeconds?: number | null;
90
+ /** Race: share of playtest player rounds that reached the finish. */
91
+ readonly finishRate?: number | null;
92
+ /** Race: falls (platformer) or wall hits (racer) per playtest player per round. */
93
+ readonly mishapsPerRound?: number | null;
94
+ /** Race: seconds until a playtest player first collected a coin. */
95
+ readonly firstCoinSeconds?: number | null;
96
+ /** Race: per course, whether the route planner (platformer) found a way to the finish, and its seconds. */
97
+ readonly routes?: readonly {
98
+ readonly course: number;
99
+ readonly found: boolean;
100
+ readonly seconds: number | null;
101
+ }[];
102
+ }
103
+ export interface HarnessReport {
104
+ readonly ticks: number;
105
+ readonly bots: number;
106
+ /** Arena: deaths. Game: times a player went down. */
107
+ readonly deaths: number;
108
+ readonly kills: number;
109
+ /** Game mode: highest wave reached (1 + waves cleared), 0 in the arena. */
110
+ readonly wave: number;
111
+ /** Game mode: runs that ended, by outcome. */
112
+ readonly runs: {
113
+ readonly won: number;
114
+ readonly lost: number;
115
+ };
116
+ /** Game mode: highest player level reached. */
117
+ readonly maxLevel: number;
118
+ /** Game mode: most enemies alive at once. */
119
+ readonly peakEnemies: number;
120
+ /** Arena: the most kills one player scored in a round (0 for survivor games). */
121
+ readonly topScore: number;
122
+ /** Arena: players (bots included) who scored at least one kill. */
123
+ readonly scorers: number;
124
+ /** Game mode: earn awards by rule id (units). */
125
+ readonly earned: Readonly<Record<string, number>>;
126
+ /** Mean wall-clock milliseconds per sim tick: the FPS-equivalent server cost. */
127
+ readonly msPerTick: number;
128
+ readonly p99MsPerTick: number;
129
+ readonly earnEmitted: number;
130
+ /** True when nothing changed (no kills, no damage, no movement) for 30 s of sim time. */
131
+ readonly softlock: boolean;
132
+ /** Fun metrics (game mode only). */
133
+ readonly fun?: FunMetrics;
134
+ /**
135
+ * Board genres (tower defense, idle, cards, collector): the genre's own
136
+ * playtest checks (a failing check fails the gate) and how far the bots
137
+ * got through its arc (0..1, the progression score).
138
+ */
139
+ readonly rules?: {
140
+ readonly template: string;
141
+ readonly checks: readonly {
142
+ readonly ok: boolean;
143
+ readonly line: string;
144
+ }[];
145
+ readonly progress: number;
146
+ };
147
+ readonly failed: readonly string[];
148
+ }
149
+ export declare function runHarness(opts: HarnessOptions): Promise<HarnessReport>;