Maxime Perniola
← Retour au journal

#012 · · Scopa Fortuna · 6 min

La règle appliquée trop bien

Le 21 février, à 15 h 30, un commit de Scopa Fortuna corrige deux tickets d’un coup, BUG-01 et BUG-03. Le message tient en trois phrases. Une carte améliorée ne doit plus être forcée à une prise directe quand la correspondance ne vient que de son amélioration. Et le plafond de valeur saute, pour qu’un Re +1 puisse prendre 9 + 2. Le commit ajoute 110 lignes de tests. Le dépôt avait trois jours.

Ce que je croyais

Que la règle de prise était réglée depuis le premier jour. À la Scopa, quand une carte de la table a la même valeur que celle qu’on joue, on doit la prendre. Sinon, on peut prendre plusieurs cartes dont la somme fait cette valeur, ou poser sa carte.

Le 18 février, le document de décisions du dépôt tranchait déjà les cas ambigus. DR-05 : la somme est facultative, seule la prise directe est obligatoire. DR-02 : Il Mago, qui capture à plus ou moins un, doit d’abord prendre une carte de sa propre valeur si elle est sur la table. Sa souplesse ne joue qu’à défaut. DR-03 applique le même principe à La Luna.

Le premier commit, le même jour, codait la règle de base sans faute :

// 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);
  // ...
}

Les cartes spéciales sont arrivées le 20 au soir, les améliorations Forza (+1) et Agilità (±1) le 21 à 1 h 29. Je pensais qu’elles s’appuyaient sur cette règle sans la toucher.

Ce que c’était

Elles l’avaient recopiée, en l’élargissant. Pour une carte améliorée, toute carte de la table atteinte grâce au bonus comptait comme une prise directe. Donc obligatoire, et donc bloquante pour les sommes. Le ticket BUG-01 donne l’exemple : un 7 amélioré face à un 6 et un 1. Le jeu imposait de prendre le 6 seul. Le 6 + 1, qui fait 7, était interdit.

Une autre ligne ne gardait que les valeurs de 1 à 10. C’est un garde-fou raisonnable pour une carte, et une erreur pour une valeur de prise. Un Re, qui vaut 10, ne pouvait pas viser 11.

La correction sépare ce que la règle visait de ce qu’elle avait attrapé en route :

// 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 avait le même défaut, et un second, en miroir. Son code mettait le 4, le 5 et le 6 sur le même plan. Avec un 5 et un 4 sur la table, il pouvait prendre le 4, ce que DR-02 interdisait. La décision était écrite depuis deux jours, et le code ne la suivait pas. Un des tests ajoutés le vérifie maintenant : « should force direct capture when base value matches ».

Ce code, c’est Claude Code qui l’a écrit, à ma demande. Les décisions étaient dans le dépôt. Je n’ai pas relu les fonctions des cartes spéciales en regard de DR-02. Les tests de la règle de base passaient, parce que la règle de base était juste.

Ce que j’en retiens

Une règle a un déclencheur. Ici, c’était la valeur faciale de la carte jouée. Quand on l’étend à un cas nouveau, la question n’est pas seulement « faut-il l’appliquer ? », mais « à quoi ? ». Le code avait répondu « à toutes les valeurs atteignables », sans que personne l’ait décidé.

Le document de décisions avait la bonne réponse. Aucun test des cartes spéciales n’y renvoyait. Une décision qu’aucun test ne porte reste une intention.

La règle était juste. C’est son extension silencieuse qui était fausse.

Recevoir le journal

Une entrée par semaine environ. Ce que je construis, ce qui casse, ce que j'apprends. Pas de promotion, désinscription en un clic.

Vous pouvez aussi suivre le flux RSS.