Examples/water (real)

water (real)

Created from a prompt

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

The prompt

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.

Live environment

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

Generated workspace

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,
        )