Collapse Tetris' variable clusters, and give the long runs a home
Next move held four clusters of Set variable in a row, fourteen single nodes
in all; clear-lines read three variables one after the other. They are now
one plural node each, which takes Next move 40 -> 30 nodes and clear-lines
25 -> 23. Every remaining single node was checked and really is single: ok/no
are the same variable in opposite branches, take/keep the same one across a
write, and the three key setters hang on three different events.
Two contracts carry those merges, both pinned as tests. Set variables writes
per port with writeTyped exactly as the singular node does, so a Pack{rot}
still merges instead of overwriting - without it, turning would have erased
the other three fields. And the interpreter collects every input before the
node runs, so no input may come from a variable the same node writes; a data
node is evaluated once per pulse and keeps its answer. That second rule is
invisible in the game, because OnUpdate re-reads each frame and heals it, so
it lives in a unit test rather than in the smoke.
/opt/node22 and /opt/npm are gone from the tree. build.sh ran into the empty
path once they were moved away; the other seven mentions were documentation.
e2e.sh and check.sh replace the bare npm calls. A long run does not survive
the end of its shell - four full e2e runs died mid-suite with oom_kill 0 -
so e2e.sh start hands the run to a transient game-maker-e2e.service, which
does. e2e.sh status reports the running job or the last result, and calls a
log without a closing line ABORTED rather than reporting the passes it got
to. Both scripts end on an explicit exit line, because set -o pipefail inside
a script does not protect the caller: measured, ./check.sh | tail still
reports 0. Logs carry a timestamp and land in logs/, which is ignored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f60358c951
commit
fe3c63c48c
15 files changed
+439
-183
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Headless end-to-end checks of the editor, driven over the Chrome DevTools Protocol.
|
||||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-e2e.mjs && node tools/e2e.mjs [projectId] [baseUrl]
|
||||
* Usage: node tools/seed-e2e.mjs && node tools/e2e.mjs [projectId] [baseUrl]
|
||||
* Prints PASS/FAIL per check, writes screenshots to /tmp/gm-e2e-*.png. No dependencies.
|
||||
*
|
||||
* Runs against the fixture project written by tools/seed-e2e.mjs and mutates it heavily; the
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
* child each, a camera, the Mover behavior owning the red crate and a Spawner. The objects are
|
||||
* uniquely named and positioned exactly where the checks expect them.
|
||||
*
|
||||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-e2e.mjs [projectId] [baseUrl]
|
||||
* Usage: node tools/seed-e2e.mjs [projectId] [baseUrl]
|
||||
*/
|
||||
import { edge, node, sceneObject, variable } from './lib/builders.mjs';
|
||||
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* switch the inspector shows for every object. Everything else is nodes: gravity and jumping
|
||||
* on the player, timer, coin counter and screen flow on the "Game" behavior.
|
||||
*
|
||||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-jumprun.mjs [projectId] [baseUrl]
|
||||
* Usage: node tools/seed-jumprun.mjs [projectId] [baseUrl]
|
||||
*/
|
||||
import { edge, node, sceneObject, variable } from './lib/builders.mjs';
|
||||
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
|
||||
|
||||
+151
-170
@@ -12,6 +12,10 @@
|
||||
* cells. That is what lets `Next move` hand back a whole candidate for `Fits` to judge and the
|
||||
* master to accept in a single write, and it halves the index arithmetic in `Piece cell`.
|
||||
*
|
||||
* Where a step needs several variables at once, one `Get variables` / `Set variables` stands for the
|
||||
* lot – the call to `Next move` has one node at each side of it instead of five. How far such a node
|
||||
* may reach is bounded by the pulse cache; `graphBuilder` below says how.
|
||||
*
|
||||
* The game is ten behaviors rather than one graph. `Tetris` owns the state and does none of the
|
||||
* work: every step is a behavior of its own, called as a node – `Spawn piece`, `Next move`,
|
||||
* `Fits`, `Draw piece`, `Lock piece`, `Clear lines`, `Show lines`, `Draw board`, with `Piece cell`
|
||||
@@ -22,7 +26,7 @@
|
||||
* What the player sees is a pool of objects the graphs address by name: `c0…c199` for the board
|
||||
* and `p0…p3` for the piece that is falling. Nothing is spawned or destroyed while playing.
|
||||
*
|
||||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-tetris.mjs [projectId] [baseUrl]
|
||||
* Usage: node tools/seed-tetris.mjs [projectId] [baseUrl]
|
||||
*/
|
||||
import { edge, field, node, port, sceneObject, variable } from './lib/builders.mjs';
|
||||
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
|
||||
@@ -167,14 +171,24 @@ const boardTemplate = [
|
||||
),
|
||||
];
|
||||
|
||||
/** A `sets` entry whose value arrives through an edge instead of being typed on the node. */
|
||||
const LINKED = Symbol('linked');
|
||||
|
||||
/**
|
||||
* One graph under construction. Nodes are laid out in bands, one per strand, so the editor shows a
|
||||
* behavior as a stack of readable blocks rather than one cloud.
|
||||
* behavior as a stack of readable blocks rather than one cloud. `variables` is what the plural nodes
|
||||
* declare their ports from – pass the behavior's own list.
|
||||
*/
|
||||
function graphBuilder() {
|
||||
function graphBuilder(variables = []) {
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
let band = 0;
|
||||
const kinds = Object.fromEntries(variables.map((v) => [v.key, v.kind]));
|
||||
const declare = (keys) =>
|
||||
keys.map((key) => {
|
||||
if (!kinds[key]) throw new Error(`no variable "${key}" to declare a port for`);
|
||||
return port(key, kinds[key]);
|
||||
});
|
||||
|
||||
const at = (id, type, column, row, params = {}, ports) => {
|
||||
nodes.push(node(id, type, column * 220, band * 1400 + row * 90, params, ports));
|
||||
@@ -196,6 +210,20 @@ function graphBuilder() {
|
||||
get: (id, key, column, row) => at(id, 'GetVariable', column, row, { variable: key }),
|
||||
set: (id, key, column, row, value) =>
|
||||
at(id, 'SetVariable', column, row, value === undefined ? { variable: key } : { variable: key, value }),
|
||||
/**
|
||||
* The plural nodes: one node for a whole cluster, one port per variable. A data node is cached
|
||||
* for the entire pulse, so a `gets` must not span a write of one of its variables – the second
|
||||
* read would serve the value from before the write. Split it in two where it does. A `sets`
|
||||
* reads all its inputs before it writes any, so none of them may come from a variable it writes.
|
||||
*/
|
||||
gets: (id, keys, column, row) => at(id, 'GetVariables', column, row, {}, declare(keys)),
|
||||
sets: (id, spec, column, row) => {
|
||||
const keys = Array.isArray(spec) ? spec : Object.keys(spec);
|
||||
const params = Array.isArray(spec)
|
||||
? {}
|
||||
: Object.fromEntries(Object.entries(spec).filter(([, v]) => v !== LINKED));
|
||||
return at(id, 'SetVariables', column, row, params, declare(keys));
|
||||
},
|
||||
/** A mistyped id would silently leave an input unconnected, so the seed refuses to write one. */
|
||||
done() {
|
||||
const ids = new Set();
|
||||
@@ -329,36 +357,41 @@ function fitsGraph() {
|
||||
return g.done();
|
||||
}
|
||||
|
||||
const CLEAR_LINES_VARS = [
|
||||
variable('board', 'list', [], 'number'),
|
||||
variable('cleared', 'number', 0),
|
||||
variable('row', 'number', 0),
|
||||
];
|
||||
|
||||
/**
|
||||
* Removes every full row and counts it. Walks the board from the bottom up; a cleared row does not
|
||||
* move the walk on, because everything above has dropped into it – so the same row is looked at
|
||||
* again, which is how two full rows on top of each other both go.
|
||||
*/
|
||||
function clearLinesGraph() {
|
||||
const g = graphBuilder();
|
||||
const g = graphBuilder(CLEAR_LINES_VARS);
|
||||
g.at('start', 'BehaviorStart', 0, 0, {}, [port('grid', 'list'), port('lines', 'number')]);
|
||||
g.set('start-board', 'board', 1, 0);
|
||||
g.set('start-count', 'cleared', 2, 0);
|
||||
g.set('row', 'row', 3, 0, ROWS - 1);
|
||||
g.sets('start-vars', { board: LINKED, cleared: LINKED, row: ROWS - 1 }, 1, 0);
|
||||
g.at('loop', 'Repeat', 4, 0, { times: ROWS + 4 });
|
||||
g.link('start', 'grid', 'start-board', 'value');
|
||||
g.link('start', 'lines', 'start-count', 'value');
|
||||
g.flow('start', 'start-board', 'start-count', 'row', 'loop');
|
||||
g.link('start', 'grid', 'start-vars', 'board');
|
||||
g.link('start', 'lines', 'start-vars', 'cleared');
|
||||
g.flow('start', 'start-vars', 'loop');
|
||||
|
||||
g.get('rowvar', 'row', 5, 3);
|
||||
// Every round reads all three before it writes any of them, and the loop drops the pulse cache
|
||||
// between rounds – so one node serves the whole round.
|
||||
g.gets('row-vars', ['row', 'board', 'cleared'], 5, 3);
|
||||
g.at('left', 'Compare', 6, 3, { op: '>=', b: 0 });
|
||||
g.at('brleft', 'Branch', 6, 0);
|
||||
g.at('rowstart', 'Multiply', 7, 3, { b: COLS });
|
||||
g.get('grid', 'board', 7, 5);
|
||||
g.at('cells', 'SubList', 8, 4, { count: COLS });
|
||||
g.at('gap', 'IndexOf', 9, 4, { value: EMPTY });
|
||||
g.at('full', 'Compare', 10, 4, { op: '<', b: 0 });
|
||||
g.at('brfull', 'Branch', 8, 0);
|
||||
g.link('loop', 'body', 'brleft', 'exec');
|
||||
g.link('rowvar', 'value', 'left', 'a');
|
||||
g.link('row-vars', 'row', 'left', 'a');
|
||||
g.link('left', 'result', 'brleft', 'condition');
|
||||
g.link('rowvar', 'value', 'rowstart', 'a');
|
||||
g.link('grid', 'value', 'cells', 'list');
|
||||
g.link('row-vars', 'row', 'rowstart', 'a');
|
||||
g.link('row-vars', 'board', 'cells', 'list');
|
||||
g.link('rowstart', 'result', 'cells', 'start');
|
||||
g.link('cells', 'result', 'gap', 'list');
|
||||
g.link('gap', 'index', 'full', 'a');
|
||||
@@ -371,37 +404,33 @@ function clearLinesGraph() {
|
||||
g.at('blank', 'Fill', 9, 12, { value: EMPTY, count: COLS });
|
||||
g.at('top', 'Concat', 10, 8);
|
||||
g.at('rest', 'Concat', 11, 9);
|
||||
g.set('setgrid', 'board', 10, 0);
|
||||
g.get('lines', 'cleared', 11, 3);
|
||||
g.at('onemore', 'Add', 12, 3, { b: 1 });
|
||||
g.set('setlines', 'cleared', 12, 0);
|
||||
g.link('brfull', 'then', 'setgrid', 'exec');
|
||||
g.link('grid', 'value', 'above', 'list');
|
||||
g.sets('cleared-row', ['board', 'cleared'], 10, 0);
|
||||
g.link('brfull', 'then', 'cleared-row', 'exec');
|
||||
g.link('row-vars', 'board', 'above', 'list');
|
||||
g.link('rowstart', 'result', 'above', 'count');
|
||||
g.link('rowstart', 'result', 'past', 'a');
|
||||
g.link('grid', 'value', 'below', 'list');
|
||||
g.link('row-vars', 'board', 'below', 'list');
|
||||
g.link('past', 'result', 'below', 'start');
|
||||
g.link('blank', 'result', 'top', 'a');
|
||||
g.link('above', 'result', 'top', 'b');
|
||||
g.link('top', 'result', 'rest', 'a');
|
||||
g.link('below', 'result', 'rest', 'b');
|
||||
g.link('rest', 'result', 'setgrid', 'value');
|
||||
g.link('lines', 'value', 'onemore', 'a');
|
||||
g.link('onemore', 'result', 'setlines', 'value');
|
||||
g.flow('setgrid', 'setlines');
|
||||
g.link('rest', 'result', 'cleared-row', 'board');
|
||||
g.link('row-vars', 'cleared', 'onemore', 'a');
|
||||
g.link('onemore', 'result', 'cleared-row', 'cleared');
|
||||
|
||||
g.at('back', 'Subtract', 9, 14, { b: 1 });
|
||||
g.set('setrow', 'row', 10, 13);
|
||||
g.link('brfull', 'else', 'setrow', 'exec');
|
||||
g.link('rowvar', 'value', 'back', 'a');
|
||||
g.link('row-vars', 'row', 'back', 'a');
|
||||
g.link('back', 'result', 'setrow', 'value');
|
||||
|
||||
g.at('end', 'BehaviorEnd', 13, 0, {}, [port('grid', 'list'), port('lines', 'number')]);
|
||||
g.get('outgrid', 'board', 12, 6);
|
||||
g.get('outlines', 'cleared', 12, 8);
|
||||
g.gets('out-vars', ['board', 'cleared'], 12, 6);
|
||||
g.link('loop', 'done', 'end', 'exec');
|
||||
g.link('outgrid', 'value', 'end', 'grid');
|
||||
g.link('outlines', 'value', 'end', 'lines');
|
||||
g.link('out-vars', 'board', 'end', 'grid');
|
||||
g.link('out-vars', 'cleared', 'end', 'lines');
|
||||
return g.done();
|
||||
}
|
||||
|
||||
@@ -459,14 +488,22 @@ function spawnPieceGraph() {
|
||||
return g.done();
|
||||
}
|
||||
|
||||
const NEXT_MOVE_VARS = [
|
||||
variable('next', 'struct', NO_PIECE, undefined, PIECE_FIELDS),
|
||||
variable('kind', 'text', ''),
|
||||
variable('outFall', 'number', 0),
|
||||
variable('outDx', 'number', 0),
|
||||
variable('outRot', 'number', 0),
|
||||
];
|
||||
|
||||
/**
|
||||
* The one move a frame attempts, and nothing else: turning beats moving beats falling. It only
|
||||
* proposes – whether the piece may go there is `Fits`, one step later. A wish that was granted
|
||||
* comes back as 0, which is how a key press is spent exactly once.
|
||||
*/
|
||||
function nextMoveGraph() {
|
||||
const g = graphBuilder();
|
||||
const { at, link, flow, get, set } = g;
|
||||
const g = graphBuilder(NEXT_MOVE_VARS);
|
||||
const { at, link, flow, get, set, sets } = g;
|
||||
|
||||
at('start', 'BehaviorStart', 0, 0, {}, [
|
||||
port('piece', 'struct'),
|
||||
@@ -477,21 +514,17 @@ function nextMoveGraph() {
|
||||
port('stepTime', 'number'),
|
||||
]);
|
||||
|
||||
// The candidate starts out as the piece itself – one write, because it is one value. Each
|
||||
// strand below then packs only the field it changes: writing a bundle keeps the fields it
|
||||
// does not name, so "the same piece, one row lower" needs no copying.
|
||||
// The frame starts out having decided nothing: the candidate is the piece itself and no wish has
|
||||
// been used up. Each strand below then packs only the field it changes, because writing a bundle
|
||||
// keeps the fields it does not name – "the same piece, one row lower" needs no copying.
|
||||
at('of', 'ExpandStruct', 1, 3, {}, PIECE_FIELDS);
|
||||
link('start', 'piece', 'of', 'value');
|
||||
set('i-next', 'next', 1, 0);
|
||||
set('i-kind', 'kind', 2, 0, '');
|
||||
set('i-fall', 'outFall', 3, 0);
|
||||
set('i-dx', 'outDx', 4, 0);
|
||||
set('i-rot', 'outRot', 5, 0);
|
||||
link('start', 'piece', 'i-next', 'value');
|
||||
link('start', 'fall', 'i-fall', 'value');
|
||||
link('start', 'wantDx', 'i-dx', 'value');
|
||||
link('start', 'wantRot', 'i-rot', 'value');
|
||||
flow('start', 'i-next', 'i-kind', 'i-fall', 'i-dx', 'i-rot', 't-br');
|
||||
sets('i-vars', { next: LINKED, kind: '', outFall: LINKED, outDx: LINKED, outRot: LINKED }, 1, 0);
|
||||
link('start', 'piece', 'i-vars', 'next');
|
||||
link('start', 'fall', 'i-vars', 'outFall');
|
||||
link('start', 'wantDx', 'i-vars', 'outDx');
|
||||
link('start', 'wantRot', 'i-vars', 'outRot');
|
||||
flow('start', 'i-vars', 't-br');
|
||||
|
||||
// Turning wins: a quarter turn clockwise is one step around the four rotations of the shape.
|
||||
g.strand();
|
||||
@@ -500,17 +533,15 @@ function nextMoveGraph() {
|
||||
at('t-plus1', 'Add', 1, 3, { b: 1 });
|
||||
at('t-wrap', 'Modulo', 2, 3, { b: 4 });
|
||||
at('t-pack', 'PackStruct', 3, 3, {}, [field('rot')]);
|
||||
set('t-set', 'next', 1, 0);
|
||||
set('t-kind', 'kind', 2, 0, 'turn');
|
||||
set('t-used', 'outRot', 3, 0, 0);
|
||||
sets('t-vars', { next: LINKED, kind: 'turn', outRot: 0 }, 1, 0);
|
||||
link('start', 'wantRot', 't-want', 'a');
|
||||
link('t-want', 'result', 't-br', 'condition');
|
||||
link('t-br', 'then', 't-set', 'exec');
|
||||
link('t-br', 'then', 't-vars', 'exec');
|
||||
link('of', 'rot', 't-plus1', 'a');
|
||||
link('t-plus1', 'result', 't-wrap', 'a');
|
||||
link('t-wrap', 'result', 't-pack', 'rot');
|
||||
link('t-pack', 'value', 't-set', 'value');
|
||||
flow('t-set', 't-kind', 't-used', 'end');
|
||||
link('t-pack', 'value', 't-vars', 'next');
|
||||
flow('t-vars', 'end');
|
||||
|
||||
// Then sideways, one column per key press.
|
||||
at('m-still', 'Equals', 4, 3, { b: 0 });
|
||||
@@ -518,19 +549,17 @@ function nextMoveGraph() {
|
||||
at('m-br', 'Branch', 5, 0);
|
||||
at('m-shift', 'Add', 6, 3);
|
||||
at('m-pack', 'PackStruct', 7, 3, {}, [field('x')]);
|
||||
set('m-set', 'next', 6, 0);
|
||||
set('m-kind', 'kind', 7, 0, 'move');
|
||||
set('m-used', 'outDx', 8, 0, 0);
|
||||
sets('m-vars', { next: LINKED, kind: 'move', outDx: 0 }, 6, 0);
|
||||
link('t-br', 'else', 'm-br', 'exec');
|
||||
link('start', 'wantDx', 'm-still', 'a');
|
||||
link('m-still', 'result', 'm-moving', 'value');
|
||||
link('m-moving', 'result', 'm-br', 'condition');
|
||||
link('m-br', 'then', 'm-set', 'exec');
|
||||
link('m-br', 'then', 'm-vars', 'exec');
|
||||
link('of', 'x', 'm-shift', 'a');
|
||||
link('start', 'wantDx', 'm-shift', 'b');
|
||||
link('m-shift', 'result', 'm-pack', 'x');
|
||||
link('m-pack', 'value', 'm-set', 'value');
|
||||
flow('m-set', 'm-kind', 'm-used', 'end');
|
||||
link('m-pack', 'value', 'm-vars', 'next');
|
||||
flow('m-vars', 'end');
|
||||
|
||||
// And otherwise the clock: holding the down key lets it run ten times as fast.
|
||||
g.strand();
|
||||
@@ -553,23 +582,21 @@ function nextMoveGraph() {
|
||||
at('f-due', 'Compare', 6, 3, { op: '>=' });
|
||||
at('f-br', 'Branch', 5, 0);
|
||||
at('f-minus', 'Subtract', 7, 3);
|
||||
set('f-rest', 'outFall', 6, 0);
|
||||
at('f-down1', 'Add', 7, 5, { b: 1 });
|
||||
at('f-pack', 'PackStruct', 8, 5, {}, [field('y')]);
|
||||
set('f-ny', 'next', 7, 0);
|
||||
set('f-kind', 'kind', 8, 0, 'fall');
|
||||
sets('f-vars', { outFall: LINKED, next: LINKED, kind: 'fall' }, 6, 0);
|
||||
flow('f-set', 'f-br');
|
||||
link('f-fall', 'value', 'f-due', 'a');
|
||||
link('start', 'stepTime', 'f-due', 'b');
|
||||
link('f-due', 'result', 'f-br', 'condition');
|
||||
link('f-br', 'then', 'f-rest', 'exec');
|
||||
link('f-br', 'then', 'f-vars', 'exec');
|
||||
link('f-fall', 'value', 'f-minus', 'a');
|
||||
link('start', 'stepTime', 'f-minus', 'b');
|
||||
link('f-minus', 'result', 'f-rest', 'value');
|
||||
link('f-minus', 'result', 'f-vars', 'outFall');
|
||||
link('of', 'y', 'f-down1', 'a');
|
||||
link('f-down1', 'result', 'f-pack', 'y');
|
||||
link('f-pack', 'value', 'f-ny', 'value');
|
||||
flow('f-rest', 'f-ny', 'f-kind', 'end');
|
||||
link('f-pack', 'value', 'f-vars', 'next');
|
||||
flow('f-vars', 'end');
|
||||
// Not due yet: nothing moves, and "kind" stays empty so the fit test below changes nothing.
|
||||
link('f-br', 'else', 'end', 'exec');
|
||||
|
||||
@@ -583,16 +610,19 @@ function nextMoveGraph() {
|
||||
port('wantRot', 'number'),
|
||||
]);
|
||||
const back = [
|
||||
['e-next', 'next', 'next'],
|
||||
['e-kind', 'kind', 'kind'],
|
||||
['e-fall', 'outFall', 'fall'],
|
||||
['e-dx', 'outDx', 'wantDx'],
|
||||
['e-rot', 'outRot', 'wantRot'],
|
||||
['next', 'next'],
|
||||
['kind', 'kind'],
|
||||
['outFall', 'fall'],
|
||||
['outDx', 'wantDx'],
|
||||
['outRot', 'wantRot'],
|
||||
];
|
||||
back.forEach(([id, key, portKey], i) => {
|
||||
get(id, key, 0, i * 2 + 2);
|
||||
link(id, 'value', 'end', portKey);
|
||||
});
|
||||
g.gets(
|
||||
'e-vars',
|
||||
back.map(([key]) => key),
|
||||
0,
|
||||
2,
|
||||
);
|
||||
for (const [key, portKey] of back) link('e-vars', key, 'end', portKey);
|
||||
return g.done();
|
||||
}
|
||||
|
||||
@@ -715,23 +745,18 @@ function showLinesGraph() {
|
||||
* each answer lands in.
|
||||
*/
|
||||
function tetrisGraph() {
|
||||
const g = graphBuilder();
|
||||
const { at, link, flow, get, set, strand } = g;
|
||||
const g = graphBuilder(tetrisVariables);
|
||||
const { at, link, flow, get, set, gets, sets, strand } = g;
|
||||
|
||||
// 1) On start: an empty board, no piece, nothing pressed. The world is rebuilt for every run, so
|
||||
// this only matters when the behavior was left running in the editor.
|
||||
strand();
|
||||
at('s-start', 'OnStart', 0, 0);
|
||||
at('s-empty', 'Fill', 0, 3, { value: EMPTY, count: COLS * ROWS });
|
||||
set('s-grid', 'grid', 1, 0);
|
||||
set('s-lines', 'lines', 2, 0, 0);
|
||||
set('s-piece', 'hasPiece', 3, 0, false);
|
||||
set('s-fall', 'fall', 4, 0, 0);
|
||||
set('s-dx', 'wantDx', 5, 0, 0);
|
||||
set('s-turn', 'wantRot', 6, 0, 0);
|
||||
at('s-show', 'RunBehavior', 7, 0, { behavior: 'show-lines', lines: 0 });
|
||||
link('s-empty', 'result', 's-grid', 'value');
|
||||
flow('s-start', 's-grid', 's-lines', 's-piece', 's-fall', 's-turn', 's-dx', 's-show');
|
||||
sets('s-vars', { grid: LINKED, lines: 0, hasPiece: false, fall: 0, wantDx: 0, wantRot: 0 }, 1, 0);
|
||||
at('s-show', 'RunBehavior', 2, 0, { behavior: 'show-lines', lines: 0 });
|
||||
link('s-empty', 'result', 's-vars', 'grid');
|
||||
flow('s-start', 's-vars', 's-show');
|
||||
|
||||
// 2) The keys do not move anything themselves – they leave a wish that the next frame grants. That
|
||||
// way every attempt to move goes through the one test below, whatever asked for it.
|
||||
@@ -757,66 +782,47 @@ function tetrisGraph() {
|
||||
// 3a) No piece: ask for one and write down what came back.
|
||||
at('n-spawn', 'RunBehavior', 2, 5, { behavior: 'spawn-piece' });
|
||||
get('n-columns', 'spawnX', 2, 8);
|
||||
set('n-piece', 'piece', 3, 5);
|
||||
set('n-fall', 'fall', 4, 5, 0);
|
||||
set('n-has', 'hasPiece', 5, 5, true);
|
||||
sets('n-vars', { piece: LINKED, fall: 0, hasPiece: true }, 3, 5);
|
||||
link('u-br', 'else', 'n-spawn', 'exec');
|
||||
link('n-columns', 'value', 'n-spawn', 'spawnX');
|
||||
link('n-spawn', 'piece', 'n-piece', 'value');
|
||||
flow('n-spawn', 'n-piece', 'n-fall', 'n-has');
|
||||
link('n-spawn', 'piece', 'n-vars', 'piece');
|
||||
flow('n-spawn', 'n-vars');
|
||||
|
||||
// 3b) A piece is falling: one behavior decides which single move this frame attempts.
|
||||
strand();
|
||||
at('c-move', 'RunBehavior', 0, 0, { behavior: 'next-move' });
|
||||
link('u-br', 'then', 'c-move', 'exec');
|
||||
const moveIn = [
|
||||
['piece', 'piece'],
|
||||
['wantDx', 'wantDx'],
|
||||
['wantRot', 'wantRot'],
|
||||
['fall', 'fall'],
|
||||
['stepTime', 'stepTime'],
|
||||
];
|
||||
moveIn.forEach(([key, variableKey], i) => {
|
||||
get(`c-in-${key}`, variableKey, 0, i * 2 + 3);
|
||||
link(`c-in-${key}`, 'value', 'c-move', key);
|
||||
});
|
||||
// Every port of the call is a variable of the same name, so one node stands at each side of it.
|
||||
const moveIn = ['piece', 'wantDx', 'wantRot', 'fall', 'stepTime'];
|
||||
gets('c-in', moveIn, 0, 3);
|
||||
for (const key of moveIn) link('c-in', key, 'c-move', key);
|
||||
link('u-update', 'dt', 'c-move', 'dt');
|
||||
|
||||
const moveOut = [
|
||||
['next', 'next'],
|
||||
['kind', 'kind'],
|
||||
['fall', 'fall'],
|
||||
['wantDx', 'wantDx'],
|
||||
['wantRot', 'wantRot'],
|
||||
];
|
||||
moveOut.forEach(([key, variableKey], i) => {
|
||||
set(`c-out-${key}`, variableKey, i + 1, 0);
|
||||
link('c-move', key, `c-out-${key}`, 'value');
|
||||
});
|
||||
flow('c-move', ...moveOut.map(([key]) => `c-out-${key}`));
|
||||
const moveOut = ['next', 'kind', 'fall', 'wantDx', 'wantRot'];
|
||||
sets('c-out', moveOut, 1, 0);
|
||||
for (const key of moveOut) link('c-move', key, 'c-out', key);
|
||||
flow('c-move', 'c-out');
|
||||
|
||||
// 4) The one test, and it is one node: does the candidate fit? Every attempt above ends here.
|
||||
strand();
|
||||
at('t-fits', 'RunBehavior', 0, 0, { behavior: 'fits' });
|
||||
get('t-shapes', 'shapes', 0, 3);
|
||||
get('t-grid', 'grid', 0, 5);
|
||||
get('t-next', 'next', 0, 7);
|
||||
flow('c-out-wantRot', 't-fits');
|
||||
link('t-shapes', 'value', 't-fits', 'shapes');
|
||||
link('t-grid', 'value', 't-fits', 'grid');
|
||||
link('t-next', 'value', 't-fits', 'piece');
|
||||
gets('t-in', ['shapes', 'grid', 'next'], 0, 3);
|
||||
flow('c-out', 't-fits');
|
||||
link('t-in', 'shapes', 't-fits', 'shapes');
|
||||
link('t-in', 'grid', 't-fits', 'grid');
|
||||
link('t-in', 'next', 't-fits', 'piece');
|
||||
|
||||
// 5) It fits: the candidate becomes the piece. It does not: only a fall means anything – the
|
||||
// piece has landed (or, still at the very top, the well is full).
|
||||
at('m-br', 'Branch', 1, 0);
|
||||
get('m-next', 'next', 2, 3);
|
||||
// `piece` is not in here: it is written a line below, and `m-py2` has to see that write.
|
||||
gets('m-in', ['next', 'kind'], 2, 3);
|
||||
set('m-piece', 'piece', 2, 0);
|
||||
flow('t-fits', 'm-br');
|
||||
link('t-fits', 'ok', 'm-br', 'condition');
|
||||
link('m-br', 'then', 'm-piece', 'exec');
|
||||
link('m-next', 'value', 'm-piece', 'value');
|
||||
link('m-in', 'next', 'm-piece', 'value');
|
||||
|
||||
get('m-kind', 'kind', 2, 7);
|
||||
at('m-landed', 'Equals', 3, 7, { b: 'fall' });
|
||||
at('m-brland', 'Branch', 3, 5);
|
||||
get('m-py2', 'piece', 4, 8);
|
||||
@@ -825,7 +831,7 @@ function tetrisGraph() {
|
||||
at('m-brover', 'Branch', 5, 5);
|
||||
at('m-stop', 'StopGame', 6, 5);
|
||||
link('m-br', 'else', 'm-brland', 'exec');
|
||||
link('m-kind', 'value', 'm-landed', 'a');
|
||||
link('m-in', 'kind', 'm-landed', 'a');
|
||||
link('m-landed', 'result', 'm-brland', 'condition');
|
||||
link('m-brland', 'then', 'm-brover', 'exec');
|
||||
link('m-py2', 'value', 'm-pieceY', 'value');
|
||||
@@ -837,48 +843,43 @@ function tetrisGraph() {
|
||||
strand();
|
||||
at('d-draw', 'RunBehavior', 0, 0, { behavior: 'draw-piece' });
|
||||
flow('m-piece', 'd-draw');
|
||||
flow('n-has', 'd-draw');
|
||||
['shapes', 'piece', 'colors'].forEach((key, i) => {
|
||||
get(`d-${key}`, key, 0, i * 2 + 3);
|
||||
link(`d-${key}`, 'value', 'd-draw', key);
|
||||
});
|
||||
flow('n-vars', 'd-draw');
|
||||
const drawIn = ['shapes', 'piece', 'colors'];
|
||||
gets('d-in', drawIn, 0, 3);
|
||||
for (const key of drawIn) link('d-in', key, 'd-draw', key);
|
||||
|
||||
// 7) It landed: it becomes part of the board, full lines go, the counter and the board follow.
|
||||
strand();
|
||||
at('l-lock', 'RunBehavior', 0, 0, { behavior: 'lock-piece' });
|
||||
get('l-grid', 'grid', 0, 3);
|
||||
get('l-piece', 'piece', 0, 5);
|
||||
gets('l-in', ['grid', 'piece'], 0, 3);
|
||||
at('l-shape', 'ExpandStruct', 0, 7, {}, [field('shape')]);
|
||||
set('l-setgrid', 'grid', 1, 0);
|
||||
link('m-brover', 'else', 'l-lock', 'exec');
|
||||
link('l-grid', 'value', 'l-lock', 'grid');
|
||||
link('l-piece', 'value', 'l-shape', 'value');
|
||||
link('l-in', 'grid', 'l-lock', 'grid');
|
||||
link('l-in', 'piece', 'l-shape', 'value');
|
||||
link('l-shape', 'shape', 'l-lock', 'shape');
|
||||
link('l-lock', 'grid', 'l-setgrid', 'value');
|
||||
flow('l-lock', 'l-setgrid');
|
||||
|
||||
at('r-clear', 'RunBehavior', 2, 0, { behavior: 'clear-lines' });
|
||||
get('r-grid', 'grid', 2, 3);
|
||||
get('r-lines', 'lines', 2, 5);
|
||||
set('r-setgrid', 'grid', 3, 0);
|
||||
set('r-setlines', 'lines', 4, 0);
|
||||
link('r-grid', 'value', 'r-clear', 'grid');
|
||||
link('r-lines', 'value', 'r-clear', 'lines');
|
||||
link('r-clear', 'grid', 'r-setgrid', 'value');
|
||||
link('r-clear', 'lines', 'r-setlines', 'value');
|
||||
flow('l-setgrid', 'r-clear', 'r-setgrid', 'r-setlines');
|
||||
gets('r-in', ['grid', 'lines'], 2, 3);
|
||||
sets('r-out', ['grid', 'lines'], 3, 0);
|
||||
link('r-in', 'grid', 'r-clear', 'grid');
|
||||
link('r-in', 'lines', 'r-clear', 'lines');
|
||||
link('r-clear', 'grid', 'r-out', 'grid');
|
||||
link('r-clear', 'lines', 'r-out', 'lines');
|
||||
flow('l-setgrid', 'r-clear', 'r-out');
|
||||
|
||||
at('b-show', 'RunBehavior', 5, 0, { behavior: 'show-lines' });
|
||||
get('b-lines', 'lines', 5, 3);
|
||||
at('b-draw', 'RunBehavior', 6, 0, { behavior: 'draw-board' });
|
||||
get('b-grid', 'grid', 6, 3);
|
||||
get('b-colors', 'colors', 6, 5);
|
||||
// A second reader of `grid` and `lines`, because `r-out` wrote them since `r-in` was asked.
|
||||
gets('b-in', ['lines', 'grid', 'colors'], 5, 3);
|
||||
set('b-nopiece', 'hasPiece', 7, 0, false);
|
||||
link('b-lines', 'value', 'b-show', 'lines');
|
||||
link('b-grid', 'value', 'b-draw', 'grid');
|
||||
link('b-colors', 'value', 'b-draw', 'colors');
|
||||
link('b-in', 'lines', 'b-show', 'lines');
|
||||
link('b-in', 'grid', 'b-draw', 'grid');
|
||||
link('b-in', 'colors', 'b-draw', 'colors');
|
||||
// The board is drawn and the piece is part of it now: the next frame draws a new one.
|
||||
flow('r-setlines', 'b-show', 'b-draw', 'b-nopiece');
|
||||
flow('r-out', 'b-show', 'b-draw', 'b-nopiece');
|
||||
return g.done();
|
||||
}
|
||||
|
||||
@@ -948,30 +949,10 @@ const project = {
|
||||
{ id: 'board', type: 'container', name: 'Board', props: {}, objects: boardTemplate },
|
||||
behavior('piece-cell', 'Piece cell', [], pieceCellGraph()),
|
||||
behavior('fits', 'Fits', [variable('ok', 'boolean', true)], fitsGraph()),
|
||||
behavior(
|
||||
'clear-lines',
|
||||
'Clear lines',
|
||||
[
|
||||
variable('board', 'list', [], 'number'),
|
||||
variable('cleared', 'number', 0),
|
||||
variable('row', 'number', 0),
|
||||
],
|
||||
clearLinesGraph(),
|
||||
),
|
||||
behavior('clear-lines', 'Clear lines', CLEAR_LINES_VARS, clearLinesGraph()),
|
||||
behavior('draw-board', 'Draw board', [], drawBoardGraph()),
|
||||
behavior('spawn-piece', 'Spawn piece', [], spawnPieceGraph()),
|
||||
behavior(
|
||||
'next-move',
|
||||
'Next move',
|
||||
[
|
||||
variable('next', 'struct', NO_PIECE, undefined, PIECE_FIELDS),
|
||||
variable('kind', 'text', ''),
|
||||
variable('outFall', 'number', 0),
|
||||
variable('outDx', 'number', 0),
|
||||
variable('outRot', 'number', 0),
|
||||
],
|
||||
nextMoveGraph(),
|
||||
),
|
||||
behavior('next-move', 'Next move', NEXT_MOVE_VARS, nextMoveGraph()),
|
||||
behavior('draw-piece', 'Draw piece', [], drawPieceGraph()),
|
||||
behavior('lock-piece', 'Lock piece', [variable('board', 'list', [], 'number')], lockPieceGraph()),
|
||||
behavior('show-lines', 'Show lines', [], showLinesGraph()),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* button, running, jumping, collecting a coin, the win screen, restart and the lose screen.
|
||||
* It is the proof that the engine features this game needs actually work together.
|
||||
*
|
||||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
* Usage: node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
* Screenshots land in /tmp/gm-jr-*.png.
|
||||
*/
|
||||
import { instanceChild } from './lib/builders.mjs';
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* It is not part of the e2e suite – like smoke-jumprun.mjs it proves one seeded project, and is
|
||||
* run when that project or the engine underneath it changes.
|
||||
*
|
||||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
* Usage: node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
* Screenshots land in /tmp/gm-tetris-*.png.
|
||||
*/
|
||||
import { instanceChild } from './lib/builders.mjs';
|
||||
@@ -27,13 +27,13 @@ const OBJECT_COUNT = STORED_COUNT + 1 + COLS * ROWS + 4;
|
||||
|
||||
/** The master graph and the nine behaviors the game is split into. */
|
||||
const GRAPHS = {
|
||||
tetris: { nodes: 69, edges: 76 },
|
||||
tetris: { nodes: 44, edges: 64 },
|
||||
'spawn-piece': { nodes: 6, edges: 7 },
|
||||
'next-move': { nodes: 44, edges: 61 },
|
||||
'next-move': { nodes: 30, edges: 51 },
|
||||
fits: { nodes: 18, edges: 26 },
|
||||
'draw-piece': { nodes: 15, edges: 24 },
|
||||
'lock-piece': { nodes: 20, edges: 23 },
|
||||
'clear-lines': { nodes: 29, edges: 36 },
|
||||
'clear-lines': { nodes: 23, edges: 33 },
|
||||
'show-lines': { nodes: 5, edges: 5 },
|
||||
'draw-board': { nodes: 11, edges: 17 },
|
||||
'piece-cell': { nodes: 11, edges: 17 },
|
||||
|
||||
Reference in new issue
Block a user