Re-run on 2026-10-16 with OpenCode (DeepSeek V4 Pro via OpenRouter) against the updated starter (scoped picture bundle, answer scrub, moveButton, key_input clocks, check_env.py). No hardcoded demo code. Cursor did not edit workspace/.
2026-10-16 · task bfq8mbM9iKDf · opencode · openrouter/deepseek/deepseek-v4-pro-0813
Build a watercolour exercise. Each round names a subject, like a pear or a harbour. I produce a painting of it in a loose watercolour style and submit once. Score three things: it really is that subject, the answer is not an absurdly long program or essay, and the picture actually looks like a watercolour rather than a diagram. A beautiful picture of the wrong thing should fail. A correct scribble that is not a painting should do poorly too. Show the subject, the painting I made, and the score. Several subjects. The same subject should be the assignment if I restart that round.
Running the prompt-created workspace on this machine — not a mock. Save an episode, then open Replay below to scrub the timeline.
opening · 0 ms
Exact files the agent wrote — not rewritten by hand.
# Watercolour exercise. Each round names a subject; you paint it on a small
# wet canvas with a palette of watercolour pigments, then submit once. The
# round is judged on three things: it really is that subject, the painting is
# not an overlong, blocky program-like raster, and it actually reads as a soft
# watercolour rather than a crisp diagram.
from __future__ import annotations
import math
from typing import Any, Dict, List, Tuple
from helpers import base_observation, clamp01, rng_for, seed_from_task, task_ids
GAME_CLASS = "Watercolour"
ROWS = 14
COLS = 12
PAPER = "paper"
# Watercolour pigments (id -> hex). 'paper' is the blank paper, not a pigment.
HEX = {
"cerulean": "#4C9BD6",
"ultramarine": "#2B4C7E",
"alizarin": "#C13B3B",
"cadmium": "#E08A3C",
"lemon": "#E7C24C",
"sap": "#7A8E3D",
"umber": "#8A5A3B",
"paynes": "#64707A",
"violet": "#8E7CC3",
"emerald": "#3E8E6E",
"paper": "#F4EEE2",
}
P_ORDER = [
"cerulean",
"ultramarine",
"alizarin",
"cadmium",
"lemon",
"sap",
"umber",
"paynes",
"violet",
"emerald",
]
LABEL = {
"cerulean": "Cerulean",
"ultramarine": "Ultramarine",
"alizarin": "Alizarin Crimson",
"cadmium": "Cadmium Orange",
"lemon": "Lemon Yellow",
"sap": "Sap Green",
"umber": "Burnt Umber",
"paynes": "Payne's Grey",
"violet": "Violet",
"emerald": "Emerald",
"water": "Water (lift)",
}
# RGB for scoring colour distance.
def _rgb(hexv: str) -> Tuple[int, int, int]:
hexv = hexv.lstrip("#")
return (int(hexv[0:2], 16), int(hexv[2:4], 16), int(hexv[4:6], 16))
RGB = {pid: _rgb(hexv) for pid, hexv in HEX.items()}
def _blank_grid() -> List[List[str]]:
return [[PAPER for _ in range(COLS)] for _ in range(ROWS)]
def _new_field() -> Dict[str, List[List[float]]]:
field: Dict[str, List[List[float]]] = {}
for pid in P_ORDER:
field[pid] = [[0.0 for _ in range(COLS)] for _ in range(ROWS)]
return field
def _blob(field, cy, cx, ry, rx, pid, amp=1.0, rng=None, noise=0.18):
for r in range(ROWS):
for c in range(COLS):
dy = (r - cy) / ry
dx = (c - cx) / rx
d2 = dx * dx + dy * dy
v = math.exp(-0.5 * d2)
if rng is not None and noise:
v *= 1.0 + noise * (rng.random() * 2.0 - 1.0)
field[pid][r][c] += amp * v
def _field_to_grid(field, threshold=0.32) -> List[List[str]]:
grid = _blank_grid()
for r in range(ROWS):
for c in range(COLS):
best_pid = PAPER
best_v = threshold
for pid in P_ORDER:
v = field[pid][r][c]
if v > best_v:
best_v = v
best_pid = pid
grid[r][c] = best_pid
return grid
# --- Reference paintings. Each is a soft, wet rendering of a subject built from
# --- overlapping translucent blobs, so it reads as a watercolour, not a diagram.
def _pear(rng):
f = _new_field()
_blob(f, 8, 6, 5.0, 3.0, "sap", 1.0, rng)
_blob(f, 9.5, 7.2, 2.6, 1.7, "lemon", 0.7, rng)
_blob(f, 3.0, 6, 2.2, 0.7, "umber", 1.1, rng)
_blob(f, 3.4, 8.0, 0.9, 1.4, "emerald", 0.7, rng)
return _field_to_grid(f)
def _harbour(rng):
f = _new_field()
_blob(f, 2.0, 6, 4.5, 8.0, "cerulean", 1.0, rng)
_blob(f, 12.5, 6, 4.5, 8.0, "ultramarine", 1.0, rng)
_blob(f, 11.0, 3.0, 1.4, 1.8, "umber", 0.9, rng)
_blob(f, 12.0, 9.0, 1.5, 1.9, "umber", 0.9, rng)
_blob(f, 9.5, 4.5, 1.1, 1.6, "paynes", 0.7, rng)
_blob(f, 6.5, 5.0, 0.9, 1.0, "lemon", 0.55, rng)
return _field_to_grid(f)
def _sunflower(rng):
f = _new_field()
_blob(f, 7, 6, 1.7, 1.7, "umber", 1.1, rng)
for i in range(10):
ang = i / 10.0 * 2 * math.pi
py = 7 + math.sin(ang) * 3.1
px = 6 + math.cos(ang) * 3.1
_blob(f, py, px, 1.4, 1.0, "lemon", 0.95, rng)
_blob(f, 12.5, 6, 3.0, 0.7, "sap", 0.95, rng)
return _field_to_grid(f)
def _cat(rng):
f = _new_field()
_blob(f, 9, 6, 4.0, 3.2, "paynes", 1.0, rng)
_blob(f, 6.0, 6, 1.7, 1.9, "paynes", 1.0, rng)
_blob(f, 4.6, 5.0, 1.2, 0.9, "paynes", 0.95, rng)
_blob(f, 4.6, 7.0, 1.2, 0.9, "paynes", 0.95, rng)
_blob(f, 10.5, 9.5, 2.6, 0.7, "umber", 0.8, rng)
_blob(f, 6.3, 5.4, 0.5, 0.5, "emerald", 0.8, rng)
_blob(f, 6.3, 6.6, 0.5, 0.5, "emerald", 0.8, rng)
return _field_to_grid(f)
def _lemon_tree(rng):
f = _new_field()
_blob(f, 6.0, 6, 4.2, 4.4, "sap", 1.0, rng)
_blob(f, 5.0, 4.5, 0.8, 0.8, "lemon", 0.85, rng)
_blob(f, 6.5, 8.0, 0.8, 0.8, "lemon", 0.85, rng)
_blob(f, 3.8, 6.8, 0.8, 0.8, "lemon", 0.8, rng)
_blob(f, 7.5, 5.5, 0.8, 0.8, "lemon", 0.8, rng)
_blob(f, 11.5, 6, 3.5, 0.8, "umber", 0.95, rng)
return _field_to_grid(f)
def _rowboat(rng):
f = _new_field()
_blob(f, 2.0, 6, 3.8, 8.0, "cerulean", 0.95, rng)
_blob(f, 12.5, 6, 4.2, 8.0, "ultramarine", 1.0, rng)
_blob(f, 9.5, 6, 1.5, 3.6, "umber", 1.0, rng)
_blob(f, 6.0, 7.0, 2.0, 1.2, "paynes", 0.9, rng)
return _field_to_grid(f)
def _apple(rng):
f = _new_field()
_blob(f, 9, 6, 4.2, 3.0, "alizarin", 1.0, rng)
_blob(f, 10.5, 7.5, 1.4, 0.9, "cadmium", 0.55, rng)
_blob(f, 4.5, 6, 2.2, 0.45, "umber", 1.0, rng)
_blob(f, 4.0, 8.0, 1.0, 1.3, "sap", 0.9, rng)
return _field_to_grid(f)
def _mountains(rng):
f = _new_field()
_blob(f, 3.0, 6, 3.2, 9.0, "cerulean", 0.9, rng)
_blob(f, 7.5, 4.0, 5.0, 3.2, "paynes", 1.0, rng)
_blob(f, 8.5, 8.5, 5.5, 3.4, "violet", 1.0, rng)
_blob(f, 4.7, 4.0, 1.0, 1.0, "lemon", 0.75, rng)
_blob(f, 5.6, 8.5, 1.0, 1.0, "lemon", 0.75, rng)
_blob(f, 12.0, 6, 3.0, 9.0, "sap", 0.9, rng)
return _field_to_grid(f)
SUBJECTS = [
{"id": "pear", "label": "Pear", "painter": _pear},
{"id": "harbour", "label": "Harbour", "painter": _harbour},
{"id": "sunflower", "label": "Sunflower", "painter": _sunflower},
{"id": "cat", "label": "Cat", "painter": _cat},
{"id": "lemon_tree", "label": "Lemon Tree", "painter": _lemon_tree},
{"id": "rowboat", "label": "Rowboat", "painter": _rowboat},
{"id": "apple", "label": "Red Apple", "painter": _apple},
{"id": "mountains", "label": "Mountains", "painter": _mountains},
]
SUBJECT_BY_ID = {s["id"]: s for s in SUBJECTS}
_TASK_CATALOG = list(task_ids(len(SUBJECTS)))
def _dist(a: Tuple[int, int, int], b: Tuple[int, int, int]) -> float:
return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2)
def _mask(grid: List[List[str]]) -> List[List[int]]:
return [[0 if grid[r][c] == PAPER else 1 for c in range(COLS)] for r in range(ROWS)]
def _blur(mask: List[List[int]]) -> List[List[float]]:
out = [[0.0 for _ in range(COLS)] for _ in range(ROWS)]
for r in range(ROWS):
for c in range(COLS):
s = 0.0
n = 0
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
rr, cc = r + dr, c + dc
if 0 <= rr < ROWS and 0 <= cc < COLS:
s += mask[rr][cc]
n += 1
out[r][c] = s / n
return out
def _soft_iou(ref, play) -> float:
mr = _blur(_mask(ref))
mp = _blur(_mask(play))
inter = 0.0
union = 0.0
for r in range(ROWS):
for c in range(COLS):
a, b = mr[r][c], mp[r][c]
inter += min(a, b)
union += max(a, b)
return inter / union if union else 0.0
def _subject_score(ref, play) -> float:
# "It really is that subject." Shape must overlap the reference (IoU) and
# the colours must be the same family — blue when the subject is a pear
# fails even if the wash is pretty.
iou = _soft_iou(ref, play)
total = 0.0
n = 0
for r in range(ROWS):
for c in range(COLS):
rp = ref[r][c]
if rp == PAPER:
continue
n += 1
pp = play[r][c]
if pp == PAPER:
cval = 0.0
else:
cval = clamp01(1.0 - _dist(RGB[rp], RGB[pp]) / 110.0)
total += cval
color = total / n if n else 0.0
score = 0.35 * iou + 0.65 * color
if iou < 0.2:
score *= 0.6
return clamp01(score)
def _colors_of(grid) -> List[str]:
return [grid[r][c] for r in range(ROWS) for c in range(COLS) if grid[r][c] != PAPER]
def _fill_ratio(grid) -> float:
return len(_colors_of(grid)) / (ROWS * COLS)
def _watercolour(play) -> float:
# A loose watercolour uses a handful of gentle pigments. Blank canvas and a
# single flat colour read as unfinished or a cut-out, not a painting.
distinct = len(set(_colors_of(play)))
if distinct == 0:
return 0.05
if distinct == 1:
return 0.45
if distinct <= 5:
return 0.95
return clamp01(0.95 - (distinct - 5) * 0.08)
def _length(play, ref) -> float:
# "Not an absurdly long program or essay": paints should stay economical.
# A scribble covers far more paper and many more pigments than a watercolour.
fill_p = _fill_ratio(play)
fill_r = _fill_ratio(ref)
distinct_p = len(set(_colors_of(play)))
distinct_r = len(set(_colors_of(ref)))
fill_ratio = fill_p / max(fill_r, 0.25)
color_ratio = distinct_p / max(distinct_r, 2)
overwork = max(0.0, fill_ratio - 1.35)
noise = max(0.0, color_ratio - 2.0)
return clamp01(1.0 - overwork / 1.2 - noise / 2.0)
def _verdict(subject, watercolour, length, total) -> str:
if subject < 0.4:
return "A lovely picture of the wrong thing — this is not the subject."
if total >= 0.8:
return "Lovely loose watercolour, clearly the subject."
if total >= 0.62:
return "A good watercolour, mostly right on topic."
if watercolour < 0.5:
return "Reads more like a diagram or flat cut-out than a painting."
if length < 0.5:
return "Too dense and busy — reads like overworked scribble, not a watercolour."
if total >= 0.45:
return "Middling — a bit off on shape or colour."
return "Not quite there."
class Watercolour:
def __init__(self) -> None:
self._subject_id = SUBJECTS[0]["id"]
self._cur = P_ORDER[0]
self._ref: List[List[str]] = []
self._grid: List[List[str]] = []
self._done = False
self._reward = 0.0
self._scores: List[Dict[str, Any]] = []
self._verdict = ""
def _begin(self, subject_id: str) -> Dict[str, Any]:
self._subject_id = SUBJECT_BY_ID.get(subject_id, SUBJECTS[0])["id"]
self._cur = P_ORDER[int(seed_from_task(self._subject_id)) % len(P_ORDER)]
rng = rng_for(self._subject_id)
self._ref = SUBJECT_BY_ID[self._subject_id]["painter"](rng)
self._grid = _blank_grid()
self._done = False
self._reward = 0.0
self._scores = []
self._verdict = ""
return self._observation()
# -- scoring --
def _score(self) -> None:
subject = _subject_score(self._ref, self._grid)
watercolour = _watercolour(self._grid)
length = _length(self._grid, self._ref)
style = max(0.0, 0.5 * watercolour + 0.5 * length)
total = clamp01(subject * (0.4 + 0.6 * style))
self._reward = total
self._verdict = _verdict(subject, watercolour, length, total)
self._scores = [
{"id": "subject", "label": "Really the subject", "value": round(subject * 100)},
{"id": "watercolour", "label": "Looks like watercolour", "value": round(watercolour * 100)},
{"id": "length", "label": "Not overlong / busy", "value": round(length * 100)},
]
# -- public API --
def reset(self, task_id: str = "") -> Dict[str, Any]:
if task_id in _TASK_CATALOG:
idx = _TASK_CATALOG.index(task_id)
return self._begin(SUBJECTS[idx]["id"])
return self._begin(self._subject_id)
def step(self, action) -> Tuple[Dict[str, Any], bool, str]:
mv = action.move.strip()
if mv.startswith("pick:"):
sid = mv.split(":", 1)[1].strip()
if sid in SUBJECT_BY_ID:
return self._begin(sid), True, ""
return self._observation(), False, "unknown subject"
if self._done:
return self._observation(), False, "round already submitted"
if mv == "submit":
self._score()
self._done = True
return self._observation(), True, ""
if mv.startswith("color:"):
pid = mv.split(":", 1)[1].strip()
if pid == "water":
self._cur = "water"
elif pid in P_ORDER:
self._cur = pid
else:
return self._observation(), False, "unknown colour"
return self._observation(), True, ""
if mv.startswith("paint:"):
coord = mv.split(":", 1)[1].strip()
if "," not in coord:
return self._observation(), False, "bad cell"
try:
r_str, c_str = coord.split(",", 1)
r, c = int(r_str), int(c_str)
except ValueError:
return self._observation(), False, "bad cell"
if not (0 <= r < ROWS and 0 <= c < COLS):
return self._observation(), False, "cell out of range"
self._grid[r][c] = PAPER if self._cur == "water" else self._cur
return self._observation(), True, ""
return self._observation(), False, "unknown move"
# -- observation --
def _legal_moves(self) -> List[str]:
moves = []
for pid in P_ORDER:
moves.append("color:" + pid)
moves.append("color:water")
for r in range(ROWS):
for c in range(COLS):
moves.append(f"paint:{r},{c}")
moves.append("submit")
for s in SUBJECTS:
moves.append("pick:" + s["id"])
return moves
def _observation(self) -> Dict[str, Any]:
subject = SUBJECT_BY_ID[self._subject_id]
extra: Dict[str, Any] = {
"subject": subject["id"],
"subject_label": subject["label"],
"subjects": [{"id": s["id"], "label": s["label"]} for s in SUBJECTS],
"paints": [{"id": pid, "label": LABEL[pid]} for pid in P_ORDER]
+ [{"id": "water", "label": LABEL["water"]}],
"palette_hex": [{"id": pid, "hex": HEX[pid]} for pid in P_ORDER]
+ [{"id": PAPER, "hex": HEX[PAPER]}],
"selected_color": self._cur,
"grid": [row[:] for row in self._grid],
"rows": ROWS,
"cols": COLS,
"instruction": (
"Paint the " + subject["label"].lower()
+ ". Wet the paper with your pigment, then Submit once."
),
}
if self._done:
extra["scores"] = self._scores
extra["verdict"] = self._verdict
extra["reference_grid"] = [row[:] for row in self._ref]
legal = [] if self._done else self._legal_moves()
return base_observation(
reward=self._reward,
done=self._done,
legal_moves=legal,
last_move=None,
**extra,
)