← Back to Projects
Coin Duel cover
Web Game · 3D Coin Duel

Coin Duel

Five coins are tossed, and whichever face lands on more of them wins. It’s a neon 3D betting arena where the price changes every round but the maths behind it never does.

Role
  • Developer
  • Game designer
Genre
  • Coin-toss duel
  • 2 buttons
Return
Exactly 97.000%
Stack
  • Next.js 15
  • React 19
  • three.js
Rendering
  • WebGL
  • UnrealBloom
  • procedural
Fairness
Block seed, sealed per duel

01The Game

A slot in the floor of a neon arena mints five coins and fires them up into a column of light. Each coin has a SUN on one side and a MOON on the other. You bet on the face you think will land on more of them.

A coin flip is the easiest bet in the world to price, and one of the hardest to make interesting to watch. My goal was to keep the bet exactly that simple, with one question, two buttons and no ties, while giving the game some shape, momentum and the chance of a comeback. The important rule was that none of that extra drama could ever affect the coins themselves.

One question

Two buttons and nothing else. I also built bets on the winning margin and a 5-0 lottery bet, and they were priced correctly, but I cut them because they made the game harder to understand.

Something to watch

Whichever face lands on the majority of coins settles the bet. The size of the win then decides how hard the hit lands in a duel that keeps a running score.

A price that moves

Each face gets a new price every round, taken from a ladder of prices that is never in the player’s favour.

02Screenshots

These screenshots were captured straight from the game. Tap any of them to see it bigger.

03Core Loop

  1. 1

    Lobby

    Two fighters stand on their platforms, each starting with 12 life and a hexagonal shield.

  2. 2

    Bet

    You get 20 seconds to back SUN or MOON at this round’s price.

  3. 3

    Toss

    The mint shoots five coins up into the column of light.

  4. 4

    Settle

    The faces are counted, and the camera keeps all five coins in view until the strike lands.

  5. 5

    Strike

    The score turns into damage. Shields wear down, and once a shield breaks, hits do double damage.

  6. 6

    Review

    This is a proper phase with no timer. It only ends when you press the button.

04Key Features

Five minted coins

With an odd number of coins, every bet is settled. There’s no tie and no refund.

Breakable shields

Shields wear down at a rate that usually breaks them around round seven, which is why the last third of a duel is where the big hits happen.

A procedural neon pit

The coin faces, hex shields, floor patterns, LED scoreboard and a holographic crowd are all generated in code. On top of that, there’s a bloom effect and a custom vignette.

Synthesised sound

Sound effects are generated in WebAudio. The one recorded music track only gets downloaded the first time music is actually needed.

DOM accessibility mirror

Every control and readout in the 3D world has a real HTML copy, so the game works with keyboards and screen readers.

One source of geometry

The sun and moon designs come from a single file of vector paths. That same file is used to draw the logo, the SVG buttons and the canvas texture on the 3D coins.

05The Maths

Let k be the number of coins that land SUN-up, so k follows Binomial(5, ½). The score is 2k − 5, which means it’s always ±1, ±3 or ±5 and never 0.

The bet

The chance of SUN winning is P(k ≥ 3) = 16/32 = ½, which follows from symmetry. So the fair price on either face is exactly 2.00×.

The fight

Damage equals the size of the score. A 3-2 graze happens 62.50% of the time, a 4-1 hit 31.25% and a 5-0 sweep 6.25%. The average damage is 1.875, so a duel starting at 12 life lasts about 12 rounds.

The price ladder

Every round, each face is priced from six matching steps between 1.89× and 1.99×, picked using a second slice of the sealed block value. The average comes out at exactly 1.94×, which makes the return to player exactly 97.000%.

Never above fair

Even the best price, 1.99×, is below the fair 2.00×, so no single round ever favours the player. Someone who only ever bet at the best price would get 99.5% back. A swing of ±0.05 is as wide as the price can go while still being honest.

The shield never touches the coin

Shields only change the damage after the score is known, and the bet is settled on the score alone. check() confirms this, because if a worn-down fighter were more likely to lose a toss, the real price would quietly change.

06Under the Hood

React ↔ three.js contract

React looks after the wallet, the bet, the duel and the current phase, and it hands the 3D world a read-only WorldView. Player input comes back as Intents. The engine/ folder never imports React and components/ never imports three.js. Three other games in the arcade use this same setup.

Crop-proof camera

Every camera shot defines an area that must never be cut off, and the camera works out how far back it needs to be for the current screen shape. That way, phones never lose a coin off the edge.

Self-hosted fonts

The 3D layer draws its text with fillText, so if a web font failed to load, the whole interface would fall back to Times New Roman. Hosting the fonts myself avoids that.

Duel-wide seal

A server route fetches the block value. It seeds mulberry32 for every toss in the duel and is revealed once one of the fighters goes down.

07Challenges & Solutions

ChallengeIf three coins are enough to settle the bet, the other two would just be decoration.
SolutionThe size of the win drives the duel and decides the damage, while the bet itself stays one simple question with an exact price.
ChallengeA fixed price on an even bet makes for a board that nobody bothers to look at twice.
SolutionA moving price that’s the same for both sides, with the best price still below fair. It’s shown before the toss and can be recalculated afterwards.
ChallengePlaytesters had no idea what "Sol takes this volley" meant.
SolutionAfter two rounds of playtesting, I rewrote every word in the game, using plain terms like SUN and MOON, round, score, shield and "lands on more coins". None of the numbers changed.

08Results & Takeaways

97.000%
RTP, exact to every decimal
0
ties - every stake resolves
6
price rungs, none above fair
2
buttons on the board
Next.js 15React 19TypeScript (strict)three.jsUnrealBloomWebAudiomulberry32
  • A finished 3D betting game with a generated arena, breakable shields, a holographic crowd, generated sound and a full HTML copy of the interface for accessibility.
  • Every duel is locked to a block value from the first toss to the final knockout.
  • Next up: publishing a hash of the seed in advance, settling bets on the server, and letting two players duel each other on the same sealed coins.

What I learned

  • Cutting things is the hard part. Every square I removed was priced correctly, but each one made the game worse.
  • Words are part of the design. Getting the wording right took longer than getting the maths right.
  • There’s no good length for a fixed review timer, so the review screen doesn’t have one.