Skip to content
sp1le.tech
UA
Running 2026 Realtime browser game

Voice Duel

A question appears; whoever says the correct answer out loud first hits the other one. Health runs out, the duel ends. It has accounts, friends, private rooms with codes, a public lobby and a bot for the very common case of arriving alone. It runs on one Node process with a single runtime dependency.

Node · WebSocket · Web Speech API Open it · sp1le.tech/voiceduel

Constraint

Speech recognition, matchmaking and an opponent for people who arrive alone - on a hobby budget, with no API key for the player to obtain and no audio leaving their browser.

Decisions

  • 4 records
  • each with its bill

Decision record

Plain web, using the browser's own Web Speech API

Instead of
Unity WebGL
Because
Recognition is already in the browser: free, no keys, no audio upload, latency in the hundreds of milliseconds. A WebGL build would reach the same API through a JS bridge and charge a multi-megabyte download for the privilege.
Costs me
Web Speech support differs across browsers, so the client carries its own recovery path for recognition dying in the middle of a round.

One runtime dependency: ws

Instead of
A framework, an ORM, a test runner
Because
Node already ships an HTTP server, a SQLite driver and a test runner. Every dependency I decline is one I never have to patch at midnight on a box I maintain alone.
Costs me
Routing, sessions and schema handling are mine to write, and they are plainer than the libraries I skipped.

One answer parser, imported by both the client and the server

Instead of
Comparing recognised text against the expected string
Because
Speech gives you "twenty one", "21" and "twenty-one" for one answer. If the rule for what counts as correct exists twice, players eventually watch the judge disagree with itself - and that ends the game more effectively than any bug.
Costs me
That file can never diverge, so it lives in shared/ and is the most heavily tested thing in the project.

A bot opponent modelled as a reaction-time distribution

Instead of
Matchmaking only
Because
Almost everyone who opens a hobby game arrives alone. An empty lobby is not a smaller experience, it is the end of the session.
Costs me
Its difficulty is a hand-tuned opinion rather than something measured against real players.

Numbers

  • measured, not rounded

Size of the thing

2 401
lines of JavaScript
1
runtime dependency
4
test suites, no framework
0
bytes of audio leaving the browser

Stack

  • Node.js
  • ws
  • node:sqlite
  • node:test
  • Web Speech API
  • Vertex AI
  • nginx

Retro

  • written after, not before

What I would change

The answer parser lives in shared/ because the alternative - two parsers agreeing by luck - is the kind of bug that only shows up in front of players, when it is too late to be quiet about it. The game also lives under a path rather than a subdomain, which means no absolute path may ever appear inside it: a constraint I now carry in my head forever instead of having configured once.

Next

Contact

  • Full-cycle software engineer

Start a conversation

Tell me what you are building and what is in the way. I answer personally, usually the same day.