← Back to Projects
Kamba Adeema cover
Web Game · Cultural Sport

Kamba Adeema

කඹ ඇදීම - the tug of war from the Sinhala and Tamil New Year (Avurudu) festivities, turned into a simple 3D betting game played in front of a crowd of two hundred.

Role
  • Developer
  • Game designer
Genre
  • Tug of war
  • draw-no-bet
Return
97.09% (closed form)
Stack
  • Next.js 15
  • React 19
  • three.js
Scale
  • ~16k lines
  • 200 instanced spectators
Fairness
Block seed, sealed per match

01The Game

Kamba Adeema is the tug of war that usually wraps up a Sri Lankan Avurudu sports day, rebuilt as a betting game. Two halves of a village, Uda Pila and Yati Pila, put four people each on one rope, with around two hundred spectators watching.

On every heave, each of the eight pullers either grips or slips, like eight separate fair coin tosses. The difference between the two sides settles the bet and drags the rope that many marks. Traditional games don’t come with a paytable, so most of the work was figuring out the one honest question you can bet on, and deciding what to leave out.

One honest question

Which side wins this heave? It’s settled in about four seconds, and it’s exactly what anyone watching a tug of war is already wondering.

A separable contest

Stamina, collapsing and the lime lines turn it into a real contest, but none of them are allowed to affect the price.

Put the village in

Without people, the field is just a physics demo. Two hundred spectators, flags and parasols only cost five draw calls.

02Screenshots

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

03Core Loop

  1. 1

    Take the rope

    Four pullers on each side line up on the generated field.

  2. 2

    Back a side

    Pick Uda or Yati and set your bet on a plank with a minus and a plus button.

  3. 3

    Heave

    Eight fair coin tosses decide whether each puller grips or slips.

  4. 4

    Haul

    The difference settles the bet and drags the rope. If one side is exhausted and collapses, the rope moves twice as far.

  5. 5

    The line

    The match ends when one side is dragged over the line, and then the seed is revealed.

04Key Features

A rope that owns the contest

The characters check the rope’s position every frame. Their hands are placed onto its moving curve, and their bodies lean based on how tight it is.

Stamina & collapse

Gripping uses up stamina. If a tired side gets pulled two marks or more, it’s dragged twice as far, which makes real comebacks possible.

An instanced crowd

Five instanced meshes draw around 200 spectators, along with their arms, flags and parasols, on a terraced bank and two corner stands.

Village-fair look

Sun-faded gold, terracotta for Uda, lime green for Yati, hand-painted signs and a carved medallion for the title. The whole thing is meant to feel like a village fair.

DOM mirror

The 3D interface has a hidden HTML copy underneath, with focusable buttons that have ARIA labels, real slider inputs and spoken commentary.

A draw refunds

A four-all heave doesn’t belong to either side, so you get your stake back, the same way a draw works in any draw-no-bet market.

05The Maths

The swing is Uda grips minus Yati grips, a number between −4 and +4. Using Vandermonde’s identity, this simplifies to a shifted Binomial(8, ½), giving 1, 8, 28, 56, 70, 56, 28, 8 and 1 ways out of 256.

The one market

Yati win it 93/256 of the time (36.33%), it’s four all 70/256 of the time (27.34%), and Uda win it 93/256 of the time. Both sides pay 1.92×, and a draw gives your stake back.

Fair is exactly evens

The fair price is (1 − 70/256) / (93/256) = 2.00×, because both sides are the same. The return is 1.92 × 93/256 + 70/256 = 97.09375%.

Stamina never touches the coin

A collapse doubles how far the rope moves, but the bet is always settled on the swing. check() makes sure the probabilities add up to 1, that each outcome is counted only once and that both sides are priced the same.

Measured, not asserted

I wrote a faithful copy of the game logic and ran 6,000,000 heaves and 500,000 matches through it. The swing matched Binomial(8, ½) to within 0.045 percentage points, and the measured return came out between 97.08% and 97.11%.

A match-wide seal

One block value seeds a whole match of about 18 heaves. It’s locked in when the match starts and revealed when one side crosses the line.

06Under the Hood

React ↔ three.js contract

React looks after the wallet, the bet, the match and the current phase. The 3D world gets a read-only WorldView and sends back Intents, which means the 3,500-line scene and the 1,500-line phase machine can change independently.

Wait on the rope, not a clock

The haul phase waits until the rope has finished moving (ropeBusy), so a one-mark nudge and a four-mark collapse each take as long as they look like they should.

Generated pitch

Player positions, rope length, mark size and the lines are all worked out from a few constants, so the field, the rules and the characters always stay in sync.

Server seed route

The RNG token stays on the server, and the endpoint always responds with a 200, so if the upstream service is slow to wake up the browser never sees an error.

07Challenges & Solutions

ChallengeIt’s realistic for a tired team to grip less often, but that would break the price.
SolutionStamina only changes how far the rope moves once the swing has already been decided.
ChallengeThe betting board started out with fifteen priced squares, four chip sizes and a second market.
SolutionI cut it down to seven squares, then three, and finally to two buttons and one stake plank. Every version was priced correctly before I cut it further.
ChallengeWhen the bodies and the rope are animated on separate clocks, the hands come off the rope.
SolutionsetMark is the only thing that can move the contest along, and everything else just reads the rope’s position.

08Results & Takeaways

97.09%
closed-form RTP, measured 97.08-97.11%
2.00×
fair price, exact by symmetry
200
spectators in five draw calls
1
bet, down from fifteen squares
Next.js 15React 19TypeScript (strict)three.jsInstancedMeshWebAudiomulberry32
  • A finished 3D game based on a traditional Sri Lankan sport, with characters that grip the rope properly, an instanced crowd and a full accessibility layer.
  • Every match is locked to a block value from the first heave until one side crosses the line.
  • Next up: publishing a hash of the seed in advance, settling bets on the server, and village-against-village multiplayer.

What I learned

  • The best pricing work I did was cutting things. Fifteen correctly priced squares were worse than one.
  • How easy a bet is to understand matters as much as the maths behind it.
  • Check by measuring. Running a faithful copy over millions of heaves catches things that reasoning alone misses.