Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions games/bcook/meltdown/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.wasm
*.out
# native dev binary (`go build` drops it, named after the module dir)
/meltdown
# smoke output (`go run . -smoke` / `shellcade-kit smoke`)
/smoke-out
21 changes: 21 additions & 0 deletions games/bcook/meltdown/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Brandon Cook

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
135 changes: 135 additions & 0 deletions games/bcook/meltdown/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Meltdown ☢

A co-op reactor-room panic for the terminal, built on the
[shellcade/kit](https://github.com/shellcade/kit) developer kit and playable
over SSH at [shellcade.com](https://shellcade.com).

The whole 80×24 screen is a failing reactor ship: rooms and corridors drawn in
box characters around a glowing core. You and your crew (one engineer per
player) run the halls putting out faults that keep erupting at the stations and
**get worse the longer you ignore them**. Every unfixed fault eats the core's
shared integrity, and faults spawn faster and faster — so the run *always* ends.
Your score is how long the crew survives.

```
CORE 100% 0s
┌─────────────────────────┬──────────────────────────┬─────────────────────────┐
│ │ │ │
│ · ☺ · · · · · ≈ · │
│ │
│ │ │ │
├────────────· ───────────┼──────────── ·────────────┼──────────── ·───────────┤
│ │ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ · · · ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓· ▲ · · │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ │ ☺ │
├──────────── ───────────┼──────────── ────────────┼──────────── ───────────┤
│ ≈ · · │ Φ · · │ · · · │
│ │ │ │
│ · Φ · · · · · · · │
└─────────────────────────┴──────────────────────────┴─────────────────────────┘
LEAK [████████░░░░░░░░] MASH SPACE
↑↓←→/hjkl move SPACE patch/hold type valve keys Q leave FAULTS 5
```

The `▓` block at the centre is the reactor core — it **pulses, and its glow
shifts from hot yellow toward a dim cold red as integrity falls**. The glow *is*
the shared health bar. Each engineer (`☺`, your own drawn reverse-video so you
can find yourself) runs the bays fixing whatever erupts.

## Controls

| Key | Action |
|---------------------------|---------------------------------------------------|
| `↑ ↓ ← →` (or `k j h l`) | Walk one cell |
| `Space` | Patch a leak (mash) / smother a fire (hold) |
| letter keys | Crack a jammed valve — type the shown sequence |
| `Q` / `Esc` | Leave the reactor |

## The faults

| Glyph | Fault | How to fix it |
|-------|---------------|--------------------------------------------------------------------|
| `≈` | **Leak** | Stand on it and **mash `Space`** to patch it shut. |
| `▲` | **Fire** | Stand on or next to it and **hold `Space`** — let go early and it regrows. |
| `Φ` | **Jammed valve** | Stand on it and **type the shown 3–4 key sequence**; a wrong key re-jams it. |
| `◊` | **Breach** | Takes **two crew standing on it at once** — only ever spawns with a crew of 2+. |

While you're on (or beside) a fault, a labelled **progress bar** shows your fix
filling up, plus a hint of what to press. Walk away from a half-fixed fire and
it grows back; leave any fault unattended and it keeps draining the core, biting
harder the longer it festers.

## Solo & crew

**Solo is first-class.** A lone engineer plays the exact same loop with the
fault cadence scaled down to one pair of hands, and two-person breaches never
spawn. As more crew join, the spawn rate rises **sub-linearly** (it scales with
the square root of crew size), so the work per person *falls* — a bigger crew is
an easier shift. Recruiting friends is the whole incentive.

When the core finally hits zero the reactor melts down, and an end screen shows
the crew's survival time and each member's fix count. Best survival time (in
seconds) rides the arcade leaderboard.

## Develop

This is a standard Go program in the inner loop — no wasm, no network, no setup.

```sh
go run . # play it in your terminal
go run . -seats 3 # hot-seat 3 engineers (Ctrl-T switches the active seat)
go run . -heartbeat 33ms # snappier frame rate
go test ./... # logic tests (each fault's fix, core damage, scaling, scoring)
```

`go test -run TestSnapshot -v` prints a plain-text snapshot of a composed frame,
handy for eyeballing layout changes.

## Build the arcade artifact

The arcade runs a sandboxed WebAssembly build. With
[TinyGo](https://tinygo.org) installed:

```sh
tinygo build -opt=1 -no-debug -gc=leaking \
-o meltdown.wasm -target wasip1 -buildmode=c-shared .
```

Then verify, play, and smoke it via the kit CLI:

```sh
shellcade-kit check meltdown.wasm
shellcade-kit play meltdown.wasm --seats 3
shellcade-kit smoke . # runs smoke.yaml, writes the preview shots
```

## How it works

The game implements the kit `Game` + `Handler` contract — callbacks the arcade
invokes one at a time:

- `OnStart` builds the reactor floor plan and schedules the first fault.
- `OnJoin` / `OnLeave` add and remove engineers (state is keyed by `AccountID`
so it survives room hibernation) and load/persist the durable best time.
- `OnInput` walks an engineer one cell, mashes a leak, or taps a valve key. Fire
and breach **holds** are derived from terminal auto-repeat via the kit
`keyhold` helper (terminals have no key-up event).
- `OnWake` is the ~20 Hz heartbeat: it advances the time-based fixes, drains the
core by every active fault, erupts new faults on the accelerating schedule,
ends the run when the core dies, and renders a per-engineer frame.

| File | Responsibility |
|---------------|-------------------------------------------------------------|
| `main.go` | Game metadata + room factory + native entrypoint |
| `exports.go` | The eight ABI export trampolines (wasm build only) |
| `types.go` | Geometry, fault/crew structs, tuning constants |
| `room.go` | Handler callbacks, ship layout, movement, faults, spawning |
| `render.go` | Composing the 80×24 frame (reactor, core, faults, HUD) |

## Future ideas

A *saboteur* mode — one crew member secretly worsening faults instead of fixing
them — is an obvious extension, deliberately left out of v1 to keep the co-op
loop tight.
33 changes: 33 additions & 0 deletions games/bcook/meltdown/exports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build wasip1 || tinygo.wasm

package main

import kit "github.com/shellcade/kit/v2"

func init() { kit.Run(Game{}) }

// The eight ABI exports, trampolined to the kit SDK.

//go:export shellcade_abi
func expABI() int32 { return kit.ExportABI() }

//go:export meta
func expMeta() int32 { return kit.ExportMeta() }

//go:export start
func expStart() int32 { return kit.ExportStart() }

//go:export join
func expJoin() int32 { return kit.ExportJoin() }

//go:export leave
func expLeave() int32 { return kit.ExportLeave() }

//go:export input
func expInput() int32 { return kit.ExportInput() }

//go:export wake
func expWake() int32 { return kit.ExportWake() }

//go:export close
func expClose() int32 { return kit.ExportClose() }
12 changes: 12 additions & 0 deletions games/bcook/meltdown/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module shellcade.games/bcook/meltdown

go 1.25.0

require github.com/shellcade/kit/v2 v2.11.0

require (
github.com/extism/go-pdk v1.1.3 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/term v0.43.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
12 changes: 12 additions & 0 deletions games/bcook/meltdown/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/shellcade/kit/v2 v2.11.0 h1:JCdxEn7hgspkhQGsPJCGiJxm8sNBxdp4k/ujyNl8lgw=
github.com/shellcade/kit/v2 v2.11.0/go.mod h1:6OXhYu2vu468CiBcO8ps8DVlGdvOdAFGr3ZK4Z2VNgk=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
61 changes: 61 additions & 0 deletions games/bcook/meltdown/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Meltdown — a co-op reactor-room panic for shellcade.
//
// The whole 80x24 terminal is a failing reactor ship: rooms and corridors
// drawn in box characters around a glowing core. One crew member per player
// runs the halls (arrows / hjkl) putting out faults that erupt at the
// stations and worsen if neglected — LEAKs to patch, FIREs to smother,
// JAMMED VALVEs to key open, and (with a full crew) BREACHes that take two
// bodies at once. Every unfixed fault eats the core's shared integrity, and
// faults spawn faster and faster, so the run ALWAYS ends. Your score is how
// long the crew survives.
//
// Solo is first-class: the same loop, faults scaled down for one engineer and
// no two-person faults. More crew raises the spawn rate only sub-linearly, so
// a bigger crew is an easier shift — bring friends.
//
// Dev loop: go run . (plays in this terminal; -seats N for hot-seat)
//
// Artifact: tinygo build -opt=1 -no-debug -gc=leaking \
// -o meltdown.wasm -target wasip1 -buildmode=c-shared .
package main

import kit "github.com/shellcade/kit/v2"

func main() { kit.Main(Game{}) }

// Game is the module registry entry: static metadata plus the room factory.
type Game struct{}

// Meta returns the static game metadata shown in the arcade. The bare slug
// "meltdown" is namespaced to "bcook/meltdown" by the platform from the
// catalog path; this Meta carries the bare name only.
func (Game) Meta() kit.GameMeta {
return kit.GameMeta{
Slug: "meltdown",
Name: "Meltdown",
ShortDescription: "Keep a failing reactor alive: patch leaks, smother fires, and crack jammed valves before the core dies.",
MinPlayers: 1,
MaxPlayers: 6,
Tags: []string{"co-op", "action", "party", "survival"},

// Player characters: each crew member's tile renders beside their name
// on the roster and as their body in the reactor, visible to everyone.
CtxFeatures: kit.CtxFeatCharacter,

QuickModeLabel: "Crew shift",
SoloModeLabel: "Lone engineer",
PrivateInviteLine: "Friends join your reactor crew when they enter the code.",

Leaderboard: &kit.LeaderboardSpec{
MetricLabel: "Survival",
Direction: kit.HigherBetter,
Aggregation: kit.BestResult,
Format: kit.Integer,
},
}
}

// NewRoom returns the per-room behavior.
func (Game) NewRoom(cfg kit.RoomConfig, svc kit.Services) kit.Handler {
return newRoom(cfg, svc)
}
Loading
Loading