The drop cooldown was shorter than the combo window
Pebble Merge has a constant called DROP_COOLDOWN set to 380 milliseconds, and another one called CASCADE_GAP set to 900. I picked the first by playing. I picked the second from a measurement. They sat in the same file for weeks without either of them knowing the other existed, and the first time a player leaned on both at once the combo counter ran to 34.
Where each number came from
DROP_COOLDOWN is the pause between releasing a pebble and the next one appearing in your hand. 380 ms. I set it by feel, over perhaps twenty minutes of throwing stones and nudging the value up and down. Too long and the board feels like it is making you ask permission. Too short and a pebble stops reading as a physical thing you let go of, because there is always already another one waiting. 380 was the value at which it stopped irritating me. That is the entire derivation. No probe, no table, no record of what I tried before it, which in hindsight was the actual mistake, because the number went into a file full of other numbers without ever being introduced to any of them.
CASCADE_GAP came from the other direction entirely. We wanted to credit a chain of merges set off by a single drop, and the first question was how to know where one chain ends. We measured the gaps between consecutive merges across simulated games run on the real physics engine. The 25th percentile gap was 0.15 seconds and the median was 1.20 seconds. 900 ms sits past the fast tail and comfortably short of the median, so merges belonging to one physical event fall inside the window and unrelated ones fall outside it. I was quite pleased with that number. It had a reason attached to it.
380 is smaller than 900. That is the whole bug. Obvious now. It took a player pressing both buttons at once for either of us to notice that the two constants were in the same argument, and by then the argument was over and the shorter one had won.
What it did
If you throw at the fastest rate the game allows, the next pebble appears 380 ms later, so you can throw every 380 ms, which means a merge caused by one drop lands within 900 ms of a merge caused by the next one. The window never gets the chance to expire. The counter resets only when two merges arrive more than 900 ms apart, and a player working at full speed never leaves a hole that big, so it just climbs. Forever, in principle. The board never complained about it.
I found it through the label. The game prints “Cascade ×N” when a chain pays out, and during one session it printed Cascade ×16. My first thought was that the text was rendering a stale value, because sixteen chained merges from one pebble is not a thing the board can physically do, or so I believed. It was not stale. The counter had simply been counting across sixteen separate throws, and the multiplier table caps at 1.30, so the player had been collecting the maximum bonus on every merge for quite a while without doing anything to earn it. The mechanic existed specifically to reward setting up chains. Mashing paid better. I had built the opposite of what I meant to build, and it was invisible until somebody played fast enough to trip it.
So we measured it properly, which took four minutes to write and about four minutes to run. Same engine, same spawn weights, twelve simulated games per row. “Aimed” means the simulated player drops onto the highest pebble of the matching tier, roughly what a competent human does once they notice height decides whether a chain continues. “Old” is the shipped rule at the time: reset the chain only when two merges are far apart in time. “New” resets it on every drop.
| Throw rate | Policy | Old: mean depth | Old: max | New: mean depth | New: max |
|---|---|---|---|---|---|
| 380 ms (maximum) | random | 4.90 | 26 | 1.59 | 7 |
| 380 ms (maximum) | aimed | 6.82 | 34 | 1.46 | 6 |
| 1000 ms | random | 2.70 | 18 | 1.78 | 11 |
| 1000 ms | aimed | 2.04 | 11 | 1.45 | 6 |
| 1800 ms | random | 2.22 | 17 | 1.81 | 9 |
| 1800 ms | aimed | 1.66 | 15 | 1.54 | 9 |
Read the aimed column down the page. Under the old rule, playing at the maximum rate gave a mean chain depth of 6.82 against 1.66 for a slow player. Mashing was worth four times the chain of playing carefully, and 64.5% of all merges under fast aimed play collected the top 1.30 multiplier. That is not a mechanic. That is a metronome with a prize attached. The measurement did not change the game so much as it changed the way I read my own code afterward, because once you have watched a single constant quietly invert the intent of a system you designed by hand, you stop trusting any number that sits alone, and you start assuming that every pacing value is in a silent argument with at least one other that you have not yet introduced it to.
Under the new rule those two numbers are 1.46 and 1.54. The fast player now gets a marginally shorter average chain, which makes sense, because throwing before the board has settled genuinely does break chains apart. The reward has stopped responding to button speed, which was the only thing I actually wanted from it. A one line change, shipped nine months after the constant went in, which is the part of the story I would rather not tell anyone who signs the cheques.
What the fix cost, and the other clocks on the board
The fix is one line. Clear the chain counter at the top of every drop. It is not free. A chain that a slow player would have been credited for in full now ends early if they throw again while it is still resolving, and I went back and forth on whether that was acceptable. What settled it for me is that the definition became honest. A chain now means “merges set off by one drop”, which is a thing a player can aim at. The old definition meant “merges that happened recently”, which nobody can aim at, because it depends on how fast their hands are rather than where they put the stone.
Once I started looking for constants that talk to each other, I found more than I wanted. Two of them are 900 and mean completely different things. OVERFLOW_HOLD is how long a settled pebble must sit above the danger line before you lose. OVERFLOW_GRACE is the 1000 ms head start a freshly spawned pebble gets before it can count against you at all. Together they mean the fastest possible loss from one bad spawn is about 1.9 seconds. I like that number now. I did not design it. It fell out of two decisions made several weeks apart, and finding out that my game had a property I never chose is the part of this that actually bothered me. I would much rather have picked it on purpose, the way I picked the spawn weights and the tier curve, than learn it by watching a number I never wrote quietly decide how the game felt in the worst possible moment.
The tutorial has one too. After a drop we wait 800 ms before deciding whether to say “Match!”, because a drop is not a match until the physics says so, and a pebble needs a few ticks to fall plus one merge pass to resolve. Sampling immediately would have congratulated players for drops that did nothing. The cap on how many drops the tutorial runs for is six, and that one is deliberately not a round number: the probe needed fourteen drops to reach the first merge, so my original cap of three would almost always have retired the tutorial before it taught anything at all, while still spending the player’s attention. Six with an early exit on the first success keeps the median case to two or three drops.
The rule I took from it
Numbers chosen by feel are fine. Most of the constants that decide whether a game feels good cannot be derived from anything, and pretending otherwise is how you end up with a spreadsheet full of confidently wrong values. What is not fine is leaving them scattered, because two constants in the same file are not independent. They are two claims about the same board, and if one of them is shorter than the other, the shorter one wins and you will not notice until a player finds it.
The thing I do now is write the pacing constants down in one block at the top of the file with a sentence each, including the ones I picked by feel. Not because the comments are useful on their own. Because when I have to type “380, picked by feel, no measurement” next to “900, from the measured merge-gap distribution”, the contradiction is visible on screen instead of waiting for someone to press both buttons at once. Boring. Unglamorous. Worth it, because the alternative is finding out from a player, weeks later, that the reward system you designed to pay for precision has been paying for speed the whole time and nobody could tell you why their scores felt wrong. The honest version of the lesson is not "measure everything," because measuring everything is how you end up with a hundred numbers and no relationships between them. The lesson is that two numbers describing the same board are not two numbers. They are one argument you forgot you were having with yourself, and the only reliable way to find the argument before a player does is to put the numbers next to each other and read them like they are allowed to disagree.