# One room more per gallery

A change request for `masterpieceornot_dashboard`. It is small, and it is
entirely on your side: the game asks the API what the levels are and ramps
through them in the order you list them, so the shape of the ramp is yours to
decide and the game needs no change at all.

**What is wanted.** Today a player who clears a gallery jumps from 2 rooms to 6,
then to 10, 14, 20. That is four rooms at a step, and it reads as a different
game each time rather than as a museum getting bigger. Instead: **each new
gallery has exactly one room more than the last.** 2, then 3, then 4, and on up.

---

## 1. The one function to change

`lib/includes/run.inc.php`, `difficultyTable()`. It is the only place the ramp is
described, and its comment already says so ("Modifiable ici sans toucher au reste
du code"), which is true.

```php
// Barème : une salle de plus par galerie. La proportion de fakes monte doucement
// avec la taille : la moitié dans les petites, trois cinquièmes dans les plus
// grandes — la difficulté est d'abord dans le nombre de salles, ensuite dans la
// proportion.
function difficultyTable() {
    $table = [];
    for ($total = 2; $total <= 20; $total++) {
        $part  = 0.5 + 0.1 * (($total - 2) / 18);          // 50 % → 60 %
        $fakes = (int) round($total * $part);
        $fakes = max(1, min($total - 1, $fakes));           // jamais 0, jamais tout
        $table['gallery-' . $total] = ['total' => $total, 'fakes' => $fakes];
    }
    return $table;
}
```

Notes on that:

- **`min($total - 1, …)` matters.** A gallery with no original in it is not a
  game: the player would be right to answer "fake" every time. One original at
  least, always.
- **Order is the ramp.** The game walks the array in the order `/api/difficulties`
  returns it and steps one entry per cleared gallery. PHP preserves insertion
  order, so counting upward in the loop is all that is needed.
- **19 rungs is a long game.** That is fine, and the ending still works: see §3.
- The upper bound (20) and the ratio are yours to tune. The game reads whatever
  it is told.

## 2. What must not change

**`difficultyLabels()` stays exactly as it is.** The word "difficulty" means two
different things in this codebase and they share a vocabulary today:

| | what it is | who reads it |
|---|---|---|
| `difficultyTable()` | the **shape of a run**: how many rooms, how many fake | `/api/difficulties`, `/api/run`, `dashboard/api.php` |
| `difficultyLabels()` | a **variant's own authored level**, `super-easy`…`very-hard` | `dashboard/oeuvre.php` (the fake editor), and the `difficulty` field on a fake in `/api/run` |

Only the first is changing. A fake authored as "hard" is still authored as
"hard"; that is a property of the forgery, not of the gallery it turns up in.
Keep the two functions apart, and do not be tempted to generate the labels from
the table.

**The response shape of `/api/difficulties` stays the same** — a `difficulties`
array of `{ key, total, fakes, originals }`. Only its length and contents change.

**The 422 body stays the same.** The game reads it (see §3).

## 3. What happens when the catalogue is smaller than the ladder

Nothing bad, and this is worth knowing before you worry about a 20-rung ladder on
a catalogue of nine ready artworks.

`GET /api/run` already answers **422** when the catalogue cannot fill a level.
The game treats that as *"there is nothing bigger left to play"* and shows its
ending screen — it does not error, and it does not retry. So the ladder can be
longer than the catalogue: a player simply reaches the end of what exists and
wins there. The ladder growing later, as artworks are added, extends the game
without any release.

That is also why the ladder should keep counting upward rather than stopping at
the current catalogue size: the catalogue is the limit, and it moves.

## 4. Two small display consequences

Neither is a bug, both are worth a minute:

- `dashboard/player.php` and `dashboard/run.php` print a run's level as
  `difficultyLabels()[$run['difficulty']] ?? $run['difficulty']`. New keys are
  not in the labels, so they will print as `gallery-7`. That is readable, so it
  is acceptable — but if you want it prettier, give the ladder its own label
  function (`"Gallery of 7 rooms"`) rather than adding the keys to
  `difficultyLabels()`, which belongs to the fakes.
- `dashboard/api.php` builds a level dropdown from `difficultyTable()`. Nineteen
  entries in it is a longer list than five; still fine, and the tester page is
  the one place a long list is useful.
- Old rows in the runs log hold the retired keys (`medium`, `very-hard`). They
  will keep printing through the `?? $run['difficulty']` fallback, so history
  stays readable. If you would rather they resolved, keep the five old keys in
  the table as aliases of the nearest rung — but the fallback is honestly enough.

## 5. Checking it

```bash
curl "$API_BASE/api/difficulties"        # 2, 3, 4, … in order, fakes ≥ half
curl "$API_BASE/api/run?difficulty=gallery-3&uuid=demo-player-0001"
curl "$API_BASE/api/run?difficulty=gallery-20&uuid=demo-player-0001"   # 422 on a small catalogue
```

The middle call should return three items with two of them `authentic: false`,
and never the same `oeuvre_id` twice.

## 6. The game side

Nothing to do. For the record: the game fills `GameState.levels` from
`/api/difficulties` at boot, sends `key` back on every `/api/run`, and steps one
level per cleared gallery. It never displays the key and never assumes how many
levels there are. Its headless test stub has been changed to a `gallery-2`,
`gallery-3`, `gallery-4` ladder so its tests exercise the new shape.
