@modelstatus/cli 0.1.35 → 0.1.37

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.
@@ -1,39 +1,113 @@
1
- /* Donkey Kong — PURE game core. NO React, NO ink, NO Math.random, NO I/O.
1
+ /* Donkey Kong — PURE game engine. NO React, NO ink, NO Math.random, NO Date.now,
2
+ * NO I/O. The single source of game truth, unit-tested in isolation.
2
3
  *
3
- * This is a small, deterministic state machine: initGame(opts) builds a board
4
+ * This is a deterministic discrete map. initGame(opts) builds a board
4
5
  * procedurally from the terminal's width/height (so it scales + can refuse a
5
- * too-small terminal), and stepGame(state, { input }) advances exactly one
6
- * frame. All randomness flows through a seeded mulberry32 RNG carried in the
7
- * state, so unit tests pass a fixed seed and assert exact barrel paths /
8
- * collisions. The renderer (dk-render.js) and the Ink wrapper (DkGame.js) never
9
- * mutate state they only read it — keeping this file the single source of
10
- * game truth and trivially unit-testable.
6
+ * too-small terminal). stepGame(prev, { input, dt }) advances exactly one tick
7
+ * (1/60s) and returns a NEW state. All randomness flows through a seeded
8
+ * mulberry32 RNG carried in the state, so unit tests pass a fixed seed and
9
+ * assert EXACT positions, barrel paths and collisions and two runtimes (node
10
+ * and the bun-compiled binary) produce byte-identical state.
11
11
  *
12
- * Coordinate system: integer grid. x = column (0 = left), y = row (0 = TOP).
13
- * "Up" decreases y. Girders ("platforms") are horizontal-ish rows that drift
14
- * (classic DK zig-zag); ladders are vertical runs linking adjacent girders. */
12
+ * === WHY A REWRITE (sub-cell fixed-point) ===
13
+ * The old core stepped a single integer cell per 100ms frame and jumped a
14
+ * 4-frame triangle. That coarse 10fps single-cell grid is the "stiffness". This
15
+ * engine runs true 60Hz with SUB-CELL fixed-point positions so the player
16
+ * accelerates/slides across girders and the jump is a readable parabolic arc.
17
+ *
18
+ * === COORDINATE / FIXED-POINT MODEL ===
19
+ * Public grid stays INTEGER: x = column (0 = left), y = row (0 = TOP), up = -y.
20
+ * Girders/ladders/DK/princess and the RENDERER all read integer cells. The
21
+ * renderer reads jumpman.x/y and barrels[].x/y, which we keep as integer fields
22
+ * SYNCED each tick from the sub-cell px/py (round-to-nearest). So dk-render.js
23
+ * is unchanged in contract.
24
+ *
25
+ * Internally the player + barrels carry Int32 fixed-point position/velocity in
26
+ * units of 1/SCALE cell (SCALE = 256, a power of two → exact, no float drift
27
+ * across machines/runtimes). The render cell is round-to-nearest: (fx+128)>>8.
28
+ * Integer fixed-point with >>/* is bit-identical under V8 and JSC; that is the
29
+ * determinism guarantee the tests assert.
30
+ */
31
+
32
+ // ---- Fixed-point helpers ---------------------------------------------------
33
+ export const SCALE = 256; // 1 cell = 256 fixed-point units (power of two → exact)
34
+ export const toFx = (n) => (n * SCALE) | 0; // integer cell -> fx
35
+ export const cell = (fx) => (fx + (SCALE >> 1)) >> 8; // fx -> round-to-nearest cell
36
+ export const floorCell = (fx) => fx >> 8; // fx -> floor cell (>> on Int32 = floor for our range)
37
+ const clampFx = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
38
+
39
+ // ---- Tunables (per 1/60s tick unless noted) --------------------------------
40
+ export const TICK_HZ = 60;
41
+ export const FIXED_DT = 1; // the loop ALWAYS steps with dt=1 (true fixed timestep)
42
+ // Legacy alias so old imports of FRAME_MS keep working (now = one 60Hz tick).
43
+ export const FRAME_MS = 1000 / TICK_HZ;
44
+ export const JUMP_LEN = 4; // legacy export (old triangle length); unused by physics
15
45
 
16
- // ---- Tunables --------------------------------------------------------------
17
- export const FRAME_MS = 100; // 10fps game loop (the wrapper gates this on useTick)
18
- export const JUMP_LEN = 4; // frames a jump arc lasts
19
46
  export const MIN_W = 28; // minimum playfield width
20
47
  export const MIN_H = 9; // minimum playfield height
21
- const GRAVITY = 1; // rows/frame fallen when unsupported
48
+
49
+ // Player horizontal
50
+ export const RUN_ACCEL = 22; // fx/tick (ramps to top speed in ~6 ticks ≈ 100ms)
51
+ export const RUN_MAX = 130; // fx/tick (~0.5 cell/tick → brisk but readable)
52
+ export const GROUND_FRICTION = 30; // fx/tick decel when no input (the "slide")
53
+ export const AIR_ACCEL = 14; // weaker air-control
54
+ export const AIR_MAX = 130;
55
+
56
+ // Gravity / jump
57
+ export const GRAVITY = 14; // fx/tick added to vy while airborne (full jump → apex ≈ 3 cells)
58
+ export const JUMP_CUT_GRAVITY = 42; // stronger gravity while RISING after release → short hop (~1.5 cells)
59
+ export const TERMINAL_VY = 220; // fx/tick max fall speed
60
+ export const JUMP_VY = -150; // initial up-velocity → apex ≈ 3 cells, hang ≈ 21 ticks
61
+ export const COYOTE_TICKS = 5; // jump still allowed N ticks after walking off
62
+ export const JUMP_BUFFER_TICKS = 6; // a press N ticks before landing still fires on land
63
+
64
+ // Ladder
65
+ export const CLIMB_SPEED = 70; // fx/tick (gravity suspended while latched)
66
+
67
+ // Barrels
68
+ export const BARREL_ROLL = 95; // fx/tick base roll speed
69
+ export const MAX_BARRELS = 24; // hard cap (oldest despawns) — bounded state growth
70
+
71
+ // Geometry / scoring
22
72
  const SLOPE_EVERY = 6; // a 1-row step every ~6 columns on a girder
23
73
  const PRINCESS_REACH = 2; // |dx| within which touching the princess wins
24
74
 
