Let a variable hold a list

A variable could only be a number, a text or a yes/no, so every set of values had
to be smuggled through a string - which is where Tetris got its unreadable
"03066326048763714" colours from. A list is now a value of its own, with an item
kind (number, text, yes/no, colour) that decides how the inspector reads the one
comma-separated line it is typed as, and eight nodes to work with: List, Fill,
Count, Item at, Set item, Sub list, Join lists, Index of.

Two traps came with it. An array is typeof 'object', so every place that told a
value from an entity had to learn the difference, or a list would have been
passed around as a game object. And a value was compared by identity in three
places - with lists that marks everything as an overridden property after each
reload, so those comparisons now compare contents.

There is no list of objects: a list is written to the project file and an object
is a live handle that no reload survives. A set of objects stays a list of names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-09 21:26:31 +02:00
1 parent c6f907b4e0
commit a9c3111377
25 files changed
+469 -25

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
import type { NodeDef } from '../../NodeRegistry';
import { asList } from '../../values';
import { list } from '../shared';
export const Concat: NodeDef = {
type: 'Concat',
label: 'Join lists',
category: 'data',
description: 'Two lists one after the other. A single value counts as a list of one.',
inputs: [list('a'), list('b')],
outputs: [list('result')],
params: [],
run: (_ctx, inputs) => ({ outputs: { result: [...asList(inputs.a), ...asList(inputs.b)] } }),
};
+14
View File
@@ -0,0 +1,14 @@
import type { NodeDef } from '../../NodeRegistry';
import { asList } from '../../values';
import { list, num } from '../shared';
export const Count: NodeDef = {
type: 'Count',
label: 'Count',
category: 'data',
description: 'How many items a list holds.',
inputs: [list('list')],
outputs: [num('count')],
params: [],
run: (_ctx, inputs) => ({ outputs: { count: asList(inputs.list).length } }),
};
+20
View File
@@ -0,0 +1,20 @@
import type { NodeDef } from '../../NodeRegistry';
import { asListItem, asNumber } from '../../values';
import { any, list, num, withDefault } from '../shared';
/** Same order of magnitude as the interpreter's step limit – a typo in `count` must not eat memory. */
const MAX_ITEMS = 100_000;
export const Fill: NodeDef = {
type: 'Fill',
label: 'Fill',
category: 'data',
description: 'A new list of the same value repeated – how an empty board is made.',
inputs: [any('value'), withDefault(num('count'), 1)],
outputs: [list('result')],
params: [],
run: (_ctx, inputs) => {
const count = Math.min(MAX_ITEMS, Math.max(0, Math.floor(asNumber(inputs.count))));
return { outputs: { result: new Array(count).fill(asListItem(inputs.value)) } };
},
};
+16
View File
@@ -0,0 +1,16 @@
import type { NodeDef } from '../../NodeRegistry';
import { asList, asListItem } from '../../values';
import { any, list, num } from '../shared';
export const IndexOf: NodeDef = {
type: 'IndexOf',
label: 'Index of',
category: 'data',
description: 'Where a value sits in a list, or -1 if it is not in there – how "is this row full" is asked.',
inputs: [list('list'), any('value')],
outputs: [num('index')],
params: [],
run: (_ctx, inputs) => ({
outputs: { index: asList(inputs.list).indexOf(asListItem(inputs.value)) },
}),
};
+18
View File
@@ -0,0 +1,18 @@
import type { NodeDef } from '../../NodeRegistry';
import { asList, asNumber } from '../../values';
import { any, list, num } from '../shared';
export const ItemAt: NodeDef = {
type: 'ItemAt',
label: 'Item at',
category: 'data',
description: 'The item at an index, counting from 0. Out of range gives 0.',
inputs: [list('list'), num('index')],
outputs: [any('item')],
run: (_ctx, inputs) => {
const items = asList(inputs.list);
const i = Math.floor(asNumber(inputs.index));
return { outputs: { item: items[i] ?? 0 } };
},
params: [],
};
+14
View File
@@ -0,0 +1,14 @@
import type { NodeDef } from '../../NodeRegistry';
import { asList } from '../../values';
import { list } from '../shared';
export const ListLiteral: NodeDef = {
type: 'List',
label: 'List',
category: 'data',
description: 'A constant list, typed as one comma-separated line.',
inputs: [],
outputs: [list('value')],
params: [{ key: 'value', label: 'Value', kind: 'list', default: [], items: 'number' }],
run: (_ctx, _inputs, params) => ({ outputs: { value: asList(params.value) } }),
};
+21
View File
@@ -0,0 +1,21 @@
import type { NodeDef } from '../../NodeRegistry';
import { asList, asListItem, asNumber } from '../../values';
import { any, list, num } from '../shared';
export const SetItem: NodeDef = {
type: 'SetItem',
label: 'Set item',
category: 'data',
description: 'The same list with one item replaced. The list that came in is left alone.',
inputs: [list('list'), num('index'), any('value')],
outputs: [list('result')],
run: (_ctx, inputs) => {
const items = asList(inputs.list);
const i = Math.floor(asNumber(inputs.index));
if (i < 0 || i >= items.length) return { outputs: { result: items } };
const next = [...items];
next[i] = asListItem(inputs.value);
return { outputs: { result: next } };
},
params: [],
};
+19
View File
@@ -0,0 +1,19 @@
import type { NodeDef } from '../../NodeRegistry';
import { asList, asNumber } from '../../values';
import { list, num, withDefault } from '../shared';
export const SubList: NodeDef = {
type: 'SubList',
label: 'Sub list',
category: 'data',
description: 'A part of a list: "count" items starting at "start".',
inputs: [list('list'), num('start'), withDefault(num('count'), 1)],
outputs: [list('result')],
params: [],
run: (_ctx, inputs) => {
const items = asList(inputs.list);
const start = Math.max(0, Math.floor(asNumber(inputs.start)));
const count = Math.max(0, Math.floor(asNumber(inputs.count)));
return { outputs: { result: items.slice(start, start + count) } };
},
};