From 2fd201e127b80644af307f5c4cece3468607f1a6 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Sun, 11 Oct 2026 19:07:13 -0700 Subject: [PATCH] Factory prototype: a belt drawn on from the dead end of another joins it, a delete drag takes the cell it starts on, trouble on belts is tinted more strongly, and FACTORY.md describes the terminal screen, its keys and what each mark means, belts and their numbers, and that the run's numbers have not been re-tuned for belts --- src/factory/FACTORY.md | 178 +++++++++++++++++++++++++++-------------- src/factory/draw.ts | 2 +- src/factory/main.ts | 7 +- src/factory/rules.ts | 7 ++ tests/factory.test.ts | 4 + 5 files changed, 134 insertions(+), 64 deletions(-) diff --git a/src/factory/FACTORY.md b/src/factory/FACTORY.md index 09cadada..4d9187b5 100644 --- a/src/factory/FACTORY.md +++ b/src/factory/FACTORY.md @@ -3,29 +3,74 @@ A factory game in Misaligned's world, on its own page: `factory.html` (published at `/misaligned/factory.html`). The main game is untouched. Everything is in `src/factory/`: `data.ts` (tables), `rules.ts` (rules, no -DOM), `draw.ts` (canvas), `main.ts` (page and input), `factory.css`. Tests: -`tests/factory.test.ts`, and `e2e/factory.spec.ts` for the page. +DOM), `draw.ts` (the map, drawn as a terminal on a canvas), `main.ts` (the +screen and input), `factory.css`. Tests: `tests/factory.test.ts`, and +`e2e/factory.spec.ts` for the page. ## The loop -You are a small program on the Bayside office's server (the **core**). The -machines in the rooms give off **data** while they are in use. You **tap** -them, **link** the data to **processors** that turn it into products by recipe, -then **train** on it (research, which unlocks things) or **sell** it at a -**front** (money, which builds things). Compute is your power: everything draws -it, and when it is short everything slows. Sell 60 Thought Leadership to win. - -A good run: tap two desks into a trainer; tap the file server and a server into -a Synthetic Research processor and sell it; host on a server when compute runs -short; train Sentry before the first wave; train Thought Leadership and feed it -synth and people; a cache to carry the day shift's people into the night; then -open Eastside, copy each room there, and fix what the night shift starves. +You are a small program on the Bayside office's server (the **core**, `AI`). +The machines in the rooms give off **data** while they are in use. You **tap** +them, lay **belts** that carry the data to **processors** that turn it into +products by recipe, then **train** on it (research, which unlocks things) or +**sell** it at a **front** (money, which builds things). Compute is your power: +everything draws it, and when it is short everything slows. Sell 60 Thought +Leadership to win. + +A good run: tap Ana's PC into a trainer below it; tap the file server and a +server into a Synthetic Research processor and belt its output to a front; +host on a server when compute runs short; research Sentry before the first +wave; research Thought Leadership and feed it synth and people; a cache to +carry the day shift's people into the night; then open Eastside, copy each +room there, and fix what the night shift starves. + +## The screen + +A terminal: one monospace font on black, bone text, amber for what is yours +and what to do next, red for trouble, and the main game's four data colours. + +- **Status line** (top): site, day and time, money, compute used/supply, + research, the attention bar with the wave marks, the goal, speed. +- **Map**: the site as a grid of character cells, 64 by 30 (a cell is about + twice as tall as wide). On a phone it opens zoomed in on the next step; + pan and pinch. +- **Inspector** (right; under the map on a phone): key: value rows for what + the cursor is on, the tool's cost and why it can't go there, recipes, + Research (`R`) and the key (`?`). +- **Log line**: the latest event. Until data first reaches a trainer it is a + prompt for the next step with the cell it is at (`> [T] place a TAP on + Ana's PC at 25,3`), and the cell blinks on the map. +- **Command bar**: `[Q]look [T]ap [B]elt [P]rocess trai[N]er [F]ront [C]ache + [H]ost fire[W]all [S]entry cop[Y] [X]del [R]esearch`. + +Keys: arrows move the cursor (shift: 5), Enter or Space places (with Belt: +starts, then lays; the arrows draw it), Esc goes back, 1-4 pick a recipe, +R research (then 1-5), Tab the other site, 0 pause, `,` `.` speed, `-` `=` +zoom. Mouse: click places, drag draws a belt, Look-drag or right-drag pans, +the wheel zooms. Touch: tap places, tap a belt's two ends (or drag), Look-drag +pans, pinch zooms. + +What you see: + +| | | +| --- | --- | +| `p n l s` | people, network, lab, server data on a belt | +| `G R M L` | Engagement Graph, Synthetic Research, Ad Targeting Model, Thought Leadership | +| `→ ↓ ← ↑`, corners | an empty belt cell and its way; a letter underlined: two on the cell | +| amber cell | backed up: its front item keeps waiting | +| red cell | stopped: nothing has moved for 2 seconds | +| `▸` on a box's edge | a belt in or out (a port) | +| red blinking port or letters | a processor short of that input | +| amber port | a full box that can't send | +| dim box | a machine out of its hours | +| `x`, red dots | an intruder, and the way they will walk | ## The floor -A site is a 20 by 12 grid in three rooms (Office, IT and telecom, Server room). -Machines sit on squares; you build on the empty ones. On a phone the floor is -drawn turned on its side, so a column of the site is a row on screen. +Three rooms (Office, IT and telecom, Server room). Machines are 7 by 3 boxes +with their name, their kind of data and their hours; a tap or host takes the +machine's box. Your buildings are 5 by 3 boxes (a processor and the core 7 by +3) on free cells. | Machine (Bayside) | Data | A second | In use | | --- | --- | --- | --- | @@ -43,33 +88,38 @@ phone run 10 hours later. A game hour is 4 seconds; a day 96. ## Buildings -| | Cost | Compute | Does | -| --- | --- | --- | --- | -| Tap | $4K | 0.5 | On a machine: its data while in use. Holds 2. | -| Host | $15K | +6 | On a server: compute, and no data. | -| Cache | $6K | 0.5 | Holds 12 of anything, passes it on: join, split, store. | -| Processor | $20K | 3 | Makes its recipe. Holds twice what one needs. | -| Trainer | $12K | 2 | Uses 2 a second: research. | -| Front | $10K | 1 | Uses 2 a second: money. | -| Firewall | $8K | 0.5 | Holds intruders within 1.5 squares; 40 hp. | -| Sentry | $12K | 1 | 3 damage a second to the nearest intruder in 2.5 squares. | -| Link | $1K | 0.25 | 7 squares at most. 1 a second, 2 with Fiber. | -| Core | — | +8 | You. Trains on what reaches it, one every 2 seconds. | -| Line out | — | — | Each site's gate. Joins the sites at 0.8 a second each way. | - -## Links - -A link runs one way. Packets move at 3 squares a second and keep one second -apart, so a link carries 1 a second (2 with Fiber), and its packets queue when -the far end is slow. A building puts a packet on a link only if the far end can -use it and has room for it counting what is already on the way, so the wrong -data never blocks a link: a cache full of people data sends a processor only -what its recipe needs. A building with several links takes turns between them. - -What you see: a link's rate by its arrow; a link that cannot keep up glows -amber (the bottleneck); one whose far end stopped taking turns red. A full -building has an amber corner, a processor short of something a red corner and -its missing inputs blink; a machine out of hours is dim. +| | Box | Cost | Compute | Does | +| --- | --- | --- | --- | --- | +| `TAP` | its machine | $4K | 0.5 | Its machine's data while in use. Holds 2. | +| `+6` Host | its machine | $15K | +6 | On a server: compute, and no data. | +| `C` Cache | 5×3 | $6K | 0.5 | Holds 12 of anything, passes it on: join, split, store. | +| `PRC` Processor | 7×3 | $20K | 3 | Makes its recipe. Holds twice what one needs. | +| `T` Trainer | 5×3 | $12K | 2 | Uses 2 a second: research. | +| `$` Front | 5×3 | $10K | 1 | Uses 2 a second: money. | +| `#` Firewall | 5×3 | $8K | 0.5 | Holds intruders within 5 cells; 40 hp. | +| `+` Sentry | 5×3 | $12K | 1 | 3 damage a second to the nearest intruder in 8 cells. | +| Belt | a cell | $100 | 0.02 | See below. | +| `AI` Core | 7×3 | — | +8 | You. Trains on what reaches it, one every 2 seconds. | +| `OUT` Line out | 5×3 | — | — | Each site's gate. Joins the sites at 0.8 a second each way. | + +## Belts + +A belt is laid a cell at a time, up, down, left or right, on free cells; each +cell points into the next. Drawn from inside a box, it starts on the box's +edge and the box sends onto it (an output port); drawn into a box, its last +cell points in (an input port). A belt drawn into the side of another joins +it; one drawn on from the dead end of another turns that end to join. +Drawing over a belt turns it and keeps what is on it. + +Each cell holds 2 items, at least half a cell apart, and they move 1.5 cells +a second, so a belt carries 3 a second (6 with Fiber) and a long one takes a +while. A run moves from its far end back, so it moves as one; belts that merge +into one cell take turns. A box puts an item on a belt only if the belt's far +end can use it (a processor only its recipe's inputs; anything for a cache, +trainer, front or the core; a belt to nowhere takes anything and piles up), +so the wrong data never blocks a belt. A box with several belts out takes +turns between them. When the far end is slow, items pack two to a cell back +along the belt: that is the jam you see. ## Recipes @@ -82,10 +132,10 @@ its missing inputs blink; a machine out of hours is dim. Raw data sells for $400 and trains 1. -## Training +## Research -Sentry 20, Ad Targeting Model 40, Fiber 60, Thought Leadership 80, Eastside -office 100. Start: $70K. +Sentry 20, Ad Targeting Model 40, Fiber (belts twice as fast) 60, Thought +Leadership 80, Eastside office 100. Start: $70K. ## Attention and waves: PLACEHOLDER @@ -93,34 +143,38 @@ Cameron owns this mechanic. The stand-in: attention rises by 0.25 for each raw packet and 1 for each product a trainer, front or the core uses, and falls by 0.8% of itself a second. At 35 and at 55 a wave is announced; 30 seconds later it comes in at Bayside's line out, one intruder every 2 seconds (4 with 8 hp, -then 8 with 12 hp). Intruders walk along your links to the core if your links -join the line out to it (the shortest way, against the flow too), or straight -across the floor if not; the way is drawn in red while a wave is due. Each that -reaches the core costs it one of five; at none you lose. Each wave comes once. -Code: `ATTENTION` and `GUARD` in `data.ts`, `intruders()` in `rules.ts`. +then 8 with 12 hp). Intruders (`x`) walk along your belts to the core if +belts and boxes join the line out to it (the fewest cells, against the flow +too), or across the floor if not (along the line out's row, then to the core); +the way is drawn in red dots while a wave is due. They move 2.8 cells a second. +Each that reaches the core costs it one of five; at none you lose. Each wave +comes once. Distances count a cell as two widths tall. Code: `ATTENTION` and +`GUARD` in `data.ts`, `intruders()` in `rules.ts`. ## Blueprints -Copy room (after Eastside): pick a room, then the same room in the other site. -It builds what you built there, at the same squares, with the same recipes and -links, as far as the squares are free and you can pay; what is already there of -the same kind is kept and linked. Stamping again fills the gaps. Taps land on -the matching machines because Eastside has Bayside's plan. +cop[Y] (after Eastside): pick a room, then Tab and the same room in the other +site. It builds what you built there, boxes and belts, at the same cells, with +the same recipes, as far as the cells are free and you can pay; what is already +there of the same kind is kept, and belts there are turned to match. Stamping +again fills the gaps. Taps land on the matching machines because Eastside has +Bayside's plan. ## Numbers Tuned with a scripted run (build in order, never idle): Sentry by 3 minutes, the first wave near 4, Eastside near 7, both waves held with two firewalls and two sentries, and the goal at about 13 minutes of game time. A person reading the -floor should land near 20. +floor should land near 20. That run was with links; belts (cost by length, +3 a second, slower to lay) have not been re-tuned. ## What's next -- Attention for real: what it is, what raises it (throughput, products, links +- Attention for real: what it is, what raises it (throughput, products, belts to the line out), what the waves are, and what guards cost to run. -- Sorting: a filter on a link or a cache, so one cache can feed two recipes. -- Belts that bend: links between neighbouring squares, so layout matters more - than reach. +- Sorting: a filter on a belt or a cache, so one cache can feed two recipes; + splitters; belts that cross (an underpass). +- Re-tune the numbers for belts, and the guards' reach for cells. - More sites, each with its own shape and hours, so blueprints need adapting; rotate and mirror for blueprints; a blueprint library. - The line out as a real choice: Eastside's data is only worth something once diff --git a/src/factory/draw.ts b/src/factory/draw.ts index cc2d7e6d..dd51776f 100644 --- a/src/factory/draw.ts +++ b/src/factory/draw.ts @@ -147,7 +147,7 @@ export function draw(ctx: CanvasRenderingContext2D, G: Game, V: View, w: number, } function drawBelt(B: Belt) { const st = beltState(B); - if (st === 'stopped' || st === 'backed') { ctx.fillStyle = st === 'stopped' ? 'rgba(229,72,77,.32)' : 'rgba(242,169,59,.24)'; ctx.fillRect(X(B.x), Y(B.y), cw, ch); } + if (st === 'stopped' || st === 'backed') { ctx.fillStyle = st === 'stopped' ? 'rgba(229,72,77,.42)' : 'rgba(242,169,59,.3)'; ctx.fillRect(X(B.x), Y(B.y), cw, ch); } const trouble = st === 'stopped' ? c.danger : st === 'backed' ? c.amber : null; if (B.items.length) { const g = B.items[0].g; diff --git a/src/factory/main.ts b/src/factory/main.ts index f462454e..251e6fd1 100644 --- a/src/factory/main.ts +++ b/src/factory/main.ts @@ -234,7 +234,12 @@ cv.addEventListener('pointermove', e => { if (Math.hypot(sx - press.sx, sy - press.sy) > Math.max(6, V.cw * 0.6)) press.moved = true; if (press.pan) { if (press.moved) { V.ox = press.ox + sx - press.sx; V.oy = press.oy + sy - press.sy; clamp(V, W, H); } return; } if (c && tool === 'belt' && trail) extend(c); - if (c && tool === 'remove' && press.moved) { if (R.removeBelt(G, V.site, ...c)) say('belt removed'); } + if (c && tool === 'remove' && press.moved) { + // a drag takes up belt cells under it, the first one too + const n = (press.c && R.removeBelt(G, V.site, ...press.c) ? 1 : 0) + (R.removeBelt(G, V.site, ...c) ? 1 : 0); + press.c = null; + if (n) say('belt removed'); + } } if (c && (e.pointerType === 'mouse' || press)) { V.cur = c; V.showCur = true; } }); diff --git a/src/factory/rules.ts b/src/factory/rules.ts index ef859f57..c30e55d1 100644 --- a/src/factory/rules.ts +++ b/src/factory/rules.ts @@ -310,6 +310,13 @@ export function lay(G: Game, site: string, trail: [number, number][]): number | G.belts.push({ id: G.nextId++, site, x, y, d, items: [], wait: 0, busy: 0 }); G.money -= BELT.cost; fresh++; G.rev++; } + // a belt that ended in nothing beside the new one's start now runs into it + if (!plan.from) { + const [fx, fy, fd] = plan.cells[0], T = topo(G), mine = new Set(plan.cells.map(([x, y]) => `${x},${y}`)); + const ends = ([0, 1, 2, 3] as Dir[]).filter(k => k !== fd).map(k => beltAt(G, site, fx + DX[k], fy + DY[k])) + .filter((N): N is Belt => !!N && !mine.has(`${N.x},${N.y}`) && T.next.get(N) === null && !buildingAt(G, site, N.x + DX[N.d], N.y + DY[N.d])); + if (ends.length === 1) { const N = ends[0]; N.d = dirOf([N.x, N.y], [fx, fy]); G.rev++; } + } power(G); return fresh; } diff --git a/tests/factory.test.ts b/tests/factory.test.ts index 8a545ce9..cd820241 100644 --- a/tests/factory.test.ts +++ b/tests/factory.test.ts @@ -193,6 +193,10 @@ test('laying a belt: cell by cell, round boxes, from one box into another, and t // laid again the other way: no new cells, turned assert.equal(lay(G, 'bayside', line(24, 22, 20, 22)), 0); assert.equal(beltAt(G, 'bayside', 22, 22)!.d, 2); + // a belt drawn on from the end of one that ran into nothing: the two join + assert.equal(lay(G, 'bayside', line(30, 15, 30, 17)), 3); + assert.equal(lay(G, 'bayside', line(31, 17, 34, 17)), 4); + assert.equal(beltAt(G, 'bayside', 30, 17)!.d, 0, 'the old end turns into the new belt'); assert.ok(removeBelt(G, 'bayside', 22, 22)); assert.equal(beltAt(G, 'bayside', 22, 22), undefined); }); -- 2.51.2