25
- /** DK barrel-throw cadence by level: faster (shorter cooldown) as you climb. */
75
+ // State-machine timers (ticks)
76
+ export const GETREADY_TICKS = 90; // ≈1.5s orient beat
77
+ export const DEATH_TICKS = 60; // death-pop / pause before respawn
78
+ export const LEVELCLEAR_TICKS = 120; // banner + bonus count-up
79
+ export const RESPAWN_INVULN = 120; // ≈2s of blink after a respawn
80
+
81
+ // Bonus timer
82
+ export const BONUS_DRAIN = 10; // points removed each drain
83
+ export const BONUS_TICK = 24; // drain every 24 ticks (~0.4s)
84
+ export function bonusForLevel(level) {
85
+ return 5000 + level * 1000;
86
+ }
87
+
88
+ const SCHEMA = 2;
89
+
90
+ // ---- Level scaling (pure, tested monotonic) --------------------------------
91
+ /** DK barrel-throw cadence in TICKS — faster (shorter) as you climb. */
26
92
  export function cooldownForLevel(level) {
27
- return Math.max(7, 22 - (level - 1) * 3);
93
+ return Math.max(45, 132 - (level - 1) * 18);
28
94
  }
29
- /** Probability (0..1) a rolling barrel takes a ladder down; rises with level. */
95
+ /** Barrel roll speed (fx/tick) faster each level. */
96
+ export function barrelSpeedForLevel(level) {
97
+ return Math.min(170, BARREL_ROLL + (level - 1) * 12);
98
+ }
99
+ /** Probability a rolling barrel takes a ladder down per eligibility check. */
30
100
  export function ladderChanceForLevel(level) {
31
101
  return Math.min(0.6, 0.18 + (level - 1) * 0.1);
32
102
  }
103
+ /** By construction the jump apex (~3 cells) clears a 1-cell barrel + player at
104
+ * every level — exposed so a test can assert the invariant numerically. */
105
+ export function jumpClearsBarrel(/* level */) {
106
+ const apexCells = (JUMP_VY * JUMP_VY) / (2 * GRAVITY) / SCALE; // ≈ 3.14
107
+ return apexCells > 2; // barrel(1) + player(1) with margin
108
+ }
33
109
 
34
110
  // ---- Seeded RNG (mulberry32) — pure, threaded through state ----------------
