Let a list hold structs, and read or write variables in bundles
A list item may now be a struct. The depth is bounded in the declaration
rather than by a comment: FieldKind (number|text|boolean|color) says what a
struct field holds, ListItemKind adds struct on top and says what a list
holds, so list -> struct -> primitive is the deepest storable shape and
sameValue stays finite. A struct item renders inline in the List tab, which
is why it needs no path in the slot address.
Four paths silently assumed a list item was a primitive: sameValue compared
items with ===, textOf would have printed [object Object], asListItem turned
a struct into text on Set item and Fill, and Index of used indexOf, which is
reference equality and could never have found a struct.
Tetris now runs on bundles. shapes is 112 {x, y} structs instead of 224 loose
numbers, piece and the candidate are structs, so Next move hands back a whole
candidate and each strand packs only the field it changes, since mergeStruct
leaves the rest standing. Declared behavior ports drop 49 -> 35, the master's
variables 18 -> 13, its graph 84/94 -> 69/76 nodes/edges.
Get variables and Set variables declare their ports from a list of variable
names, the way Pack and Expand already do, so one node can read or write
several at once. Renaming a variable rewires them, because there the port key
is the variable name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d62dfea7b
commit
f60358c951
32 files changed
+1083
-253
No files matched your search
+77
-1
@@ -110,8 +110,84 @@ export async function listTab(t) {
|
||||
check('a deleted target empties the tab', info.title === '' && info.items.length === 0, info.title);
|
||||
await shot('04-empty');
|
||||
|
||||
// 8. A list whose items are structs: each row is the bundle's fields under each other, built by
|
||||
// the inspector's own struct renderer. Values are two digits so no row's index can match one.
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets.push({
|
||||
id: 'celly',
|
||||
type: 'behavior',
|
||||
name: 'Celly',
|
||||
props: { enabled: true, runInEditor: false, cells: [{ x: 30, y: 11 }, { x: 44, y: 55 }] },
|
||||
variables: [
|
||||
{
|
||||
key: 'cells',
|
||||
label: 'cells',
|
||||
kind: 'list',
|
||||
default: [],
|
||||
items: 'struct',
|
||||
fields: [{ key: 'x', kind: 'number' }, { key: 'y', kind: 'number' }],
|
||||
},
|
||||
],
|
||||
graph: { nodes: [], edges: [] },
|
||||
});
|
||||
__gm.state.select({ kind: 'asset', assetId: 'celly' });
|
||||
__gm.state.openList({ kind: 'asset', assetId: 'celly', key: 'cells' });
|
||||
`);
|
||||
await sleep(250);
|
||||
info = await json('__gm.list.info()');
|
||||
check('a list of structs opens with a row per bundle', info.items.length === 2, JSON.stringify(info));
|
||||
check(
|
||||
'and a row reads as its fields, not as an object',
|
||||
info.items[0] === 'x: 30, y: 11',
|
||||
JSON.stringify(info.items),
|
||||
);
|
||||
check('the tab knows its items are structs', info.kind === 'struct', info.kind);
|
||||
check('the field labels of a bundle are on screen', (await find('x')) !== null);
|
||||
await shot('05-struct-rows');
|
||||
|
||||
// Editing one field must leave the others and the neighbouring item alone – the whole item is
|
||||
// rewritten on every keystroke, so a snapshot anywhere would silently revive an old value.
|
||||
const firstField = await json("__gm.list.fieldAt(0, 'x')");
|
||||
await click(firstField.x, firstField.y);
|
||||
await typeText('77');
|
||||
await press('Enter');
|
||||
await sleep(250);
|
||||
const cells = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
|
||||
check(
|
||||
'editing one field of one item leaves every other value untouched',
|
||||
JSON.stringify(cells) ===
|
||||
JSON.stringify([
|
||||
{ x: 77, y: 11 },
|
||||
{ x: 44, y: 55 },
|
||||
]),
|
||||
JSON.stringify(cells),
|
||||
);
|
||||
await undo();
|
||||
await sleep(200);
|
||||
const back = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
|
||||
check(
|
||||
'and undo takes back just that field',
|
||||
JSON.stringify(back) ===
|
||||
JSON.stringify([
|
||||
{ x: 30, y: 11 },
|
||||
{ x: 44, y: 55 },
|
||||
]),
|
||||
JSON.stringify(back),
|
||||
);
|
||||
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => a.id !== 'celly');
|
||||
__gm.state.select(null);
|
||||
__gm.state.preview();
|
||||
`);
|
||||
await sleep(200);
|
||||
|
||||
await mutate('__gm.state.openList(null)');
|
||||
await sleep(200);
|
||||
const left = await json('__gm.state.project.assets.filter((a) => a.id === "listy").length');
|
||||
const left = await json(
|
||||
'__gm.state.project.assets.filter((a) => a.id === "listy" || a.id === "celly").length',
|
||||
);
|
||||
check('the step leaves the fixture as it found it', left === 0, String(left));
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { edge, node, port, variable } from '../lib/builders.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 step brings its own box and behavior; everything it adds is removed at the end.
|
||||
*/
|
||||
|
||||
const SCALE = 100;
|
||||
|
||||
/**
|
||||
* `On update → Set variables{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.
|
||||
*/
|
||||
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-move', 'Move', 560, 0, { scale: SCALE }),
|
||||
],
|
||||
edges: [
|
||||
edge('vp-e1', 'vp-update', 'exec', 'vp-set', 'exec'),
|
||||
edge('vp-e2', 'vp-set', 'exec', 'vp-move', 'exec'),
|
||||
edge('vp-e3', 'vp-get', 'vx', 'vp-move', 'dx'),
|
||||
edge('vp-e4', 'vp-get', 'vy', 'vp-move', 'dy'),
|
||||
edge('vp-e5', 'vp-update', 'dt', 'vp-move', 'dt'),
|
||||
edge('vp-e6', 'vp-owner', 'object', 'vp-move', 'object'),
|
||||
],
|
||||
};
|
||||
|
||||
export async function variableNodes(t) {
|
||||
const { check, sleep, evaluate, json, shot, click, locate, undo, entity, asset, mutate, stop } = t;
|
||||
const portAt = (nodeId, key, isOutput) =>
|
||||
json(`__gm.graph.portAt(${JSON.stringify(nodeId)}, ${JSON.stringify(key)}, ${isOutput})`);
|
||||
const portsOf = async (nodeId) =>
|
||||
(await asset('vp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? [];
|
||||
const select = async (nodeId) => {
|
||||
await evaluate(
|
||||
`__gm.state.select({ kind: 'node', assetId: 'vp-logic', nodeId: ${JSON.stringify(nodeId)}, nodeIds: [${JSON.stringify(nodeId)}] })`,
|
||||
);
|
||||
await sleep(300);
|
||||
};
|
||||
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
||||
const clickLast = async (text) => {
|
||||
const p = await locate(text, true);
|
||||
await click(p.x, p.y);
|
||||
await sleep(200);
|
||||
};
|
||||
|
||||
// 23a. A box wearing a behavior whose two speeds travel through its variables.
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
const scene = __gm.state.activeScene;
|
||||
const proto = scene.objects[0];
|
||||
const mover = p.assets.find((a) => a.id === 'mover');
|
||||
const a = structuredClone(mover);
|
||||
Object.assign(a, {
|
||||
id: 'vp-logic',
|
||||
name: 'Speeds',
|
||||
props: { enabled: true, runInEditor: false, vx: 0, vy: 0, vz: 0 },
|
||||
variables: ${JSON.stringify([variable('vx', 'number', 0), variable('vy', 'number', 0), variable('vz', 'number', 0)])},
|
||||
graph: ${JSON.stringify(logicGraph)},
|
||||
});
|
||||
p.assets.unshift(a);
|
||||
const make = (id, assetId, parentId, name) => {
|
||||
const o = structuredClone(proto);
|
||||
Object.assign(o, {
|
||||
id, assetId, name,
|
||||
position: { x: 0, y: 900, z: 0 },
|
||||
parentId, overrides: {}, instanceEdits: {},
|
||||
});
|
||||
return o;
|
||||
};
|
||||
scene.objects.push(make('vp-box', 'crate', '', 'Speedy box'), make('vp-brain', 'vp-logic', 'vp-box', 'Speeds'));
|
||||
`);
|
||||
await sleep(400);
|
||||
|
||||
// 23b. The run: both variables are written by one node and read back by another, and the two
|
||||
// speeds stay apart – 2 to the right for every 3 down.
|
||||
await t.play();
|
||||
await sleep(300);
|
||||
const before = await entity('vp-box');
|
||||
await sleep(1000);
|
||||
const after = await entity('vp-box');
|
||||
const dx = after.x - before.x;
|
||||
const dy = after.y - before.y;
|
||||
check(
|
||||
'one node writes both variables, another reads both back, and the run uses them',
|
||||
dx > SCALE * 1.4 && dx < SCALE * 2.6 && dy > SCALE * 2.4 && dy < SCALE * 3.6,
|
||||
`moved (${dx.toFixed(1)}, ${dy.toFixed(1)}) px in ~1 s – (200, 300) means both ports carried their own variable`,
|
||||
);
|
||||
await stop();
|
||||
await sleep(300);
|
||||
|
||||
// 23c. The node draws one port per variable it names, on the side its type says.
|
||||
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);
|
||||
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 }),
|
||||
);
|
||||
|
||||
// 23d. The inspector picks a port from the behavior's variables instead of having it typed.
|
||||
const add = await locate('+ Variable');
|
||||
check('the node offers its variables in the inspector', Boolean(add), JSON.stringify(add));
|
||||
await shot('01-variable-ports');
|
||||
await click(add.x, add.y);
|
||||
await sleep(200);
|
||||
await clickLast('vz');
|
||||
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',
|
||||
JSON.stringify(grown),
|
||||
);
|
||||
check('and the node grew that input', Boolean(await portAt('vp-set', 'vz', false)));
|
||||
await shot('02-variable-added');
|
||||
await undo();
|
||||
await sleep(300);
|
||||
check('undo takes it 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.
|
||||
await select('vp-get');
|
||||
await clickLast('vx');
|
||||
await sleep(200);
|
||||
await clickLast('vz');
|
||||
await sleep(300);
|
||||
const swapped = await portsOf('vp-get');
|
||||
const edgeStill = await json(
|
||||
`(__gm.state.project.assets.find((a) => a.id === 'vp-logic').graph.edges.find((e) => e.id === 'vp-e3') || {}).from`,
|
||||
);
|
||||
check(
|
||||
'a port pointed at another variable keeps its edge',
|
||||
swapped[0].key === 'vz' && edgeStill.port === 'vz',
|
||||
JSON.stringify({ swapped, edgeStill }),
|
||||
);
|
||||
await undo();
|
||||
await sleep(300);
|
||||
check('and undo points it back', (await portsOf('vp-get'))[0].key === 'vx');
|
||||
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => a.id !== 'vp-logic');
|
||||
const scene = __gm.state.activeScene;
|
||||
scene.objects = scene.objects.filter((o) => o.id !== 'vp-box' && o.id !== 'vp-brain');
|
||||
__gm.state.select(null);
|
||||
__gm.state.preview();
|
||||
`);
|
||||
await sleep(300);
|
||||
const left = await json('__gm.state.project.assets.filter((a) => a.id === "vp-logic").length');
|
||||
check('the step leaves the fixture as it found it', left === 0, String(left));
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { nestedBehavior } from './19-nested-behavior.mjs';
|
||||
import { keyRepeat } from './20-key-repeat.mjs';
|
||||
import { listTab } from './21-list.mjs';
|
||||
import { structVariable } from './22-struct.mjs';
|
||||
import { variableNodes } from './23-variable-nodes.mjs';
|
||||
|
||||
export const STEPS = [
|
||||
hierarchy,
|
||||
@@ -49,4 +50,5 @@ export const STEPS = [
|
||||
keyRepeat,
|
||||
listTab,
|
||||
structVariable,
|
||||
variableNodes,
|
||||
];
|
||||
@@ -46,11 +46,18 @@ export const node = (id, type, x, y, params = {}, ports) => ({
|
||||
/** A port a behavior's start or end node declares. */
|
||||
export const port = (key, kind) => ({ key, kind });
|
||||
|
||||
/** A behavior variable declaration. `items` is the item kind of a `list`, ignored otherwise. */
|
||||
export const variable = (key, kind, value, items) => ({
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const variable = (key, kind, value, items, fields) => ({
|
||||
key,
|
||||
label: key,
|
||||
kind,
|
||||
default: value,
|
||||
...(items ? { items } : {}),
|
||||
...(fields ? { fields } : {}),
|
||||
});
|
||||
|
||||
/** A struct field declaration, and the same shape a list of structs declares for its items. */
|
||||
export const field = (key, kind = 'number') => ({ key, kind });
|
||||
+112
-134
@@ -7,6 +7,11 @@
|
||||
* empty row joined in front of everything above and below it – with no text encoding in between,
|
||||
* so the numbers in the inspector are the numbers the game means.
|
||||
*
|
||||
* What travels as a bundle rather than as loose numbers: the falling piece (`piece`) and the one
|
||||
* move being tried on it (`next`) are structs of shape/rot/x/y, and `shapes` is a list of `{x, y}`
|
||||
* 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`.
|
||||
*
|
||||
* 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`
|
||||
@@ -19,7 +24,7 @@
|
||||
*
|
||||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-tetris.mjs [projectId] [baseUrl]
|
||||
*/
|
||||
import { edge, node, port, sceneObject, variable } from './lib/builders.mjs';
|
||||
import { edge, field, node, port, sceneObject, variable } from './lib/builders.mjs';
|
||||
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
|
||||
|
||||
const { projectId, baseUrl } = cliArgs('seed-tetris.mjs', PROJECTS.tetris);
|
||||
@@ -123,21 +128,25 @@ const SHAPES = [
|
||||
const turned = (cells, size) => cells.map(([x, y]) => [size - 1 - y, x]);
|
||||
|
||||
/**
|
||||
* All 28 rotations in one list, eight numbers each (x and y of the four cells), so one rotation
|
||||
* starts at (shape·4 + rotation)·8 and its n-th cell two numbers further along.
|
||||
* All 28 rotations in one list, four cells each, so one rotation starts at (shape·4 + rotation)·4
|
||||
* and its n-th cell is one entry further along.
|
||||
*/
|
||||
function shapeTable() {
|
||||
const table = [];
|
||||
for (const shape of SHAPES) {
|
||||
let cells = shape.cells;
|
||||
for (let rotation = 0; rotation < 4; rotation++) {
|
||||
for (const [x, y] of cells) table.push(x, y);
|
||||
for (const [x, y] of cells) table.push({ x, y });
|
||||
cells = turned(cells, shape.size);
|
||||
}
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/** The four numbers that say which piece is where. */
|
||||
const PIECE_FIELDS = [field('shape'), field('rot'), field('x'), field('y')];
|
||||
const NO_PIECE = { shape: 0, rot: 0, x: 0, y: 0 };
|
||||
|
||||
/** The column a shape starts in, so every piece appears centred over the well. */
|
||||
const spawnColumns = () => SHAPES.map((s) => Math.floor((COLS - s.size) / 2));
|
||||
|
||||
@@ -224,41 +233,36 @@ function pieceCellGraph() {
|
||||
const g = graphBuilder();
|
||||
g.at('start', 'BehaviorStart', 0, 0, {}, [
|
||||
port('shapes', 'list'),
|
||||
port('shape', 'number'),
|
||||
port('rot', 'number'),
|
||||
port('piece', 'struct'),
|
||||
port('corner', 'number'),
|
||||
port('baseX', 'number'),
|
||||
port('baseY', 'number'),
|
||||
]);
|
||||
|
||||
g.at('x4', 'Multiply', 1, 2, { b: 4 });
|
||||
g.at('plus', 'Add', 2, 2);
|
||||
g.at('x8', 'Multiply', 3, 2, { b: 8 });
|
||||
g.at('c2', 'Multiply', 1, 5, { b: 2 });
|
||||
g.at('ix', 'Add', 4, 2);
|
||||
g.at('iy', 'Add', 4, 5, { b: 1 });
|
||||
g.at('ax', 'ItemAt', 5, 1);
|
||||
g.at('ay', 'ItemAt', 5, 5);
|
||||
g.at('sx', 'Add', 6, 1);
|
||||
g.at('sy', 'Add', 6, 5);
|
||||
g.at('end', 'BehaviorEnd', 7, 0, {}, [port('x', 'number'), port('y', 'number')]);
|
||||
// The whole piece arrives as one bundle and is taken apart here, where its parts are needed.
|
||||
g.at('of', 'ExpandStruct', 1, 2, {}, PIECE_FIELDS);
|
||||
g.at('x4', 'Multiply', 2, 2, { b: 4 });
|
||||
g.at('plus', 'Add', 3, 2);
|
||||
g.at('x4b', 'Multiply', 4, 2, { b: 4 });
|
||||
g.at('index', 'Add', 5, 2);
|
||||
g.at('cell', 'ItemAt', 6, 2);
|
||||
g.at('at', 'ExpandStruct', 7, 2, {}, [field('x'), field('y')]);
|
||||
g.at('sx', 'Add', 8, 1);
|
||||
g.at('sy', 'Add', 8, 4);
|
||||
g.at('end', 'BehaviorEnd', 9, 0, {}, [port('x', 'number'), port('y', 'number')]);
|
||||
|
||||
g.link('start', 'shape', 'x4', 'a');
|
||||
g.link('start', 'piece', 'of', 'value');
|
||||
g.link('of', 'shape', 'x4', 'a');
|
||||
g.link('x4', 'result', 'plus', 'a');
|
||||
g.link('start', 'rot', 'plus', 'b');
|
||||
g.link('plus', 'result', 'x8', 'a');
|
||||
g.link('start', 'corner', 'c2', 'a');
|
||||
g.link('x8', 'result', 'ix', 'a');
|
||||
g.link('c2', 'result', 'ix', 'b');
|
||||
g.link('ix', 'result', 'iy', 'a');
|
||||
g.link('start', 'shapes', 'ax', 'list');
|
||||
g.link('ix', 'result', 'ax', 'index');
|
||||
g.link('start', 'shapes', 'ay', 'list');
|
||||
g.link('iy', 'result', 'ay', 'index');
|
||||
g.link('ax', 'item', 'sx', 'a');
|
||||
g.link('start', 'baseX', 'sx', 'b');
|
||||
g.link('ay', 'item', 'sy', 'a');
|
||||
g.link('start', 'baseY', 'sy', 'b');
|
||||
g.link('of', 'rot', 'plus', 'b');
|
||||
g.link('plus', 'result', 'x4b', 'a');
|
||||
g.link('x4b', 'result', 'index', 'a');
|
||||
g.link('start', 'corner', 'index', 'b');
|
||||
g.link('start', 'shapes', 'cell', 'list');
|
||||
g.link('index', 'result', 'cell', 'index');
|
||||
g.link('cell', 'item', 'at', 'value');
|
||||
g.link('at', 'x', 'sx', 'a');
|
||||
g.link('of', 'x', 'sx', 'b');
|
||||
g.link('at', 'y', 'sy', 'a');
|
||||
g.link('of', 'y', 'sy', 'b');
|
||||
g.link('sx', 'result', 'end', 'x');
|
||||
g.link('sy', 'result', 'end', 'y');
|
||||
g.flow('start', 'end');
|
||||
@@ -274,10 +278,7 @@ function fitsGraph() {
|
||||
g.at('start', 'BehaviorStart', 0, 0, {}, [
|
||||
port('shapes', 'list'),
|
||||
port('grid', 'list'),
|
||||
port('shape', 'number'),
|
||||
port('rot', 'number'),
|
||||
port('x', 'number'),
|
||||
port('y', 'number'),
|
||||
port('piece', 'struct'),
|
||||
]);
|
||||
g.set('ok', 'ok', 1, 0, true);
|
||||
g.at('loop', 'Repeat', 2, 0, { times: 4 });
|
||||
@@ -285,10 +286,8 @@ function fitsGraph() {
|
||||
|
||||
g.at('cell', 'RunBehavior', 3, 0, { behavior: 'piece-cell' });
|
||||
g.link('loop', 'body', 'cell', 'exec');
|
||||
for (const key of ['shapes', 'shape', 'rot']) g.link('start', key, 'cell', key);
|
||||
for (const key of ['shapes', 'piece']) g.link('start', key, 'cell', key);
|
||||
g.link('loop', 'index', 'cell', 'corner');
|
||||
g.link('start', 'x', 'cell', 'baseX');
|
||||
g.link('start', 'y', 'cell', 'baseY');
|
||||
|
||||
g.at('left', 'Compare', 4, 3, { op: '<', b: 0 });
|
||||
g.at('right', 'Compare', 4, 5, { op: '>', b: COLS - 1 });
|
||||
@@ -446,12 +445,16 @@ function spawnPieceGraph() {
|
||||
g.at('random', 'Random', 1, 2, { min: 0, max: SHAPES.length });
|
||||
g.at('shape', 'Floor', 2, 2);
|
||||
g.at('column', 'ItemAt', 3, 2);
|
||||
g.at('end', 'BehaviorEnd', 4, 0, {}, [port('shape', 'number'), port('x', 'number')]);
|
||||
// Handed back as a finished piece rather than as parts the caller has to assemble: the two
|
||||
// fields it does not choose are the ones a new piece is defined to start at.
|
||||
g.at('piece', 'PackStruct', 4, 2, { rot: 0, y: 0 }, PIECE_FIELDS);
|
||||
g.at('end', 'BehaviorEnd', 5, 0, {}, [port('piece', 'struct')]);
|
||||
g.link('random', 'result', 'shape', 'value');
|
||||
g.link('start', 'spawnX', 'column', 'list');
|
||||
g.link('shape', 'result', 'column', 'index');
|
||||
g.link('shape', 'result', 'end', 'shape');
|
||||
g.link('column', 'item', 'end', 'x');
|
||||
g.link('shape', 'result', 'piece', 'shape');
|
||||
g.link('column', 'item', 'piece', 'x');
|
||||
g.link('piece', 'value', 'end', 'piece');
|
||||
g.flow('start', 'end');
|
||||
return g.done();
|
||||
}
|
||||
@@ -466,9 +469,7 @@ function nextMoveGraph() {
|
||||
const { at, link, flow, get, set } = g;
|
||||
|
||||
at('start', 'BehaviorStart', 0, 0, {}, [
|
||||
port('px', 'number'),
|
||||
port('py', 'number'),
|
||||
port('rot', 'number'),
|
||||
port('piece', 'struct'),
|
||||
port('wantDx', 'number'),
|
||||
port('wantRot', 'number'),
|
||||
port('fall', 'number'),
|
||||
@@ -476,21 +477,21 @@ function nextMoveGraph() {
|
||||
port('stepTime', 'number'),
|
||||
]);
|
||||
|
||||
// The candidate starts out as where the piece already is, and no wish is spent yet.
|
||||
set('i-nx', 'nx', 1, 0);
|
||||
set('i-ny', 'ny', 2, 0);
|
||||
set('i-nrot', 'nrot', 3, 0);
|
||||
set('i-kind', 'kind', 4, 0, '');
|
||||
set('i-fall', 'outFall', 5, 0);
|
||||
set('i-dx', 'outDx', 6, 0);
|
||||
set('i-rot', 'outRot', 7, 0);
|
||||
link('start', 'px', 'i-nx', 'value');
|
||||
link('start', 'py', 'i-ny', 'value');
|
||||
link('start', 'rot', 'i-nrot', 'value');
|
||||
// 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.
|
||||
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-nx', 'i-ny', 'i-nrot', 'i-kind', 'i-fall', 'i-dx', 'i-rot', 't-br');
|
||||
flow('start', 'i-next', 'i-kind', 'i-fall', 'i-dx', 'i-rot', 't-br');
|
||||
|
||||
// Turning wins: a quarter turn clockwise is one step around the four rotations of the shape.
|
||||
g.strand();
|
||||
@@ -498,15 +499,17 @@ function nextMoveGraph() {
|
||||
at('t-br', 'Branch', 0, 0);
|
||||
at('t-plus1', 'Add', 1, 3, { b: 1 });
|
||||
at('t-wrap', 'Modulo', 2, 3, { b: 4 });
|
||||
set('t-set', 'nrot', 1, 0);
|
||||
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);
|
||||
link('start', 'wantRot', 't-want', 'a');
|
||||
link('t-want', 'result', 't-br', 'condition');
|
||||
link('t-br', 'then', 't-set', 'exec');
|
||||
link('start', 'rot', 't-plus1', 'a');
|
||||
link('of', 'rot', 't-plus1', 'a');
|
||||
link('t-plus1', 'result', 't-wrap', 'a');
|
||||
link('t-wrap', 'result', 't-set', 'value');
|
||||
link('t-wrap', 'result', 't-pack', 'rot');
|
||||
link('t-pack', 'value', 't-set', 'value');
|
||||
flow('t-set', 't-kind', 't-used', 'end');
|
||||
|
||||
// Then sideways, one column per key press.
|
||||
@@ -514,7 +517,8 @@ function nextMoveGraph() {
|
||||
at('m-moving', 'Not', 5, 3);
|
||||
at('m-br', 'Branch', 5, 0);
|
||||
at('m-shift', 'Add', 6, 3);
|
||||
set('m-set', 'nx', 6, 0);
|
||||
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);
|
||||
link('t-br', 'else', 'm-br', 'exec');
|
||||
@@ -522,9 +526,10 @@ function nextMoveGraph() {
|
||||
link('m-still', 'result', 'm-moving', 'value');
|
||||
link('m-moving', 'result', 'm-br', 'condition');
|
||||
link('m-br', 'then', 'm-set', 'exec');
|
||||
link('start', 'px', 'm-shift', 'a');
|
||||
link('of', 'x', 'm-shift', 'a');
|
||||
link('start', 'wantDx', 'm-shift', 'b');
|
||||
link('m-shift', 'result', 'm-set', 'value');
|
||||
link('m-shift', 'result', 'm-pack', 'x');
|
||||
link('m-pack', 'value', 'm-set', 'value');
|
||||
flow('m-set', 'm-kind', 'm-used', 'end');
|
||||
|
||||
// And otherwise the clock: holding the down key lets it run ten times as fast.
|
||||
@@ -550,7 +555,8 @@ function nextMoveGraph() {
|
||||
at('f-minus', 'Subtract', 7, 3);
|
||||
set('f-rest', 'outFall', 6, 0);
|
||||
at('f-down1', 'Add', 7, 5, { b: 1 });
|
||||
set('f-ny', 'ny', 7, 0);
|
||||
at('f-pack', 'PackStruct', 8, 5, {}, [field('y')]);
|
||||
set('f-ny', 'next', 7, 0);
|
||||
set('f-kind', 'kind', 8, 0, 'fall');
|
||||
flow('f-set', 'f-br');
|
||||
link('f-fall', 'value', 'f-due', 'a');
|
||||
@@ -560,8 +566,9 @@ function nextMoveGraph() {
|
||||
link('f-fall', 'value', 'f-minus', 'a');
|
||||
link('start', 'stepTime', 'f-minus', 'b');
|
||||
link('f-minus', 'result', 'f-rest', 'value');
|
||||
link('start', 'py', 'f-down1', 'a');
|
||||
link('f-down1', 'result', 'f-ny', 'value');
|
||||
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');
|
||||
// Not due yet: nothing moves, and "kind" stays empty so the fit test below changes nothing.
|
||||
link('f-br', 'else', 'end', 'exec');
|
||||
@@ -569,18 +576,14 @@ function nextMoveGraph() {
|
||||
// Whatever the frame decided is handed back – the caller owns the variables, not this behavior.
|
||||
g.strand();
|
||||
at('end', 'BehaviorEnd', 2, 0, {}, [
|
||||
port('nx', 'number'),
|
||||
port('ny', 'number'),
|
||||
port('nrot', 'number'),
|
||||
port('next', 'struct'),
|
||||
port('kind', 'text'),
|
||||
port('fall', 'number'),
|
||||
port('wantDx', 'number'),
|
||||
port('wantRot', 'number'),
|
||||
]);
|
||||
const back = [
|
||||
['e-nx', 'nx', 'nx'],
|
||||
['e-ny', 'ny', 'ny'],
|
||||
['e-nrot', 'nrot', 'nrot'],
|
||||
['e-next', 'next', 'next'],
|
||||
['e-kind', 'kind', 'kind'],
|
||||
['e-fall', 'outFall', 'fall'],
|
||||
['e-dx', 'outDx', 'wantDx'],
|
||||
@@ -604,20 +607,18 @@ function drawPieceGraph() {
|
||||
|
||||
at('start', 'BehaviorStart', 0, 0, {}, [
|
||||
port('shapes', 'list'),
|
||||
port('shape', 'number'),
|
||||
port('rot', 'number'),
|
||||
port('px', 'number'),
|
||||
port('py', 'number'),
|
||||
port('piece', 'struct'),
|
||||
port('colors', 'list'),
|
||||
]);
|
||||
at('loop', 'Repeat', 1, 0, { times: 4 });
|
||||
at('cell', 'RunBehavior', 2, 0, { behavior: 'piece-cell' });
|
||||
flow('start', 'loop');
|
||||
link('loop', 'body', 'cell', 'exec');
|
||||
for (const key of ['shapes', 'shape', 'rot']) link('start', key, 'cell', key);
|
||||
for (const key of ['shapes', 'piece']) link('start', key, 'cell', key);
|
||||
link('loop', 'index', 'cell', 'corner');
|
||||
link('start', 'px', 'cell', 'baseX');
|
||||
link('start', 'py', 'cell', 'baseY');
|
||||
// Only the shape number is wanted here, for the colour.
|
||||
at('of', 'ExpandStruct', 2, 10, {}, [field('shape')]);
|
||||
link('start', 'piece', 'of', 'value');
|
||||
|
||||
const object = objectOfRound(g, 'p', 'loop', 'p', 3, 12);
|
||||
at('x', 'Multiply', 3, 4, { b: CELL });
|
||||
@@ -630,7 +631,7 @@ function drawPieceGraph() {
|
||||
link('cell', 'y', 'y', 'a');
|
||||
link('y', 'result', 'aty', 'a');
|
||||
link('start', 'colors', 'color', 'list');
|
||||
link('start', 'shape', 'color', 'index');
|
||||
link('of', 'shape', 'color', 'index');
|
||||
|
||||
at('show', 'SetEnabled', 6, 0, { enabled: true });
|
||||
at('place', 'SetPosition', 7, 0);
|
||||
@@ -756,26 +757,20 @@ 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-shape', 'shape', 3, 5);
|
||||
set('n-rot', 'rot', 4, 5, 0);
|
||||
set('n-px', 'px', 5, 5);
|
||||
set('n-py', 'py', 6, 5, 0);
|
||||
set('n-fall', 'fall', 7, 5, 0);
|
||||
set('n-has', 'hasPiece', 8, 5, true);
|
||||
set('n-piece', 'piece', 3, 5);
|
||||
set('n-fall', 'fall', 4, 5, 0);
|
||||
set('n-has', 'hasPiece', 5, 5, true);
|
||||
link('u-br', 'else', 'n-spawn', 'exec');
|
||||
link('n-columns', 'value', 'n-spawn', 'spawnX');
|
||||
link('n-spawn', 'shape', 'n-shape', 'value');
|
||||
link('n-spawn', 'x', 'n-px', 'value');
|
||||
flow('n-spawn', 'n-shape', 'n-rot', 'n-px', 'n-py', 'n-fall', 'n-has');
|
||||
link('n-spawn', 'piece', 'n-piece', 'value');
|
||||
flow('n-spawn', 'n-piece', 'n-fall', 'n-has');
|
||||
|
||||
// 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 = [
|
||||
['px', 'px'],
|
||||
['py', 'py'],
|
||||
['rot', 'rot'],
|
||||
['piece', 'piece'],
|
||||
['wantDx', 'wantDx'],
|
||||
['wantRot', 'wantRot'],
|
||||
['fall', 'fall'],
|
||||
@@ -788,9 +783,7 @@ function tetrisGraph() {
|
||||
link('u-update', 'dt', 'c-move', 'dt');
|
||||
|
||||
const moveOut = [
|
||||
['nx', 'nx'],
|
||||
['ny', 'ny'],
|
||||
['nrot', 'nrot'],
|
||||
['next', 'next'],
|
||||
['kind', 'kind'],
|
||||
['fall', 'fall'],
|
||||
['wantDx', 'wantDx'],
|
||||
@@ -807,56 +800,45 @@ function tetrisGraph() {
|
||||
at('t-fits', 'RunBehavior', 0, 0, { behavior: 'fits' });
|
||||
get('t-shapes', 'shapes', 0, 3);
|
||||
get('t-grid', 'grid', 0, 5);
|
||||
get('t-shape', 'shape', 0, 7);
|
||||
get('t-nrot', 'nrot', 0, 9);
|
||||
get('t-nx', 'nx', 0, 11);
|
||||
get('t-ny', 'ny', 0, 13);
|
||||
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-shape', 'value', 't-fits', 'shape');
|
||||
link('t-nrot', 'value', 't-fits', 'rot');
|
||||
link('t-nx', 'value', 't-fits', 'x');
|
||||
link('t-ny', 'value', 't-fits', 'y');
|
||||
link('t-next', 'value', '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-nx', 'nx', 2, 3);
|
||||
get('m-ny', 'ny', 3, 3);
|
||||
get('m-nrot', 'nrot', 4, 3);
|
||||
set('m-px', 'px', 2, 0);
|
||||
set('m-py', 'py', 3, 0);
|
||||
set('m-rot', 'rot', 4, 0);
|
||||
get('m-next', 'next', 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-px', 'exec');
|
||||
link('m-nx', 'value', 'm-px', 'value');
|
||||
link('m-ny', 'value', 'm-py', 'value');
|
||||
link('m-nrot', 'value', 'm-rot', 'value');
|
||||
flow('m-px', 'm-py', 'm-rot');
|
||||
link('m-br', 'then', 'm-piece', 'exec');
|
||||
link('m-next', 'value', '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', 'py', 4, 8);
|
||||
at('m-attop', 'Compare', 5, 8, { op: '<=', b: 0 });
|
||||
get('m-py2', 'piece', 4, 8);
|
||||
at('m-pieceY', 'ExpandStruct', 5, 10, {}, [field('y')]);
|
||||
at('m-attop', 'Compare', 6, 8, { op: '<=', b: 0 });
|
||||
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-landed', 'result', 'm-brland', 'condition');
|
||||
link('m-brland', 'then', 'm-brover', 'exec');
|
||||
link('m-py2', 'value', 'm-attop', 'a');
|
||||
link('m-py2', 'value', 'm-pieceY', 'value');
|
||||
link('m-pieceY', 'y', 'm-attop', 'a');
|
||||
link('m-attop', 'result', 'm-brover', 'condition');
|
||||
link('m-brover', 'then', 'm-stop', 'exec');
|
||||
|
||||
// 6) Wherever the piece ended up – newly spawned or moved – it is drawn there.
|
||||
strand();
|
||||
at('d-draw', 'RunBehavior', 0, 0, { behavior: 'draw-piece' });
|
||||
flow('m-rot', 'd-draw');
|
||||
flow('m-piece', 'd-draw');
|
||||
flow('n-has', 'd-draw');
|
||||
['shapes', 'shape', 'rot', 'px', 'py', 'colors'].forEach((key, i) => {
|
||||
['shapes', 'piece', 'colors'].forEach((key, i) => {
|
||||
get(`d-${key}`, key, 0, i * 2 + 3);
|
||||
link(`d-${key}`, 'value', 'd-draw', key);
|
||||
});
|
||||
@@ -865,11 +847,13 @@ function tetrisGraph() {
|
||||
strand();
|
||||
at('l-lock', 'RunBehavior', 0, 0, { behavior: 'lock-piece' });
|
||||
get('l-grid', 'grid', 0, 3);
|
||||
get('l-shape', 'shape', 0, 5);
|
||||
get('l-piece', 'piece', 0, 5);
|
||||
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-shape', 'value', 'l-lock', 'shape');
|
||||
link('l-piece', 'value', 'l-shape', 'value');
|
||||
link('l-shape', 'shape', 'l-lock', 'shape');
|
||||
link('l-lock', 'grid', 'l-setgrid', 'value');
|
||||
flow('l-lock', 'l-setgrid');
|
||||
|
||||
@@ -910,16 +894,12 @@ function behavior(id, name, variables, graph) {
|
||||
|
||||
const tetrisVariables = [
|
||||
variable('grid', 'list', EMPTY_GRID, 'number'),
|
||||
variable('shapes', 'list', shapeTable(), 'number'),
|
||||
variable('shapes', 'list', shapeTable(), 'struct', [field('x'), field('y')]),
|
||||
variable('colors', 'list', SHAPE_COLORS, 'color'),
|
||||
variable('spawnX', 'list', spawnColumns(), 'number'),
|
||||
variable('shape', 'number', 0),
|
||||
variable('rot', 'number', 0),
|
||||
variable('px', 'number', 3),
|
||||
variable('py', 'number', 0),
|
||||
variable('nx', 'number', 0),
|
||||
variable('ny', 'number', 0),
|
||||
variable('nrot', 'number', 0),
|
||||
// The falling piece and the one move being tried on it – four numbers each, and never apart.
|
||||
variable('piece', 'struct', NO_PIECE, undefined, PIECE_FIELDS),
|
||||
variable('next', 'struct', NO_PIECE, undefined, PIECE_FIELDS),
|
||||
variable('kind', 'text', ''),
|
||||
variable('hasPiece', 'boolean', false),
|
||||
variable('fall', 'number', 0),
|
||||
@@ -984,9 +964,7 @@ const project = {
|
||||
'next-move',
|
||||
'Next move',
|
||||
[
|
||||
variable('nx', 'number', 0),
|
||||
variable('ny', 'number', 0),
|
||||
variable('nrot', 'number', 0),
|
||||
variable('next', 'struct', NO_PIECE, undefined, PIECE_FIELDS),
|
||||
variable('kind', 'text', ''),
|
||||
variable('outFall', 'number', 0),
|
||||
variable('outDx', 'number', 0),
|
||||
|
||||
+12
-10
@@ -27,16 +27,16 @@ 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: 84, edges: 94 },
|
||||
'spawn-piece': { nodes: 5, edges: 6 },
|
||||
'next-move': { nodes: 44, edges: 63 },
|
||||
fits: { nodes: 18, edges: 29 },
|
||||
'draw-piece': { nodes: 14, edges: 26 },
|
||||
tetris: { nodes: 69, edges: 76 },
|
||||
'spawn-piece': { nodes: 6, edges: 7 },
|
||||
'next-move': { nodes: 44, edges: 61 },
|
||||
fits: { nodes: 18, edges: 26 },
|
||||
'draw-piece': { nodes: 15, edges: 24 },
|
||||
'lock-piece': { nodes: 20, edges: 23 },
|
||||
'clear-lines': { nodes: 29, edges: 36 },
|
||||
'show-lines': { nodes: 5, edges: 5 },
|
||||
'draw-board': { nodes: 11, edges: 17 },
|
||||
'piece-cell': { nodes: 12, edges: 19 },
|
||||
'piece-cell': { nodes: 11, edges: 17 },
|
||||
};
|
||||
|
||||
/** The steps the master calls. `piece-cell` is missing on purpose: three of these call it. */
|
||||
@@ -82,6 +82,8 @@ await runChecks({ projectId, baseUrl, port: 9337, shotPrefix: 'gm-tetris' }, asy
|
||||
|
||||
/** The variables of the Tetris behavior, live from the running world. */
|
||||
const vars = async () => (await entity('ogame')).props;
|
||||
/** The falling piece as the game now keeps it: one bundle, not four loose numbers. */
|
||||
const piece = async () => (await vars()).piece;
|
||||
const filledCells = (grid) => grid.filter((c) => c !== EMPTY).length;
|
||||
const pieceCells = async () => {
|
||||
const w = await world();
|
||||
@@ -151,7 +153,7 @@ await runChecks({ projectId, baseUrl, port: 9337, shotPrefix: 'gm-tetris' }, asy
|
||||
check(
|
||||
'playing puts a piece into the well',
|
||||
spawned.hasPiece === true && cells.every((c) => c && c.props.enabled === true),
|
||||
JSON.stringify({ hasPiece: spawned.hasPiece, shape: spawned.shape, px: spawned.px }),
|
||||
JSON.stringify({ hasPiece: spawned.hasPiece, piece: spawned.piece }),
|
||||
);
|
||||
const start = await piecePosition();
|
||||
check(
|
||||
@@ -184,10 +186,10 @@ await runChecks({ projectId, baseUrl, port: 9337, shotPrefix: 'gm-tetris' }, asy
|
||||
back.every((c, i) => c.column === beforeMove[i].column),
|
||||
back.map((c) => c.column).join(','),
|
||||
);
|
||||
const beforeTurn = (await vars()).rot;
|
||||
const beforeTurn = (await piece()).rot;
|
||||
await press('ArrowUp');
|
||||
await sleep(150);
|
||||
const afterTurn = (await vars()).rot;
|
||||
const afterTurn = (await piece()).rot;
|
||||
check(
|
||||
'the up arrow turns the piece',
|
||||
afterTurn === (beforeTurn + 1) % 4,
|
||||
@@ -266,7 +268,7 @@ await runChecks({ projectId, baseUrl, port: 9337, shotPrefix: 'gm-tetris' }, asy
|
||||
over
|
||||
? JSON.stringify({
|
||||
mode,
|
||||
py: over.py,
|
||||
piece: over.piece,
|
||||
hasPiece: over.hasPiece,
|
||||
kind: over.kind,
|
||||
lines: over.lines,
|
||||
|
||||
Reference in new issue
Block a user