Skip to content

Three kinds of list

.map() does one of three things, chosen by the type of what you map over. The build lays out an interface’s tree before the game runs, so how many rows a list has matters then, not only what they hold.

src/shop.ts2
const TABS = ['Buy', 'Sell', 'Info'];

Mapping over this writes the three tabs out, as if by hand. Each copy has its own constant values, so a row with gap={4} can add up the widths, and each entry may carry a reference the build resolves, such as an item:

src/shop.ts2
const STOCK = [
{ item: Items.BRONZE_DAGGER, name: 'Bronze dagger', price: 12, stock: 30 },
{ item: Items.IRON_SCIMITAR, name: 'Iron scimitar', price: 154, stock: 8 },
…
];

Unrolling is the only way to read a different game variable per entry. A plain array literal, a plain object and an enum are all constant.

src/lists.ts2
{PRAYERS.map((prayer, i) => (
<PrayerCell prayer={prayer} unlocked={prayer.level <= level}
x={(i % COLUMNS) * CELL} y={(i / COLUMNS) * CELL} />
))}

PRAYERS is declared with listOf, packed into the game’s data by the build, and read a row at a time while the interface runs; a table the game ships is the same. The loop produces as many rows as the data has, with fields typed by the declaration, but no per-entry positions and no per-entry variable references. .scan() carries a running total through it, for rows of uneven height.

src/inventory.ts2
{inventory.map((item, slot) => (
<Slot item={item} count={inventory.count(slot)} slot={slot} />
))}

The game fixes a container’s size, twenty-eight slots for the backpack, and the loop visits every slot whether or not it holds anything. Row k always lands at the same place, so when slot 6 empties, slot 7 stays where it was.

The build sets aside room for the longest a list can be, and takes the number from the reference: a declared collection has its entries, a game table’s reference is generated from the data, and an inventory’s carries its slot count.

src/lists.ts2
{PRAYERS
.filter((prayer) => prayer.level <= STARTING_LEVEL)
.map((prayer, i) => (
<sprite x={92 + i * 20} y="center" w={18} h={18} sprite={prayer.icon} />
))}

There is nowhere to keep an intermediate list, so filter becomes a test inside the same loop. The index handed to .map() counts the rows that passed, and a filtered list closes up. Rows decided while the game runs cannot be found by an update, so the component holding this loop is rebuilt when a value it reads changes; a test inside the row keeps it updated in place.

Nothing compares a list with its previous version. A list occupies a fixed span of the tree and the nth entry is the nth slot, so an entry’s identity is its position. A filter changes which entries exist and shifts everything after them; a condition inside the row changes what is drawn at a position that stays put. A bank grid uses the second.