From a8ff8981e772c629c501247f642efae07f756f6c Mon Sep 17 00:00:00 2001 From: Jakub Duras Date: Mon, 2 Nov 2026 21:47:03 +0100 Subject: [PATCH 01/16] Reimagine Chips and Code as the playable Signal Isles --- README.md | 10 + cypress/e2e/about.cy.ts | 2 +- cypress/e2e/accessibility.cy.ts | 2 +- cypress/e2e/experiment.cy.ts | 4 +- cypress/e2e/game.cy.ts | 150 ++ cypress/e2e/home.cy.ts | 27 +- cypress/e2e/learn.cy.ts | 4 +- cypress/e2e/profile.cy.ts | 2 +- docs/signal-isles-art-direction.md | 41 + docs/signal-isles-content-audit.md | 56 + docs/signal-isles-game-design.md | 163 ++ src/lib/editor/hdl/parse.ts | 4 +- src/lib/editor/tst/parse.ts | 4 +- src/lib/game/BenchMachine.svelte | 254 ++ src/lib/game/FieldGuide.svelte | 1345 +++++++++++ src/lib/game/Game.svelte | 1289 ++++++++++ src/lib/game/MissionWorkbench.svelte | 2118 +++++++++++++++++ src/lib/game/WorldMap.svelte | 326 +++ src/lib/game/circuit.test.ts | 227 ++ src/lib/game/circuit.ts | 183 ++ src/lib/game/missions.ts | 220 ++ .../chips/CustomChip.test.ts | 44 + .../hardware-simulator/chips/CustomChip.ts | 12 +- src/routes/+page.svelte | 119 +- 24 files changed, 6467 insertions(+), 139 deletions(-) create mode 100644 cypress/e2e/game.cy.ts create mode 100644 docs/signal-isles-art-direction.md create mode 100644 docs/signal-isles-content-audit.md create mode 100644 docs/signal-isles-game-design.md create mode 100644 src/lib/game/BenchMachine.svelte create mode 100644 src/lib/game/FieldGuide.svelte create mode 100644 src/lib/game/Game.svelte create mode 100644 src/lib/game/MissionWorkbench.svelte create mode 100644 src/lib/game/WorldMap.svelte create mode 100644 src/lib/game/circuit.test.ts create mode 100644 src/lib/game/circuit.ts create mode 100644 src/lib/game/missions.ts diff --git a/README.md b/README.md index c6486f5..1d62d76 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,16 @@ ## Features +### Signal Isles: the playable redesign + +The home screen is now a miniature world restored by building real logic circuits. Seven stations cover the supplied NAND gate and the existing NOT, AND, OR, XOR, Mux and DMux exercises. Tap a signal source and a destination pin to connect them, or use the labeled connection controls. Test the complete truth table before restoring a station. Drafts, discoveries and completed inventions are stored in this browser; no account or database is required to play. + +The visual builder generates HDL and runs the existing hardware simulator. The field guide provides optional interactive explanations of Boolean expressions, composition, HDL, buses and multi-way selectors. The original reading and full hardware IDE remain available. Multi-bit circuits, arithmetic, memory and CPU construction are future work, not playable campaign stages. + +See the [game design](docs/signal-isles-game-design.md), [art direction](docs/signal-isles-art-direction.md), and [content audit](docs/signal-isles-content-audit.md) for the redesign, its relationship to the existing material, and the boundary between the current implementation and later ideas. + +The animation below shows the original hardware IDE, which remains accessible from the game footer. + Currently contains and fully supports the first week of Nand2Tetris. diff --git a/cypress/e2e/about.cy.ts b/cypress/e2e/about.cy.ts index e072f9c..804b38b 100644 --- a/cypress/e2e/about.cy.ts +++ b/cypress/e2e/about.cy.ts @@ -6,7 +6,7 @@ describe('About', () => { // TODO: Find a way to suppress dynamic ESM loading for Cypress tests cy.wait(1500); - cy.contains('nav a', 'About').click(); + cy.contains('footer a', 'About').click(); cy.contains('Chips and Code'); diff --git a/cypress/e2e/accessibility.cy.ts b/cypress/e2e/accessibility.cy.ts index 5f6740e..8c4aa4b 100644 --- a/cypress/e2e/accessibility.cy.ts +++ b/cypress/e2e/accessibility.cy.ts @@ -2,7 +2,7 @@ describe('Accessibility Statement', () => { it('Can be navigated to via Footer and contains everything important', () => { cy.visit('/'); - cy.contains('a', 'Accessibility Statement').click(); + cy.contains('footer a', 'Accessibility Statement').click(); cy.contains('h1', 'Accessibility'); cy.location('pathname').should('eq', '/accessibility-statement'); diff --git a/cypress/e2e/experiment.cy.ts b/cypress/e2e/experiment.cy.ts index 992d30b..7802e05 100644 --- a/cypress/e2e/experiment.cy.ts +++ b/cypress/e2e/experiment.cy.ts @@ -42,9 +42,7 @@ describe('Experiment - Hardware IDE', () => { // TODO: Find a way to suppress dynamic ESM loading for Cypress tests cy.wait(1500); - cy.contains('nav button', 'Experiment').click(); - - cy.contains('nav a', 'Hardware IDE').click(); + cy.contains('footer a', 'Open hardware IDE').click(); cy.findByLabelText('Experiment name').should('have.value', 'Untitled Experiment'); diff --git a/cypress/e2e/game.cy.ts b/cypress/e2e/game.cy.ts new file mode 100644 index 0000000..0fba625 --- /dev/null +++ b/cypress/e2e/game.cy.ts @@ -0,0 +1,150 @@ +function openNotWorkshop() { + cy.findByRole('region', { name: 'Stations, list view' }) + .findByRole('button', { name: /Not Beacon point/ }) + .click(); + cy.findByRole('button', { name: 'Enter workshop' }).click(); + cy.findByRole('heading', { name: 'Bring back the beacon' }).should('be.visible'); +} + +function buildInverter() { + cy.findByRole('button', { name: /^Nand\b/ }).click(); + cy.findByLabelText('Part 1 Nand input a').select('in'); + cy.findByLabelText('Part 1 Nand input b').select('in'); + cy.findByLabelText('Machine output out').select('g1'); +} + +function expectNoPageOverflow() { + cy.document().should((document) => { + const viewport = document.documentElement.clientWidth; + const pageWidth = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth); + expect(pageWidth, 'page content fits the viewport').to.be.at.most(viewport + 1); + }); +} + +describe('Signal Isles', () => { + beforeEach(() => { + cy.visit('/', { + onBeforeLoad(window) { + window.localStorage.clear(); + }, + }); + cy.contains('Progress saved on this device'); + }); + + it('Discovers NAND, remembers the restored station, and earns a working NOT chip', () => { + cy.findByRole('button', { name: 'Bring it to life' }).click(); + + // A wrong prediction cannot complete a case or unlock restoration. + cy.findByRole('button', { name: 'Predict 0' }).click(); + cy.contains('The NAND output is 1 here.'); + cy.findByRole('button', { name: 'Bring station online' }).should('not.exist'); + + const cases = [ + { a: 0, b: 0, output: 1 }, + { a: 0, b: 1, output: 1 }, + { a: 1, b: 0, output: 1 }, + { a: 1, b: 1, output: 0 }, + ]; + cases.forEach(({ a, b, output }, index) => { + cy.findByRole('button', { name: `Predict ${output}` }).click(); + cy.contains(`Exactly. a=${a}, b=${b} produces ${output}.`); + cy.findByRole('button', { name: `Try input a ${a}, b ${b}, discovered` }); + if (index < cases.length - 1) { + cy.findByRole('button', { name: 'Next undiscovered signal' }).click(); + cy.findByRole('button', { name: 'Bring station online' }).should('not.exist'); + } + }); + + cy.findByRole('button', { name: 'Bring station online' }).click(); + cy.contains('Nand verified. Your world remembers.'); + cy.contains('1 OF 7 SYSTEMS RESTORED'); + cy.reload(); + cy.contains('1 OF 7 SYSTEMS RESTORED'); + cy.findByRole('region', { name: 'Stations, list view' }) + .findByRole('button', { name: /Nand Arrival dock/ }) + .should('contain.text', '✓'); + + openNotWorkshop(); + buildInverter(); + cy.findByRole('button', { name: /Test circuit/ }).click(); + cy.contains('Signal confirmed.'); + cy.contains('2 of 2 combinations work.'); + cy.findByRole('button', { name: 'Bring station online' }).click(); + cy.contains('Not verified. Your world remembers.'); + cy.contains('2 OF 7 SYSTEMS RESTORED'); + + cy.findByRole('button', { name: /Your chips/ }).click(); + cy.contains('article', 'Bring back the beacon').within(() => { + cy.contains('VERIFIED IN YOUR WORKSHOP'); + cy.contains('Not'); + }); + cy.reload(); + cy.contains('2 OF 7 SYSTEMS RESTORED'); + }); + + it('Invalidates a passed test after a connection changes and restores the edited draft', () => { + openNotWorkshop(); + buildInverter(); + cy.findByRole('button', { name: /Test circuit/ }).click(); + cy.contains('2 of 2 combinations work.'); + cy.findByRole('button', { name: 'Bring station online' }).should('be.visible'); + + // A direct wire is a valid circuit, but it is not an inverter. + cy.findByLabelText('Machine output out').select('in'); + cy.contains('2 of 2 combinations work.').should('not.exist'); + cy.findByRole('button', { name: 'Bring station online' }).should('not.exist'); + cy.contains('Draft saved on this device'); + + cy.reload(); + openNotWorkshop(); + cy.findByLabelText('Part 1 Nand input a').should('have.value', 'in'); + cy.findByLabelText('Part 1 Nand input b').should('have.value', 'in'); + cy.findByLabelText('Machine output out').should('have.value', 'in'); + cy.findByRole('button', { name: 'Bring station online' }).should('not.exist'); + cy.findByRole('button', { name: /Test circuit/ }).click(); + cy.contains('0 of 2 combinations work.'); + cy.findAllByRole('button', { name: /Inspect case \d, needs work/ }).should('have.length', 2); + + cy.findByLabelText('Machine output out').select('g1'); + cy.contains('0 of 2 combinations work.').should('not.exist'); + cy.findByRole('button', { name: /Test circuit/ }).click(); + cy.contains('2 of 2 combinations work.'); + cy.findByRole('button', { name: 'Bring station online' }).should('be.visible'); + }); + + it('Wires directly on the board and can undo clearing the saved circuit', () => { + openNotWorkshop(); + cy.findByRole('button', { name: /^Nand\b/ }).click(); + cy.findByRole('button', { name: 'Select source input in' }).click(); + cy.findByRole('button', { name: 'Connect to part 1 Nand input a' }).click(); + cy.findByRole('button', { name: 'Connect to part 1 Nand input b' }).click(); + cy.findByRole('button', { name: 'Select source part 1 Nand output' }).click(); + cy.findByRole('button', { name: 'Connect machine output out' }).click(); + cy.findByRole('button', { name: /Test circuit/ }).click(); + cy.contains('2 of 2 combinations work.'); + cy.findByRole('button', { name: 'Clear circuit' }).click(); + cy.findByLabelText('Part 1 Nand input a').should('not.exist'); + cy.findByRole('button', { name: 'Undo clear' }).click(); + cy.findByLabelText('Part 1 Nand input a').should('have.value', 'in'); + cy.findByLabelText('Machine output out').should('have.value', 'g1'); + cy.findByRole('button', { name: /Test circuit/ }).click(); + cy.contains('2 of 2 combinations work.'); + cy.reload(); + openNotWorkshop(); + cy.findByLabelText('Machine output out').should('have.value', 'g1'); + }); + + [320, 1280].forEach((width) => { + it(`Keeps the world and workshop within a ${width}px viewport`, () => { + cy.viewport(width, 900); + expectNoPageOverflow(); + openNotWorkshop(); + expectNoPageOverflow(); + buildInverter(); + expectNoPageOverflow(); + cy.findByRole('button', { name: /Test circuit/ }).click(); + cy.contains('2 of 2 combinations work.'); + expectNoPageOverflow(); + }); + }); +}); diff --git a/cypress/e2e/home.cy.ts b/cypress/e2e/home.cy.ts index 650cf13..c8e5889 100644 --- a/cypress/e2e/home.cy.ts +++ b/cypress/e2e/home.cy.ts @@ -1,21 +1,22 @@ describe('Home', () => { - it('Contains welcome message and link to content', () => { + it('Introduces the Signal Isles and opens the first playable workshop', () => { cy.visit('/'); + cy.contains('Progress saved on this device'); cy.injectAxe(); - cy.contains('Wondering how computers work?'); - cy.contains('Find out by embarking on the journey of building your own computer'); - - // Unfortunately, images take a bit to load - cy.wait(1000); - cy.matchImage(); - // ... and screenshots cause the app to reload which may take some time - cy.wait(500); - + cy.findByRole('heading', { name: /A little logic\.\s*A living world\./ }).should('be.visible'); + cy.findByRole('region', { + name: 'Signal Isles world map. Select a station to open its mission.', + }).should('be.visible'); + cy.findByRole('region', { name: 'Stations, list view' }) + .findAllByRole('button') + .should('have.length', 7); + cy.contains('0 OF 7 SYSTEMS RESTORED'); cy.checkA11y(); - cy.contains('Start Learning').click(); - - cy.location('pathname').should('eq', '/learn/hardware/boolean-logic'); + cy.findByRole('button', { name: 'Bring it to life' }).click(); + cy.findByRole('heading', { name: 'Find the first signal' }).should('be.visible'); + cy.findByRole('region', { name: 'Discover NAND' }).should('be.visible'); + cy.location('pathname').should('eq', '/'); }); }); diff --git a/cypress/e2e/learn.cy.ts b/cypress/e2e/learn.cy.ts index 4876a2a..45708bd 100644 --- a/cypress/e2e/learn.cy.ts +++ b/cypress/e2e/learn.cy.ts @@ -6,9 +6,7 @@ describe('Learn', () => { // TODO: Find a way to suppress dynamic ESM loading for Cypress tests cy.wait(1500); - cy.contains('nav button', 'Learn').click(); - - cy.contains('nav a', 'Chip Hardware').click(); + cy.contains('footer a', 'Original reading').click(); cy.contains('Boolean Logic'); diff --git a/cypress/e2e/profile.cy.ts b/cypress/e2e/profile.cy.ts index d479c59..181e80d 100644 --- a/cypress/e2e/profile.cy.ts +++ b/cypress/e2e/profile.cy.ts @@ -6,7 +6,7 @@ describe('Profile', () => { // TODO: Find a way to suppress dynamic ESM loading for Cypress tests cy.wait(1500); - cy.findByLabelText('User account').click(); + cy.get('footer').findByLabelText('User account').click(); cy.contains('Hi, Anonym'); diff --git a/docs/signal-isles-art-direction.md b/docs/signal-isles-art-direction.md new file mode 100644 index 0000000..1ea3108 --- /dev/null +++ b/docs/signal-isles-art-direction.md @@ -0,0 +1,41 @@ +# The Signal Isles — visual direction + +A miniature archipelago of machines comes to life as the player learns to build logic from NAND. The map foregrounds places and their state. Existing explanations remain available as a field guide at the workbench. + +## Implemented station map + +The map uses an exact `1200 × 720` SVG coordinate system. Each station has a real HTML button over separately rendered SVG art. The callback returns the stable curriculum identifier; `completed` changes light and power states, while `selected` changes the selected location and label. + +| Order | ID | Label | Place / interaction idea | Center | +| --- | --- | --- | --- | --- | +| 01 | nand | The spark | Origin generator; discover NAND by manipulating inputs | 178, 500 | +| 02 | not | Flip the signal | Inverter tower; flip a signal using NAND | 354, 340 | +| 03 | and | Better together | Paired generators; activate only when both inputs are high | 536, 510 | +| 04 | or | Another way | Radio receiver; receive either input | 716, 355 | +| 05 | xor | One or the other | Paired observatory; distinguish exactly one active input | 1002, 505 | +| 06 | mux | Choose a path | Router workshop; use a selector to route one input | 988, 210 | +| 07 | dmux | Send it onward | Signal depot; route an input to a chosen output | 675, 145 | + +The beacon at 219,177 is a non-playable landmark, not an extra curriculum item. Small ferries, buoys, vegetation, copper traces, contour lines, and land strata provide art density without claiming extra playable content. + +## Materials and color + +Ink-blue sea `#10212d`, raised cream instruments `#f4eedf`, dark instrument text `#172839`, copper `#ee9255`, mint power `#7bd9b5`, warm muted land, and subdued gray-green inactive structures. Mint is meaningful: it indicates a live signal or restored place. Selection uses warm cream and copper. Functional circuits should use precise geometry and verified state rather than painted or generated connections. + +Use readable sans-serif interface type, with monospace confined to short instrument readings, numbering, and code. Pair the miniature world with generous typography in the surrounding shell. Avoid an XP dashboard, lesson-card wall, or constant glowing sci-fi borders. + +## Desktop and mobile + +On desktop, let the map dominate. The shell should put a concise current objective beside or over its edge and expose one obvious action into the selected workshop. The map is intentionally compatible with a right-hand mission panel, but labels remain inside the art rather than becoming a list of course cards. + +At narrow widths, the map keeps its readable miniature scale and scrolls horizontally. A compact set of real station buttons beneath it provides immediate access to every location without requiring spatial panning. Portrait workbenches should use a full-width machine, bottom tool tray, reachable Test action, and instrument details in a sheet. Drag interactions must also support select-then-tap and keyboard controls. + +## Motion and sound + +Completed locations illuminate and power their outbound cable. Cable animation follows its actual path. Selection changes local emphasis; controls move by only a couple of pixels. Respect `prefers-reduced-motion`; the map already stops current animation and transitions. Restoring a location can add a brief local power-on sequence in the owning game shell. Do not animate the whole map continuously or let decorative movement compete with a task. + +Sound should be optional and remembered: soft contact clicks, relay ticks, and a short restoration tone. The map itself emits no audio. + +## Scope + +This art establishes a visual wrapper for the verified first-week gate sequence. It does not imply that later multi-bit, arithmetic, memory, or computer-building exercises already exist. Progress must be derived from actual game state; the component never completes or unlocks a station itself. diff --git a/docs/signal-isles-content-audit.md b/docs/signal-isles-content-audit.md new file mode 100644 index 0000000..4add2a3 --- /dev/null +++ b/docs/signal-isles-content-audit.md @@ -0,0 +1,56 @@ +# Chipsandcode: verified content and game adaptation audit + +Inspected the current `main` branch of [durasj/chipsandcode](https://github.com/durasj/chipsandcode) through the GitHub connector on 2 November 2026. This is a source audit, not a claim that browser or simulator execution was tested. + +## Actual shipped scope + +The README describes a browser-based, no-prerequisites, free experience and claims the first week of Nand2Tetris. The actual project page contains a supplied NAND demonstration, **six embedded construction exercises**, and descriptions of multi-bit and multi-way gates. Treat those as distinct levels of support. + +| Existing source | Verified contents | Appropriate game treatment | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `src/routes/learn/hardware/boolean-logic/intro.md` | Build gates from NAND; later processing/storage chapters described as coming soon | Brief opening premise: restore a machine from one primitive part | +| `src/routes/learn/hardware/boolean-logic/background.md` | Binary values, truth tables, expressions, canonical representation, gate abstraction/composition, physical construction context, HDL and simulation/testing; worked XOR example | Contextual field-guide entries; inspect signals and truth-table evidence; optional algebra/HDL explanation | +| `src/routes/learn/hardware/boolean-logic/project.md` | NAND demo; NOT, AND, OR, XOR, Mux, DMux exercises; descriptive bus/multi-way extensions; perspective on alternative primitives and optimization | Six construction missions after a short NAND discovery, then explicitly marked expansion plans | +| `src/lib/hardware-simulator/examples/BuiltinNand.ts` | Supplied NAND with test and comparison fixtures | Onboarding device that players probe; do not award construction mastery for merely toggling it | +| `src/lib/hardware-simulator/examples/ExerciseNot.ts` | Empty NOT implementation; 2 input cases | Invert a daylight signal so a beacon comes on in darkness | +| `src/lib/hardware-simulator/examples/ExerciseAnd.ts` | Empty AND implementation; all 4 cases | Dual-switch interlock: lift moves only with both safety signals | +| `src/lib/hardware-simulator/examples/ExerciseOr.ts` | Empty OR implementation; all 4 cases | Pump powered by either of two sources | +| `src/lib/hardware-simulator/examples/ExerciseXor.ts` | Empty XOR implementation; all 4 cases | Difference detector: warning light when exactly one sensor is active | +| `src/lib/hardware-simulator/examples/ExerciseMux.ts` | Empty Mux implementation; all 8 cases | Receiver selects one of two input signals; selector routes data, rather than acting as an enable | +| `src/lib/hardware-simulator/examples/ExerciseDMux.ts` | Empty DMux implementation; 4 cases and 2 expected output pins | Router sends an input signal to one of two destinations; inactive destination must remain 0 | + +## Existing simulation capabilities and limits + +The existing simulator is a valuable reusable implementation. `ChipFactory.ts` builds composite chips from HDL AST parts and internal wires; `CustomChip.ts` runs a network of those parts. Real implementations exist for `Nand`, `Not`, `And`, `Or`, and `Xor` in `src/lib/hardware-simulator/chips/builtin/`. + +**Mux and DMux native classes are placeholders**, both extending `NotImplemented`. That does not prevent constructing their composite HDL from working NOT/AND/OR/NAND parts, which is what their exercise fixtures request. The new game must not instantiate a built-in Mux/DMux to stand in for a player's completed construction without implementing it or using that construction. + +**The player's reusable chip library is not implemented in the factory.** `ChipFactory.LOADED` is an unused empty object; `fromDefined` resolves only `ChipFactory.BUILTIN`. The game fantasy of earning parts and reusing the actual custom designs needs registration with dependency resolution or equivalent graph flattening. A badge or inventory label alone does not provide this functionality. Restricting available primitives by mission also needs explicit enforcement because the current factory exposes its built-ins independently of progression. + +**Multi-bit and later computer stages are future work.** `CustomChip.ts` explicitly lists support for words and clocked behavior as TODOs. `Not16Chip.ts` is a `NotImplemented` class. Descriptive bus and multi-way content can be covered through a field-guide visual now, but full construction missions require simulator and fixture work. Do not present ALU, RAM, CPU, assembler, or later weeks as playable based on README aspirations. + +**Animation is an interpretation of combinational evaluation.** The existing runtime uses breadth-first propagation and a 9,999 iteration loop guard, not modeled physical electrical delay. Show an educational signal trace, not claims about measured voltage, timing, energy, or real current. A new visual graph should reject cycles, missing connections and unsupported parts and keep failures recoverable. + +## Recommended playable slice + +Prove the redesign with a full NOT or AND construction mission, rather than a world selector plus a gate-choice quiz. Let players inspect the target behavior, build a network, toggle inputs, see intermediate 0/1 states, and run exhaustive tests. A mission completes only if the player's network passes all required input combinations and both outputs where applicable. Keep expected and actual values visible, including an actionable counterexample on failure. + +The six existing fixtures already define reliable small-machine contracts. Preserve those contracts and exercise IDs, even if the public-facing names become Beacon, Lift, Pump, Detector, Receiver, and Router. The story effect should visualize the actual output of the same circuit; it must not be a disconnected animation triggered by a success button. + +## Content corrections worth making during adaptation + +- The background section titled “Two-Input Boolean Functions” contains an eight-row, three-input table copied from the earlier example. It does **not** show all sixteen two-input functions as its caption claims. Replace with a correct representation or remove the incorrect table from the adapted field guide. +- Its function-count formula is rendered as `${2^2}^n`; the correct count for Boolean functions of `n` inputs is `2^(2^n)`. +- The background expands VHDL as “Virtual”; that expansion is incorrect. For the game, simply call the project's syntax “HDL” and explain what it describes. +- The project calls NOT a “converter”; use the conventional “inverter.” +- `ExerciseAnd.ts` starts its comment with “Not gate”; fix to “And gate” when reusing the fixture text. +- The project’s multi-way demultiplexor prose ends with “8-way 1-bit multiplexor”; the context indicates demultiplexor. Keep this a documented future capability until implemented. +- The introduction's descriptions of uniform chapter structure and later chapters are aspirations rather than shipped navigation. Adapt the guide to available sections. + +## Attribution and continuity + +`README.md` states **software: MIT; learning content: CC BY-NC-SA 3.0**. `src/routes/about/+page.svelte` explicitly credits Nand2Tetris content under that license and says Chipsandcode is not affiliated with or endorsed by Nand2Tetris. Retain the applicable credits and license links when adapting existing explanations, diagrams, exercises, and source text. Keep original game art and new narrative identifiable separately from adapted course material. Preserve existing `/learn/hardware/...` links as deep links into or alongside the field guide, and keep the Hardware IDE accessible for free exploration. + +## Sources inspected + +`README.md`; `src/routes/about/+page.svelte`; the three Boolean logic Markdown files listed above; `BuiltinNand.ts`; the six exercise fixtures listed above; `src/lib/hardware-simulator/chips/ChipFactory.ts`; `CustomChip.ts`; `builtin/{NotImplemented,NandChip,NotChip,AndChip,OrChip,XorChip,MuxChip,DMuxChip,Not16Chip}.ts`. diff --git a/docs/signal-isles-game-design.md b/docs/signal-isles-game-design.md new file mode 100644 index 0000000..006af23 --- /dev/null +++ b/docs/signal-isles-game-design.md @@ -0,0 +1,163 @@ +# Signal Isles game design: vision and implementation scope + +This document is a design proposal, not a claim that every described interaction is shipped. + +## Implementation snapshot — 2 November 2026 + +Implemented in the current redesign source: a selectable seven-station world, locally saved completion and circuit drafts, a chip collection, a field guide, a NAND discovery activity, six visual circuit builders (NOT, AND, OR, XOR, Mux, DMux), direct source-to-pin wiring with labeled connection menus, mission-specific mechanical scenes driven by live circuit outputs, generated read-only HDL, hints, one-step undo after clearing a circuit, and exhaustive truth-table checks for every single-bit mission. Composed designs compile through the existing HDL parser and hardware simulator. Mux and DMux are constructed from supported elementary parts rather than relying on missing built-in implementations. The proposed `isles` geography below was subsequently refined into the Signal Isles map. + +Proposed later features, **not implemented by this source snapshot**: an explorable character-controlled world; step-by-step signal execution; general undo/redo; bidirectional HDL editing; player-authored reusable component packages; adaptive or fresh-context transfer challenges; a separate Applied mastery state; server/account progress synchronization; a migration adapter for old completion records; multi-bit and multi-way playable missions; and a new district synthesis challenge. + +Current completion means passing the exhaustive single-bit contract (or discovering NAND's four cases), not independently demonstrated transfer mastery. Later workshops provide primitive equivalents of earlier chips; they do not instantiate a saved earlier player-authored circuit. All stations are available to explore, with a recommended next station rather than hard prerequisite locks. Progress is stored on the device. Publication and deployment status must be reported separately by the release owner. + +The source-grounded scope statements in the proposal below describe the original audit before the redesign. In particular, “executable support must be verified/implemented” for scalar Mux/DMux has now been addressed through composition and checked by the circuit test suite; the described bus/multi-way extensions remain future scope. + +--- + +# Chipsandcode: a world you bring to life + +## Product thesis + +Chipsandcode becomes a playable digital workshop: restore a quiet machine-world by designing the chips that make its systems work. Every successful design becomes a reusable tool and produces an immediate, persistent change in the world. Boolean logic is the material the player handles; the lesson text becomes a field notebook beside that activity. + +This is a proposed interaction design grounded in the repository inventory supplied by the lead agent. The current material covers the first week of Nand2Tetris. Six embedded exercises are verified: Not, And, Or, Xor, Mux, and DMux. The project also describes multi-bit and multi-way extensions, whose embedded exercises are not present. Runtime primitives currently include Not, Nand, And, Or, and Xor; that does not establish that composed circuits, Mux, DMux, or the extensions are production-ready. Every implementation must distinguish content coverage from executable simulator coverage. + +The central design test: **could a player finish this interaction without using the intended logic concept?** If yes, improve the interaction before adding more rewards or animation. + +## Core fantasy and tone + +You are a small, capable systems builder arriving in an interconnected workshop district. Its equipment is intact but its logic is missing. You restore it one useful behavior at a time. A small expressive maintenance bot can respond to your designs, carry parts, and point at visible signals; it never becomes a talking textbook that blocks play. + +The world should look handmade and tactile: miniature devices, visible ports, little bridges, chunky modular surfaces, expressive lights, satisfying physical controls. Electrical metaphors are visual representations of binary logic, not claims to simulate voltage, resistance, current, or real-world electrical safety. + +Keep language human and concrete. The player sees “Make the light respond to both switches” before seeing “AND gate.” After solving, naming the behavior gives them a useful abstraction they already understand. A chip label and notation remain available throughout for people who prefer the formal view. + +## The game loop + +1. **Notice a visible problem.** A machine visibly responds incorrectly; its target behavior can be previewed with controllable inputs. +2. **Inspect the contract.** Input names, output names, allowed parts, and target states sit beside the workbench. No hidden trick requirements. +3. **Build a model.** Place chips, connect ports, branch wires, and observe signals. Dragging is optional: select source, then select destination; keyboard controls support the same actions. +4. **Run and investigate.** A signal pulse travels through the actual graph, values appear on ports, and the first diverging output is easy to find. The player can pause and step evaluation. +5. **Revise without penalty.** Failed cases give evidence, never lost lives. Undo, reset, and hints are always available. +6. **Verify the design.** Every required truth-table case passes. A successful-looking animation is insufficient. +7. **Use what you built.** The repaired machine starts working, a route or interaction becomes usable, and the verified chip enters the player's toolbox. +8. **Apply it elsewhere.** A short transfer problem changes the story framing or input arrangement so recognition alone cannot establish mastery. + +The map is a navigable index of working systems. The workbench is the main play surface. The notebook contains the existing conceptual explanations. Move between these layers without losing the current circuit. + +## First ten minutes + +This sequence introduces the actual NAND foundation and then the first two verified exercises. Times are design targets, not enforced timers. Players can inspect, replay, or take as long as they need. + +| Time | Player action | Learning underneath | Visible result | +| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| 0:00–0:40 | Touch two switches on the supplied NAND device; see both input values and its output. A tiny four-state tester is visible. | Inputs and outputs; NAND's behavior; truth tables. | The output and attached signal lamp respond immediately. | +| 0:40–1:10 | Open the first broken device: a night marker. Toggle its “daylight detected” input and preview what it should do. | Specify a Boolean function before implementing it. | Target marker is lit for input 0 and dark for input 1. | +| 1:10–3:30 | Build the night marker controller using the provided NAND part. Wire, run both test cases, inspect errors, revise. | Construct NOT from NAND. | Both truth cases pass; the marker becomes a permanent lit landmark. | +| 3:30–4:15 | Name and save the discovered chip as NOT. Inspect its reusable package, with the internal circuit expandable. | Abstraction and hierarchical composition. | NOT appears in the toolbox; its ports and behavior remain inspectable. | +| 4:15–5:00 | Reach a beacon with two interlocks. Toggle the four combinations and examine the desired response. | AND's four-case contract. | A ghost preview shows the beacon active only when both inputs are high. | +| 5:00–7:45 | Build its controller from allowed parts. Test the full four-case table, then inspect any failing input combination. | Construct AND using established components/NAND; systematic testing. | The actual beacon reacts to the player's circuit, including incorrect states before repair. | +| 7:45–8:30 | Verify all four cases. The beacon lights and an adjacent workshop lane opens. | Functional equivalence across the complete input space. | A tangible world change and a reusable AND chip, rather than a lesson-complete modal. | +| 8:30–10:00 | Optional short transfer: use AND for a different pair of binary conditions, with inputs in different physical positions. Then select the next visible job. | Transfer beyond story labels; pin identity and composition. | Player chooses between a new job and free experimentation. | + +The NOT challenge states the desired behavior and allowed parts. It does not immediately reveal that the NAND inputs should be joined. Hints progress from “Try both possible inputs” to a conceptual clue to a visual example only when requested. The AND challenge likewise presents its contract without drawing the solution. + +### Concrete beacon contract + +Both “power ready” and “dish aligned” are binary signals. They are deliberately independent. The output is a logical signal controlling a fictional beacon indicator, not an electrical power calculation. + +| Power ready (`a`) | Dish aligned (`b`) | Required beacon (`out`) | +| ----------------- | ------------------ | ----------------------- | +| 0 | 0 | 0 | +| 0 | 1 | 0 | +| 1 | 0 | 0 | +| 1 | 1 | 1 | + +Testing should show expected and actual outputs for the selected case. A failing case should read, for example: “With power ready = 0 and dish aligned = 1, the beacon should be off. Your output is 1.” Keep the precise fault visible. Let players step through their own circuit without highlighting a canned solution. + +## Campaign mapping to the existing material + +The standard Project 1 extension names below are proposed identifiers for the described multi-bit/multi-way families; verify exact headings and canonical IDs before importing. The mapping creates no new claim that those exercises or runtimes already exist. + +| Existing content or exercise | Proposed playable job | Actual player interaction | Completion evidence | Scope | +| ---------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Boolean logic introduction | Wake the supplied NAND test rig | Toggle binary inputs; inspect output; make a prediction before a run. | All four states are inspectable; initial orientation is skippable. | Intro content exists; proposed game interaction. | +| Boolean logic background | Field notebook, attached to selected signals/chips | Reveal truth tables, terminology, notation, and existing explanatory text in context. | Knowledge remains searchable and accessible outside missions. | Existing text reused. | +| Built-in NAND foundation | First working part on the bench | Explore the supplied chip as a black box; see its truth table; branch its inputs. | NAND contract is correct in every input state. | Built-in primitive exists. | +| `ExerciseNot` | Night markers | Build an inverter that lights for a low input and turns off for a high input. | Both truth cases pass. | Embedded exercise exists; proposed visual adaptation. | +| `ExerciseAnd` | Double-interlock beacon | Build a controller that enables output only when both independent conditions are high. | All four truth cases pass. | Embedded exercise exists; proposed visual adaptation. | +| `ExerciseOr` | Two-call help bell | Either of two call buttons can request the same bell; both pressed still requests one output. | All four truth cases pass. | Embedded exercise exists; proposed visual adaptation. | +| `ExerciseXor` | Mismatch detector | Flag a disagreement between two independent sensor bits. Equal values remain quiet. | All four truth cases pass, especially 1/1 = 0. | Embedded exercise exists; proposed visual adaptation. | +| `ExerciseMux` | Signal selector | Switch which of two input signals reaches a single output using a separate select signal. | All eight scalar input combinations pass. | Embedded exercise exists; executable support must be verified/implemented. | +| `ExerciseDMux` | Signal dispatch | Send one input to one selected output while the other stays low. | All four scalar input combinations produce both correct outputs. | Embedded exercise exists; executable support must be verified/implemented. | +| Multi-bit NOT (`Not16`) | Invert a signal panel | Apply the same behavior independently to every lane of a 16-bit bus. Expand/collapse lanes. | Per-lane equivalence; one-hot, all-low/all-high, alternating and generated vectors. | Described extension; new interaction/runtime work. | +| Multi-bit AND (`And16`) | Pair two signal panels | Combine corresponding lanes from two input buses. | Per-lane AND equivalence with crossed-lane tests. | Described extension; new interaction/runtime work. | +| Multi-bit OR (`Or16`) | Merge indicator panels | Combine corresponding lanes without losing their positions. | Per-lane OR equivalence with crossed-lane tests. | Described extension; new interaction/runtime work. | +| Multi-bit Mux (`Mux16`) | Choose a whole signal panel | Select one complete 16-bit input bus with one select bit. | Selected bus reproduced exactly; unselected bus cannot affect output. | Described extension; new interaction/runtime work. | +| Multi-way OR (`Or8Way`) | Any-signal alarm board | Reduce eight independent bits to one “any active” indicator. | All 256 input combinations can be exhaustively tested. | Described extension; new interaction/runtime work. | +| Multi-way Mux (`Mux4Way16`, `Mux8Way16`) | Four/eight-source routing desk | Hierarchically choose one bus using two/three select bits. | Every select address routes the correct bus; targeted/generated vector checks. | Described extension; verify exact source names; new interaction/runtime work. | +| Multi-way DMux (`DMux4Way`, `DMux8Way`) | Four/eight-destination dispatch | Route one signal to exactly one addressed output. | Exhaustive single-bit input and select combinations; other outputs remain low. | Described extension; verify exact source names; new interaction/runtime work. | +| Project completion | District systems check | Combine earned parts across several small machines, with independent inputs and a visible signal contract. | Correct component behavior and composition; no unrelated new prerequisite. | Proposed synthesis challenge, explicitly new content. | + +The six embedded exercises form the first fully interactive campaign slice. The described extensions become a separately scoped next release unless their missing runtimes and validators are completed. Do not present their map nodes as playable while they are documentation-only. + +## World layout and navigation + +Use three compact places for the verified six-exercise slice: **Marker Walk** (NOT/AND), **Signal Yard** (OR/XOR), and **Routing House** (Mux/DMux). Places reflect different uses of logic rather than arbitrary levels. A later **Bus Workshop** holds the multi-bit/multi-way material once implemented. + +World navigation must preserve a direct, keyboard-friendly mission list with state, prerequisites, and “continue where I left off.” Spatial discovery adds delight, while actual access never depends on perceiving color, guessing where to click, or playing a dexterity game. All visible active objects need clear hover/focus treatment and names. + +The player can revisit any finished mission, experiment freely, and inspect its saved circuit. Persist the current workspace and the last selected test input. Preserve existing completion records through a canonical content-ID adapter; do not assume an old completion proves new mastery evidence that was never collected. + +## The workbench as the main game + +The board occupies most of the challenge screen. The target machine stays visible beside it and responds to the current output. The player's toolbox is small and contextual. Chips are visibly modular and expandable, with clear input/output direction and named ports. High signals have both a visual state and a numeric value; low signals remain legible rather than disappearing into darkness. + +Four reusable tools make the environment feel like a game while teaching engineering habits: + +- **Inspect:** select any port, wire, or chip to see its value, role, and local explanation. +- **Trace:** follow a signal from an input through the circuit, with visible branch relationships. +- **Step:** evaluate a combinational circuit in a pedagogical sequence. This is a visualization of dependency evaluation, not a claim about physical gate timing. +- **Test:** run the contract, inspect failing cases, and jump directly to their input states. + +Always include undo/redo, disconnect, clear-board with confirmation, and a recovery path for invalid wiring. Fan-out should be supported and visually understandable. Fan-in to one input is invalid unless a component explicitly provides it. Reject or diagnose cycles clearly; the current combinational-logic chapter does not need to introduce sequential state through accidental feedback. + +Formal HDL remains reachable. A later bidirectional HDL/visual view can show that the playful circuit is a real specification, but it requires a genuine shared model and parser. Do not fake synchronization in a prototype or silently discard constructs the visual editor cannot represent. For the first prototype, clearly choose and implement one authoritative editing model. + +## Mastery, rewards, and replay + +Completion and mastery are distinct. **Built** means the circuit passes its required cases. **Applied** means the player has used the concept in a fresh context. Hints do not disqualify either; the relevant evidence is the resulting behavior and transfer. + +Rewards should reinforce competence: a reusable chip, a changed machine, newly available systems, cosmetic workshop pieces, a growing collection of the player's own designs. Keep XP secondary if used at all. The world should not pretend a five-minute interaction produces broad professional expertise. + +No hearts, energy limits, mandatory streaks, punitive countdowns, or public failure boards. Optional timed challenges can appear after mastery with unlimited ordinary practice still available. Skipping celebratory animation, muting audio, reducing motion, and choosing a quieter visual mode must not reduce progress. + +Players can ask for help in three useful levels: a restatement of the contract, a concept clue attached to the failing case, and an inspectable worked example. Worked examples should be labeled; the following transfer task changes the context so reproducing the example is not the only assessed action. + +## Validation and runtime requirements + +Small truth tables should be exhaustively checked: NOT has 2 rows; two-input gates 4; scalar Mux 8; scalar DMux 4. Each case must inspect every declared output. The UI may animate a single case, but completion uses the actual evaluator across all required cases. + +For large buses, indiscriminate exhaustive input enumeration is impractical. Prefer compositional validation of a restricted, well-defined circuit model, complemented by targeted and generated tests. Explain whether a check is exhaustive or sampled. Include all-zero/all-one, alternating patterns, one-hot lane tests, and distinct source patterns that expose bus swapping and select-address errors. Reorder or refresh transfer cases without using randomness as the only correctness oracle. + +Separate the circuit model, evaluator, test contracts, player evidence, and presentation. The same actual output must drive the truth table, wire state, machine animation, and completion result. A local prototype may store progress locally, but real accounts need durable, versioned progress and circuit saves connected to the existing auth/data layer. + +## A disciplined first release + +1. Build one polished world region and a complete NOT → AND loop, including incorrect circuits, test evidence, hints, part reuse, saved state, and restoration. +2. Validate the interaction with the actual source contracts and the current simulator. Confirm the mission is enjoyable before adding campaign surface area. +3. Expand to OR/XOR using the same tools and contracts; add Mux/DMux once their executable path is verified. +4. Import the existing introduction/background into contextual notebook entries, retaining canonical content identity and direct access. +5. Add bus/multi-way systems only when their model, editor, and validators work. Treat the final district systems check as new authored synthesis content. + +Useful validation observations: Can a first-time player discover the objective without reading a tutorial page? Can they explain why a failing case fails? Can they intentionally repair it? Can they build the same function in a new setting? Can a keyboard-only player complete the same work? Does returning after several days reveal the next useful action immediately? + +## Pitfalls to avoid + +- A conventional lesson list covered with XP, gemstones, and a map. The actual problem-solving interaction must change. +- A spectacular world selector with an ordinary embedded article as the main activity. +- Lore that teaches the wrong abstraction, such as treating a single binary signal as stored power or a combinational gate as a memory element. +- Rewarding one lucky input state. Every small gate requires all truth cases. +- A large empty world, unimplemented playable-looking nodes, or promises of a CPU campaign unsupported by current content. +- Drag-only wiring, color-only signal values, mandatory movement, tiny ports, or animation that obscures state. +- Testing a hidden hardcoded solution while showing an unrelated editable board. The player’s actual graph must determine the outcome. diff --git a/src/lib/editor/hdl/parse.ts b/src/lib/editor/hdl/parse.ts index 0087fd2..5ef1e89 100644 --- a/src/lib/editor/hdl/parse.ts +++ b/src/lib/editor/hdl/parse.ts @@ -1,4 +1,4 @@ -import { Parser, Grammar } from 'nearley'; +import nearley from 'nearley'; import grammar from './grammar.ne'; import type { Root } from './tree'; @@ -9,7 +9,7 @@ import type { Root } from './tree'; * @param input HDL source code */ function parse(input: string): Root | undefined { - const parser = new Parser(Grammar.fromCompiled(grammar)); + const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)); parser.feed(input); if (parser.results.length > 1) { diff --git a/src/lib/editor/tst/parse.ts b/src/lib/editor/tst/parse.ts index ba334ba..14aea83 100644 --- a/src/lib/editor/tst/parse.ts +++ b/src/lib/editor/tst/parse.ts @@ -1,4 +1,4 @@ -import { Parser, Grammar } from 'nearley'; +import nearley from 'nearley'; import grammar from './grammar.ne'; import type { Root } from './tree'; @@ -9,7 +9,7 @@ import type { Root } from './tree'; * @param input test script source code */ function parse(input: string): Root | undefined { - const parser = new Parser(Grammar.fromCompiled(grammar)); + const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)); parser.feed(input); if (parser.results.length > 1) { diff --git a/src/lib/game/BenchMachine.svelte b/src/lib/game/BenchMachine.svelte new file mode 100644 index 0000000..14818e2 --- /dev/null +++ b/src/lib/game/BenchMachine.svelte @@ -0,0 +1,254 @@ + + + + + diff --git a/src/lib/game/FieldGuide.svelte b/src/lib/game/FieldGuide.svelte new file mode 100644 index 0000000..c2a4c7b --- /dev/null +++ b/src/lib/game/FieldGuide.svelte @@ -0,0 +1,1345 @@ + + +
+
+ +
+

