Spaces:
Running
Running
File size: 8,588 Bytes
6c471d8 4de8125 50f2856 6c471d8 50f2856 6c471d8 50f2856 4de8125 50f2856 6c471d8 4de8125 50f2856 4de8125 50f2856 4de8125 50f2856 4de8125 50f2856 4de8125 50f2856 4de8125 50f2856 4de8125 50f2856 4de8125 50f2856 4de8125 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | ---
title: Scott Wu Math
emoji: π
colorFrom: green
colorTo: gray
sdk: docker
app_port: 7860
pinned: false
license: apache-2.0
short_description: Scott Wu's card game as an OpenEnv RL environment
tags:
- openenv
- reinforcement-learning
- math
- reasoning
---
# Scott Wu Math
Deal some playing cards, name a number, and make that number from the cards with + β Γ Γ·, using every
card exactly once. People and language models play the same environment: the page at `/` drives the
same OpenEnv session over `/ws` that an agent does.
Named after the game Scott Wu plays in the clip below, as a fan tribute. It is not affiliated with or
endorsed by Scott Wu or Cognition.
## Where it comes from
In a clip filmed at a pub, Cognition's CEO Scott Wu explains the game he plays with a deck of cards.
It starts as the 24 game: four cards, make 24. "Once that got too easy", you take eight cards and make
the current year. Dealt K Q J 10 7 9 5 5, he says it is quite easy because 13 β 12 is 1, 11 β 10 is 1,
1 + 1 + 7 is 9, and 9 Γ 9 Γ 5 Γ 5 is exactly 2025. Then someone names a four-digit number, 6,843, and
with nine cards (6 4 Q 10 10 8 J 7 3) he goes: 6 + 4 is 10, 12 β 10 is 2, 2 Γ 10 Γ 10 is 200, plus 8 is
208, times 11 is 2,288, minus 7 is 2,281, times 3 is 6,843.
Both hands are in the `video` split, as he played them.
## Rules
- Use every card exactly once. A = 1, J = 11, Q = 12, K = 13.
- Combine with + β Γ Γ· and parentheses. Division is exact, so fractions along the way are allowed, as in the 24 game.
- No joining cards into longer numbers, no powers, no unary minus.
- One graded answer per deal.
## Levels
| Level | Cards | Target | Notes |
|---|---|---|---|
| `24` | 4 | 24 | The classic 24 game. About a third of random deals have no solution; those are redealt. |
| `three-digit` | 6 | 100β999 | A step between 24 and the year. |
| `year` | 8 | 1900β2100 | The play page and a bare `reset()` use the current year, as in the video. |
| `four-digit` | 9 | 1000β9999 | "Give me your favourite four-digit number". |
| custom | 4β9 | any up to 1,000,000 | `reset(target=6843)`; card count by size unless `n_cards` is given. |
Cards come from one 52-card deck, so a value appears at most four times. Every generated deal is checked
with the solver and redealt until it has a solution, so a task is never impossible.
## Episode
```
reset(...) -> observation: cards, target, prompt (done = false)
step(CardAction(expression="...")) -> reward, result, a reference solution (done = true)
```
`reset` arguments, all optional:
| Argument | Meaning |
|---|---|
| `split`, `index` | A fixed task from `train` (4,000), `test` (200) or `video` (2). Without `index`, a random one. |
| `level` | A fresh deal at that level. |
| `target`, `n_cards` | A fresh deal for your own number. |
| `cards` + `target` | Your own hand, as values 1β13. It may have no solution. |
| `seed` | Makes a fresh deal reproducible. |
| `reward_mode` | `shaped` (default) or `binary`. |
The observation carries `prompt` and `system_prompt`, the exact text a model reads. The answer goes in
`CardAction.expression`; an empty expression gives up. After the step, the observation adds `result`
(the grader's report), `solution` and `solution_steps` (one way to make the target, one combination
per line), and `elapsed_s`. The reference solution is never shown before the episode ends.
## Reward
| Answer | Reward |
|---|---|
| Uses every card once and equals the target | 1.0 |
| Uses every card once, within 10% of the target (shaped mode) | `0.3 Γ cΒ²`, `c = 1 β |value β target| / (0.1 Γ target)` |
| Anything else: no answer, parse error, division by zero, a missing or extra card | 0 |
The grader parses the expression with Python's `ast`, allows only non-negative integers, + β Γ Γ· and
parentheses, compares the multiset of numbers to the cards, and evaluates with exact fractions. Γ Γ· β
and the letters A J Q K are accepted; anything after `=` is ignored. Partial credit keeps a GRPO group of
near misses from all scoring 0, and is capped far below an exact answer, so getting close never pays
more than getting it right.
## Use it
```python
from scott_wu_math import CardAction, ScottWuMathEnv
with ScottWuMathEnv(base_url="https://fineenvs-scottwumath.hf.space").sync() as env:
obs = env.reset(split="video", index=1).observation
print(obs.prompt)
result = env.step(CardAction(expression="(((12 - 10) * (6 + 4) * 10 + 8) * 11 - 7) * 3"))
print(result.reward, result.observation.result["check"]["value"]) # 1.0 6843
```
Over plain WebSocket (what the play page does): connect to `/ws`, send
`{"type": "reset", "data": {"level": "four-digit"}}`, then `{"type": "step", "data": {"expression": "..."}}`.
Task discovery follows OpenEnv's Task API: `GET /scott_wu_math/splits`, `POST /scott_wu_math/tasks`
with `{"split": "test"}`, and so on. Task listings leave out the reference solutions.
For text-only RL (no server), `scott_wu_math.game.reward.grade(expression, card_values, target)` and
`extract_answer(completion)` are the same grader as a plain function, and `game.prompts.messages(cards, target)`
builds the same prompt.
## The solver
Every expression tree splits the cards at its root into two groups, and the smaller group has at most
half the cards. The solver enumerates every value a group of up to four cards can make, then asks the
remaining cards for the one number that completes the target (`target = v + rest` needs
`rest = target β v`, and so on for the six ways to place `v`), recursing until four cards remain, where
the answer is a table lookup. That is complete for up to nine cards, and finds a solution for an
eight- or nine-card deal in a few milliseconds. A first pass keeps every intermediate value a whole
non-negative number, the way people play; a second pass allows fractions.
## Run locally
```bash
pip install -e .
uvicorn scott_wu_math.server.app:app --port 8012 # play at http://localhost:8012
python scripts/build_tasks.py # rebuild data/tasks (deterministic)
pytest
```
## Evaluations
`eval/rollout.py` in the [FineEnvs repository](https://github.com/adithya-s-k/FineEnvs/tree/main/12-scott-wu-math)
runs OpenAI, Anthropic and open models on Hugging Face Inference Providers against a running server, in
parallel, one OpenEnv session per episode.
### Results (2026-10-23)
Twelve models on the first five `test` deals of each level (20 deals, one sample each) and on the two
`video` hands (three samples each), in three modes:
- **Reasoning**: the model's own reasoning at high effort, up to 32k output tokens.
- **No reasoning**: reasoning switched off, or as low as the API allows (`*`), up to 32k tokens, so the
model may still work it out in its visible reply.
- **At once**: no reasoning, a system line asking for the answer tag alone, and 300 output tokens: the
mental-arithmetic version of the game.
| Model | Reasoning | No reasoning | At once | Video hands: reasoning / none / at once |
|---|---|---|---|---|
| GPT-6.1 Sol | 20/20 | 20/20* | 14/20* | 6/6 / 6/6 / 3/6 |
| Claude Opus 5.5 | 20/20 | 19/20* | 8/20* | 6/6 / 6/6 / 5/6 |
| GLM-5.3 | 17/20 | 19/20 | 5/20 | 6/6 / 5/6 / 0/6 |
| Qwen3.8 2.4T | 20/20 | 19/20 | 4/20 | 6/6 / 5/6 / 0/6 |
| Claude Sonnet 5.5 | 20/20 | 15/20 | 4/20 | 6/6 / 4/6 / 0/6 |
| Claude Haiku 5.5 | 20/20 | 11/20 | 3/20 | 6/6 / 5/6 / 0/6 |
| Kimi K2.6 | 19/20 | 20/20 | 2/20 | 6/6 / 6/6 / 0/6 |
| DeepSeek V4 Pro | 20/20 | 10/20 | 2/20 | 6/6 / 3/6 / 0/6 |
| gpt-oss-120b | 16/20 | 6/20* | 2/20* | 6/6 / 1/6 / 0/6 |
| GPT-6 Luna | 20/20 | 2/20 | 2/20 | 5/6 / 0/6 / 0/6 |
| Qwen3.8 27B | 18/20 | 20/20 | 1/20 | 6/6 / 6/6 / 0/6 |
| Gemma 4 31B | 19/20 | 18/20 | 1/20 | 5/6 / 5/6 / 0/6 |
`*` lowest effort the API allows: GPT-6.1 Sol and gpt-oss-120b at `low`, Claude Opus 5.5 at `low` (its
thinking cannot be disabled). Open models ran on Hugging Face Inference Providers (Qwen3.8 2.4T and Kimi
K2.6 on DeepInfra; DeepSeek V4 Pro, GLM-5.3 and Qwen3.8 27B on Novita; gpt-oss-120b on Cerebras; Gemma 4
31B on DeepInfra). Gemma and Kimi each lost one or two episodes to provider errors, counted as failures.
With reasoning the game is close to solved: every model makes 6,843 on the first try. Answering at once
separates them: only GPT-6.1 Sol and Claude Opus 5.5, which still reason briefly at their lowest effort,
get far; most other models start working out loud and run out of room. That gap, and the no-reasoning
column for the smaller models, is where there is something to train.
|