Give the graph a loop, and build Tetris with it
A Tetris example project needs to draw 200 board cells and scan rows for a full line. Neither is expressible without iteration, so the node system gets a Repeat node plus the missing text side: Substring, Find text and To number. The interpreter drops its data cache once per round -- otherwise every round would recompute with the first round's values. Object by name takes its name as an input now, not just as a setting; addressing "c" + index is impossible otherwise. An unconnected input reads the stored setting, so saved projects are untouched. The board itself is a 200-character text, "." for empty. That makes both operations of Tetris a single expression instead of a loop, and each frame attempts exactly one move -- rotate beats sideways beats fall -- so the fit check exists once in the whole graph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c4148dc1b
commit
4f78c7518c
16 files changed
+1479
-25
No files matched your search
@@ -18,6 +18,8 @@ interface Case {
|
||||
outputs?: Record<string, Value>;
|
||||
/** Expected exec continuation; `null` asserts the flow ends here. */
|
||||
next?: string | string[] | null;
|
||||
/** Expected loop request; `null` asserts the node does not loop. */
|
||||
repeat?: RunResult['repeat'] | null;
|
||||
/** Assertions on what the node did to the context or to the handles it was given. */
|
||||
after?(ctx: FakeContext): void;
|
||||
}
|
||||
@@ -43,6 +45,19 @@ const CASES: Record<string, Case[]> = {
|
||||
{ name: '"false" as text is false', inputs: { condition: 'false' }, next: 'else' },
|
||||
],
|
||||
Sequence: [{ name: 'runs all three outputs in order', next: ['a', 'b', 'c'] }],
|
||||
Repeat: [
|
||||
{
|
||||
name: 'asks for as many body rounds as it was given, then continues at done',
|
||||
inputs: { times: 3 },
|
||||
repeat: { port: 'body', index: 'index', times: 3 },
|
||||
next: 'done',
|
||||
},
|
||||
{
|
||||
name: 'a fractional count is cut and a negative one runs nothing',
|
||||
inputs: { times: -2.7 },
|
||||
repeat: { port: 'body', index: 'index', times: 0 },
|
||||
},
|
||||
],
|
||||
|
||||
// --- variables -------------------------------------------------------------------------------
|
||||
|
||||
@@ -84,11 +99,11 @@ const CASES: Record<string, Case[]> = {
|
||||
ObjectByName: [
|
||||
{
|
||||
name: 'asks the context for the name',
|
||||
params: { name: 'Player' },
|
||||
inputs: { name: 'Player' },
|
||||
context: { findByName: (name) => (name === 'Player' ? fakeEntity('Player') : null) },
|
||||
outputs: { object: fakeEntity('Player') },
|
||||
},
|
||||
{ name: 'an unknown name is null', params: { name: 'Ghost' }, outputs: { object: null } },
|
||||
{ name: 'an unknown name is null', inputs: { name: 'Ghost' }, outputs: { object: null } },
|
||||
],
|
||||
NameOf: [
|
||||
{ name: 'reads the name', inputs: { object: fakeEntity('Coin') }, outputs: { name: 'Coin' } },
|
||||
@@ -229,6 +244,39 @@ const CASES: Record<string, Case[]> = {
|
||||
{ name: 'text passes through unformatted', inputs: { value: 'x', decimals: 2 }, outputs: { text: 'x' } },
|
||||
{ name: 'decimals are clamped to 0…10', inputs: { value: 1, decimals: -5 }, outputs: { text: '1' } },
|
||||
],
|
||||
ToNumber: [
|
||||
{ name: 'reads a text as a number', inputs: { value: '42' }, outputs: { result: 42 } },
|
||||
{ name: 'a word is 0', inputs: { value: 'twelve' }, outputs: { result: 0 } },
|
||||
],
|
||||
Substring: [
|
||||
{
|
||||
name: 'takes count characters from start',
|
||||
inputs: { text: 'abcdef', start: 2, count: 3 },
|
||||
outputs: { text: 'cde' },
|
||||
},
|
||||
{
|
||||
name: 'a negative count reaches to the end',
|
||||
inputs: { text: 'abcdef', start: 4, count: -1 },
|
||||
outputs: { text: 'ef' },
|
||||
},
|
||||
{
|
||||
name: 'asking past the end gives what is there',
|
||||
inputs: { text: 'ab', start: 1, count: 10 },
|
||||
outputs: { text: 'b' },
|
||||
},
|
||||
],
|
||||
FindText: [
|
||||
{
|
||||
name: 'finds where the search first appears',
|
||||
inputs: { text: '..#.#', search: '#' },
|
||||
outputs: { index: 2 },
|
||||
},
|
||||
{
|
||||
name: 'text that does not appear is -1',
|
||||
inputs: { text: '....', search: '#' },
|
||||
outputs: { index: -1 },
|
||||
},
|
||||
],
|
||||
Join: [
|
||||
{ name: 'sticks values together as text', inputs: { a: 'Coins: ', b: 3 }, outputs: { text: 'Coins: 3' } },
|
||||
{
|
||||
@@ -541,6 +589,7 @@ for (const [type, cases] of Object.entries(CASES)) {
|
||||
const { result, ctx } = run(type, c);
|
||||
if (c.outputs) expect(result.outputs ?? {}).toEqual(c.outputs);
|
||||
if (c.next !== undefined) expect(result.next ?? null).toEqual(c.next);
|
||||
if (c.repeat !== undefined) expect(result.repeat ?? null).toEqual(c.repeat);
|
||||
c.after?.(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user