35
- // Returns [value in [0,1), nextSeed] so the core never holds hidden mutable
36
- // state; stepGame folds the new seed back into the returned state.
37
111
  function rng(seed) {
38
112
  let t = (seed + 0x6d2b79f5) | 0;
39
113
  t = Math.imul(t ^ (t >>> 15), t | 1);
@@ -42,20 +116,15 @@ function rng(seed) {
42
116
  return [v, t >>> 0];
43
117
  }
44
118
 
45
- // ---- Board geometry --------------------------------------------------------
119
+ // ---- Board geometry (KEPT from the old core — integer grid, good) ----------
46
120
  export function boardSize(width, height) {
47
- const HUD_ROWS = 2; // scan-progress line + score/lives line (rendered by wrapper)
48
- const KEY_ROW = 1; // in-game keybar (rendered by wrapper)
49
- const BOARD_W = Math.max(MIN_W, Math.min(width - 2, 64)); // 1-col rail gutter each side
50
- const BOARD_H = height - HUD_ROWS - KEY_ROW; // identical total rows to the list view
121
+ const HUD_ROWS = 2; // scan-progress line + score/lives line (rendered by loop)
122
+ const KEY_ROW = 1; // in-game keybar (rendered by loop)
123
+ const BOARD_W = Math.max(MIN_W, Math.min(width - 2, 64));
124
+ const BOARD_H = height - HUD_ROWS - KEY_ROW;
51
125
  return { BOARD_W, BOARD_H };
52
126
  }
53
127
 
54
- /** Build the per-column y of one girder: a base row that steps 1 cell every
55
- * SLOPE_EVERY columns, alternating drift direction per level (classic zig-zag),
56
- * bounded to a `±amp` envelope around baseRow so adjacent girders never overlap
57
- * and none drifts off the board. Returned as an Int array of length BOARD_W so
58
- * collision/standing math reads the exact row a girder occupies at any column. */
59
128
  function buildSlope(baseRow, w, down, amp) {
60
129
  const offs = new Array(w);
61
130
  for (let x = 0; x < w; x++) {
@@ -66,8 +135,7 @@ function buildSlope(baseRow, w, down, amp) {
66
135
  return offs;
67
136
  }
68
137
 
69
- /** The girder index whose standing row is at-or-just-below y at column x, i.e.
70
- * the platform a body at (x,y) would land on when falling. Returns -1 if none. */
138
+ /** Girder index whose standing row is at-or-just-below row y at column x. */
71
139
  function girderBelow(platforms, x, y) {
72
140
  let best = -1, bestRow = Infinity;
73
141
  for (let i = 0; i < platforms.length; i++) {
@@ -83,92 +151,90 @@ function isGirderAt(platforms, x, y) {
83
151
  return false;
84
152
  }
85
153
 
154
+ /** The platform index whose girder row is exactly `y` at column `x`, or -1. */
155
+ function girderIndexAt(platforms, x, y) {
156
+ for (let i = 0; i < platforms.length; i++) if (platforms[i].slopeOffsets[x] === y) return i;
157
+ return -1;
158
+ }
159
+
86
160
  /** The ladder occupying column x within [yTop..yBottom] at row y, or null. */
87
161
  function ladderAt(ladders, x, y) {
88
162
  for (const l of ladders) if (l.col === x && y >= l.yTop && y <= l.yBottom) return l;
89
163
  return null;
90
164
  }
91
165
 
92
- /** Inclusive "is `v` between `a` and `b`" regardless of order — for swept hit
93
- * tests (the moving body crossed cell v this frame). */
166
+ /** Inclusive "is v between a and b" regardless of order — swept hit test. */
94
167
  function between(v, a, b) {
95
168
  return v >= Math.min(a, b) && v <= Math.max(a, b);
96
169
  }
97
170
 
98
171
  // ---- initGame --------------------------------------------------------------
99
172
  /**
100
- * Build a fresh level. Carries score/lives across level-ups. Returns
101
- * { tooSmall:true } (and nothing else) when the terminal can't host a playable
102
- * board, so the wrapper can refuse to enter game mode without ever ticking.
173
+ * Build a fresh game. Carries score/lives/best across level-ups. Returns
174
+ * { tooSmall:true } when the terminal can't host a playable board so the loop
175
+ * can refuse to enter without ever ticking.
176
+ *
177
+ * status starts at 'getready' (a brief orient beat) — the loop can set 'title'
178
+ * for the cold-start title card; both are valid entry states.
103
179
  */
104
- export function initGame({ width = 80, height = 14, level = 1, score = 0, lives = 3, seed } = {}) {
180
+ export function initGame({
181
+ width = 80, height = 14, level = 1, score = 0, lives = 3, best = 0, seed,
182
+ status = "getready",
183
+ } = {}) {
105
184
  const { BOARD_W, BOARD_H } = boardSize(width, height);
106
185
  if (BOARD_W < MIN_W || BOARD_H < MIN_H) return { tooSmall: true, BOARD_W, BOARD_H };
107
186
 
108
- // Lay out N girders evenly across the height. Highest girder (index 0) sits
109
- // near the top (DK + princess); lowest (last) near the bottom (Jumpman start).
110
187
  const N = Math.max(4, Math.min(5, Math.floor(BOARD_H / 3)));
111
- const gap = Math.floor((BOARD_H - 2) / N); // rows between girder bands
112
- // Slope amplitude must stay strictly inside the gap so neighboring girders
113
- // never overlap (which would break standing/collision math) and the lowest
114
- // girder's lowest column stays on-board. ±1 is plenty of zig-zag at our sizes.
188
+ const gap = Math.floor((BOARD_H - 2) / N);
115
189
  const amp = Math.max(0, Math.min(2, Math.floor((gap - 1) / 2)));
116
190
  const platforms = [];
117
191
  for (let i = 0; i < N; i++) {
118
- // Reserve `amp` rows of headroom at the bottom so the down-sloped lowest
119
- // girder can't exceed BOARD_H - 1.
120
192
  const baseRow = BOARD_H - 2 - amp - i * gap; // 0 = bottom-most girder
121
- const down = i % 2 === 0; // alternate slope direction per level band
193
+ const down = i % 2 === 0;
122
194
  platforms.push({ row: baseRow, slopeOffsets: buildSlope(baseRow, BOARD_W, down, amp) });
123
195
  }
124
196
  // platforms[0] is the LOWEST (largest row); platforms[N-1] the HIGHEST.
125
197
 
126
- // Ladders: 1 per gap, at a staggered column, linking the STANDING ROW of the
127
- // lower girder (girderRow - 1) to the standing row of the upper girder, so a
128
- // climber moves directly between the two walkable rows. yTop < yBottom.
129
198
  const ladders = [];
130
199
  for (let i = 0; i < N - 1; i++) {
131
200
  const lower = platforms[i];
132
201
  const upper = platforms[i + 1];
133
202
  const col = Math.max(2, Math.min(BOARD_W - 3, Math.floor(((i + 1) / N) * BOARD_W) + (i % 2 ? -3 : 3)));
134
- const yBottom = lower.slopeOffsets[col] - 1; // stand on the lower girder
135
- const yTop = upper.slopeOffsets[col] - 1; // stand on the upper girder
203
+ const yBottom = lower.slopeOffsets[col] - 1;
204
+ const yTop = upper.slopeOffsets[col] - 1;
136
205
  ladders.push({ col, yTop: Math.min(yTop, yBottom), yBottom: Math.max(yTop, yBottom) });
137
206
  }
138
207
 
139
208
  const top = platforms[N - 1];
140
209
  const dkCol = 2;
141
- const donkeyKong = { x: dkCol, y: top.slopeOffsets[dkCol] - 1, throwCooldown: 2 };
142
- // Princess one row above DK's girder, near top-center (the goal zone).
210
+ const donkeyKong = { x: dkCol, y: top.slopeOffsets[dkCol] - 1, throwCooldown: 2, animPhase: 0 };
143
211
  const princessCol = Math.floor(BOARD_W / 2);
144
- const princess = { x: princessCol, y: top.slopeOffsets[princessCol] - 1 };
212
+ const princess = { x: princessCol, y: top.slopeOffsets[princessCol] - 1, animPhase: 0 };
145
213
 
146
- const bottom = platforms[0];
147
- const startCol = 2;
148
- const jumpman = {
149
- x: startCol,
150
- y: bottom.slopeOffsets[startCol] - 1, // stand ON the lowest girder
151
- vy: 0,
152
- onGround: true,
153
- onLadder: false,
154
- facing: 1,
155
- jumpFrames: 0,
156
- alive: true,
157
- };
214
+ const player = makePlayer(platforms);
158
215
 
159
216
  return {
217
+ schema: SCHEMA,
160
218
  BOARD_W,
161
219
  BOARD_H,
162
220
  platforms,
163
221
  ladders,
164
- jumpman,
222
+ player,
223
+ // `jumpman` is a same-object alias of `player` so dk-render.js (which reads
224
+ // state.jumpman.x/y) stays unchanged in contract. Re-pointed after each clone.
225
+ jumpman: player,
165
226
  donkeyKong,
166
227
  princess,
167
228
  barrels: [],
229
+ // --- meta / state machine ---
230
+ status, // title | getready | playing | dying | levelclear | over
231
+ statusTimer: status === "getready" ? GETREADY_TICKS : 0,
168
232
  score,
169
233
  lives,
170
234
  level,
171
- status: "playing", // playing | won | dead | over
235
+ bonus: bonusForLevel(level),
236
+ bonusDrainTick: 0,
237
+ best: Math.max(best, score) >>> 0,
172
238
  rngSeed: (seed == null ? 1234567 + level * 99991 : seed) >>> 0,
173
239
  frame: 0,
174
240
  message: level > 1 ? `level ${level}` : "",
@@ -176,221 +242,418 @@ export function initGame({ width = 80, height = 14, level = 1, score = 0, lives
176
242
  };
177
243
  }
178
244
 
179
- // ---- respawn (after a death, board/barrels keep going) ---------------------
180
- function respawnJumpman(state) {
181
- const bottom = state.platforms[0];
245
+ /** Fresh player standing on the lowest girder at the start column. */
246
+ function makePlayer(platforms, invuln = 0) {
247
+ const bottom = platforms[0];
182
248
  const startCol = 2;
183
- state.jumpman = {
184
- x: startCol,
185
- y: bottom.slopeOffsets[startCol] - 1,
249
+ const standRow = bottom.slopeOffsets[startCol] - 1;
250
+ return {
251
+ px: toFx(startCol),
252
+ py: toFx(standRow),
253
+ vx: 0,
186
254
  vy: 0,
255
+ facing: 1,
187
256
  onGround: true,
188
257
  onLadder: false,
189
- facing: 1,
190
- jumpFrames: 0,
258
+ jumpHeld: false,
259
+ coyote: 0,
260
+ jumpBuffer: 0,
261
+ invuln,
191
262
  alive: true,
263
+ // integer mirror read by the renderer (synced each tick)
264
+ x: startCol,
265
+ y: standRow,
192
266
  };
193
267
  }
194
268
 
269
+ /** Sync the integer cell mirror the renderer reads from sub-cell fx. */
270
+ function syncCell(body) {
271
+ body.x = cell(body.px);
272
+ body.y = cell(body.py);
273
+ }
274
+
195
275
  // ---- stepGame --------------------------------------------------------------
196
276
  /**
197
- * Advance one frame. PURE: returns a NEW state (shallow-cloned then mutated on
198
- * the clone) so React setState(g => stepGame(g, …)) is safe. `input` is a flat
199
- * intent object the wrapper fills from keypresses and clears each frame:
200
- * { left, right, up, down, jump } (booleans).
277
+ * Advance one tick. PURE: returns a NEW state. `input` is a flat intent object:
278
+ * { left, right, up, down, jump, start, restart } (booleans).
279
+ * `jump` is EDGE-triggered (true only on the press tick); held is derived.
280
+ * dt is a tick multiplier (default 1); the loop ALWAYS passes dt=1.
201
281
  */
202
- export function stepGame(prev, { input = {} } = {}) {
203
- // Frozen states ignore gameplay input (the wrapper handles r/q out-of-band).
204
- if (prev.status === "over" || prev.status === "won" || prev.tooSmall) {
205
- return prev;
206
- }
282
+ export function stepGame(prev, { input = {}, dt = FIXED_DT } = {}) {
283
+ if (prev.tooSmall) return prev;
284
+ // 'over' is frozen a hard no-op (preserves the loop-freeze contract).
285
+ if (prev.status === "over") return prev;
207
286
 
208
- // Clone (deep-enough: entities + arrays we mutate).
287
+ // Clone (entities + arrays we mutate).
209
288
  const s = {
210
289
  ...prev,
211
- jumpman: { ...prev.jumpman },
290
+ player: { ...prev.player },
212
291
  donkeyKong: { ...prev.donkeyKong },
292
+ princess: { ...prev.princess },
213
293
  barrels: prev.barrels.map((b) => ({ ...b })),
214
294
  frame: prev.frame + 1,
215
295
  };
216
- const { platforms, ladders, BOARD_W, BOARD_H } = s;
217
- let seed = s.rngSeed;
218
- const rnd = () => { const [v, n] = rng(seed); seed = n; return v; };
296
+ s.jumpman = s.player; // keep the renderer's alias pointing at the live player
297
+
298
+ switch (s.status) {
299
+ case "title": return stepTitle(s, input);
300
+ case "getready": return stepGetReady(s, input);
301
+ case "playing": return stepPlaying(s, input, dt);
302
+ case "dying": return stepDying(s, dt);
303
+ case "levelclear": return stepLevelClear(s, dt);
304
+ default: return s;
305
+ }
306
+ }
307
+
308
+ // ---- transient-status steps ------------------------------------------------
309
+ function stepTitle(s, input) {
310
+ s.donkeyKong.animPhase = (s.frame >> 4) & 1; // slow idle chest-beat on the title only
311
+ if (input.start) {
312
+ s.status = "getready";
313
+ s.statusTimer = GETREADY_TICKS;
314
+ s.player = makePlayer(s.platforms);
315
+ s.jumpman = s.player;
316
+ s.message = "GET READY";
317
+ }
318
+ return s;
319
+ }
320
+
321
+ function stepGetReady(s, _input) {
322
+ s.message = "GET READY";
323
+ s.statusTimer -= 1;
324
+ if (s.statusTimer <= 0) {
325
+ s.status = "playing";
326
+ s.statusTimer = 0;
327
+ s.message = s.level > 1 ? `level ${s.level}` : "";
328
+ }
329
+ return s;
330
+ }
219
331
 
220
- const jm = s.jumpman;
221
- const prevX = jm.x;
222
- const prevY = jm.y;
223
-
224
- // ---- 1. Jumpman intent: horizontal move + ladder + jump ----
225
- if (input.left) { jm.x = Math.max(0, jm.x - 1); jm.facing = -1; }
226
- if (input.right) { jm.x = Math.min(BOARD_W - 1, jm.x + 1); jm.facing = 1; }
227
-
228
- // Ladder logic. A ladder spans the walkable run [yTop..yBottom] where BOTH
229
- // ends are standing rows (yTop = upper girder's standing row, yBottom = lower
230
- // girder's). When the player's column + row sit on the run, up/down moves one
231
- // row along it (gravity suspended). At yTop, up steps onto the upper girder
232
- // (off the ladder); at yBottom, down is a no-op (already on the lower girder).
233
- const onLad = ladderAt(ladders, jm.x, jm.y);
234
- if (jm.jumpFrames === 0) {
235
- if (input.up && onLad && jm.y > onLad.yTop) {
236
- jm.y -= 1; jm.onGround = false;
237
- jm.onLadder = jm.y > onLad.yTop; // off the ladder once on the upper girder's row
238
- } else if (input.down && onLad && jm.y < onLad.yBottom) {
239
- jm.y += 1; jm.onGround = false;
240
- jm.onLadder = jm.y < onLad.yBottom; // off the ladder once on the lower girder's row
332
+ function stepDying(s, _dt) {
333
+ // Death-pop: a small hop then fall, purely visual; barrels keep rolling.
334
+ const p = s.player;
335
+ p.vy = Math.min(p.vy + GRAVITY, TERMINAL_VY);
336
+ p.py = clampFx(p.py + p.vy, 0, toFx(s.BOARD_H - 1));
337
+ syncCell(p);
338
+ advanceBarrels(s, makeRnd(s)); // juice: hazards stay live during the death beat
339
+ s.statusTimer -= 1;
340
+ if (s.statusTimer <= 0) {
341
+ if (s.lives > 0) {
342
+ // respawn with invulnerability (blink window)
343
+ s.player = makePlayer(s.platforms, RESPAWN_INVULN);
344
+ s.jumpman = s.player;
345
+ s.status = "getready";
346
+ s.statusTimer = Math.floor(GETREADY_TICKS / 2);
347
+ s.message = "GET READY";
241
348
  } else {
242
- // Standing at an endpoint counts as on the girder, not the ladder.
243
- jm.onLadder = !!onLad && jm.y > onLad.yTop && jm.y < onLad.yBottom;
349
+ s.status = "over";
350
+ s.statusTimer = 0;
351
+ s.best = Math.max(s.best, s.score) >>> 0;
352
+ s.message = `GAME OVER — score ${s.score}`;
244
353
  }
245
354
  }
355
+ return finalize(s, s.rngSeed);
356
+ }
357
+
358
+ function stepLevelClear(s, _dt) {
359
+ // Count the remaining bonus up into the score over the banner (juice).
360
+ if (s.bonus > 0) {
361
+ const award = Math.min(s.bonus, 100);
362
+ s.bonus -= award;
363
+ s.score += award;
364
+ s.best = Math.max(s.best, s.score) >>> 0;
365
+ }
366
+ s.statusTimer -= 1;
367
+ if (s.statusTimer <= 0) {
368
+ return nextLevel(s, { width: s.BOARD_W + 2, height: s.BOARD_H + 3 });
369
+ }
370
+ return s;
371
+ }
372
+
373
+ // ---- the playing pipeline --------------------------------------------------
374
+ function makeRnd(s) {
375
+ let seed = s.rngSeed;
376
+ const rnd = () => { const [v, n] = rng(seed); seed = n; return v; };
377
+ rnd.flush = () => { s.rngSeed = seed >>> 0; };
378
+ return rnd;
379
+ }
380
+
381
+ function stepPlaying(s, input, dt) {
382
+ const { platforms, ladders, BOARD_W, BOARD_H } = s;
383
+ const rnd = makeRnd(s);
384
+ const p = s.player;
385
+ const maxPx = toFx(BOARD_W - 1);
386
+ const maxPy = toFx(BOARD_H - 1);
246
387
 
247
- // Jump: only from the ground (not mid-air, not on a ladder).
248
- if (input.jump && jm.onGround && jm.jumpFrames === 0 && !jm.onLadder) {
249
- jm.jumpFrames = JUMP_LEN;
250
- jm.onGround = false;
388
+ // 1. LADDER LATCH ----------------------------------------------------------
389
+ // Latch when the player's cell sits on a ladder run and up/down is pressed.
390
+ const col = cell(p.px);
391
+ const row = cell(p.py);
392
+ const onLad = ladderAt(ladders, col, row);
393
+ // Latch when the player's cell sits on a ladder run and is pressing up/down
394
+ // (and isn't trying to jump). Up only latches if there's run above; down only
395
+ // if there's run below — so standing at an endpoint and pressing "off" doesn't
396
+ // glue you to the ladder.
397
+ // Gate on the FIXED-POINT position, not the rounded cell: once the player rounds
398
+ // into the top cell (cell(py)===yTop) but py hasn't yet reached the exact snap
399
+ // point toFx(yTop), `cell(py) > yTop` is false — the climb would stall a fraction
400
+ // below the girder AND the unlatch can't fire while up is held → a deadlock at
401
+ // the top of every ladder. Comparing px/py in fx lets the climb finish onto the
402
+ // girder (the step-off below snaps + unlatches). Same fix for descending.
403
+ const wantUp = input.up && onLad && p.py > toFx(onLad.yTop);
404
+ const wantDown = input.down && onLad && p.py < toFx(onLad.yBottom);
405
+ if ((wantUp || wantDown) && !input.jump) {
406
+ p.onLadder = true;
407
+ p.vx = 0; p.vy = 0;
408
+ p.px = toFx(onLad.col); // snap to the ladder column while latched
409
+ if (wantUp) p.py = Math.max(toFx(onLad.yTop), p.py - CLIMB_SPEED);
410
+ else p.py = Math.min(toFx(onLad.yBottom), p.py + CLIMB_SPEED);
411
+ // Reached an endpoint EXACTLY → step off onto that girder (off the ladder).
412
+ if (p.py <= toFx(onLad.yTop)) { p.onLadder = false; p.onGround = true; p.py = toFx(onLad.yTop); }
413
+ else if (p.py >= toFx(onLad.yBottom)) { p.onLadder = false; p.onGround = true; p.py = toFx(onLad.yBottom); }
414
+ } else if (p.onLadder) {
415
+ // Left the column, pressed jump, or no climb input → unlatch.
416
+ const stillOn = onLad && cell(p.px) === onLad.col;
417
+ if (!stillOn || input.jump || !(input.up || input.down)) {
418
+ p.onLadder = false;
419
+ // settle onto a girder if one is at/below this cell; else gravity takes over.
420
+ if (isGirderAt(platforms, cell(p.px), cell(p.py) + 1) || isGirderAt(platforms, cell(p.px), cell(p.py))) p.onGround = true;
421
+ }
251
422
  }
252
423
 
253
- // ---- 2. Jump arc + gravity ----
254
- if (jm.jumpFrames > 0) {
255
- // Symmetric parabola: rise on the first half, fall on the second.
256
- const half = Math.ceil(JUMP_LEN / 2);
257
- if (jm.jumpFrames > half) jm.y -= 1; // going up
258
- else jm.y += 1; // coming down
259
- jm.jumpFrames -= 1;
260
- if (jm.jumpFrames === 0) {
261
- // settle onto the girder at the landing column
262
- const gi = girderBelow(platforms, jm.x, jm.y);
263
- if (gi >= 0) jm.y = platforms[gi].slopeOffsets[jm.x] - 1;
424
+ if (!p.onLadder) {
425
+ // 2. HORIZONTAL --------------------------------------------------------
426
+ const accel = p.onGround ? RUN_ACCEL : AIR_ACCEL;
427
+ const max = p.onGround ? RUN_MAX : AIR_MAX;
428
+ if (input.left && !input.right) { p.vx = Math.max(-max, p.vx - accel * dt); p.facing = -1; }
429
+ else if (input.right && !input.left) { p.vx = Math.min(max, p.vx + accel * dt); p.facing = 1; }
430
+ else if (p.onGround) {
431
+ // friction (the slide) — decel toward 0
432
+ if (p.vx > 0) p.vx = Math.max(0, p.vx - GROUND_FRICTION * dt);
433
+ else if (p.vx < 0) p.vx = Math.min(0, p.vx + GROUND_FRICTION * dt);
434
+ }
435
+ p.px = clampFx(p.px + p.vx * dt, 0, maxPx);
436
+ if (p.px === 0 || p.px === maxPx) p.vx = 0; // wall contact
437
+
438
+ // 3. JUMP --------------------------------------------------------------
439
+ if (p.coyote > 0) p.coyote -= 1;
440
+ if (p.jumpBuffer > 0) p.jumpBuffer -= 1;
441
+ if (input.jump) p.jumpBuffer = JUMP_BUFFER_TICKS; // buffer the press
442
+ const canJump = (p.onGround || p.coyote > 0);
443
+ if (p.jumpBuffer > 0 && canJump) {
444
+ p.vy = JUMP_VY;
445
+ p.onGround = false;
446
+ p.coyote = 0;
447
+ p.jumpBuffer = 0;
448
+ p.jumpHeld = true;
264
449
  }
265
- } else if (!jm.onLadder) {
266
- // Gravity: stand on a girder if the cell directly below is one; else fall to
267
- // the nearest girder beneath us.
268
- const onSomeGirder = isGirderAt(platforms, jm.x, jm.y + 1);
269
- if (onSomeGirder) {
270
- jm.onGround = true;
271
- jm.vy = 0;
450
+ // Variable height: holding through the rise = full jump (~3 cells); letting
451
+ // go while still rising applies a stronger cut-gravity a short hop.
452
+ if (!input.jump && p.vy < 0) p.jumpHeld = false;
453
+ if (p.vy >= 0) p.jumpHeld = false;
454
+
455
+ // 4. GRAVITY + SWEPT LANDING ------------------------------------------
456
+ if (!p.onGround) {
457
+ const rising = p.vy < 0;
458
+ const g = rising && !p.jumpHeld ? JUMP_CUT_GRAVITY : GRAVITY;
459
+ p.vy = Math.min(p.vy + g * dt, TERMINAL_VY);
460
+ const prevPy = p.py;
461
+ let nextPy = p.py + p.vy * dt;
462
+ const c = cell(p.px);
463
+ if (p.vy < 0) {
464
+ // Rising: bonk a girder directly overhead (can't pass up THROUGH one).
465
+ const headRow = cell(prevPy) - 1;
466
+ if (headRow >= 0 && isGirderAt(platforms, c, headRow)) {
467
+ // stop the rise just below the girder (stand row of THIS body unchanged)
468
+ p.py = toFx(cell(prevPy));
469
+ p.vy = 0;
470
+ p.jumpHeld = false;
471
+ } else {
472
+ p.py = clampFx(nextPy, 0, maxPy);
473
+ }
474
+ } else if (p.vy > 0) {
475
+ // Falling: land on the HIGHEST girder top crossed top-down this tick
476
+ // (prevPy at/above the stand row, nextPy at/below it → a real landing).
477
+ const y0 = Math.max(0, floorCell(prevPy));
478
+ const y1 = Math.min(BOARD_H - 1, floorCell(nextPy) + 1);
479
+ let landRow = -1;
480
+ for (let gy = y0; gy <= y1; gy++) {
481
+ if (!isGirderAt(platforms, c, gy)) continue;
482
+ const stand = gy - 1; // stand ON TOP of the girder
483
+ if (prevPy <= toFx(stand) && nextPy >= toFx(stand)) { landRow = stand; break; }
484
+ }
485
+ if (landRow >= 0) {
486
+ p.py = toFx(landRow);
487
+ p.vy = 0;
488
+ p.onGround = true;
489
+ p.jumpHeld = false;
490
+ } else {
491
+ p.py = clampFx(nextPy, 0, maxPy);
492
+ }
493
+ } else {
494
+ p.py = clampFx(nextPy, 0, maxPy);
495
+ }
272
496
  } else {
273
- const gi = girderBelow(platforms, jm.x, jm.y + 1);
274
- const standRow = gi >= 0 ? platforms[gi].slopeOffsets[jm.x] : -1;
275
- if (standRow >= 0 && standRow - 1 > jm.y) {
276
- jm.y = Math.min(jm.y + GRAVITY, standRow - 1);
277
- jm.onGround = jm.y === standRow - 1;
497
+ // Grounded: follow the slope at the current column, detect walk-off.
498
+ const c = cell(p.px);
499
+ const gi = girderIndexAt(platforms, c, cell(p.py) + 1);
500
+ if (gi >= 0) {
501
+ p.py = toFx(platforms[gi].slopeOffsets[c] - 1); // re-snap to slope
502
+ p.vy = 0;
278
503
  } else {
279
- jm.onGround = false;
504
+ // walked off the edge → coyote window, begin falling
505
+ p.onGround = false;
506
+ p.coyote = COYOTE_TICKS;
280
507
  }
281
508
  }
282
509
  }
283
- // Clamp inside the board.
284
- jm.y = Math.max(0, Math.min(BOARD_H - 1, jm.y));
510
+ syncCell(p);
285
511
 
286
- // ---- 3. DK throws barrels ----
512
+ // 5. DK THROW --------------------------------------------------------------
287
513
  const dk = s.donkeyKong;
288
- if (dk.throwCooldown > 0) {
289
- dk.throwCooldown -= 1;
290
- } else {
514
+ dk.throwCooldown -= 1;
515
+ if (dk.throwCooldown <= 0) {
291
516
  const top = platforms[platforms.length - 1];
517
+ const jitter = rnd() < 0.5 ? 0 : 1; // seeded ±1 col so barrels don't stack
518
+ const spawnCol = Math.min(BOARD_W - 1, dk.x + 1 + jitter);
292
519
  s.barrels.push({
293
- id: s.nextBarrelId,
294
- x: dk.x + 1,
295
- y: top.slopeOffsets[dk.x + 1] - 1,
296
- vx: 1, // roll to the right off the top girder
520
+ id: s.nextBarrelId++,
521
+ px: toFx(spawnCol),
522
+ py: toFx(top.slopeOffsets[spawnCol] - 1),
523
+ vx: barrelSpeedForLevel(s.level), // roll right
524
+ vy: 0,
297
525
  falling: false,
526
+ onLadder: false,
298
527
  scored: false,
528
+ spin: 0,
529
+ x: spawnCol,
530
+ y: top.slopeOffsets[spawnCol] - 1,
299
531
  });
300
- s.nextBarrelId += 1;
532
+ if (s.barrels.length > MAX_BARRELS) s.barrels.shift(); // cap (oldest despawns)
301
533
  dk.throwCooldown = cooldownForLevel(s.level);
534
+ dk.animPhase ^= 1; // chest-beat juice (only on a throw)
535
+ }
536
+
537
+ // 6/7. BARRELS -------------------------------------------------------------
538
+ advanceBarrels(s, rnd);
539
+
540
+ // 8. COLLISIONS + SCORING --------------------------------------------------
541
+ const pCol = cell(p.px), pRow = cell(p.py);
542
+ for (const b of s.barrels) {
543
+ const bCol = cell(b.px), bRow = cell(b.py);
544
+ const endOn = bCol === pCol && bRow === pRow;
545
+ // Swept overlap using pre-move floor cells (no tunneling).
546
+ const bPrevCol = b._pCol ?? bCol, bPrevRow = b._pRow ?? bRow;
547
+ const sameRowSweep = (bRow === pRow || bPrevRow === pRow) && between(pCol, bPrevCol, bCol);
548
+ const sameColSweep = (bCol === pCol || bPrevCol === pCol) && between(pRow, bPrevRow, bRow);
549
+ const playerSweep = bRow === pRow && between(bCol, p._prevCol ?? pCol, pCol);
550
+ const hit = endOn || sameRowSweep || sameColSweep || playerSweep;
551
+ if (hit && p.invuln === 0 && p.alive) {
552
+ s.lives -= 1;
553
+ s.status = "dying";
554
+ s.statusTimer = DEATH_TICKS;
555
+ s.message = "ouch! barrel hit";
556
+ p.vy = -90; // death-pop kick
557
+ rnd.flush();
558
+ return finalize(s, s.rngSeed);
559
+ }
560
+ // Jump-over credit: airborne, barrel below + shares column, once per barrel.
561
+ const airborne = !p.onGround || p.vy !== 0;
562
+ if (!b.scored && airborne && (bCol === pCol || bPrevCol === pCol) && bRow > pRow) {
563
+ b.scored = true;
564
+ s.score += 10;
565
+ s.best = Math.max(s.best, s.score) >>> 0;
566
+ s.message = "+10 jumped!";
567
+ }
568
+ }
569
+
570
+ // 9. BONUS DRAIN -----------------------------------------------------------
571
+ s.bonusDrainTick += 1;
572
+ if (s.bonusDrainTick >= BONUS_TICK) {
573
+ s.bonusDrainTick = 0;
574
+ s.bonus = Math.max(0, s.bonus - BONUS_DRAIN);
575
+ }
576
+
577
+ // 10. WIN ------------------------------------------------------------------
578
+ const dxP = Math.abs(pCol - s.princess.x);
579
+ const dyP = Math.abs(pRow - s.princess.y);
580
+ if (dxP <= PRINCESS_REACH && dyP <= 1) {
581
+ s.status = "levelclear";
582
+ s.statusTimer = LEVELCLEAR_TICKS;
583
+ s.score += 100;
584
+ s.best = Math.max(s.best, s.score) >>> 0;
585
+ s.message = "YOU SAVED HER! +100";
586
+ rnd.flush();
587
+ return finalize(s, s.rngSeed);
302
588
  }
303
589
 
304
- // ---- 4. Barrels roll / fall / descend ladders ----
590
+ // 11. invuln decay + record prev cells for next tick's sweep
591
+ if (p.invuln > 0) p.invuln -= 1;
592
+ p._prevCol = pCol;
593
+ rnd.flush();
594
+ return finalize(s, s.rngSeed);
595
+ }
596
+
597
+ /** Advance every barrel one tick (roll / fall / descend ladders) in fx. */
598
+ function advanceBarrels(s, rnd) {
599
+ const { platforms, ladders, BOARD_W, BOARD_H } = s;
305
600
  const ladderChance = ladderChanceForLevel(s.level);
601
+ const maxPx = toFx(BOARD_W - 1);
306
602
  const surviving = [];
307
603
  for (const b of s.barrels) {
308
- // Pre-move position, so collision (below) can catch a barrel that rolled
309
- // THROUGH the player's cell in one frame, not just where it ended up.
310
- b.px = b.x; b.py = b.y;
604
+ b._pCol = cell(b.px); b._pRow = cell(b.py); // pre-move cells for the sweep
605
+ b.spin = (b.spin + 1) & 0xffff;
606
+ const c = cell(b.px);
311
607
  if (b.falling) {
312
- // Free-fall until it lands on the next girder below, then resume rolling.
313
- const gi = girderBelow(platforms, b.x, b.y + 1);
314
- const landRow = gi >= 0 ? platforms[gi].slopeOffsets[b.x] - 1 : Infinity;
315
- if (b.y + GRAVITY >= landRow && landRow !== Infinity) {
316
- b.y = landRow;
608
+ b.vy = Math.min((b.vy || 0) + GRAVITY, TERMINAL_VY);
609
+ const nextPy = b.py + b.vy;
610
+ const gi = girderIndexAt(platforms, c, floorCell(nextPy) + 1) >= 0
611
+ ? girderIndexAt(platforms, c, floorCell(nextPy) + 1)
612
+ : girderBelow(platforms, c, cell(b.py) + 1);
613
+ const landRow = gi >= 0 ? platforms[gi].slopeOffsets[c] - 1 : -1;
614
+ if (landRow >= 0 && nextPy >= toFx(landRow) && toFx(landRow) >= b.py - 1) {
615
+ b.py = toFx(landRow);
616
+ b.vy = 0;
317
617
  b.falling = false;
318
- // continue in the same horizontal direction
618
+ b.onLadder = false;
319
619
  } else {
320
- b.y += GRAVITY;
620
+ b.py = nextPy;
321
621
  }
322
622
  } else {
323
- // Maybe descend a ladder it's standing over.
324
- const l = ladderAt(ladders, b.x, b.y + 1);
325
- if (l && rnd() < ladderChance) {
623
+ // On a girder: maybe descend a ladder it's over.
624
+ const lad = ladderAt(ladders, c, cell(b.py) + 1);
625
+ if (lad && rnd() < ladderChance) {
326
626
  b.falling = true;
327
- b.y += GRAVITY;
627
+ b.onLadder = true;
628
+ b.px = toFx(lad.col);
629
+ b.vy = GRAVITY;
630
+ b.py = b.py + b.vy;
328
631
  } else {
329
- const nx = b.x + b.vx;
330
- if (nx < 0 || nx >= BOARD_W) {
331
- // hit the wall: fall to the girder below and reverse
332
- b.vx = -b.vx;
632
+ const nextPx = b.px + b.vx;
633
+ const nc = cell(nextPx);
634
+ if (nextPx < 0 || nextPx > maxPx) {
635
+ b.vx = -b.vx; // bounce off the wall and fall to the girder below
333
636
  b.falling = true;
334
- b.y += GRAVITY;
637
+ b.vy = GRAVITY;
638
+ b.py = b.py + b.vy;
335
639
  } else {
336
- // Follow the girder's slope: snap onto the girder row at the new col
337
- // if there is one; otherwise it rolled off the low end fall.
338
- const giHere = (() => { for (let i = 0; i < platforms.length; i++) if (platforms[i].slopeOffsets[b.x] === b.y + 1) return i; return -1; })();
339
- if (giHere >= 0) {
340
- const nextRow = platforms[giHere].slopeOffsets[nx];
341
- b.x = nx;
342
- b.y = nextRow - 1;
640
+ const giHere = girderIndexAt(platforms, c, cell(b.py) + 1);
641
+ if (giHere >= 0 && nc >= 0 && nc < BOARD_W && platforms[giHere].slopeOffsets[nc] !== undefined) {
642
+ b.px = nextPx;
643
+ b.py = toFx(platforms[giHere].slopeOffsets[nc] - 1); // follow slope
343
644
  } else {
344
- // no girder under us anymore start falling
345
- b.x = nx;
645
+ b.px = nextPx; // rolled off the endfall
346
646
  b.falling = true;
347
- b.y += GRAVITY;
647
+ b.vy = GRAVITY;
648
+ b.py = b.py + b.vy;
348
649
  }
349
650
  }
350
651
  }
351
652
  }
352
- if (b.y < BOARD_H) surviving.push(b); // despawn below the board
653
+ syncCell(b);
654
+ if (cell(b.py) < BOARD_H) surviving.push(b); // despawn below the board
353
655
  }
354
656
  s.barrels = surviving;
355
-
356
- // ---- 5. Collisions + jump-over scoring (swept to avoid tunneling) ----
357
- for (const b of s.barrels) {
358
- // Hit if the barrel ends on the player's cell, OR it rolled/fell THROUGH the
359
- // player this frame (swept on both axes using the captured pre-move pos), OR
360
- // the player walked into where the barrel ended up (player's x sweep).
361
- const endOn = b.x === jm.x && b.y === jm.y;
362
- const barrelSwept =
363
- (b.py === jm.y && b.y === jm.y && between(jm.x, b.px, b.x)) || // rolled across the row
364
- (b.px === jm.x && b.x === jm.x && between(jm.y, b.py, b.y)); // fell down the column
365
- const playerSwept = b.y === jm.y && between(b.x, prevX, jm.x);
366
- if ((endOn || barrelSwept || playerSwept) && jm.alive) {
367
- s.lives -= 1;
368
- s.message = "ouch! barrel hit";
369
- if (s.lives <= 0) { s.status = "over"; s.message = `GAME OVER — score ${s.score}`; return finalize(s, seed); }
370
- respawnJumpman(s);
371
- return finalize(s, seed); // brief pause; next frame resumes
372
- }
373
- // Jump-over credit: while airborne (mid-jump), a barrel that is below the
374
- // player and shares his column has been cleared — +10, once per barrel.
375
- // Checked against the barrel's pre- OR post-move column so a fast barrel
376
- // that rolled under during the same frame still earns the credit.
377
- if (!b.scored && jm.jumpFrames > 0 && (b.x === jm.x || b.px === jm.x) && b.y > jm.y) {
378
- b.scored = true;
379
- s.score += 10;
380
- s.message = "+10 jumped!";
381
- }
382
- }
383
-
384
- // ---- 6. Win: reach the princess ----
385
- const dxP = Math.abs(jm.x - s.princess.x);
386
- const dyP = Math.abs(jm.y - s.princess.y);
387
- if (dxP <= PRINCESS_REACH && dyP <= 1) {
388
- s.score += 100;
389
- s.status = "won";
390
- s.message = "you saved her! +100";
391
- }
392
-
393
- return finalize(s, seed);
394
657
  }
395
658
 
396
659
  function finalize(s, seed) {
@@ -398,7 +661,8 @@ function finalize(s, seed) {
398
661
  return s;
399
662
  }
400
663
 
401
- /** Build the next level after a win, carrying score+lives. */
664
+ // ---- nextLevel / serialize -------------------------------------------------
665
+ /** Build the next level after a win, carrying score + lives + best. */
402
666
  export function nextLevel(state, dims) {
403
667
  return initGame({
404
668
  width: dims.width,
@@ -406,8 +670,25 @@ export function nextLevel(state, dims) {
406
670
  level: state.level + 1,
407
671
  score: state.score,
408
672
  lives: state.lives,
673
+ best: state.best,
674
+ status: "getready",
409
675
  });
410
676
  }
411
677
 
412
- // Expose a couple of internals for focused unit tests.
413
- export const _internals = { girderBelow, isGirderAt, ladderAt, rng };
678
+ /** serialize/deserialize: the state is plain JSON already (Int32 fx + arrays).
679
+ * serialize drops the `jumpman` alias (it duplicates `player`); deserialize and
680
+ * stepGame re-point it, so round-tripping is byte-stable and compact. */
681
+ export function serialize(state) {
682
+ const { jumpman, ...rest } = state; // jumpman === player; don't double-encode
683
+ return JSON.parse(JSON.stringify(rest));
684
+ }
685
+ export function deserialize(obj) {
686
+ obj.jumpman = obj.player; // re-establish the renderer alias
687
+ return obj;
688
+ }
689
+
690
+ // Expose internals for focused unit tests.
691
+ export const _internals = {
692
+ girderBelow, isGirderAt, girderIndexAt, ladderAt, rng, between,
693
+ makePlayer, advanceBarrels, syncCell,
694
+ };