Maxime Perniola
← Back to the journal

#012 · · Scopa Fortuna · 6 min

The rule applied too well

On 21 February, at 3:30 pm, a Scopa Fortuna commit fixes two tickets at once, BUG-01 and BUG-03. The message is three sentences long. An upgraded card must no longer be forced into a direct capture when the match only comes from its upgrade. And the value cap goes, so that a Re +1 can take 9 + 2. The commit adds 110 lines of tests. The repository was three days old.

What I believed

That the capture rule had been settled since day one. In Scopa, when a card on the table has the same value as the one you play, you must take it. Otherwise, you may take several cards that add up to that value, or lay your card down.

On 18 February, the repository’s decisions document already settled the ambiguous cases. DR-05: the sum is optional, only the direct capture is mandatory. DR-02: Il Mago, which captures at plus or minus one, must first take a card of its own value if there is one on the table. Its flexibility only applies otherwise. DR-03 applies the same principle to La Luna.

The first commit, the same day, coded the base rule without a fault:

// Direct matches
const directMatches = table.filter((t) => t.value === card.value);
// ...
// Sum captures (only if no direct match — sum captures are optional per DR-05)
if (directMatches.length === 0) {
  const sumSets = this.findSumCombinations(card.value, table);
  // ...
}

The special cards arrived on the evening of the 20th, the Forza (+1) and Agilità (±1) upgrades on the 21st at 1:29 am. I thought they sat on top of that rule without touching it.

What it was

They had copied it, and widened it. For an upgraded card, any table card reached thanks to the bonus counted as a direct capture. So it was mandatory, and so it blocked sums. The BUG-01 ticket gives the example: an upgraded 7 facing a 6 and a 1. The game forced you to take the 6 alone. The 6 + 1, which makes 7, was forbidden.

Another line only kept values from 1 to 10. That is a sensible safeguard for a card, and a mistake for a capture value. A Re, worth 10, could not aim for 11.

The fix separates what the rule was meant for from what it had caught along the way:

// Filter valid range (min 1, no upper cap — allows sum captures > 10)
const validValues = [...matchValues].filter((v) => v >= 1);

// Base-value direct matches are mandatory (standard Scopa rule)
const baseDirectMatches = table.filter((t) => t.value === card.value);
if (baseDirectMatches.length > 0) {
  // ...
  return captures;
}

// No base-value match: expanded single-card captures + sum captures (player chooses)

Il Mago had the same flaw, plus a second one, its mirror image. Its code put the 4, the 5 and the 6 on the same footing. With a 5 and a 4 on the table, it could take the 4, which DR-02 forbade. The decision had been written down for two days, and the code did not follow it. One of the new tests now checks it: “should force direct capture when base value matches”.

That code was written by Claude Code, at my request. The decisions were in the repository. I did not reread the special-card functions against DR-02. The tests for the base rule passed, because the base rule was right.

What I take from it

A rule has a trigger. Here, it was the face value of the card played. When you extend it to a new case, the question is not only “should it apply?”, but “to what?”. The code had answered “to every reachable value”, without anyone deciding so.

The decisions document had the right answer. No special-card test pointed back to it. A decision that no test carries remains an intention.

The rule was right. Its silent extension was what was wrong.

Get the journal by email

About one entry a week. What I build, what breaks, what I learn. No promotion, one-click unsubscribe.

You can also follow the RSS feed.