A real, physically-built pinball machine, Portal-themed, running on the Mission Pinball
Framework (MPF).
MPF 0.80 (config_version: 6) — the game logic / rules engine, Python. Config lives in
machinefolder/ as YAML.
OPP hardware platform, gen2 driver boards — real switches, coils, and addressable LEDs.
Three serial chains:
COM4 (chain 0) and COM5 (chain 1) — CobraPin boards, get switches and coils and driveNEO0/NEO1) — the project's board-priority policy fills chain 0COM6 (chain 2) — a 4-board PSOC/red-board chain (2-0-x through 2-3-x), the fallback forSee docs/opp-hardware-reference.md for the full rulebook behind this split.
mpf-gmc (machinefolder/addons/mpf-gmc) — the score display, built on Godot 4. It's a
separate process that talks to MPF core over BCP (a local socket protocol). The addon itself
is stock/unmodified; the project's own slides live under modes/<name>/slides/ (attract, base,
one hit overlay per feature mode, multiball, the story screens of periods 1-4 generated by
tools/display/story_p1_slides.py ... story_p4_slides.py, and the 76
attract pages generated by tools/display/attract_slides.py) — see design/SCREENS.md. Its
known bugs and this project's workarounds: docs/gmc-bugs.md.
board Overviews.xlsx at the repo root is the hardware planning reference — board pinouts per
wing, plus a "Modes" sheet of early game-design notes for the Portal-themed features (orbits,
ramps, lane spells, sling combos, skillshot, drop targets, an "aerial plate", and the game's
namesake ball-transfer "portal" feature). design/drawings/ holds the playfield's technical
drawing (hole positions in mm; the basics so far, toys to follow) - see its README.
Setup (one-time): run install.ps1 from the repo root — creates a .venv and installs
mpf==0.80.0 into it (matches config_version: 6 / the "MPF 0.80" comment in config.yaml).
See docs/testing-strategy.md for how this was verified.
Virtual, no hardware/cabinet needed (recommended for dev): from machinefolder/, run
../.venv/Scripts/mpf -X -t -b. -X forces the smart_virtual platform regardless of the
committed platform: opp, so no config edits are needed and nothing to accidentally leave
committed; it auto-simulates ball devices (trough eject, drop-target resets, etc.) so the ball
routing skeleton works with no manual switch toggling. -t disables MPF's console UI (needed in
non-interactive shells) and -b skips the BCP/Godot connection for a pure backend run — drop -b
to run the Godot mpf-gmc app alongside for a full display+audio playtest. gmc.cfg's [keyboard]
section maps a few switches to number keys for manual testing; extend that mapping as more of the
game gets wired up in code.
Automated tests, no manual interaction at all: .venv/Scripts/python tools/test/stage1.py
from the repo root runs the unit tests (unittest discover tests), the boot check, the config
references check, the fuzz test and the registry check. (Not mpf test — that command requires
kivy/legacy mpf-mc, which this project doesn't use.) The four test stages - this one, scripted
playtests with the display, the guided hardware check, and playing by hand with MPF Monitor -
are in docs/testing-strategy.md.
Real hardware: ../.venv/Scripts/mpf (no -X) from machinefolder/ — MPF uses the opp
platform from config.yaml and talks to the boards over COM4/COM5/COM6. Board/port
assignments can drift if USB enumeration changes — re-run MPF's hardware scan
(mpf hardware scan, a read-only connectivity check) and compare against the pasted scan output
in machinefolder/config/hardware-basic.yaml if switches/coils stop responding.
From a non-interactive session (e.g. Claude Code): plain mpf crashes here — its text UI
(asciimatics) can't open a console screen buffer without a real console attached. Use
tools/mpf-session.ps1 -Action Start instead (run via a PowerShell tool/shell, not Bash — it
tracks the real Windows PID, which a Bash shell's ps/kill don't reliably see for a
backgrounded Windows process). See the script's header comment for the full rationale, and
-Action Stop/Status/Log to manage it. This is unnecessary in your own interactive terminal,
where plain mpf and its text UI work normally.
Display: open machinefolder/ as a Godot 4 project (it autoloads mpf_gmc.gd per
project.godot) and run it alongside a running mpf instance — it connects over BCP
automatically.
tools/wiring_test.py checks a newly-wired switch/coil pair for real — continuity for
switches, and a driver pulse for coils so the CobraPin board's activation LED can be checked
without 50V present (per this project's OPP bring-up guidance,
docs/opp-hardware-reference.md: "test without coil power first"). Defaults to the flipper
pair; pass --switches/--coils (comma-separated device names) to run it against any other
component as it gets wired for real, or --monitor to just watch every switch live — for a
freshly-wired switch whose MPF name you don't know yet, tap it and read the name off whatever
line prints, instead of naming it up front. Add -Probe when starting the session so switches
on free inputs show up too: every unassigned input becomes a temporary probe-<number> switch
(tools/wiring_probe.py), and a tap prints NEW INPUT <number>.
tools\mpf-session.ps1 -Action Start -NoBcp -Probe # -NoBcp is required, see the script's header
.venv\Scripts\python.exe tools\wiring_test.py --monitor # read all switches, live
.venv\Scripts\python.exe tools\wiring_test.py # test the flipper pair
Confirmation prompts are a single Enter press, not typed text, and only appear when a coil is
actually about to be pulsed — --monitor and switch-only runs (no --coils) skip them
entirely, since there's no HV/motion risk to confirm.
It reuses MPF's own service-mode BCP commands (the same ones the interactive mpf service
CLI uses) rather than talking to the hardware directly. See docs/wiring-guide.md for a
readable table of every component's current pin assignments and bring-up status,
docs/board-silkscreen-reference.md for identifying a physical pin from a board photo (real
board photos + a per-pin table cross-referencing every silkscreen label against its MPF number),
docs/wiring-workflow.md for the bench process this fits into,
and docs/wiring-test-tool.md for how it works and the real-hardware gotchas (the -NoBcp
requirement, a Windows USB
selective-suspend gotcha, and flipper coil pulse tuning).
tools/hw_console/ is a small local web tool for tracking real-hardware wiring progress —
per-board status, per-component build-status flags (obtained, restored, wired, tested, ...),
wiring checklists, and a collision check against
hardware-switches.yaml/hardware-coils.yaml before a new component's numbers get assigned. It's
a separate tracking layer, not a replacement for MPF's own config. See
tools/hw_console/README.md for how to run it and the intended workflow.
See docs/online-services.md for where to browse this project's data online (parts inventory,
and the git-synced wiki) from any device — one pointer instead of scattering links
across this file, CLAUDE.md, and the NAS dashboard.
mpf-gmc bugs and pitfalls found while building the display and sound are collected in
docs/gmc-bugs.md, each with this project's workaround.
All printable pages live in print/ (the index lists each one and its generator).
print/wiring-guide.html (the printable bench-test checklist) and
docs/wiring-guide.md (the same board map/components data, browsable in the wiki, harness
diagrams as real docs/wiring-diagrams/*.svg files) are both generated, not hand-edited,
from tools/hw_console/data/components.yaml + machinefolder/config/hardware-*.yaml. So is
print/status-checklist.html: every part's build-status flags as a
printable A4 landscape table, with empty rows for writing in new parts. Open it in a browser and
print. And two design pages from design/features/*.md + the registry: design/overview.md
(features × parts × CAD × status, plus the parts no feature uses yet) and design/playfield.md
(every part by playfield area).
.venv\Scripts\python.exe tools\hw_console\generate_docs.py
Also fires automatically via the PostToolUse hook after Claude Code edits any of the sources. The
coil-bank wiring diagrams are themselves generated by
WireViz from tools/hw_console/data/harnesses/*.yaml —
this needs the wireviz Python package (already in .venv) plus the Graphviz dot binary (not
a Python package — install separately: winget install Graphviz.Graphviz, which needs one admin
prompt). Graphviz's Windows installer doesn't add itself to PATH; on the PC its bin folder is
on the user PATH (reinstalled 2026-10-11 after it went missing), and the generator also looks in C:\Program Files\Graphviz\bin
itself, so an editor or hook started before the PATH change still finds it. The generator writes nothing at all if any harness fails to
render (missing Graphviz included) — by design, so a broken/missing WireViz setup can never
silently leave any of these files half-updated; it fails loudly instead, leaving all of them
exactly as they were until the problem's fixed.
machinefolder/
config/ # config.yaml + hardware-*.yaml (switches, coils, LEDs, ball devices; hardware-draft.yaml = not-wired parts, virtual, no pin numbers)
modes/<name>/
config/<name>.yaml # mode logic
slides/<name>.tscn # Godot slide for this mode
shows/ # optional LED/sound shows
data/ # audits.yaml, machine_vars.yaml (runtime state, not hand-edited)
addons/mpf-gmc/ # the Godot display addon (stock)
sounds/ # sfx/, music/, voice/<purpose>/ (every period's lines, PA callouts, attract)
images/ # attract/ (game art, design/art/README.md), signs/ (chamber-sign icons)
docs/ # opp-hardware-reference, wiring-workflow, wiring-test-tool, wiring-guide (generated),
# board-silkscreen-reference, stern-spike-trough-opto, testing-strategy, sound-library,
# scoring, naming-conventions, gmc-bugs, online-services; references/ = archived copies
# of cited pages; reviews/ = code and config reviews
tests/ # MpfTestCase/MpfGameTestCase suite - run with `python -m unittest discover tests`
design/ # story -> feature -> parts -> mode workflow: features/*.md, STORY.md, rule-sheets.md,
# callouts.md, generated overview/playfield/shot-map; drawings/ = the playfield drawing;
# research/story-chain/ = the review page (serve.py, answers.json)
posts/ # drafts of write-ups for outside sites (e.g. the OPP website)
tools/hw_console/ # local web tool for tracking hardware bring-up (boards + components)
tools/hw_console/generate_docs.py # generates the wiring guide, status checklist, references index and design overview/playfield (see above)
tools/hw_console/templates/ # Jinja2 templates for the generator
tools/hw_console/data/harnesses/ # WireViz harness YAML, rendered into wiring-guide.html/.md
tools/mpf-session.ps1 # start/stop mpf reliably from a non-interactive shell (see Running it)
tools/wiring_test.py # no-HV switch/coil wiring check (see Wiring test tool)
tools/wiring_probe.py # probe mode: a temporary switch on every free input (see Wiring test tool)
tools/sounds/ # catalogue, tag and convert the Portal sound rip (docs/sound-library.md)
tools/sounds/edits.py # the story's audio edits into assets/sounds/edits/ (docs/sound-library.md)
tools/display/story_p1_slides.py # generates period 1's chamber sign and overlay slides (modes/story_p1)
tools/display/story_p2_slides.py # the same for period 2 (modes/story_p2), reusing story_p1_slides.sign()
tools/display/story_p3_slides.py # the same for period 3 (modes/story_p3)
tools/display/story_p4_slides.py # the same for period 4 (modes/story_p4): the sign with the era dial
tools/display/story_p5_slides.py # the same for period 5 (modes/story_p5): his wall and the dens on his murals
tools/display/small_ending_slides.py # the small endings' game-over screens (modes/small_ending)
tools/display/screen_review.py # every screen rendered offline (design/research/screen-review/<date>-<task>/), shown on the review page's Attract and Screens tabs
tools/display/caroline_slides.py, arrows_slides.py # the CAROLINE letters' and the arrows' overlays
tools/display/period_backgrounds.py # periods 2-5's own picture on the left half (a widget each on the base slide)
tools/display/feature_slides.py # the nine feature screens as top-left callouts with an icon
tools/art/callout_icon_candidates.py # icon candidates for those callouts, to pick on the Screens tab
tools/display/attract_slides.py # the attract pages, their rotation and sound (modes/attract_pages) from design/attract-pages.yaml
tools/art/extract_textures.py # game textures from the installed Portal games -> machinefolder/images/attract/ (design/art/README.md)
tools/art/sign_icons.py # the games' chamber-sign icons -> machinefolder/images/signs/ for the story signs
tools/art/attract_derived.py # pictures derived from the textures (the rejoined banner, the decals on a wall)
tools/art/render_toys.py # the toys rendered in Blender from the games' models, two looks (design/art/toy-renders.yaml, design/art/README.md)
tools/art/render_map.py # a view inside one of the games' maps (GLaDOS in her chamber), from design/art/glados-renders.yaml's maps
tools/art/game_shots.py # screenshots of key moments taken in Portal / Portal 2 themselves (design/art/game-shots.yaml; starts the games)
tools/art/online_shots.py # screenshots from the fan wikis' galleries, for picking (design/art/online-shots.yaml; download to a scratch folder)
tools/art/apply_screen_picks.py # the Screens tab's picks into the machine: the periods' left halves and the small endings' pictures
tools/sim/playthrough.py # Monte Carlo playthrough of the rule sheets (design/rule-sheets.md)
tools/sim/scoring.py # checks the scoring table against simulated games (docs/scoring.md: scoring, choices, settings)
tools/story/shot_map.py # design/shot-map.md: every shot and what uses it, per mode, plus today's scoring (generated)
tools/story/final_review.py # the final review page (design/research/story-chain/review.html; serve.py serves it)
tools/story/chain_review.py # the backup page, the whole design workshop (design/research/story-chain/backup.html)
tools/story/print_gamebook.py # printable gamebook from design/short-story-paths.html
tools/test/stage1.py # test stage 1: unit tests, boot, config refs, fuzz, registry (docs/testing-strategy.md)
tools/test/playtest.py # stages 2-3: scripted playtests on MPF + GMC (machinefolder/playtests/), the hardware check
tools/test/session.py # stage 4: play by hand with MPF Monitor, keys and notes; setup_monitor.py, monitor_layout.py
tools/atmega328p-trough-bridge/, tools/flash-atmega328p.ps1 # trough opto bridge firmware + flashing (docs/stern-spike-trough-opto.md)
assets/sounds/ # the Portal rip as OGG: voice/ (by speaker), sfx/, music/, edits/ (commentary: local only)
install.ps1 # one-time dev environment setup (.venv + mpf + jsonschema + jinja2 + flask + soundfile + av)
See design/README.md for the workflow (story idea -> feature -> registry parts -> MPF mode) and the
feature docs in design/features/*.md (Markdown with a schema-checked header) — this is the living source for feature design, replacing
board Overviews.xlsx's "Modes" sheet going forward.
See TODO.md for known gaps (TODO-done.md for the checked-off ones), the story chain review page's To review and Decisions tabs for what the owner reads, listens to or decides, HumanToDo.md for cabinet sessions and work outside the repo and CHANGES.md for the project history (the 2026-08 resumption
roadmap — flippers, rules architecture, feature modes, display/audio, progression — is finished;
see CHANGES.md #3 and #48).