← Back to Projects
Whack-A-Mole cover
Web Game · Priced Skill

Whack-A-Mole

The classic fairground game turned into a fifteen-second betting round. It’s also the only game of skill in the arcade, and the price is still completely honest.

Role
  • Developer
  • Game designer
Genre
  • Reaction game
  • 15s rounds
Return
88-97% ceiling, by speed
Stack
  • Next.js 15
  • React 19
  • three.js
Assets
0 image files - all canvas-drawn
Fairness
Board & price sealed per round

01The Game

There are nine holes in a lit-up mound at a fair after dark, one mallet, and a pace that keeps getting faster. Regular moles are worth a point and golden moles are worth five. Hitting a bomb halves your winnings, and hitting a skull ends the round straight away, but only if you swing at them. The last four seconds are the Golden Rush, where everything is worth double.

Every other game in the arcade sets its prices from a number that nobody can influence. Here, the result partly depends on the player’s skill. So the real challenge was working out how to price a game of skill without handing good players a bonus and punishing beginners.

Priced to a perfect player

The value of each point is worked out from a perfect round, so nobody, however good they are, can beat the house.

Speed is part of the bet

You choose Steady, Brisk or Frantic right next to your bet, because each speed pays out within its own range.

The picture never lies

The mallet is always exactly where your pointer is, and one calculation decides both where you’re aiming and what you hit.

02Screenshots

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

03Core Loop

  1. 1

    Speed & chip

    Choose your bet and a speed, and each speed comes with its own reward range.

  2. 2

    Price drawn

    This round’s maximum return is picked from that range, using the sealed block value.

  3. 3

    Board sealed

    Every creature that will appear in the round is decided up front from the same locked-in seed.

  4. 4

    Frenzy

    Fifteen seconds of whacking, with the Golden Rush doubling everything in the last four.

  5. 5

    Published

    Cash out your winnings, and the whole board is printed out row by row in the fairness panel.

04Key Features

Four creatures

Moles appear 72% of the time (+1), golden moles 8% (+5), bombs 16% (they halve your winnings) and skulls 4% (they end the round). You can tell them apart from their shadows alone.

The Golden Rush

The last four seconds score double, right when the creatures appear for the shortest time and mistakes are most likely.

Full keyboard play

The 3×3 grid is mapped to QWE/ASD/ZXC and to the number pad keys 7-9/4-6/1-3, and both work at the same time.

Synthesised sound

A 108bpm music track that doubles its tempo as the clock runs down, and 90ms bonk sounds, all generated with WebAudio.

Zero image files

The soil, fur, stars, puffs and glows are all drawn with a 2D canvas when the game loads and then cached, so the whole lawn only weighs tens of kilobytes.

Pooled particles

All the particles come from one pool that’s set up in advance. When it’s full, it skips effects instead of growing, so there are no garbage collection pauses during the busiest four seconds.

05The Maths

The first version was priced around a typical "arcade regular". It paid back 46.5% to beginners and 138.5% to experts, even though it said 97% on the screen. The rebuild prices everything against a perfect round instead.

Derived point value

The value of a point is pointValue = price(rtp / CLEAR_POINTS[level]). A perfect round pays back the maximum, and anything less than perfect pays back less.

Three disjoint bands

Steady pays 88-91% (3.92-4.05×), Brisk 91.5-94% (4.07-4.18×) and Frantic 94.5-97% (4.21-4.32×). There’s a 0.5-point gap between each range, so rounding down can never make two neighbouring ranges overlap.

Floored, never rounded

Every price, and every time a bomb halves your winnings, is rounded down. Rounding up would mean the house pays more than it said it would.

Closed-form returns

A bomb turns your winnings B into B/2, and a skull turns them into 0. Both of those are simple linear changes, so I could work out the exact return for five different skill levels. It ranges from about 35% for a first go at Frantic up to the level’s maximum, and never goes past it.

Fixed tempo

When creatures pop up follows a fixed schedule for each level (for example, from 1040ms down to 648ms on Frantic). Only which creatures appear and the price are random, and both are sealed.

06Under the Hood

One projection

boardPoint() places the pointer on one flat, unlimited surface in the mound’s space, and both aim() and holeAt() read from it. This replaced the old setup of one aiming surface plus nine stacked hit spheres, which was causing missed hits.

An undamped mallet

The mallet’s position follows the pointer directly with no smoothing at all. Only its height, tilt, roll and recoil are smoothed.

A fixed camera

The camera looks down at 33° over a mound tilted back 8°, so every hole looks like an open oval. I tried zooming the camera in during the Golden Rush, but it felt like the game had stopped registering clicks, so I took it out.

Server seed route

The server route keeps its token private, retries up to five times if the block clock lags, caches good results for 9 seconds and failures for 5 seconds, and always responds with a 200. It also adds a counter so four rounds on the same block never get the same board.

07Challenges & Solutions

ChallengePricing the game around a typical player meant experts got 138% back and beginners only 46.5%.
SolutionI priced it against a perfect round instead and showed the return as a range, so nobody can get more than the maximum.
ChallengePlayers felt like their hits weren’t registering near the end of a round.
SolutionThe problem wasn’t in the hit detection at all. A smoothed mallet, stacked hit surfaces and a camera zoom were each slightly misleading players about where they were aiming. I removed all three.
ChallengeThree or four rounds can happen within one roughly 12-second block, so boards could repeat, which would be an easy exploit.
SolutionA counter for each round is mixed into the seed, so every board is new.

08Results & Takeaways

4.32×
best price any board can carry
5
skill levels with derived returns
3
disjoint, ordered reward bands
0
image files in the game
Next.js 15React 19TypeScript (strict)three.jsCanvas texturesWebAudiomulberry32
  • A finished 3D betting game and the only priced game of skill in the arcade. Its payout table is calculated and then checked by check(), rather than typed in by hand.
  • Whole boards are locked to a block before the first swing, there’s full keyboard support, and the controls are real HTML on top of the canvas.
  • Next up: settling bets on the server, and bringing this calculate-then-check approach to payout tables back to the older games.

What I learned

  • The visuals can mislead players about what’s really happening, and they’ll blame themselves for it.
  • For a game of skill, the only honest promise you can make is a maximum return, not an average.
  • Feedback happens on several timelines at once. If you squash them all into one duration, no amount of art can fix it.