Replace the five variable nodes with one Variables node

A single node now lists any number of variables. Every entry is an output
that reads the current value whenever it is pulled; an entry switched to
Set is also an input and is written when the flow arrives. Exec pins only
exist while at least one entry writes, and all inputs are read before any
write, so one node can swap two variables.

Get/Set variable, Get/Set variables and Variable are gone. Old project
files are converted on load: each old node becomes a Variables node with
the same variables, edges and typed values.

A written port without a typed value now shows the zero of its kind on
the node body, matching what the inspector shows and what the run writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 16:56:24 +02:00
1 parent 854ae2e756
commit 7c63c884ef
40 files changed
+895 -590

No files matched your search

+1 -1
View File
@@ -820,7 +820,7 @@ export async function graph(t) {
);
await shot('24b-node-collapsed');
const speedOut = await json(`__gm.graph.portAt('n-speed', 'value', true)`);
const speedOut = await json(`__gm.graph.portAt('n-speed', 'speed', true)`);
await mouse('mouseMoved', speedOut.x, speedOut.y);
await mouse('mousePressed', speedOut.x, speedOut.y);
await mouse('mouseMoved', title.x, title.y, 'left', { buttons: 1 });
+69 -47
View File
@@ -1,10 +1,10 @@
import { edge, node, port, variable } from '../lib/builders.mjs';
import { edge, node, port, variable, written } from '../lib/builders.mjs';
import { MAKE_OBJECT } from './lib/scaffold.mjs';
/**
* Get variables / Set variables: one node for several of them. Their ports are the behavior's
* variables, so the step checks all three sides of that – the run, the picking of a port from the
* variable list, and pointing an existing port at another variable without losing its edge.
* The Variables node: its ports are the behavior's variables, each read at any time and written
* where it is switched to Set. The step checks the run, picking a port from the variable list,
* switching Set, and pointing an existing port at another variable without losing its edge.
*
* The step brings its own box and behavior; everything it adds is removed at the end.
*/
@@ -12,7 +12,7 @@ import { MAKE_OBJECT } from './lib/scaffold.mjs';
const SCALE = 100;
/**
* `On update → Set variables{vx = 2, vy = 3} → Move(owner, dx = vx, dy = vy)`, where the two
* `On update → Variables{set vx = 2, vy = 3} → Move(owner, dx = vx, dy = vy)`, where the two
* numbers reach Move through the *variables* – one node writes both, one node reads both back.
* Neither speed is on the Move node, so a node that dropped a port would leave the box still.
*/
@@ -20,8 +20,8 @@ const logicGraph = {
nodes: [
node('vp-update', 'OnUpdate', 0, 0),
node('vp-owner', 'Owner', 0, 300),
node('vp-set', 'SetVariables', 260, 0, { vx: 2, vy: 3 }, [port('vx', 'number'), port('vy', 'number')]),
node('vp-get', 'GetVariables', 260, 180, {}, [port('vx', 'number'), port('vy', 'number')]),
node('vp-set', 'Variables', 260, 0, { vx: 2, vy: 3 }, [written('vx', 'number'), written('vy', 'number')]),
node('vp-get', 'Variables', 260, 180, {}, [port('vx', 'number'), port('vy', 'number')]),
node('vp-move', 'Move', 560, 0, { scale: SCALE }),
],
edges: [
@@ -50,6 +50,7 @@ export async function variableNodes(t) {
stop,
typeText,
press,
has,
} = t;
const portAt = (nodeId, key, isOutput) =>
json(`__gm.graph.portAt(${JSON.stringify(nodeId)}, ${JSON.stringify(key)}, ${isOutput})`);
@@ -67,6 +68,13 @@ export async function variableNodes(t) {
await click(p.x, p.y);
await sleep(200);
};
/** The Set checkbox of the newest variable row: the control right of the last "Set" label. */
const clickLastSet = async () => {
const box = await json(`__gm.inspectorField('Set', true)`);
await click(box.x, box.y);
await sleep(300);
return box;
};
// 23a. A box wearing a behavior whose two speeds travel through its variables.
await mutate(`
@@ -104,18 +112,24 @@ export async function variableNodes(t) {
await stop();
await sleep(300);
// 23c. The node draws one port per variable it names, on the side its type says.
// 23c. Every variable is an output; a written one is an input too, and only a writing node has
// flow pins.
await select('vp-set');
const vxIn = await portAt('vp-set', 'vx', false);
const vyIn = await portAt('vp-set', 'vy', false);
const vxOut = await portAt('vp-get', 'vx', true);
const pins = {
setIn: Boolean(await portAt('vp-set', 'vx', false)),
setOut: Boolean(await portAt('vp-set', 'vx', true)),
getOut: Boolean(await portAt('vp-get', 'vx', true)),
getIn: Boolean(await portAt('vp-get', 'vx', false)),
getFlow: Boolean(await portAt('vp-get', 'exec', false)),
};
check(
'the picked variables are ports: inputs on Set, outputs on Get',
Boolean(vxIn) && Boolean(vyIn) && vyIn.y > vxIn.y && Boolean(vxOut),
JSON.stringify({ vxIn, vyIn, vxOut }),
'a written variable is an input and an output, a read one only an output, without flow pins',
pins.setIn && pins.setOut && pins.getOut && !pins.getIn && !pins.getFlow,
JSON.stringify(pins),
);
// 23d. The inspector picks a port from the behavior's variables instead of having it typed.
// 23d. The inspector picks a port from the behavior's variables instead of having it typed, and
// a picked one is read until it is switched to Set.
const add = await locate('+ Variable');
check('the node offers its variables in the inspector', Boolean(add), JSON.stringify(add));
await shot('01-variable-ports');
@@ -125,15 +139,32 @@ export async function variableNodes(t) {
await sleep(300);
const grown = await portsOf('vp-set');
check(
'picking one adds it as a port, typed from the variable',
grown.length === 3 && grown[2].key === 'vz' && grown[2].kind === 'number',
'picking one adds it as a read port, typed from the variable',
grown.length === 3 && grown[2].key === 'vz' && grown[2].kind === 'number' && !grown[2].write,
JSON.stringify(grown),
);
check('and the node grew that input', Boolean(await portAt('vp-set', 'vz', false)));
await shot('02-variable-added');
check(
'and the node grew that output only',
Boolean(await portAt('vp-set', 'vz', true)) && !(await portAt('vp-set', 'vz', false)),
);
await clickLastSet();
check(
'switching it to Set gives it an input as well',
(await portsOf('vp-set'))[2]?.write === true && Boolean(await portAt('vp-set', 'vz', false)),
JSON.stringify(await portsOf('vp-set')),
);
const drawn = await json(`__gm.graph.nodeInputs('vp-set')`);
check(
'with nothing typed the node shows the value it writes beside the name, as the inspector does',
drawn?.includes('vz 0') && drawn.includes('vx 2'),
JSON.stringify(drawn),
);
await shot('02-variable-set');
await undo();
await sleep(300);
check('undo takes it back', (await portsOf('vp-set')).length === 2);
await undo();
await sleep(300);
check('undo takes both back', (await portsOf('vp-set')).length === 2);
// 23e. Pointing a port at another variable keeps the edge hanging on it – a correction, not a
// rewiring. `vp-e3` runs from this port to Move's dx.
@@ -231,63 +262,54 @@ export async function variableNodes(t) {
JSON.stringify(helper),
);
// 23i. The Variable node: picked from the palette, titled by the variable picked in the inspector,
// and in the run `vz = vz + 1` once a frame – its own output feeding its input via Add.
// 23i. From the palette the node comes empty; picking a variable titles it, and switched to Set
// it runs `vz = vz + 1` once a frame – its own output feeding its input via Add.
const nodeIds = async () => (await asset('vp-logic')).graph.nodes.map((n) => n.id);
const idsBefore = await nodeIds();
const centre = await json('__gm.graph.center()');
// Below the Owner node; centre + (250, 250) would land on the Pack.
await click(centre.x - 250, centre.y + 250, 'right');
await sleep(150);
// Its description is the one that says "any time"; "variable" would put Get variable first.
await typeText('any time');
// Enter takes the first hit in category order, so search by a phrase only its description has.
await typeText('switched to Set');
await sleep(200);
await press('Enter');
await sleep(300);
const added = (await asset('vp-logic')).graph.nodes.find((n) => !idsBefore.includes(n.id));
check('the palette offers the Variable node', added?.type === 'Variable', JSON.stringify(added));
check('the palette offers the Variables node', added?.type === 'Variables', JSON.stringify(added));
const variableId = added?.id ?? '';
const titleOf = (id) => json(`__gm.graph.nodeLabel(${JSON.stringify(id)})`);
const untitled = await titleOf(variableId);
// The node's own title says "Variable" too, and the canvas is drawn first.
const picker = await json(`__gm.inspectorField('Variable', true)`);
await click(picker.x, picker.y);
await sleep(200);
const bare = !(await portAt(variableId, 'exec', false));
await clickLast('+ Variable');
await clickLast('vz');
await sleep(300);
await clickLastSet();
check(
'picking its variable makes that the title, and drops the row that would repeat it',
untitled === 'Variable' &&
(await titleOf(variableId)) === 'vz' &&
(await json(`__gm.graph.nodeSettings(${JSON.stringify(variableId)})`)).length === 0,
JSON.stringify({ untitled, titled: await titleOf(variableId), picker }),
'it starts without pins, and picking its variable makes that the title',
untitled === 'Variables' && bare && (await titleOf(variableId)) === 'vz',
JSON.stringify({ untitled, bare, titled: await titleOf(variableId) }),
);
check('switched to Set it grows its flow pins', Boolean(await portAt(variableId, 'exec', false)));
await shot('05-variable-node');
await mutate(`
const g = __gm.state.project.assets.find((x) => x.id === 'vp-logic').graph;
g.nodes.push(
${JSON.stringify(node('vp-add', 'Add', 800, 200, { b: 1 }))},
${JSON.stringify(node('vp-set-vx', 'SetVariable', 800, 400, { variable: 'vx' }))},
${JSON.stringify(node('vp-set-gone', 'SetVariable', 800, 500, { variable: 'gone' }))},
${JSON.stringify(node('vp-gone', 'Variables', 800, 500, {}, [port('gone', 'number')]))},
);
g.edges.push(
${JSON.stringify(edge('vp-e7', 'vp-move', 'exec', variableId, 'exec'))},
${JSON.stringify(edge('vp-e8', variableId, 'value', 'vp-add', 'a'))},
${JSON.stringify(edge('vp-e9', 'vp-add', 'result', variableId, 'value'))},
${JSON.stringify(edge('vp-e8', variableId, 'vz', 'vp-add', 'a'))},
${JSON.stringify(edge('vp-e9', 'vp-add', 'result', variableId, 'vz'))},
);
`);
await sleep(300);
const settingsOf = (id) => json(`__gm.graph.nodeSettings(${JSON.stringify(id)})`);
const named = { title: await titleOf('vp-set-vx'), rows: await settingsOf('vp-set-vx') };
const gone = { title: await titleOf('vp-set-gone'), rows: await settingsOf('vp-set-gone') };
await select('vp-gone');
check(
'Set variable names its variable too, and a deleted one keeps the row that marks it',
named.title === 'Set vx' &&
named.rows.length === 0 &&
gone.title === 'Set gone' &&
JSON.stringify(gone.rows) === '["Variable: gone?"]',
JSON.stringify({ named, gone }),
'a port naming a variable the behavior does not have is marked in the inspector',
await has('"gone" is not a variable of this behavior.'),
);
await t.play();
+4 -4
View File
@@ -1,9 +1,9 @@
import { edge, node, port, variable } from '../lib/builders.mjs';
import { edge, node, port, variable, written } from '../lib/builders.mjs';
import { MAKE_OBJECT } from './lib/scaffold.mjs';
/**
* Get properties / Set properties: several properties of an object in one node, picked the way
* Get/Set variables pick theirs. A built-in property keeps its own field and default; a name the
* the Variables node picks its variables. A built-in property keeps its own field and default; a name the
* list does not know is typed. The step brings its own box and behavior and removes them again.
*/
@@ -17,7 +17,7 @@ const logicGraph = {
port('color', 'number'),
]),
node('pp-get', 'GetProperties', 260, 300, {}, [port('width', 'number')]),
node('pp-store', 'SetVariable', 560, 0, { variable: 'seen' }),
node('pp-store', 'Variables', 560, 0, {}, [written('seen', 'number')]),
node('pp-single', 'SetProperty', 560, 300, { property: 'width' }),
],
edges: [
@@ -25,7 +25,7 @@ const logicGraph = {
edge('pp-e2', 'pp-owner', 'object', 'pp-set', 'object'),
edge('pp-e3', 'pp-set', 'exec', 'pp-store', 'exec'),
edge('pp-e4', 'pp-owner', 'object', 'pp-get', 'object'),
edge('pp-e5', 'pp-get', 'width', 'pp-store', 'value'),
edge('pp-e5', 'pp-get', 'width', 'pp-store', 'seen'),
],
};
+4 -1
View File
@@ -42,9 +42,12 @@ export const node = (id, type, x, y, params = {}, ports) => ({
...(ports ? { ports } : {}),
});
/** A port a behavior's start or end node declares. */
/** A port a node declares: an input of a start node, a field of a struct, a variable read by `Variables`. */
export const port = (key, kind) => ({ key, kind });
/** A variable a `Variables` node writes as well as reads. */
export const written = (key, kind) => ({ key, kind, write: true });
/**
* A behavior variable declaration. `items` is the item kind of a `list`, `fields` the shape of a
* `struct` – or of the items of a `list` whose items are structs.
+3 -3
View File
@@ -6,7 +6,7 @@
*
* Usage: node tools/seed-e2e.mjs [projectId] [baseUrl]
*/
import { edge, node, sceneObject, variable } from './lib/builders.mjs';
import { edge, node, port, sceneObject, variable } from './lib/builders.mjs';
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
const { projectId, baseUrl } = cliArgs('seed-e2e.mjs', PROJECTS.fixture);
@@ -21,7 +21,7 @@ const moverGraph = {
node('n-update', 'OnUpdate', 0, 0),
node('n-owner', 'Owner', 0, 120),
node('n-axis', 'AxisInput', 0, 200),
node('n-speed', 'GetVariable', 0, 400, { variable: 'speed' }),
node('n-speed', 'Variables', 0, 400, {}, [port('speed', 'number')]),
node('n-move', 'Move', 340, 0, { collide: true }),
],
edges: [
@@ -30,7 +30,7 @@ const moverGraph = {
edge('e2', 'n-owner', 'object', 'n-move', 'object'),
edge('e3', 'n-axis', 'x', 'n-move', 'dx'),
edge('e4', 'n-axis', 'y', 'n-move', 'dy'),
edge('e5', 'n-speed', 'value', 'n-move', 'scale'),
edge('e5', 'n-speed', 'speed', 'n-move', 'scale'),
],
};
+39 -39
View File
@@ -14,7 +14,7 @@
*
* Usage: node tools/seed-jumprun.mjs [projectId] [baseUrl]
*/
import { edge, node, sceneObject, variable } from './lib/builders.mjs';
import { edge, node, port, sceneObject, variable, written } from './lib/builders.mjs';
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
const { projectId, baseUrl } = cliArgs('seed-jumprun.mjs', PROJECTS.jumpRun);
@@ -132,29 +132,29 @@ const playerGraph = {
node('p-upd', 'OnUpdate', 0, 0),
node('p-owner', 'Owner', 0, 140),
node('p-axis', 'AxisInput', 0, 220, { left: 'ArrowLeft', right: 'ArrowRight', up: '', down: '' }),
node('p-speed', 'GetVariable', 0, 360, { variable: 'speed' }),
node('p-speed', 'Variables', 0, 360, {}, [port('speed', 'number')]),
node('p-mulx', 'Multiply', 220, 300),
node('p-vy', 'GetVariable', 0, 440, { variable: 'vy' }),
node('p-grav', 'GetVariable', 0, 520, { variable: 'gravity' }),
node('p-vy', 'Variables', 0, 440, {}, [port('vy', 'number')]),
node('p-grav', 'Variables', 0, 520, {}, [port('gravity', 'number')]),
node('p-mulg', 'Multiply', 220, 500),
node('p-addvy', 'Add', 420, 460),
node('p-setvy', 'SetVariable', 620, 0, { variable: 'vy' }),
node('p-vy2', 'GetVariable', 620, 340, { variable: 'vy' }),
node('p-setvy', 'Variables', 620, 0, {}, [written('vy', 'number')]),
node('p-vy2', 'Variables', 620, 340, {}, [port('vy', 'number')]),
node('p-move', 'Move', 840, 0, { collide: true }),
node('p-br', 'Branch', 1080, 0),
node('p-seq', 'Sequence', 1260, 0),
node('p-vy3', 'GetVariable', 1080, 260, { variable: 'vy' }),
node('p-vy3', 'Variables', 1080, 260, {}, [port('vy', 'number')]),
node('p-cmp', 'Compare', 1260, 260, { op: '>=', b: 0 }),
node('p-ground', 'SetVariable', 1460, 0, { variable: 'onGround' }),
node('p-zero', 'SetVariable', 1460, 140, { variable: 'vy', value: 0 }),
node('p-air', 'SetVariable', 1260, 400, { variable: 'onGround', value: false }),
node('p-ground', 'Variables', 1460, 0, {}, [written('onGround', 'boolean')]),
node('p-zero', 'Variables', 1460, 140, { vy: 0 }, [written('vy', 'number')]),
node('p-air', 'Variables', 1260, 400, { onGround: false }, [written('onGround', 'boolean')]),
node('p-key', 'OnKeyDown', 0, 700, { key: ' ' }),
node('p-og', 'GetVariable', 0, 820, { variable: 'onGround' }),
node('p-og', 'Variables', 0, 820, {}, [port('onGround', 'boolean')]),
node('p-brj', 'Branch', 240, 700),
node('p-jump', 'GetVariable', 240, 880, { variable: 'jumpSpeed' }),
node('p-jump', 'Variables', 240, 880, {}, [port('jumpSpeed', 'number')]),
node('p-neg', 'Multiply', 440, 880, { b: -1 }),
node('p-setjump', 'SetVariable', 660, 700, { variable: 'vy' }),
node('p-setjump', 'Variables', 660, 700, {}, [written('vy', 'number')]),
node('p-col', 'OnCollisionEnter', 0, 1060),
node('p-iscoin', 'IsAsset', 260, 1180, { asset: 'coin-sprite' }),
@@ -169,32 +169,32 @@ const playerGraph = {
],
edges: [
edge('pe1', 'p-upd', 'exec', 'p-setvy', 'exec'),
edge('pe2', 'p-vy', 'value', 'p-addvy', 'a'),
edge('pe3', 'p-grav', 'value', 'p-mulg', 'a'),
edge('pe2', 'p-vy', 'vy', 'p-addvy', 'a'),
edge('pe3', 'p-grav', 'gravity', 'p-mulg', 'a'),
edge('pe4', 'p-upd', 'dt', 'p-mulg', 'b'),
edge('pe5', 'p-mulg', 'result', 'p-addvy', 'b'),
edge('pe6', 'p-addvy', 'result', 'p-setvy', 'value'),
edge('pe6', 'p-addvy', 'result', 'p-setvy', 'vy'),
edge('pe7', 'p-setvy', 'exec', 'p-move', 'exec'),
edge('pe8', 'p-owner', 'object', 'p-move', 'object'),
edge('pe9', 'p-axis', 'x', 'p-mulx', 'a'),
edge('pe10', 'p-speed', 'value', 'p-mulx', 'b'),
edge('pe10', 'p-speed', 'speed', 'p-mulx', 'b'),
edge('pe11', 'p-mulx', 'result', 'p-move', 'dx'),
edge('pe12', 'p-vy2', 'value', 'p-move', 'dy'),
edge('pe12', 'p-vy2', 'vy', 'p-move', 'dy'),
edge('pe13', 'p-upd', 'dt', 'p-move', 'dt'),
edge('pe14', 'p-move', 'exec', 'p-br', 'exec'),
edge('pe15', 'p-move', 'blockedY', 'p-br', 'condition'),
edge('pe16', 'p-br', 'then', 'p-seq', 'exec'),
edge('pe17', 'p-seq', 'a', 'p-ground', 'exec'),
edge('pe18', 'p-vy3', 'value', 'p-cmp', 'a'),
edge('pe19', 'p-cmp', 'result', 'p-ground', 'value'),
edge('pe18', 'p-vy3', 'vy', 'p-cmp', 'a'),
edge('pe19', 'p-cmp', 'result', 'p-ground', 'onGround'),
edge('pe20', 'p-seq', 'b', 'p-zero', 'exec'),
edge('pe21', 'p-br', 'else', 'p-air', 'exec'),
edge('pe22', 'p-key', 'exec', 'p-brj', 'exec'),
edge('pe23', 'p-og', 'value', 'p-brj', 'condition'),
edge('pe23', 'p-og', 'onGround', 'p-brj', 'condition'),
edge('pe24', 'p-brj', 'then', 'p-setjump', 'exec'),
edge('pe25', 'p-jump', 'value', 'p-neg', 'a'),
edge('pe26', 'p-neg', 'result', 'p-setjump', 'value'),
edge('pe25', 'p-jump', 'jumpSpeed', 'p-neg', 'a'),
edge('pe26', 'p-neg', 'result', 'p-setjump', 'vy'),
edge('pe27', 'p-col', 'exec', 'p-brc', 'exec'),
edge('pe28', 'p-col', 'other', 'p-iscoin', 'object'),
@@ -229,36 +229,36 @@ const gameGraph = {
node('g-off3', 'SetEnabled', 920, 0, { enabled: false }),
node('g-upd', 'OnUpdate', 0, 520),
node('g-running', 'GetVariable', 0, 640, { variable: 'running' }),
node('g-running', 'Variables', 0, 640, {}, [port('running', 'boolean')]),
node('g-brrun', 'Branch', 220, 520),
node('g-time', 'GetVariable', 220, 700, { variable: 'timeLeft' }),
node('g-time', 'Variables', 220, 700, {}, [port('timeLeft', 'number')]),
node('g-sub', 'Subtract', 420, 700),
node('g-max', 'Max', 600, 700, { b: 0 }),
node('g-settime', 'SetVariable', 440, 520, { variable: 'timeLeft' }),
node('g-settime', 'Variables', 440, 520, {}, [written('timeLeft', 'number')]),
node('g-timelabel', 'ObjectByName', 440, 860, { name: 'TimeLabel' }),
node('g-time2', 'GetVariable', 440, 940, { variable: 'timeLeft' }),
node('g-time2', 'Variables', 440, 940, {}, [port('timeLeft', 'number')]),
node('g-ceil', 'Ceil', 640, 940),
node('g-join', 'Join', 840, 940, { a: 'Time ' }),
node('g-settext', 'SetText', 700, 520),
node('g-coinlabel', 'ObjectByName', 700, 1080, { name: 'CoinLabel' }),
node('g-coins', 'GetVariable', 700, 1160, { variable: 'coins' }),
node('g-coins', 'Variables', 700, 1160, {}, [port('coins', 'number')]),
node('g-totext2', 'ToText', 900, 1160, { decimals: 0 }),
node('g-join2', 'Join', 1080, 1160, { a: 'Coins ' }),
node('g-join3', 'Join', 1260, 1160, { b: '/10' }),
node('g-settext2', 'SetText', 960, 520),
node('g-needed', 'GetVariable', 960, 700, { variable: 'needed' }),
node('g-needed', 'Variables', 960, 700, {}, [port('needed', 'number')]),
node('g-cmpwin', 'Compare', 1160, 700, { op: '>=' }),
node('g-brwin', 'Branch', 1220, 520),
node('g-stop1', 'SetVariable', 1420, 440, { variable: 'running', value: false }),
node('g-stop1', 'Variables', 1420, 440, { running: false }, [written('running', 'boolean')]),
node('g-leveloff1', 'SetEnabled', 1620, 440, { enabled: false }),
node('g-winon', 'SetEnabled', 1820, 440, { enabled: true }),
node('g-cmplose', 'Compare', 1160, 860, { op: '<=', b: 0 }),
node('g-brlose', 'Branch', 1420, 640),
node('g-stop2', 'SetVariable', 1620, 640, { variable: 'running', value: false }),
node('g-stop2', 'Variables', 1620, 640, { running: false }, [written('running', 'boolean')]),
node('g-leveloff2', 'SetEnabled', 1820, 640, { enabled: false }),
node('g-loseon', 'SetEnabled', 2020, 640, { enabled: true }),
],
@@ -273,29 +273,29 @@ const gameGraph = {
edge('ge8', 'g-lose', 'object', 'g-off3', 'object'),
edge('ge9', 'g-upd', 'exec', 'g-brrun', 'exec'),
edge('ge10', 'g-running', 'value', 'g-brrun', 'condition'),
edge('ge10', 'g-running', 'running', 'g-brrun', 'condition'),
edge('ge11', 'g-brrun', 'then', 'g-settime', 'exec'),
edge('ge12', 'g-time', 'value', 'g-sub', 'a'),
edge('ge12', 'g-time', 'timeLeft', 'g-sub', 'a'),
edge('ge13', 'g-upd', 'dt', 'g-sub', 'b'),
edge('ge14', 'g-sub', 'result', 'g-max', 'a'),
edge('ge15', 'g-max', 'result', 'g-settime', 'value'),
edge('ge15', 'g-max', 'result', 'g-settime', 'timeLeft'),
edge('ge16', 'g-settime', 'exec', 'g-settext', 'exec'),
edge('ge17', 'g-timelabel', 'object', 'g-settext', 'object'),
edge('ge18', 'g-time2', 'value', 'g-ceil', 'value'),
edge('ge18', 'g-time2', 'timeLeft', 'g-ceil', 'value'),
edge('ge19', 'g-ceil', 'result', 'g-join', 'b'),
edge('ge20', 'g-join', 'text', 'g-settext', 'text'),
edge('ge21', 'g-settext', 'exec', 'g-settext2', 'exec'),
edge('ge22', 'g-coinlabel', 'object', 'g-settext2', 'object'),
edge('ge23', 'g-coins', 'value', 'g-totext2', 'value'),
edge('ge23', 'g-coins', 'coins', 'g-totext2', 'value'),
edge('ge24', 'g-totext2', 'text', 'g-join2', 'b'),
edge('ge25', 'g-join2', 'text', 'g-join3', 'a'),
edge('ge26', 'g-join3', 'text', 'g-settext2', 'text'),
edge('ge27', 'g-settext2', 'exec', 'g-brwin', 'exec'),
edge('ge28', 'g-coins', 'value', 'g-cmpwin', 'a'),
edge('ge29', 'g-needed', 'value', 'g-cmpwin', 'b'),
edge('ge28', 'g-coins', 'coins', 'g-cmpwin', 'a'),
edge('ge29', 'g-needed', 'needed', 'g-cmpwin', 'b'),
edge('ge30', 'g-cmpwin', 'result', 'g-brwin', 'condition'),
edge('ge31', 'g-brwin', 'then', 'g-stop1', 'exec'),
edge('ge32', 'g-stop1', 'exec', 'g-leveloff1', 'exec'),
@@ -304,7 +304,7 @@ const gameGraph = {
edge('ge35', 'g-win', 'object', 'g-winon', 'object'),
edge('ge36', 'g-brwin', 'else', 'g-brlose', 'exec'),
edge('ge37', 'g-time2', 'value', 'g-cmplose', 'a'),
edge('ge37', 'g-time2', 'timeLeft', 'g-cmplose', 'a'),
edge('ge38', 'g-cmplose', 'result', 'g-brlose', 'condition'),
edge('ge39', 'g-brlose', 'then', 'g-stop2', 'exec'),
edge('ge40', 'g-stop2', 'exec', 'g-leveloff2', 'exec'),
+33 -29
View File
@@ -7,7 +7,7 @@
*
* Usage: node tools/seed-tetris.mjs [projectId] [baseUrl]
*/
import { edge, field, node, port, sceneObject, variable } from './lib/builders.mjs';
import { edge, field, node, port, sceneObject, variable, written } from './lib/builders.mjs';
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
const { projectId, baseUrl } = cliArgs('seed-tetris.mjs', PROJECTS.tetris);
@@ -163,10 +163,10 @@ function graphBuilder(variables = []) {
const edges = [];
let band = 0;
const kinds = Object.fromEntries(variables.map((v) => [v.key, v.kind]));
const declare = (keys) =>
const declare = (keys, write = false) =>
keys.map((key) => {
if (!kinds[key]) throw new Error(`no variable "${key}" to declare a port for`);
return port(key, kinds[key]);
return write ? written(key, kinds[key]) : port(key, kinds[key]);
});
const at = (id, type, column, row, params = {}, ports) => {
@@ -186,22 +186,22 @@ function graphBuilder(variables = []) {
flow: (...ids) => {
for (let i = 0; i < ids.length - 1; i++) link(ids[i], 'exec', ids[i + 1], 'exec');
},
get: (id, key, column, row) => at(id, 'GetVariable', column, row, { variable: key }),
/** A Variables node reading one variable; its output is named after it. */
get: (id, key, column, row) => at(id, 'Variables', column, row, {}, declare([key])),
/** A Variables node writing one variable, typed in as `value` or through its input of that name. */
set: (id, key, column, row, value) =>
at(id, 'SetVariable', column, row, value === undefined ? { variable: key } : { variable: key, value }),
at(id, 'Variables', column, row, value === undefined ? {} : { [key]: value }, declare([key], true)),
/**
* 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.
* One node for a whole cluster. A `sets` reads all its inputs before it writes any, so an input
* fed from a variable it writes sees the value from before.
*/
gets: (id, keys, column, row) => at(id, 'GetVariables', column, row, {}, declare(keys)),
gets: (id, keys, column, row) => at(id, 'Variables', 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));
return at(id, 'Variables', column, row, params, declare(keys, true));
},
/** A mistyped id would silently leave an input unconnected, so the seed refuses to write one. */
done() {
@@ -279,8 +279,10 @@ function pieceCellGraph() {
* Whether the four cells of a piece may stand where it is asked to: inside the well, and on empty
* board cells. Every attempt to turn, move or drop goes through this one answer.
*/
const FITS_VARS = [variable('ok', 'boolean', true)];
function fitsGraph() {
const g = graphBuilder();
const g = graphBuilder(FITS_VARS);
g.at('start', 'BehaviorStart', 0, 0, {}, [
port('shapes', 'list'),
port('grid', 'list'),
@@ -331,7 +333,7 @@ function fitsGraph() {
g.at('end', 'BehaviorEnd', 11, 0, {}, [port('ok', 'boolean')]);
g.get('result', 'ok', 10, 3);
g.link('loop', 'done', 'end', 'exec');
g.link('result', 'value', 'end', 'ok');
g.link('result', 'ok', 'end', 'ok');
return g.done();
}
@@ -402,7 +404,7 @@ function clearLinesGraph() {
g.set('setrow', 'row', 10, 13);
g.link('brfull', 'else', 'setrow', 'exec');
g.link('row-vars', 'row', 'back', 'a');
g.link('back', 'result', 'setrow', 'value');
g.link('back', 'result', 'setrow', 'row');
g.at('end', 'BehaviorEnd', 13, 0, {}, [port('grid', 'list'), port('lines', 'number')]);
g.gets('out-vars', ['board', 'cleared'], 12, 6);
@@ -554,7 +556,7 @@ function nextMoveGraph() {
link('f-rate', 'result', 'f-step', 'b');
link('start', 'fall', 'f-add', 'a');
link('f-step', 'result', 'f-add', 'b');
link('f-add', 'result', 'f-set', 'value');
link('f-add', 'result', 'f-set', 'outFall');
get('f-fall', 'outFall', 5, 3);
at('f-due', 'Compare', 6, 3, { op: '>=' });
@@ -564,11 +566,11 @@ function nextMoveGraph() {
at('f-pack', 'PackStruct', 8, 5, {}, [field('y')]);
sets('f-vars', { outFall: LINKED, next: LINKED, kind: 'fall' }, 6, 0);
flow('f-set', 'f-br');
link('f-fall', 'value', 'f-due', 'a');
link('f-fall', 'outFall', 'f-due', 'a');
link('start', 'stepTime', 'f-due', 'b');
link('f-due', 'result', 'f-br', 'condition');
link('f-br', 'then', 'f-vars', 'exec');
link('f-fall', 'value', 'f-minus', 'a');
link('f-fall', 'outFall', 'f-minus', 'a');
link('start', 'stepTime', 'f-minus', 'b');
link('f-minus', 'result', 'f-vars', 'outFall');
link('of', 'y', 'f-down1', 'a');
@@ -654,14 +656,16 @@ function drawPieceGraph() {
}
/** Writes the four cells of the landed piece into the board, read back from where they stand. */
const LOCK_PIECE_VARS = [variable('board', 'list', [], 'number')];
function lockPieceGraph() {
const g = graphBuilder();
const g = graphBuilder(LOCK_PIECE_VARS);
const { at, link, flow, get, set } = g;
at('start', 'BehaviorStart', 0, 0, {}, [port('grid', 'list'), port('shape', 'number')]);
set('take', 'board', 1, 0);
at('loop', 'Repeat', 2, 0, { times: 4 });
link('start', 'grid', 'take', 'value');
link('start', 'grid', 'take', 'board');
flow('start', 'take', 'loop');
const object = objectOfRound(g, 'p', 'loop', 'p', 3, 4);
@@ -688,16 +692,16 @@ function lockPieceGraph() {
link('rowr', 'result', 'row10', 'a');
link('row10', 'result', 'index', 'a');
link('colr', 'result', 'index', 'b');
link('board', 'value', 'write', 'list');
link('board', 'board', 'write', 'list');
link('index', 'result', 'write', 'index');
link('start', 'shape', 'write', 'value');
link('write', 'result', 'keep', 'value');
link('write', 'result', 'keep', 'board');
link('loop', 'body', 'keep', 'exec');
get('done', 'board', 4, 3);
at('end', 'BehaviorEnd', 4, 0, {}, [port('grid', 'list')]);
link('loop', 'done', 'end', 'exec');
link('done', 'value', 'end', 'grid');
link('done', 'board', 'end', 'grid');
return g.done();
}
@@ -755,14 +759,14 @@ function tetrisGraph() {
get('u-has', 'hasPiece', 0, 2);
at('u-br', 'Branch', 1, 0);
flow('u-update', 'u-br');
link('u-has', 'value', 'u-br', 'condition');
link('u-has', 'hasPiece', 'u-br', 'condition');
// 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);
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-columns', 'spawnX', 'n-spawn', 'spawnX');
link('n-spawn', 'piece', 'n-vars', 'piece');
flow('n-spawn', 'n-vars');
@@ -799,7 +803,7 @@ function tetrisGraph() {
flow('t-fits', 'm-br');
link('t-fits', 'ok', 'm-br', 'condition');
link('m-br', 'then', 'm-piece', 'exec');
link('m-in', 'next', 'm-piece', 'value');
link('m-in', 'next', 'm-piece', 'piece');
at('m-landed', 'Equals', 3, 7, { b: 'fall' });
at('m-brland', 'Branch', 3, 5);
@@ -812,7 +816,7 @@ function tetrisGraph() {
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');
link('m-py2', 'piece', 'm-pieceY', 'value');
link('m-pieceY', 'y', 'm-attop', 'a');
link('m-attop', 'result', 'm-brover', 'condition');
link('m-brover', 'then', 'm-stop', 'exec');
@@ -836,7 +840,7 @@ function tetrisGraph() {
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');
link('l-lock', 'grid', 'l-setgrid', 'grid');
flow('l-lock', 'l-setgrid');
at('r-clear', 'RunBehavior', 2, 0, { behavior: 'clear-lines' });
@@ -926,13 +930,13 @@ 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('fits', 'Fits', FITS_VARS, fitsGraph()),
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', NEXT_MOVE_VARS, nextMoveGraph()),
behavior('draw-piece', 'Draw piece', [], drawPieceGraph()),
behavior('lock-piece', 'Lock piece', [variable('board', 'list', [], 'number')], lockPieceGraph()),
behavior('lock-piece', 'Lock piece', LOCK_PIECE_VARS, lockPieceGraph()),
behavior('show-lines', 'Show lines', [], showLinesGraph()),
behavior('tetris', 'Tetris', tetrisVariables, tetrisGraph()),
],