In early 1983 I wrote this game for the TRS-80 Color Computer, and submitted it to The Rainbow magazine.
Submission was via mailing a cassette tape with the source code. It was accepted and appeared in the January 1984 issue. I believe I was paid something on the order of $25. American!
The original source is in snaker.bas.
GAMEPLAY.md explains how to play the (ported) game.
The game relies on the screen scrolling mechanism to move everything up one line. So I draw the "cars" at the bottom and they move along the highway via the entire screen scrolling up. The snake starts at the top and gradually moves down (or back up if it collides with a car). This gives the snake-like appearance as the player moves left and right.
Taking advantage of this allowed more to happen on-screen than would be possible with plain BASIC code. To give an idea of just how slow this interpreted language was on this hardware, pausing for one second was achieved via a no-op for loop of 460 iterations. Keeping source code as small as possible (e.g. single-character variable names, avoiding all unnecessary whitespace) was a significant factor in application performance on the CoCo.
I stumbled across this idea while playing with a Timex Sinclair 1000. I was probably trying to write a Pong-style game with a paddle moving left and right at the bottom of the screen when I unintentionally triggered scrolling and saw the snake-like pattern.
As I was building my website to list my past and current projects, I knew I'd have to include Snaker. I started by using Preview on the iPhone to capture the pages with the source listing from a physical copy of the magazine, then had Claude Cowork pull out the source from the pages and save it. From there I used Claude Code to create a JavaScript port.
I wrote a few more games on the CoCo after this, mostly in assembly language, but none of them were accepted for publication. I no longer have the source code for these.
Snaker is built as a self-contained engine — drop it into any host page.
<div style="width: 768px; aspect-ratio: 4/3;">
<canvas id="snaker"></canvas>
</div>
<script type="module">
import { boot } from './src/main.js'
boot(document.getElementById('snaker'))
</script>boot(canvas, options?) → destroy
canvas— requiredHTMLCanvasElement. Must be in the DOM, or supplyoptions.container.options.container—Elementto size against. Defaults tocanvas.parentElement.- Returns a
destroy()function. Idempotent. Tears down listeners, theResizeObserver, audio, and restores the canvas's pre-boot()style state. - Throws on re-entry: calling
boot()on the same canvas withoutdestroy()first throws with a clear message.
- The engine renders at integer scale only.
- Width is required; height optional. With both, scale =
min(max(1, floor(W/256)), max(1, floor(H/192))). With width only (no explicit height and noaspect-ratio), scale =max(1, floor(W/256))and the canvas's content drives the container height. Themax(1, …)clamp guarantees the canvas always renders at least at native resolution. - The width-only mode is detected once at
boot()time by hiding the canvas and reading the container's height in isolation. If the host changes the container's height behavior at runtime (adds/removesaspect-ratio, changes flex layout, etc.), calldestroy()andboot()again to re-detect. - Resize the container freely — a
ResizeObserverre-scales the canvas. Browser viewport resize propagates if the container's size depends on viewport units.
For routers that re-render the page, call destroy() on view unmount:
const destroy = boot(canvas)
document.addEventListener('astro:before-swap', destroy, { once: true })Equivalent patterns work for any framework that fires a "view will unmount" event.
Cross-origin iframe hosts must set allow="autoplay" on the <iframe> for the title music to play. Same-origin embeds (script/module imports from the same origin) are unaffected.
Requires ResizeObserver and Web Audio — modern evergreen browsers.