Crystal Cavern

Case study

Building feedback without sound

A deaf developer builds an idle game that makes no sound at all. A record of which constraints were taken on, and where the decisions were made.

What it is
An idle game that runs in a browser — one HTML file, roughly 8,000 lines
Built with
HTML, CSS, JavaScript, Canvas 2D. No framework, no build step
External dependencies
One web font. No server, no database, no API
Languages
English, 日本語, 简体中文, 한국어, Español
When
September 2026

Three constraints, set before anything was built

Three conditions were fixed before the first line of code. None of them is the kind of thing you can bolt on later; each one shapes the design from the start.

1. Sound is not available

I am deaf. A game that answers you with sound is a game I cannot enjoy. Adding effects and telling myself "I can't hear it, but it probably feels good" would not have been honest work.

So sound was ruled out at the start — not as something missing, but as a design problem: make the feedback work with vision alone.

2. Keep maintenance to a minimum

I am a beginner at programming. If something breaks, there is no guarantee I can find the cause and fix it. I am also job-hunting with no income, so a server with a monthly bill is out of reach.

Put those two together and there is only one answer. Do not build anything that can break.

No server at all. No login, no database, no external API. Saves live in the browser. The only thing loaded from outside is a single web font.

This was not about taking the easy road. It was about keeping the whole thing inside what I can actually handle. A server you do not have cannot go down, and a service you do not depend on cannot change its terms underneath you. Published and left alone, it will very likely still work in a few years.

3. It has to be usable by me

Accessibility could not be the thing I added if there was time left over. I am the person it affects, so putting it off would have meant putting myself off. It went in from the beginning.


Decision 1: what replaces the sound

The feel of a tap is usually carried by audio. Remove it and a hole opens. Something has to fill it.

The answer turned out to be three responses of different kinds, layered on one action. A tap produces all of these at once:

  • The crystal swells — the thing you touched reacts (standing in for the feel of contact)
  • Shards scatter — they fly outward from the point you touched (evidence that force was applied)
  • A number lifts away — "+120" floats up and fades (confirmation of the result)

All three are visual, but each carries a different meaning. Miss one and the other two still land, so the response never disappears entirely.

The floating number is outlined, because the background colour changes with every seam and without an outline it becomes unreadable against certain ones.

Decision 2: never say it with colour alone

Green for affordable, grey for not — a common pattern, and invisible to some people.

Affordable buildings get a "▸" in front of the price. The border changes and so does the brightness. With no colour perception at all, the symbol and the shape still say it.

The same thinking runs through the rest: the selected setting carries a tick, and pausing dims the whole screen and prints the word. By the end there was nothing left that was signalled by colour alone.

Decision 3: text size moves everything

Text-size controls are common, but they often scale the body copy and leave the numbers and buttons where they were.

Here, one CSS custom property scales every piece of text on the page.

A multiplier called --fs is defined once, and every size is written as calc(14px * var(--fs)). The four buttons set that multiplier to 0.92, 1, 1.14 or 1.32 and nothing else changes.

The numbers drawn on the canvas read the same multiplier to pick their font size, so the HTML and the canvas — two unrelated rendering systems — stay in step from a single value.

Decision 4: contrast measured, then fixed

The first palette looked good and had pale grey text dissolving into the background. Measured, some of it came to 2.8:1. The WCAG threshold is 4.5:1.

Every text colour was measured and the failures were lifted. Then two maximum-contrast schemes were added — pure black on pure white and the reverse. Offering both the palette chosen for looks and the palette chosen for legibility, and letting the reader pick, was the resolution.

Decision 5: the save code, from 312 characters to 47

The code for moving to another device started as the game state as JSON, base64-encoded. That came to 312 characters — far too long to handle by hand.

Looking at it, most of the length was repeated labels like {"crystals":. Fix the order of the values and the labels are unnecessary.

So nineteen numbers are converted to base 36 (0–9 and a–z) and joined with hyphens. The last two characters are a checksum that rejects typos.

CC2-16j8-247h-qglk-c-y-2-14-p-e-6-2-0-0-8o-1-7-0-tlqv01-0i

312 characters became 47 to 60 — about a sixth. The old format still loads, so nobody who had already saved a code was left stranded.

Decision 6: five languages, not added afterwards

Retrofitting translation means hunting strings scattered through the code and hoping you found them all. To avoid that, every string that reaches the screen lives in one translation table.

It is not only the interface. Building names and descriptions, seam names, event names, the full text of the instructions and the wording of the share message are all in there.

Number abbreviations started out doing the same thing. Japanese counts in ten-thousands, English uses K, M and B. "1万" means nothing to an English reader, so those were recalculated per language rather than translated.

Putting them side by side exposed a different problem. Every abbreviation assumes the reader knows its convention. A Japanese reader stumbling over "10K" is the same stumble an English reader has over "1万". In a game that is almost entirely numbers, a number that gets misread is not a small thing.

In the end the abbreviations were removed and the digits left at full length. "600,000,000" is long, but it means the same thing to every reader. The length was handled by shrinking the type as the digits accumulate and letting the tight spots wrap. Only the separator still follows the language.


Two bugs found along the way

Trailing zeros were being eaten

"300 billion" was displaying as "3 billion". The cause was in the number formatter.

A replace(/\.?0+$/, '') meant to tidy up trailing zeros after a decimal point was also stripping them from whole numbers. "3000" became "3"; "12 million" became "1.2 million".

It had been there since the first version and was easy to miss, because small numbers never show the symptom. It now only runs when there is a decimal point.

One missing character in the HTML

Announcements for reaching a new seam and for rebirthing had stopped appearing. A single > was missing, and the three lines that followed had been swallowed as attributes of the unterminated tag.

Browsers parse that without complaint, so nothing looked broken. JavaScript only discovered it when it went looking for an element and got null back.

The nastier part was that the failure took the rest of that function down with it. A separate-looking bug — "the loop counter never increases" — turned out to be the same missing character. Symptom and cause were nowhere near each other.


By the numbers

ItemValue
HTML files1 (about 8,000 lines)
Loaded from outsideone web font
Languages5
Colour schemes5 (2 maximum-contrast)
Text sizes4
Seams12
Save code length47–60 characters
Server costnothing

Afterwards

Working without sound began as something I could not do. By the end it had become a specific design problem — make the feedback work with vision alone. It was the constraint that led to layering three responses on a single tap; without it I would not have looked for that answer.

Accessibility went the same way. Being the person affected meant it could not be deferred, and so it was built in from the start. That cost far less than adding it later would have.

Refusing to use a server was not only about the bill either. With nothing external to depend on, it cannot break once it is published. Opened in a few years, it will most likely still run.

The game is playable from the home page with no sign-up. If you spot something, get in touch.