Chips & Code / Explorer's edition

+

Field notes.

+
+ +
+ +
+ + +
+ {#if chapter === 'signals'} +

Note 01 / A language with two words

+

Everything starts
with a little on & off.

+

+ A Boolean signal has two values: 0 and 1. Gates + transform those signals. Flip an input and follow what changes. +

+ +
+ {#each gates as kind} + + {/each} +
+ +
+
+ + {#if gate !== 'NOT'} + + {/if} +
+ +
+ logic gate{gate}IN → OUT +
+ +
+ OUT{Number(result)}{result ? 'signal on' : 'signal off'} +
+
+

{descriptions[gate]}

+ +
+
+

No guessing. Every possibility.

+

+ A truth table lists every possible input and its output. The highlighted row is the + state you're exploring. For a two-input gate there are four cases; for a three-input + gate there are eight. +

+

+ The glow is a visual guide to 0 and 1, not a physical electricity simulation. +

+
+ + + {#if gate !== 'NOT'}{/if} + {#each rows as [x, y]}{#if gate !== 'NOT'}{/if}{/each} +
{gate} truth table
{gate === 'NOT' ? 'in' : 'A'}BOUT
{Number(x)}{Number(y)}{Number(evaluate(gate, x, y))}
+
+ {:else if chapter === 'expressions'} +

Note 02 / Same idea, different views

+

A machine can be
a sentence, too.

+

+ “A or B is on, and C is off.” That is a complete specification. We can write it as an + expression, draw connected gates, or list its truth table. +

+
f = (a OR b) AND NOT c
+
+ + + +
+ f{Number(expressionResult)}output +
+
+
+ a OR b {Number(a || b)}NOT c {Number(!c)}both true? {Number(expressionResult)} +
+

Build it from the rows that output 1.

+

+ There are three such rows. For each, AND together the inputs that describe it, using NOT + for a 0. Then OR those terms together. This is a canonical sum of products. +

+
+ {#each ['(NOT a AND b AND NOT c)', '(a AND NOT b AND NOT c)', '(a AND b AND NOT c)'] as term, i} + {#if i > 0}OR{/if} +
{term}{Number(terms[i])}
+ {/each} +
+

+ A general recipe: any Boolean function can be written using AND, OR, and NOT. Each of + those gates can itself be built from NAND. +

+ {:else if chapter === 'blueprints'} +

Note 03 / Look inside the black box

+

Small parts.
Bigger possibilities.

+

+ A chip has an interface—named inputs, outputs, and promised behavior. Its implementation + is the network of parts inside. The same interface can have more than one correct + implementation. +

+
+
+ INTERFACE +
in → NOT → out
+

Output is the opposite of input.

+
+
+ ONE IMPLEMENTATION +
+ in ↗
in ↘
NAND→ out +
+

The same input feeds both NAND pins.

+
+
+
+
+

A blueprint the simulator can read.

+

+ HDL means hardware description language. It describes the structure + of a circuit, rather than a sequence of instructions. +

+
+
CHIP / IN / OUT
+
Name the chip and its external pins.
+
PARTS
+
List the components and connect their pins.
+
Nand(a=in, b=in, out=out)
+
+ Connect both NAND inputs to the external input, and its output to the external + output. +
+
+
+
+ Not.hdl / example blueprint +
{notHDL}
+
+
+
+

Primitive or composite?

+

+ A primitive is supplied as a working part: here, NAND. A composite is built by + connecting other parts. Once a composite chip works, its interface lets another designer + use it without needing to understand every wire inside. +

+
+

+ Testing checks the blueprint against its contract. For small gates, test every input + combination. Fewer parts can be an optional design challenge after correctness. +

+ {:else if chapter === 'scale'} +

Note 04 / A preview beyond the expedition

+

One signal.
Then a whole bundle.

+

+ The current expedition builds single-bit gates. These interactive sketches explain where + the existing reading goes next: buses and multi-way selectors. +

+
Concept sketch / not a construction mission
+

A bus carries many bits together.

+

+ An 8-bit NOT applies the same inversion to each bit independently. Flip any bit below. A + 16-bit version follows the same pattern with sixteen wires. +

+
+ IN +
+ {#each bus as bit, i}{/each} +
+ NOT +
+ {#each bus as bit, i}
+ {7 - i}{Number(!bit)} +
{/each} +
+
+
+
+

More inputs. A few selector bits.

+

+ A multiplexor chooses one input to send to its output. With two selector bits, a + four-way multiplexor can choose among four inputs. +

+

+ A demultiplexor routes in the other direction: one input to a selected output, with + the other outputs set to 0. +

+

+ A four-way selector needs 2 bits. An eight-way selector needs 3. Multi-bit selectors + route a whole bus at once. +

+
+
+ 4-WAY / 4-BIT MUX +
+ {#each words as word, i}{/each} +
+

+ sel {selector.toString(2).padStart(2, '0')} → OUT + {words[selector]} +

+
+
+ {:else} +

Note 05 / Keep exploring

+

Follow the
wires further.

+

+ The expedition puts the machines first. The original reading and hardware editor are here + whenever you want more detail or a blank workbench. +

+ +
+

Built on good ideas.

+

+ These notes adapt the existing Chips and Code Boolean logic material, based on Nand2Tetris. Nand2Tetris content is licensed under + CC BY-NC-SA 3.0. The explanations and interactive presentation have been adapted for this field guide. +

+

+ Chips and Code is not affiliated with or endorsed by Nand2Tetris. Software is MIT + licensed. Project background & credits ↗ +

+
+ {/if} +
+ CHIPS & CODE / FIELD NOTES{chapters.find((item) => item.id === chapter)?.number} — 05 +
+
+
+
+ + diff --git a/src/lib/game/Game.svelte b/src/lib/game/Game.svelte new file mode 100644 index 0000000..21331df --- /dev/null +++ b/src/lib/game/Game.svelte @@ -0,0 +1,1289 @@ + + +
+ Skip to game +
+ + + +
+ +
+ {#if view === 'world'} +
+
+ THE SIGNAL ISLESCHAPTER 01 / THE FIRST SPARK +
+
+
+ {#if restoredMission} + SYSTEM RESTORED +

You made
a little magic.

+

{restoredMission.reward}

+

+ ✓ {restoredMission.chip} verified. Your world remembers. +

+ {:else} + BUILD SOMETHING THAT MATTERS +

+ A little logic.
A living world. +

+

+ The islands have gone quiet. Bring them back to life, one invention at a time. +

+ {/if} +
+
+ STATION {String(activeMission.number).padStart(2, '0')}{completed.includes(selected) + ? '✓ RESTORED' + : selected === nextMission?.id + ? 'YOUR NEXT STOP' + : 'OPEN TO EXPLORE'} +
+

{activeMission.title}

+

{activeMission.brief}

+
+ {activeMission.chip}{selected === 'nand' + ? 'Discover your first building block' + : 'Build it. Test it. Make it yours.'} +
+ {#if restored && nextMission} + + {:else} + + {/if} + {selected === 'nand' + ? 'No code. No setup. Just curiosity.' + : 'Experiment freely. Every attempt teaches you something.'} +
+ +
+
+
+ SIGNAL ARCHIPELAGO 01—07EXPLORE · INVENT · RESTORE +
+ + +
+
+
+
+ YOUR EXPEDITION{completed.length} OF {missions.length} SYSTEMS RESTORED +
+
+ {#each missions as mission} + + {/each} +
+
+
+ {:else if view === 'workbench'} + {#key selected}{/key} + {:else if view === 'guide'} + + {:else} +
+ + THE THINGS YOU CAN MAKE +

+ Small chips.
Endless possibilities. +

+

+ Every verified invention earns a place here. Later workshops use the same logic as + building blocks for bigger ideas. +

+
+ {#each missions as mission} +
+ {completed.includes(mission.id) + ? '✓ VERIFIED IN YOUR WORKSHOP' + : 'AWAITING YOUR INVENTION'} + +

{mission.title}

+

{mission.goal}

+ +
+ {/each} +
+
+ BEYOND THE HORIZON +

A computer of your own.

+

+ Buses, arithmetic, memory, and a CPU are the bigger adventure ahead. The field guide + introduces buses and selectors; this playable chapter focuses on single-bit Boolean + logic. +

+ +
+
+ {/if} +
+ + {#if notice}

{notice}

{/if} +
+ + diff --git a/src/lib/game/MissionWorkbench.svelte b/src/lib/game/MissionWorkbench.svelte new file mode 100644 index 0000000..c483a2b --- /dev/null +++ b/src/lib/game/MissionWorkbench.svelte @@ -0,0 +1,2118 @@ + + +
+
+ +
+ CHIPS & CODE + / FIELD WORKSHOP +
+ +
+ +
+
+

+ STATION {String(mission.number).padStart(2, '0')} / + {mission.location} +

+

{mission.title}

+

{mission.brief}

+
+
+ BUILD SPEC{mission.chip}{completed.includes(missionId) ? 'RESTORED · REPLAY' : 'AWAITING SIGNAL'} +
+
+ +
+
+
+
+ CIRCUIT WORKBENCHMK. 01 / {mission.chip.toUpperCase()} +
+
+ TRY A SIGNAL + {#each mission.inputs as pin} + + {/each} +
+ + {#if missionId !== 'nand'} +
+ +

{wiringMessage}

+ {#if selectedSource}{/if} +
+ {/if} +
+ + + + + + + INPUT + OUTPUT + {#each circuit.gates as gate, index} + {@const position = gatePosition(index)} + {#each gatePins(gate.type) as pin, pinIndex} + {#if gate.inputs[pin]} + + {/if} + {/each} + {/each} + {#each mission.outputs as pin, index} + {#if circuit.outputs[pin]} + + {/if} + {/each} + {#each mission.inputs as pin, index} + + + {Number(inputValues[pin])} + {pin} + + + + + + {/each} + {#each circuit.gates as gate, index} + {@const position = gatePosition(index)} + + + + + {gate.type.toUpperCase()} + PART {String(index + 1).padStart(2, '0')} + {#each gatePins(gate.type) as pin, pinIndex} + + {pin} + {#if missionId !== 'nand'} + + + + {/if} + {/each} + + {signal(gate.id, liveSignals) === null + ? '·' + : Number(signal(gate.id, liveSignals))} + {#if missionId !== 'nand'} + + + + {/if} + + {/each} + {#if circuit.gates.length === 0} + + + + + YOUR INVENTION + + Pick a part below. Make something work. + {/if} + {#each mission.outputs as pin, index} + {@const outputValue = live.result?.outputs[pin]} + + {#if outputValue}{/if} + + {outputValue === undefined ? '?' : Number(outputValue)} + {pin} + {#if missionId !== 'nand'} + + + + {/if} + + {/each} + +
+

+ {live.result + ? `Live output: ${Object.entries(live.result.outputs) + .map(([pin, value]) => `${pin} ${Number(value)}`) + .join(', ')}` + : 'Circuit is not yet fully connected.'} +

+ +
+ + {#if missionId !== 'nand'} +
+
+
+

SUPPLIED PARTS

+

A little logic. Endless possibility.

+
+
+ {#if undoCircuit}{/if} +
+
+
+ {#each mission.availableParts as type}{/each} +
+

+ Add a part, then wire its pins on the board or with the menus below. Earlier chips are + supplied as reference components; your collection tracks the ones you have built. +

+ {#if circuit.gates.length > 0} +
+ {#each circuit.gates as gate, index (gate.id)} +
+
+ {String(index + 1).padStart(2, '0')}{gate.type} +
+ {#each gatePins(gate.type) as pin} + + {/each} +
+ {/each} +
+ {/if} +
+ CONNECT THE MACHINE{#each mission.outputs as pin}{/each} +
+

+ {saveNotice || 'Your invention stays in this browser as you build.'} +

+
+ {/if} + + {#if showHDL} +
+
+

The same circuit, written in HDL.

+ +
+ {#if hdl}
{hdl}
{:else}

+ Connect every part and output to reveal the HDL for your design. +

{/if}Open the full hardware IDE +
+ {/if} +
+ + +
+
+ + diff --git a/src/lib/game/WorldMap.svelte b/src/lib/game/WorldMap.svelte new file mode 100644 index 0000000..2b06da3 --- /dev/null +++ b/src/lib/game/WorldMap.svelte @@ -0,0 +1,326 @@ + + +
+ +
+
+ + + {#each stations as station} + {@const powered = completed.includes(station.id)} + + {/each} +
+
+
+ {#each stations as station} + + {/each} +
+
+ + diff --git a/src/lib/game/circuit.test.ts b/src/lib/game/circuit.test.ts new file mode 100644 index 0000000..4453139 --- /dev/null +++ b/src/lib/game/circuit.test.ts @@ -0,0 +1,227 @@ +import { describe, expect, it } from 'vitest'; +import { + availableSources, + createCircuit, + isCircuitDraft, + simulateCircuit, + testCircuit, + toHDL, +} from './circuit'; +import type { Circuit, GateNode } from './circuit'; +import { getMission, missions, truthTable } from './missions'; +import type { MissionId } from './missions'; + +const nand = (id: string, a: string, b: string): GateNode => ({ + id, + type: 'Nand', + inputs: { a, b }, +}); +const examples: Record = { + nand: { gates: [nand('g1', 'a', 'b')], outputs: { out: 'g1' } }, + not: { gates: [nand('g1', 'in', 'in')], outputs: { out: 'g1' } }, + and: { gates: [nand('g1', 'a', 'b'), nand('g2', 'g1', 'g1')], outputs: { out: 'g2' } }, + or: { + gates: [nand('g1', 'a', 'a'), nand('g2', 'b', 'b'), nand('g3', 'g1', 'g2')], + outputs: { out: 'g3' }, + }, + xor: { + gates: [ + nand('g1', 'a', 'b'), + nand('g2', 'a', 'g1'), + nand('g3', 'b', 'g1'), + nand('g4', 'g2', 'g3'), + ], + outputs: { out: 'g4' }, + }, + mux: { + gates: [ + nand('g1', 'sel', 'sel'), + nand('g2', 'a', 'g1'), + nand('g3', 'b', 'sel'), + nand('g4', 'g2', 'g3'), + ], + outputs: { out: 'g4' }, + }, + dmux: { + gates: [ + nand('g1', 'sel', 'sel'), + nand('g2', 'in', 'g1'), + nand('g3', 'g2', 'g2'), + nand('g4', 'in', 'sel'), + nand('g5', 'g4', 'g4'), + ], + outputs: { a: 'g3', b: 'g5' }, + }, +}; + +describe('Signal Isles circuit authoring', () => { + it.each(missions)('accepts a real NAND construction for $chip across every input', (mission) => { + const report = testCircuit(mission, examples[mission.id]); + expect(report.success).toBe(true); + expect(report.passed).toBe(2 ** mission.inputs.length); + expect(report.rows.every((row) => row.passed)).toBe(true); + expect(report.hdl).toContain(`CHIP ${mission.chip}`); + }); + + it('starts builders empty and the NAND discovery with the supplied primitive', () => { + for (const mission of missions) { + const circuit = createCircuit(mission); + expect(isCircuitDraft(mission, circuit)).toBe(true); + expect(circuit.gates).toHaveLength(mission.id === 'nand' ? 1 : 0); + } + expect(testCircuit(getMission('nand'), createCircuit(getMission('nand'))).success).toBe(true); + }); + + it('accepts previously mastered primitive equivalents in later builds', () => { + const circuit: Circuit = { + gates: [ + { id: 'g1', type: 'Nand', inputs: { a: 'a', b: 'b' } }, + { id: 'g2', type: 'Not', inputs: { in: 'g1' } }, + ], + outputs: { out: 'g2' }, + }; + const mission = getMission('and'); + expect(testCircuit(mission, circuit).success).toBe(true); + expect(simulateCircuit(mission, circuit, { a: true, b: true })).toMatchObject({ + outputs: { out: true }, + signals: { a: true, b: true, g1: false, g2: true }, + }); + }); + + it.each([13, 24])( + 'runs a valid %i-stage visual inverter chain through the HDL simulator', + (length) => { + const mission = getMission('not'); + const gates = Array.from({ length }, (_, index) => { + const source = index === 0 ? 'in' : `g${index}`; + return nand(`g${index + 1}`, source, source); + }); + const circuit: Circuit = { gates, outputs: { out: `g${length}` } }; + + for (const input of [false, true]) { + const simulation = simulateCircuit(mission, circuit, { in: input }); + expect(simulation.outputs.out).toBe(length % 2 === 0 ? input : !input); + gates.forEach((gate, index) => { + expect(simulation.signals[gate.id]).toBe(index % 2 === 0 ? !input : input); + }); + } + expect(testCircuit(mission, circuit).success).toBe(length % 2 === 1); + }, + ); + + it('detects a circuit that happens to pass the current input but fails the full specification', () => { + const mission = getMission('and'); + const circuit: Circuit = { gates: [nand('g1', 'a', 'b')], outputs: { out: 'g1' } }; + const report = testCircuit(mission, circuit); + expect(report.success).toBe(false); + expect(report.passed).toBe(0); + expect(report.rows[0]).toMatchObject({ + inputs: { a: false, b: false }, + expected: { out: false }, + actual: { out: true }, + passed: false, + }); + }); + + it('requires both demultiplexor outputs, including the inactive destination', () => { + const mission = getMission('dmux'); + const circuit = { ...examples.dmux, outputs: { a: 'g3', b: 'g3' } }; + const report = testCircuit(mission, circuit); + expect(report.total).toBe(4); + expect(report.success).toBe(false); + expect(report.rows.filter((row) => !row.passed)).toHaveLength(2); + }); + + it('supports direct input wires with semantically equivalent exported HDL', () => { + const mission = getMission('mux'); + const circuit: Circuit = { gates: [], outputs: { out: 'a' } }; + expect(toHDL(mission, circuit)).toContain('gamebufferout'); + expect(simulateCircuit(mission, circuit, { a: true, b: false, sel: false }).outputs.out).toBe( + true, + ); + expect(simulateCircuit(mission, circuit, { a: false, b: true, sel: false }).outputs.out).toBe( + false, + ); + }); + + it('offers only inputs and earlier nodes when connecting a gate', () => { + const mission = getMission('and'); + expect(availableSources(mission, examples.and, 'g1')).toEqual(['a', 'b']); + expect(availableSources(mission, examples.and, 'g2')).toEqual(['a', 'b', 'g1']); + expect(availableSources(mission, examples.and)).toEqual(['a', 'b', 'g1', 'g2']); + expect(availableSources(mission, examples.and, 'missing')).toEqual(['a', 'b']); + }); + + it('retains incomplete but structurally safe drafts', () => { + const mission = getMission('not'); + const circuit: Circuit = { gates: [nand('g1', '', '')], outputs: { out: '' } }; + expect(isCircuitDraft(mission, circuit)).toBe(true); + expect(() => testCircuit(mission, circuit)).toThrow('Connect g1 a'); + expect(() => testCircuit(mission, createCircuit(mission))).toThrow('Connect output out'); + }); + + it('rejects unknown sources, missing output, forward edges and cycles before compiling', () => { + const mission = getMission('and'); + expect(() => + toHDL(mission, { gates: [nand('g1', 'nope', 'b')], outputs: { out: 'g1' } }), + ).toThrow('earlier gate'); + expect(() => + toHDL(mission, { + gates: [nand('g1', 'g2', 'b'), nand('g2', 'g1', 'b')], + outputs: { out: 'g2' }, + }), + ).toThrow('Loops'); + expect(() => + toHDL(mission, { gates: [nand('g1', 'g1', 'b')], outputs: { out: 'g1' } }), + ).toThrow('Loops'); + expect(() => toHDL(mission, { gates: [], outputs: { out: 'missing' } })).toThrow( + 'unknown signal', + ); + }); + + it('rejects malformed saved drafts and inaccessible components', () => { + const mission = getMission('not'); + const invalid: unknown[] = [ + null, + [], + {}, + { gates: [], outputs: {} }, + { gates: [], outputs: { out: '', extra: '' } }, + { gates: [{ id: 'g1', type: 'Not', inputs: { in: 'in' } }], outputs: { out: 'g1' } }, + { gates: [{ id: 'in', type: 'Nand', inputs: { a: 'in', b: 'in' } }], outputs: { out: 'in' } }, + { gates: [nand('g1', 'in', 'in'), nand('g1', 'in', 'in')], outputs: { out: 'g1' } }, + { + gates: [{ id: 'g1', type: 'Nand', inputs: { a: 'in', b: 'in', bad: 'in' } }], + outputs: { out: 'g1' }, + }, + { + gates: Array.from({ length: 33 }, (_, i) => nand(`g${i}`, 'in', 'in')), + outputs: { out: '' }, + }, + { + gates: [{ id: 'x);Nand(', type: 'Nand', inputs: { a: 'in', b: 'in' } }], + outputs: { out: '' }, + }, + ]; + for (const value of invalid) expect(isCircuitDraft(mission, value)).toBe(false); + expect(() => toHDL(mission, invalid[2] as Circuit)).toThrow('invalid components'); + }); + + it('validates live input state and mission identity', () => { + expect(() => simulateCircuit(getMission('nand'), examples.nand, { a: true })).toThrow( + 'Input b', + ); + expect(() => getMission('future-cpu')).toThrow('Unknown mission'); + }); + + it('enumerates every input exactly once, including all eight selector cases', () => { + const rows = truthTable(getMission('mux')); + expect(rows).toHaveLength(8); + expect(new Set(rows.map((row) => JSON.stringify(row.inputs))).size).toBe(8); + expect(rows[3]).toEqual({ inputs: { a: false, b: true, sel: true }, expected: { out: true } }); + expect(truthTable(getMission('dmux'))[2]).toEqual({ + inputs: { in: true, sel: false }, + expected: { a: true, b: false }, + }); + }); +}); diff --git a/src/lib/game/circuit.ts b/src/lib/game/circuit.ts new file mode 100644 index 0000000..cf63738 --- /dev/null +++ b/src/lib/game/circuit.ts @@ -0,0 +1,183 @@ +import parse from '../editor/hdl/parse'; +import ChipFactory from '../hardware-simulator/chips/ChipFactory'; +import type { GateKind, Mission, Signals, TruthCase } from './missions'; +import { truthTable } from './missions'; + +export interface GateNode { + id: string; + type: GateKind; + /** Sources are input pin names or the id of an earlier gate. Empty means unwired. */ + inputs: Record; +} +export interface Circuit { + gates: GateNode[]; + outputs: Record; +} +export interface Simulation { + outputs: Signals; + signals: Signals; + hdl: string; +} +export interface TestRow extends TruthCase { + actual: Signals; + passed: boolean; +} +export interface TestResult { + rows: TestRow[]; + passed: number; + total: number; + success: boolean; + hdl: string; +} + +export function gatePins(type: GateKind): string[] { + return type === 'Not' ? ['in'] : ['a', 'b']; +} + +export function createCircuit(mission: Mission): Circuit { + return mission.id === 'nand' + ? { gates: [{ id: 'g1', type: 'Nand', inputs: { a: 'a', b: 'b' } }], outputs: { out: 'g1' } } + : { gates: [], outputs: Object.fromEntries(mission.outputs.map((pin) => [pin, ''])) }; +} + +/** Draft shape validation deliberately permits unwired pins for local autosave. */ +export function isCircuitDraft(mission: Mission, value: unknown): value is Circuit { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const candidate = value as Circuit; + if (!Array.isArray(candidate.gates) || candidate.gates.length > 32) return false; + const output = candidate.outputs; + if (!output || typeof output !== 'object' || Array.isArray(output)) return false; + if (Object.keys(output).some((pin) => !mission.outputs.includes(pin))) return false; + if (!mission.outputs.every((pin) => typeof output[pin] === 'string' && output[pin].length < 80)) + return false; + const ids = new Set(); + for (const gate of candidate.gates) { + if ( + !gate || + typeof gate !== 'object' || + typeof gate.id !== 'string' || + !/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(gate.id) || + mission.inputs.includes(gate.id) || + mission.outputs.includes(gate.id) || + ids.has(gate.id) || + !mission.availableParts.includes(gate.type) + ) + return false; + ids.add(gate.id); + if (!gate.inputs || typeof gate.inputs !== 'object' || Array.isArray(gate.inputs)) return false; + const pins = gatePins(gate.type); + if (Object.keys(gate.inputs).some((pin) => !pins.includes(pin))) return false; + if (!pins.every((pin) => typeof gate.inputs[pin] === 'string' && gate.inputs[pin].length < 80)) + return false; + } + return true; +} + +export function availableSources( + mission: Mission, + circuit: Circuit, + beforeGateId?: string, +): string[] { + const index = + beforeGateId === undefined + ? circuit.gates.length + : circuit.gates.findIndex((gate) => gate.id === beforeGateId); + return [...mission.inputs, ...circuit.gates.slice(0, Math.max(0, index)).map((gate) => gate.id)]; +} + +function validate(mission: Mission, circuit: Circuit) { + if (!isCircuitDraft(mission, circuit)) + throw new Error('This circuit has invalid components. Start a fresh build.'); + const known = new Set(mission.inputs); + for (const gate of circuit.gates) { + for (const pin of gatePins(gate.type)) { + const source = gate.inputs[pin]; + if (!source) throw new Error(`Connect ${gate.id} ${pin} to a signal.`); + if (!known.has(source)) + throw new Error( + `${gate.id} ${pin} must use an input or an earlier gate. Loops are not supported.`, + ); + } + known.add(gate.id); + } + for (const pin of mission.outputs) { + const source = circuit.outputs[pin]; + if (!source) throw new Error(`Connect output ${pin} to a signal.`); + if (!known.has(source)) throw new Error(`Output ${pin} is connected to an unknown signal.`); + } +} + +/** + * The visual editor is an HDL authoring view, not a parallel toy simulator. + * Fan-out is expressed by repeated output connections on the same part. + * A direct input → output wire needs two NANDs because HDL has no wire alias syntax. + */ +export function toHDL(mission: Mission, circuit: Circuit): string { + validate(mission, circuit); + const wires = new Map(circuit.gates.map((gate, index) => [gate.id, `gamewire${index}`])); + const sourceName = (source: string) => wires.get(source) ?? source; + const lines = circuit.gates.map((gate) => { + const inputs = gatePins(gate.type).map((pin) => `${pin}=${sourceName(gate.inputs[pin])}`); + const outputs = [ + `out=${wires.get(gate.id)}`, + ...mission.outputs + .filter((pin) => circuit.outputs[pin] === gate.id) + .map((pin) => `out=${pin}`), + ]; + return ` ${gate.type}(${[...inputs, ...outputs].join(', ')});`; + }); + for (const pin of mission.outputs) { + const source = circuit.outputs[pin]; + if (!mission.inputs.includes(source)) continue; + lines.push(` // Two NANDs preserve a direct input-to-output wire.`); + lines.push(` Nand(a=${source}, b=${source}, out=gamebuffer${pin});`); + lines.push(` Nand(a=gamebuffer${pin}, b=gamebuffer${pin}, out=${pin});`); + } + return [ + `CHIP ${mission.chip} {`, + ` IN ${mission.inputs.join(', ')};`, + ` OUT ${mission.outputs.join(', ')};`, + '', + ' PARTS:', + ...lines, + '}', + ].join('\n'); +} + +function compile(mission: Mission, circuit: Circuit) { + const hdl = toHDL(mission, circuit); + const ast = parse(hdl)?.[0]; + if (!ast) throw new Error('The generated circuit could not be parsed.'); + return { chip: new ChipFactory().fromAST(ast), hdl }; +} + +export function simulateCircuit(mission: Mission, circuit: Circuit, inputs: Signals): Simulation { + const { chip, hdl } = compile(mission, circuit); + for (const pin of mission.inputs) { + if (typeof inputs[pin] !== 'boolean') throw new Error(`Input ${pin} needs an on/off value.`); + chip.setInput(pin, inputs[pin]); + } + chip.run(); + const signals: Signals = { ...inputs }; + circuit.gates.forEach((gate, index) => { + signals[gate.id] = chip.getPins().get(`gamewire${index}`)?.state ?? false; + }); + const outputs = Object.fromEntries(mission.outputs.map((pin) => [pin, chip.getOutput(pin)])); + return { outputs, signals, hdl }; +} + +/** Fresh simulation instances avoid carrying state between exhaustive truth cases. */ +export function testCircuit(mission: Mission, circuit: Circuit): TestResult { + const hdl = toHDL(mission, circuit); + const rows = truthTable(mission).map(({ inputs, expected }) => { + const actual = simulateCircuit(mission, circuit, inputs).outputs; + return { + inputs, + expected, + actual, + passed: mission.outputs.every((pin) => actual[pin] === expected[pin]), + }; + }); + const passed = rows.filter((row) => row.passed).length; + return { rows, passed, total: rows.length, success: passed === rows.length, hdl }; +} diff --git a/src/lib/game/missions.ts b/src/lib/game/missions.ts new file mode 100644 index 0000000..a396fc5 --- /dev/null +++ b/src/lib/game/missions.ts @@ -0,0 +1,220 @@ +/** First campaign, adapted from the existing Boolean Logic project. */ +export type MissionId = 'nand' | 'not' | 'and' | 'or' | 'xor' | 'mux' | 'dmux'; +export type GateKind = 'Nand' | 'Not' | 'And' | 'Or' | 'Xor'; +export type Signals = Record; + +export interface Mission { + id: MissionId; + chip: string; + number: number; + title: string; + location: string; + brief: string; + goal: string; + inputs: string[]; + outputs: string[]; + availableParts: GateKind[]; + hints: string[]; + explanation: string; + reward: string; + prerequisite: MissionId | null; +} + +export const missions: Mission[] = [ + { + id: 'nand', + chip: 'Nand', + number: 1, + title: 'Find the first signal', + location: 'Arrival dock', + brief: + 'An old signal chip still works. Wake it up and discover the one input combination that switches its light off.', + goal: 'Explore both switches, then run the four-case signal check.', + inputs: ['a', 'b'], + outputs: ['out'], + availableParts: ['Nand'], + hints: [ + 'Try each switch separately. What happens when both are on?', + 'NAND means NOT AND. Its output is off only when both inputs are on.', + ], + explanation: + 'A bit has two states: 0 (off) and 1 (on). A NAND gate outputs 1 for every input pair except 1,1. You start with this supplied primitive: it is the building block for everything in this campaign.', + reward: 'NAND chip discovered', + prerequisite: null, + }, + { + id: 'not', + chip: 'Not', + number: 2, + title: 'Bring back the beacon', + location: 'Beacon point', + brief: + 'The beacon needs an inverter: when its input is off, the light must be on. When the input is on, the light must go off.', + goal: 'Build an output that is always the opposite of input in.', + inputs: ['in'], + outputs: ['out'], + availableParts: ['Nand'], + hints: [ + 'A NAND has two input pins. They can both receive the same signal.', + 'Look at the NAND cases 0,0 and 1,1. Can those two cases give you an inverter?', + ], + explanation: + 'NOT reverses a bit: 0 becomes 1 and 1 becomes 0. A gate input can feed more than one pin. This is called fan-out. Connecting one signal to both NAND inputs gives the behavior you need.', + reward: 'Beacon restored · NOT chip earned', + prerequisite: 'nand', + }, + { + id: 'and', + chip: 'And', + number: 3, + title: 'Open the safety lock', + location: 'Twin-lock bridge', + brief: + 'The bridge may open only when both safety switches are on. A single switch must never open it.', + goal: 'Output 1 only when a and b are both 1.', + inputs: ['a', 'b'], + outputs: ['out'], + availableParts: ['Nand', 'Not'], + hints: [ + 'NAND already responds to both switches. What is different about its output?', + 'Try sending the NAND output through your inverter.', + ], + explanation: + 'AND outputs 1 only if both inputs are 1. NAND has exactly the opposite behavior, so inverting NAND produces AND. Internal connections let one chip feed another.', + reward: 'Bridge restored · AND chip earned', + prerequisite: 'not', + }, + { + id: 'or', + chip: 'Or', + number: 4, + title: 'Power the workshop', + location: 'Workshop quay', + brief: + 'Two generators feed the workshop. Either one should keep the lights on; the workshop goes dark only when both are off.', + goal: 'Output 1 when a, b, or both are 1.', + inputs: ['a', 'b'], + outputs: ['out'], + availableParts: ['Nand', 'Not', 'And'], + hints: [ + 'Start with the only case that must give 0: a=0 and b=0.', + 'What happens if you invert both inputs before feeding them into NAND?', + ], + explanation: + 'OR outputs 1 when at least one input is 1. De Morgan’s law says NOT(NOT(a) AND NOT(b)) equals a OR b. Different networks can implement the same truth table: all correct constructions are accepted.', + reward: 'Workshop restored · OR chip earned', + prerequisite: 'and', + }, + { + id: 'xor', + chip: 'Xor', + number: 5, + title: 'Tune the signal tower', + location: 'Relay ridge', + brief: + 'The relay needs a difference detector. It should transmit when the switches disagree, and stay quiet when they match.', + goal: 'Output 1 when exactly one of a and b is 1.', + inputs: ['a', 'b'], + outputs: ['out'], + availableParts: ['Nand', 'Not', 'And', 'Or'], + hints: [ + 'OR finds cases where at least one input is on, but also includes 1,1.', + 'Combine “at least one is on” with “not both are on”.', + ], + explanation: + 'XOR means exclusive OR. Its output is 1 for 0,1 and 1,0, but 0 for 0,0 and 1,1. One construction is (a OR b) AND NOT(a AND b). NAND-only solutions also work.', + reward: 'Relay restored · XOR chip earned', + prerequisite: 'or', + }, + { + id: 'mux', + chip: 'Mux', + number: 6, + title: 'Choose the broadcast', + location: 'Switchboard station', + brief: + 'Two signals are ready for broadcast. Build a switchboard that follows channel a when sel is off and channel b when sel is on.', + goal: 'When sel=0, out=a. When sel=1, out=b.', + inputs: ['a', 'b', 'sel'], + outputs: ['out'], + availableParts: ['Nand', 'Not', 'And', 'Or', 'Xor'], + hints: [ + 'Make two paths: one for channel a and one for channel b. Only one path should be enabled at a time.', + 'Enable a with NOT(sel), enable b with sel, then combine their outputs.', + ], + explanation: + 'A multiplexor selects one of its data inputs. The selector is a control bit, not a data channel. There are eight possible input combinations, and your circuit must handle all eight. It can be composed from the elementary gates you have already built.', + reward: 'Switchboard restored · selector blueprint earned', + prerequisite: 'xor', + }, + { + id: 'dmux', + chip: 'DMux', + number: 7, + title: 'Reconnect the islands', + location: 'Dispatch harbor', + brief: + 'One incoming signal must reach one of two islands. Send it to output a when sel is off, or output b when sel is on. The other output stays off.', + goal: 'Route in to a if sel=0, or to b if sel=1.', + inputs: ['in', 'sel'], + outputs: ['a', 'b'], + availableParts: ['Nand', 'Not', 'And', 'Or', 'Xor'], + hints: [ + 'Unlike the last machine, this one has one data input and two outputs.', + 'Each output needs its own enable condition. What enables a? What enables b?', + ], + explanation: + 'A demultiplexor routes one input to a selected output. For a two-way DMux: a = in AND NOT(sel), and b = in AND sel. These are separate output pins. Both must be correct in every case.', + reward: 'Islands reconnected · Boolean Logic campaign complete', + prerequisite: 'mux', + }, +]; + +export function getMission(id: string): Mission { + const mission = missions.find((item) => item.id === id); + if (!mission) throw new Error(`Unknown mission: ${id}`); + return mission; +} + +export interface TruthCase { + inputs: Signals; + expected: Signals; +} + +/** Exhaustive single-bit specifications, independent from the circuit under test. */ +export function truthTable(mission: Mission): TruthCase[] { + return Array.from({ length: 2 ** mission.inputs.length }, (_, index) => { + const inputs = Object.fromEntries( + mission.inputs.map((pin, offset) => [ + pin, + Boolean(index & (1 << (mission.inputs.length - offset - 1))), + ]), + ); + const { a, b, in: input, sel } = inputs; + let expected: Signals; + switch (mission.id) { + case 'nand': + expected = { out: !(a && b) }; + break; + case 'not': + expected = { out: !input }; + break; + case 'and': + expected = { out: a && b }; + break; + case 'or': + expected = { out: a || b }; + break; + case 'xor': + expected = { out: a !== b }; + break; + case 'mux': + expected = { out: sel ? b : a }; + break; + case 'dmux': + expected = { a: input && !sel, b: input && sel }; + break; + } + return { inputs, expected }; + }); +} diff --git a/src/lib/hardware-simulator/chips/CustomChip.test.ts b/src/lib/hardware-simulator/chips/CustomChip.test.ts index ae3ac41..004e5fe 100644 --- a/src/lib/hardware-simulator/chips/CustomChip.test.ts +++ b/src/lib/hardware-simulator/chips/CustomChip.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import type { ChipPin } from './Chip'; import type Chip from './Chip'; import NotChip from './builtin/NotChip'; +import NandChip from './builtin/NandChip'; import CustomChip from './CustomChip'; const name = 'Foo'; @@ -98,4 +99,47 @@ describe('Custom Chip', () => { expect(() => chip.run()).toThrowError(`Implementation contains a loop`); }); + + it.each([13, 24])( + 'evaluates a %i-stage NAND inverter chain without mistaking fan-out for a loop', + (length) => { + const gates = Array.from({ length }, () => new NandChip()); + const chainPins = new Map([ + [ + 'in', + { + state: false, + type: 'input', + connections: [ + [gates[0], 'a'], + [gates[0], 'b'], + ], + }, + ], + ['out', { state: false, type: 'output', connections: [] }], + ]); + const chainParts = new Map(); + gates.forEach((gate, index) => { + const wire = index === length - 1 ? 'out' : `wire${index}`; + if (wire !== 'out') { + chainPins.set(wire, { + state: false, + type: 'internal', + connections: [ + [gates[index + 1], 'a'], + [gates[index + 1], 'b'], + ], + }); + } + chainParts.set(gate, [['out', wire]]); + }); + const chip = new CustomChip('InverterChain', chainPins, chainParts); + + for (const input of [false, true, false]) { + chip.setInput('in', input); + chip.run(); + expect(chip.getOutput('out')).toBe(length % 2 === 0 ? input : !input); + } + }, + ); }); diff --git a/src/lib/hardware-simulator/chips/CustomChip.ts b/src/lib/hardware-simulator/chips/CustomChip.ts index 7886c15..37fbf91 100644 --- a/src/lib/hardware-simulator/chips/CustomChip.ts +++ b/src/lib/hardware-simulator/chips/CustomChip.ts @@ -87,14 +87,19 @@ class CustomChip implements Chip { public run() { // Execute using breadth-first exploring const queue = new Denque(); + const pending = new Set(); const explore = (pin: ChipPin) => { for (const [chip, pinName] of pin.connections) { // Set chip internal pin state chip.setInput(pinName, pin.state); - // Plan to visit the connected chip next - queue.push(chip); + // A signal may feed several pins on the same chip. Update every pin, + // but evaluate a pending chip only once with its latest input values. + if (!pending.has(chip)) { + pending.add(chip); + queue.push(chip); + } } }; @@ -110,6 +115,9 @@ class CustomChip implements Chip { let next = queue.shift(); while (next) { + // Remove before running so feedback can schedule another evaluation; + // real loops must still reach the iteration guard below. + pending.delete(next); // 1. Run the chip logic next.run(); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index deeefd1..7b022cc 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,115 +1,12 @@ - - Chips and Code: Learning what our computers are made of + Chips and Code — Bring a world to life, one chip at a time + - -
- -
-
-
-
-
-

- Wondering how computers work? -

-

- Find out by embarking on the journey of building your own computer from scratch, from - chips to code. No prerequisites - start for free from the browser. -

-
-
-
- - - - Start Learning 🤯 -
-
-
-
-
-
- -