A live game show control system for Split The Difference — a 5-round betting game where two teams wager points each round and win or lose them based on challenge outcomes.
Built with Node.js + Express, WebSocket, and OSC. Designed to run in Docker and integrate with QLab (score display) and Elgato Stream Deck (result triggers).
Latest Version: v2.0.0 — Automatic winner detection after Round 5 with QLab cue arming (BLUEWIN / REDWIN).
| Feature | Detail |
|---|---|
| Teams | 2 teams, split-screen left/right UI |
| Starting score | 100 points each |
| Rounds | 5 named rounds |
| Betting | +5 increments, max = current score |
| Scoring | Correct = score + bet · Incorrect = score − bet |
| OSC out | Scores pushed to QLab text cues after every change |
| OSC in | Stream Deck triggers correct/incorrect per team |
| Web UI | Operator-facing control panel at port 4002 |
| Settings | Passcode-locked settings page (default: 8888) |
- Risk It For a Biscuit
- Bet Your Bottom Dollar
- Mass Mayhem
- Push Your Luck
- Double or Disaster
| Dependency | Version | Notes |
|---|---|---|
| Docker | 24+ | Required for containerised deployment |
| Docker Compose | v2 (plugin) | Included with Docker Desktop |
| QLab | 4 or 5 | Receives score text via OSC on port 53000 |
| Elgato Stream Deck | Any | Sends OSC UDP messages to port 3003 |
| Node.js | 22 (Alpine, inside Docker) | No host install needed |
No Node.js install required on the host machine. Everything runs inside the Docker container.
git clone https://github.com/TwistedMelonIO/split-the-difference.git
cd split-the-differenceThe defaults work out of the box. To customise, copy the example file:
cp .env.example .envEdit .env as needed:
QLAB_HOST=host.docker.internal # macOS default — reaches host QLab from container
QLAB_PORT=53000 # QLab OSC listen port
OSC_LISTEN_PORT=3001 # Internal OSC receive port (mapped to 3003 externally)
SCORE_L_CUE=SCORE_L # QLab cue name for left team score
SCORE_R_CUE=SCORE_R # QLab cue name for right team scoredocker compose up -d --buildThe first build downloads the Node 22 Alpine image and installs dependencies (~30 seconds on fast connection).
Navigate to http://localhost:4002 in any browser on the host machine.
Pull the latest code and rebuild:
git pull
docker compose down
docker compose up -d --build| Port | Protocol | Purpose |
|---|---|---|
4002 |
TCP | Web UI (operator control panel) |
3003 |
UDP | OSC input — Stream Deck messages |
Port
3003/udpmust be reachable from the Stream Deck network. If the Stream Deck is on the same machine, use127.0.0.1:3003.
Configure your Stream Deck OSC plugin to send UDP messages to the host running the container:
Round Control (v1.1.0+)
| OSC Address | Action |
|---|---|
/std/round/1 |
Start Round 1 (Risk It For a Biscuit) |
/std/round/2 |
Start Round 2 (Bet Your Bottom Dollar) |
/std/round/3 |
Start Round 3 (Mass Mayhem) |
/std/round/4 |
Start Round 4 (Push Your Luck) |
/std/round/5 |
Start Round 5 (Double or Disaster) |
Result Triggers
| OSC Address | Action |
|---|---|
/std/team1/correct |
Team 1 answered correctly (score + bet) |
/std/team1/incorrect |
Team 1 answered incorrectly (score − bet) |
/std/team2/correct |
Team 2 answered correctly (score + bet) |
/std/team2/incorrect |
Team 2 answered incorrectly (score − bet) |
/std/reset |
Master reset (all scores → 100) |
- OSC messages are only acted on during the PLAYING phase (except round start commands)
- Per-round commands are available in v1.1.0+
Scores are pushed automatically after every result and on master reset.
Score Updates
/cue/SCORE_L/text "100"
/cue/SCORE_R/text "85"
In QLab, create two Text cues named SCORE_L and SCORE_R. The cue names can be changed via the Settings page.
Winner Cue Arming (v2.0.0+)
After Round 5, when both results are in, the server automatically compares final scores and arms the winning cue:
| Outcome | Armed | Disarmed |
|---|---|---|
| Blue wins (or tie) | /cue/BLUEWIN/armed 1 |
/cue/REDWIN/armed 0 |
| Red wins | /cue/REDWIN/armed 1 |
/cue/BLUEWIN/armed 0 |
In QLab, create two cues named BLUEWIN and REDWIN. Only the winner's cue will be armed after the final round.
IDLE → BETTING → PLAYING → REVEAL → IDLE (next round)
| Phase | What happens |
|---|---|
| IDLE | Waiting. Press "Start Round N" to begin. |
| BETTING | Each team sets their bet in +5 increments and locks it. |
| PLAYING | Waiting for Stream Deck result triggers. |
| REVEAL | Both results shown. Scores updated. Advance to next round. |
After Round 5 the game ends. Use Master Reset to start a new game.
| Key | Action |
|---|---|
Space |
Advance phase (Next Phase button) |
Q |
Team 1 bet +5 |
A |
Team 1 bet −5 |
W |
Lock Team 1 bet |
P |
Team 2 bet +5 |
L |
Team 2 bet −5 |
O |
Lock Team 2 bet |
Access at http://localhost:4002/settings.html
Default passcode: 8888
From the settings page you can:
- Change QLab cue names for left and right scores
- Rename Team 1 and Team 2
- View the live application log
- Trigger a master reset
┌─────────────────────────────────────────────┐
│ Docker Container │
│ │
│ Node.js server.js │
│ ├── Express (HTTP API + static files) │
│ ├── WebSocket (ws) — real-time UI sync │
│ ├── OSC Client → QLab :53000 │
│ └── OSC Server ← Stream Deck :3001 │
│ │
│ public/ │
│ ├── index.html — operator control panel │
│ ├── app.js — frontend WebSocket logic │
│ ├── styles.css — neon arena design │
│ ├── settings.html — passcode-locked config │
│ ├── settings.css │
│ └── settings.js │
└─────────────────────────────────────────────┘
│ :4002 (web) │ :3003/udp (OSC in)
▼ ▼
Browser Stream Deck
│ :53000/udp (OSC out)
▼
QLab
# Start (detached)
docker compose up -d
# Stop
docker compose down
# Rebuild after code changes
docker compose up -d --build
# View live logs
docker logs -f split-the-difference
# Restart without rebuilding
docker compose restart| Variable | Default | Description |
|---|---|---|
QLAB_HOST |
host.docker.internal |
QLab hostname (macOS Docker host alias) |
QLAB_PORT |
53000 |
QLab OSC port |
OSC_LISTEN_PORT |
3001 |
Internal OSC receive port |
SCORE_L_CUE |
SCORE_L |
QLab cue name — left team score |
SCORE_R_CUE |
SCORE_R |
QLab cue name — right team score |
QLab not receiving scores
- Confirm QLab's OSC input is enabled on port 53000 (QLab → Settings → OSC)
- Confirm
QLAB_HOST=host.docker.internal(correct for macOS Docker Desktop) - Check the application log (Settings page → Application Logs)
Stream Deck OSC not triggering results
- Confirm the Stream Deck OSC plugin target is
127.0.0.1:3003(or the host IP on a remote machine) - Confirm the container is running:
docker ps - OSC messages are only acted on during the PLAYING phase
Port conflict on 4002 or 3003
- Edit
docker-compose.ymland change the host-side port mapping (left side of the colon) - Example: change
"0.0.0.0:4002:3000"to"0.0.0.0:5002:3000"
Container won't start
- Run
docker logs split-the-differenceto see startup errors - Ensure Docker Desktop is running
Proprietary — © Twisted Melon. All rights reserved.