Taps can be unreliable inside this app's built-in browser. For the full experience, use its menu to open this page in your browser.
Skip to content

Canvas Clipped To The Wrong Path

A devlog by Neeraj Kotwani, who builds every game on Elipar.

Sweet Swap is an original match-3 — the genre is free to build in, the brand names are not, so the name, the hand-drawn candies and the copy all steer clear of anyone else's trademarks. Eight by eight board, six candy types, thirty moves, and the full special-candy set: four in a row makes a striped candy that fires along a line, an L or T makes a bomb, five in a row makes a rainbow drop.

Two of those three specials were invisible.

clip() does not take an argument

A striped candy is drawn as the normal candy plus white stripes across it. The stripes have to be clipped to the candy's shape or they run out into the neighbouring cells.

The canvas clip call does not take a path as an argument. It clips to the current path — whatever was most recently begun and built up. And by the time the stripe code ran, the last path begun was the small gloss highlight ellipse near the top of the candy.

So the stripes were being clipped to a highlight a few pixels across, in a part of the candy the stripes do not pass through. They were drawn correctly, in full, and then thrown away by the clip. No error, no warning. A striped candy looked exactly like a plain one, which means the player gets a powerful piece and cannot tell it apart from an ordinary one.

The fix is one extra beginPath and an arc the size of the candy before clipping. There is a comment on it now saying not to rely on the path that happens to be current, because the whole failure mode is that the code reads as if it works.

That was found by looking at a screenshot. There is no assertion that would have caught it — the engine was producing striped candies correctly, the rendering was running without error, and every test about match detection and special creation passed. It is only wrong in the picture.

The second rendering bug was a collision with the site

The moves counter was drawn on the canvas at the top-right. The mute button is a DOM element, positioned at the top-right.

On the real site those two overlapped. Pulled the counter in by eighty pixels. Mentioning it because it is the same lesson as the canvas clip: a game's own rendering code has no idea what the site draws on top of it, so the only way to find these is to look at the game inside the real page rather than on its own.

Making randomness testable

Match-3 cascades are the part worth testing properly and the hardest part to test, because the board refills from a random source. You cannot assert an exact score when the next candies are unknown.

The trick I used here, and would use again: in a throwaway debug copy, string-replace the random-type function so it consumes from a queue I control. Refills become fully deterministic, and exact-value assertions become possible. The cascade test asserts a score delta of exactly 540 — a first clear worth 180, then a second chain step worth double — rather than "score went up".

Two tests failed on the first run and both were test-isolation problems, not engine bugs. One read a "last clear total" field that follow-up cascades had already overwritten. The other asserted an absolute score, but score persists across a board injection — only starting a fresh game resets it. Both fixed by asserting on score deltas with a fresh start per case.

If a test for a cascading system reads an absolute value, check whether the thing you are reading is the final state or an intermediate one that something later clobbered.

A board that cannot be played is a bug, so it is impossible

Board generation guarantees two things: no pre-existing matches, and at least one valid move. Verified across fifty generated boards.

A board can still resolve into a state with no valid moves during play, and that auto-reshuffles with a message. Testing that branch needs a genuinely moveless board, which is awkward to construct by hand — but there is a neat one. Tile the grid in 2x2 blocks, so each cell's type is determined by its row and column parity. Every pair of neighbours swaps into another valid-but-unmatched arrangement. It is moveless, it is one line, and it means the reshuffle branch is unit-tested directly instead of being hoped for.

One design detail worth stating because it is easy to get backwards: when a special is created, the cell it is created in is spared from the clear and converted in place. It spawns at the position you swapped, falling back to the middle of the group for specials created by a cascade rather than a player move. Clearing it along with the rest and then spawning a special would also "work" and would feel wrong, because the piece would not appear where you acted.

The six candy types are distinguished by shape as well as colour, which is the same reasoning behind the hand-drawn flags in the quiz game on this site: colour-only differentiation is a problem for a real fraction of players, and a match-3 where two types look alike is not a hard game, it is a broken one.

Sweet Swap is free in a browser, no account, no download. The striped candies have stripes.

We use cookies to keep games running and, once ads are live, to show relevant content. See our Privacy Policy for details.