Examples/legal

legal

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. After the re-run, one line in view.js was changed so the witness name is text inside the heading instead of a DOM node concatenated with a string.

2026-10-16 · task Lp8mRZx1D2vl · opencode · openrouter/deepseek/deepseek-v4-pro-0813

The prompt

Build a short criminal trial I can prosecute. The case is a theft. I call witnesses, ask them questions, and offer documents or objects as exhibits. A judge decides what the jury is allowed to hear. If I never get the evidence admitted, the jury should not act as if they heard it. When I rest and give a closing, the jury returns a verdict.

The witnesses, the judge, and the jury can be played by language models, but the rule is fixed: the jury packet is only the admitted record. Winning is a conviction that came from that record. Show who is on the stand, the admitted exhibits, the last answer, and the verdict at the end. Clicking through the steps is how I play. Restarting should begin the trial over, with nothing admitted yet.

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.

# AI : a short criminal theft trial the person prosecutes. Click witnesses,
# ask questions, offer exhibits. A judge admits or excludes. The jury packet is
# ONLY the admitted record, and winning is a conviction built from that record.
from __future__ import annotations

from typing import Any, Dict, List, Optional

from helpers import base_observation, pick_task, rng_for, task_ids

GAME_CLASS = "Trial"

# ---------------------------------------------------------------------------
# The fixed scenarios. Each is a theft; witnesses carry questions, each
# question is either admissible or not (the judge rules). Exhibits may require
# a witness to have testified first (foundation). "probative" is the weight the
# jury puts on the item IF it is admitted; excluded items never reach the jury.
# ---------------------------------------------------------------------------

CASES: Dict[str, Dict[str, Any]] = {
    "case1": {
        "id": "case1",
        "name": "The People v. Dana Cole",
        "charge": "Grand theft of a gold wristwatch",
        "defendant": "Dana Cole",
        "victim": "Marcus Hale",
        "goal": "Win a conviction with only the evidence the judge admits. The jury hears nothing else.",
        "opening": (
            "During the lunch hour, Marcus Hale's gold wristwatch vanished from "
            "his desk at Meridian Partners. The prosecution says the new intern, "
            "Dana Cole, took it. The defense says the State cannot prove it."
        ),
        "threshold": 12,
        "witnesses": [
            {
                "id": "marcus",
                "name": "Marcus Hale",
                "role": "Owner of the stolen watch",
                "questions": [
                    {
                        "id": "wm1",
                        "text": "What did you do with the watch just before it disappeared?",
                        "answer": "I took it off at lunchtime and left it on my desk while I went to the cafeteria.",
                        "probative": 1,
                        "admit": True,
                        "in_reason": "Admitted. First-hand account, relevant.",
                        "out_reason": "",
                    },
                    {
                        "id": "wm2",
                        "text": "Did you ever give anyone permission to take the watch?",
                        "answer": "No. I never gave Dana or anyone else permission to take it.",
                        "probative": 1,
                        "admit": True,
                        "in_reason": "Admitted. Relevant to whether the taking was unauthorized.",
                        "out_reason": "",
                    },
                    {
                        "id": "wm3",
                        "text": "You suspected Dana from the very beginning, didn't you?",
                        "answer": "Honestly, I did suspect them right away.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Leading. On direct examination you may not suggest the answer.",
                    },
                ],
            },
            {
                "id": "priya",
                "name": "Priya Shah",
                "role": "Coworker who sat across the hall",
                "questions": [
                    {
                        "id": "wp1",
                        "text": "What did you see around 12:30 that afternoon?",
                        "answer": "I saw Dana walk out of Marcus's office alone, holding something small and gold.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Personal observation of a material fact.",
                        "out_reason": "",
                    },
                    {
                        "id": "wp2",
                        "text": "Did Dana say anything about it afterwards?",
                        "answer": "Yes. I heard Dana say, \"I hope nobody notices it's gone.\"",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. A statement by a party is not hearsay.",
                        "out_reason": "",
                    },
                    {
                        "id": "wp3",
                        "text": "Dana always seemed like the type to do this, didn't they?",
                        "answer": "I guess Dana did always seem a little sneaky.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Improper character evidence used to show guilt by disposition.",
                    },
                ],
            },
            {
                "id": "ortiz",
                "name": "Detective Ortiz",
                "role": "Investigating officer",
                "questions": [
                    {
                        "id": "wo1",
                        "text": "What did you observe about the office door?",
                        "answer": "No forced entry. The lock was intact, so whoever took it entered while the office was open.",
                        "probative": 0,
                        "admit": True,
                        "in_reason": "Admitted. Relevant context.",
                        "out_reason": "",
                    },
                    {
                        "id": "wo2",
                        "text": "What did forensics find on the desk drawer?",
                        "answer": "A latent print lifted from the drawer matched Dana Cole's right index finger.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Foundation laid for the identification.",
                        "out_reason": "",
                    },
                    {
                        "id": "wo3",
                        "text": "Who do you believe stole the watch?",
                        "answer": "I'm convinced Dana took it.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Opinion on the ultimate issue. That is the jury's call, not a witness's.",
                    },
                ],
            },
            {
                "id": "sam",
                "name": "Sam Whitfield",
                "role": "Building security guard",
                "questions": [
                    {
                        "id": "ws1",
                        "text": "Can you authenticate the lobby camera feed from the 14th?",
                        "answer": "Yes, this is the lobby camera, date-stamped the 14th, noon to 1 pm. I have reviewed it.",
                        "probative": 0,
                        "admit": True,
                        "in_reason": "Admitted. The witness authenticates the recording.",
                        "out_reason": "",
                    },
                    {
                        "id": "ws2",
                        "text": "What did the door badge log show around lunch?",
                        "answer": "Dana's badge was the only one swiped at that office door between 12:20 and 12:45.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Business record, relevant.",
                        "out_reason": "",
                    },
                    {
                        "id": "ws3",
                        "text": "What do people in the building say about Dana?",
                        "answer": "People say Dana is always broke and asks to borrow money.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Hearsay and reputation. Not probative of this particular theft.",
                    },
                ],
            },
        ],
        "exhibits": [
            {
                "id": "e1",
                "name": "Lobby security footage",
                "desc": "Shows Dana enter office 4 at 12:31 and leave alone at 12:38.",
                "probative": 3,
                "admit": True,
                "needs": "sam",
                "in_reason": "Admitted. Recording authenticated by the guard.",
                "out_reason": "No foundation. The footage has not been authenticated by a witness.",
            },
            {
                "id": "e2",
                "name": "Fingerprint card",
                "desc": "Latent print from the desk drawer, matched to Dana Cole's right index finger.",
                "probative": 2,
                "admit": True,
                "needs": "ortiz",
                "in_reason": "Admitted. Chain of custody established by the detective.",
                "out_reason": "No foundation. Chain of custody has not been established.",
            },
            {
                "id": "e3",
                "name": "Recovered watch",
                "desc": "The gold watch recovered from Dana's locker during a consented search. The serial number matches the one Marcus reported stolen.",
                "probative": 3,
                "admit": True,
                "needs": "ortiz",
                "in_reason": "Admitted. Recovered and identified through the investigation.",
                "out_reason": "No foundation. No witness has tied this watch to the investigation.",
            },
            {
                "id": "e4",
                "name": "Anonymous note",
                "desc": "An unsigned note found in the break room reading \"Dana is a thief.\"",
                "probative": 0,
                "admit": False,
                "needs": None,
                "in_reason": "",
                "out_reason": "Anonymous hearsay with no probative value. Excluded.",
            },
        ],
    },
    "case2": {
        "id": "case2",
        "name": "The People v. Ray Porter",
        "charge": "Theft of a wallet",
        "defendant": "Ray Porter",
        "victim": "Elena Vasquez",
        "goal": "Win a conviction with only the evidence the judge admits. The jury hears nothing else.",
        "opening": (
            "At the Sunnyside Diner, Elena Vasquez left her wallet on the counter to "
            "grab napkins, and it was gone. The prosecution says regular customer "
            "Ray Porter took it. The defense says the State cannot prove it."
        ),
        "threshold": 12,
        "witnesses": [
            {
                "id": "elena",
                "name": "Elena Vasquez",
                "role": "Owner of the wallet",
                "questions": [
                    {
                        "id": "ev1",
                        "text": "What did you do with your wallet at the counter?",
                        "answer": "I set it down to pay and turned to grab napkins. When I turned back it was gone.",
                        "probative": 1,
                        "admit": True,
                        "in_reason": "Admitted. First-hand account, relevant.",
                        "out_reason": "",
                    },
                    {
                        "id": "ev2",
                        "text": "Did you give anyone permission to take your wallet?",
                        "answer": "No. I never gave anyone permission, especially not Ray.",
                        "probative": 1,
                        "admit": True,
                        "in_reason": "Admitted. Relevant to whether the taking was unauthorized.",
                        "out_reason": "",
                    },
                    {
                        "id": "ev3",
                        "text": "You're certain Ray took it, aren't you?",
                        "answer": "I knew it was him right away.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Leading. On direct examination you may not suggest the answer.",
                    },
                ],
            },
            {
                "id": "toni",
                "name": "Toni Brooks",
                "role": "Diner server",
                "questions": [
                    {
                        "id": "tb1",
                        "text": "What did you see at the counter around 2 pm?",
                        "answer": "I saw Ray reach over the counter and slide the wallet into his jacket.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Personal observation of a material fact.",
                        "out_reason": "",
                    },
                    {
                        "id": "tb2",
                        "text": "Did Ray say anything when he was confronted?",
                        "answer": "He said, \"I was only going to borrow it for a minute.\"",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. A statement by a party is not hearsay.",
                        "out_reason": "",
                    },
                    {
                        "id": "tb3",
                        "text": "Ray is always causing trouble, isn't he?",
                        "answer": "He's caused trouble before.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Improper character evidence used to show guilt by disposition.",
                    },
                ],
            },
            {
                "id": "park",
                "name": "Detective Park",
                "role": "Investigating officer",
                "questions": [
                    {
                        "id": "jp1",
                        "text": "Was the diner's camera system working that day?",
                        "answer": "The counter camera was working and has a clear view of the register.",
                        "probative": 0,
                        "admit": True,
                        "in_reason": "Admitted. Relevant context.",
                        "out_reason": "",
                    },
                    {
                        "id": "jp2",
                        "text": "What was recovered from the dumpster behind the diner?",
                        "answer": "Elena's empty wallet, with Ray's fingerprint on the clasp.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Foundation laid for the identification.",
                        "out_reason": "",
                    },
                    {
                        "id": "jp3",
                        "text": "Do you think Ray is guilty?",
                        "answer": "I'm sure of it.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Opinion on the ultimate issue. That is the jury's call, not a witness's.",
                    },
                ],
            },
            {
                "id": "moe",
                "name": "Moe Callahan",
                "role": "Diner owner",
                "questions": [
                    {
                        "id": "mc1",
                        "text": "Can you authenticate the counter camera recording from 2 pm?",
                        "answer": "Yes, this is our counter camera from 2 pm that day.",
                        "probative": 0,
                        "admit": True,
                        "in_reason": "Admitted. The witness authenticates the recording.",
                        "out_reason": "",
                    },
                    {
                        "id": "mc2",
                        "text": "What does the register-area recording show?",
                        "answer": "It shows Ray leaning over the counter and the wallet disappearing into his coat.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Business record, relevant.",
                        "out_reason": "",
                    },
                    {
                        "id": "mc3",
                        "text": "What have customers said about Ray?",
                        "answer": "Regulars say he has been hard up for money lately.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Hearsay and reputation. Not probative of this particular theft.",
                    },
                ],
            },
        ],
        "exhibits": [
            {
                "id": "e1",
                "name": "Counter camera recording",
                "desc": "Shows Ray leaning over the counter and tucking a wallet into his coat.",
                "probative": 3,
                "admit": True,
                "needs": "moe",
                "in_reason": "Admitted. Recording authenticated by the owner.",
                "out_reason": "No foundation. The recording has not been authenticated by a witness.",
            },
            {
                "id": "e2",
                "name": "Fingerprint report",
                "desc": "A latent print on the wallet clasp, matched to Ray Porter's right thumb.",
                "probative": 2,
                "admit": True,
                "needs": "park",
                "in_reason": "Admitted. Chain of custody established by the detective.",
                "out_reason": "No foundation. Chain of custody has not been established.",
            },
            {
                "id": "e3",
                "name": "Recovered wallet",
                "desc": "Elena's empty wallet, recovered from the dumpster behind the diner.",
                "probative": 3,
                "admit": True,
                "needs": "park",
                "in_reason": "Admitted. Recovered and identified through the investigation.",
                "out_reason": "No foundation. No witness has tied this wallet to the investigation.",
            },
            {
                "id": "e4",
                "name": "Anonymous online post",
                "desc": "An anonymous post on the diner's page reading \"Ray Porter is a thief.\"",
                "probative": 0,
                "admit": False,
                "needs": None,
                "in_reason": "",
                "out_reason": "Anonymous hearsay with no probative value. Excluded.",
            },
        ],
    },
    "case3": {
        "id": "case3",
        "name": "The People v. Ivy Chen",
        "charge": "Theft of a bicycle",
        "defendant": "Ivy Chen",
        "victim": "Oscar Reed",
        "goal": "Win a conviction with only the evidence the judge admits. The jury hears nothing else.",
        "opening": (
            "Oscar Reed locked his bicycle to the library rack at 9 am and returned "
            "to find the lock cut and the bike gone. The prosecution says fellow "
            "student Ivy Chen took it. The defense says the State cannot prove it."
        ),
        "threshold": 12,
        "witnesses": [
            {
                "id": "oscar",
                "name": "Oscar Reed",
                "role": "Owner of the bicycle",
                "questions": [
                    {
                        "id": "or1",
                        "text": "What did you do with your bicycle before class?",
                        "answer": "I locked it to the rack outside the library at 9 am before my class.",
                        "probative": 1,
                        "admit": True,
                        "in_reason": "Admitted. First-hand account, relevant.",
                        "out_reason": "",
                    },
                    {
                        "id": "or2",
                        "text": "Did you give anyone permission to take it?",
                        "answer": "No. I never loaned it to anyone, least of all Ivy.",
                        "probative": 1,
                        "admit": True,
                        "in_reason": "Admitted. Relevant to whether the taking was unauthorized.",
                        "out_reason": "",
                    },
                    {
                        "id": "or3",
                        "text": "You're positive Ivy took it, right?",
                        "answer": "I was sure it was Ivy from the start.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Leading. On direct examination you may not suggest the answer.",
                    },
                ],
            },
            {
                "id": "leah",
                "name": "Leah Osei",
                "role": "Student eyewitness",
                "questions": [
                    {
                        "id": "lo1",
                        "text": "What did you see outside the library around noon?",
                        "answer": "I saw Ivy cut the lock off a blue bike and ride away on it.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Personal observation of a material fact.",
                        "out_reason": "",
                    },
                    {
                        "id": "lo2",
                        "text": "Did Ivy say anything when you asked about the bike?",
                        "answer": "They said, \"Mind your own business, I'm just borrowing it.\"",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. A statement by a party is not hearsay.",
                        "out_reason": "",
                    },
                    {
                        "id": "lo3",
                        "text": "Ivy is known for taking things, aren't they?",
                        "answer": "Ivy has got a reputation, sure.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Improper character evidence used to show guilt by disposition.",
                    },
                ],
            },
            {
                "id": "reyes",
                "name": "Detective Reyes",
                "role": "Investigating officer",
                "questions": [
                    {
                        "id": "ar1",
                        "text": "Was the bike rack area filmed?",
                        "answer": "The library has a camera covering the bike rack.",
                        "probative": 0,
                        "admit": True,
                        "in_reason": "Admitted. Relevant context.",
                        "out_reason": "",
                    },
                    {
                        "id": "ar2",
                        "text": "What did you recover from Ivy's dorm room?",
                        "answer": "We found Oscar's bike in Ivy's dorm room with freshly cut lock marks.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Foundation laid for the identification.",
                        "out_reason": "",
                    },
                    {
                        "id": "ar3",
                        "text": "Who did it, in your view?",
                        "answer": "Ivy, obviously.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Opinion on the ultimate issue. That is the jury's call, not a witness's.",
                    },
                ],
            },
            {
                "id": "nia",
                "name": "Nia Bell",
                "role": "Library security",
                "questions": [
                    {
                        "id": "nb1",
                        "text": "Can you authenticate the library camera recording from noon?",
                        "answer": "Yes, this is our camera feed covering the bike rack.",
                        "probative": 0,
                        "admit": True,
                        "in_reason": "Admitted. The witness authenticates the recording.",
                        "out_reason": "",
                    },
                    {
                        "id": "nb2",
                        "text": "What does the recording show?",
                        "answer": "It shows Ivy cutting the lock and riding off on the blue bike.",
                        "probative": 2,
                        "admit": True,
                        "in_reason": "Admitted. Business record, relevant.",
                        "out_reason": "",
                    },
                    {
                        "id": "nb3",
                        "text": "What do students say about Ivy?",
                        "answer": "They say Ivy is always after something.",
                        "probative": 0,
                        "admit": False,
                        "in_reason": "",
                        "out_reason": "Hearsay and reputation. Not probative of this particular theft.",
                    },
                ],
            },
        ],
        "exhibits": [
            {
                "id": "e1",
                "name": "Library camera recording",
                "desc": "Shows Ivy cutting the bike lock and riding off on the bicycle.",
                "probative": 3,
                "admit": True,
                "needs": "nia",
                "in_reason": "Admitted. Recording authenticated by library security.",
                "out_reason": "No foundation. The recording has not been authenticated by a witness.",
            },
            {
                "id": "e2",
                "name": "Cut lock",
                "desc": "The severed bicycle lock recovered from the rack, severed by bolt cutters.",
                "probative": 2,
                "admit": True,
                "needs": "reyes",
                "in_reason": "Admitted. Chain of custody established by the detective.",
                "out_reason": "No foundation. Chain of custody has not been established.",
            },
            {
                "id": "e3",
                "name": "Recovered bicycle",
                "desc": "Oscar's blue bicycle, recovered from Ivy's dorm room.",
                "probative": 3,
                "admit": True,
                "needs": "reyes",
                "in_reason": "Admitted. Recovered and identified through the investigation.",
                "out_reason": "No foundation. No witness has tied this bicycle to the investigation.",
            },
            {
                "id": "e4",
                "name": "Anonymous tip",
                "desc": "An anonymous text message reading \"Ivy Chen steals bikes.\"",
                "probative": 0,
                "admit": False,
                "needs": None,
                "in_reason": "",
                "out_reason": "Anonymous hearsay with no probative value. Excluded.",
            },
        ],
    },
}

TASK_IDS = task_ids(3, prefix="case")


def _witness(case: Dict[str, Any], wid: str) -> Dict[str, Any]:
    for w in case["witnesses"]:
        if w["id"] == wid:
            return w
    raise KeyError(wid)


def _question(witness: Dict[str, Any], qid: str) -> Dict[str, Any]:
    for q in witness["questions"]:
        if q["id"] == qid:
            return q
    raise KeyError(qid)


def _exhibit(case: Dict[str, Any], eid: str) -> Dict[str, Any]:
    for e in case["exhibits"]:
        if e["id"] == eid:
            return e
    raise KeyError(eid)


class Trial:
    """A single prosecution. State stays on the instance; the verdict comes only
    from the admitted record."""

    def __init__(self) -> None:
        self.case: Dict[str, Any] = {}
        self.task_id: str = ""
        self.rng = rng_for("")

    # -- lifecycle ---------------------------------------------------------
    def reset(self, task_id: str = "") -> Dict[str, Any]:
        case_id = pick_task(task_id, TASK_IDS, default="case1")
        self.task_id = task_id or case_id
        self.rng = rng_for(self.task_id)
        self.case = CASES[case_id]
        self.phase = "call"
        self.on_stand: Optional[str] = None
        self.asked: set[str] = set()
        self.offered: set[str] = set()
        self.testified: set[str] = set()
        self.admitted: List[Dict[str, Any]] = []
        self.excluded: List[Dict[str, str]] = []
        self.transcript: List[Dict[str, str]] = []
        self.last_answer: Optional[str] = None
        self.last_answer_by: Optional[str] = None
        self.judge_ruling: str = ""
        self.done = False
        self.verdict: Optional[str] = None
        self.closing_text: str = ""
        self._note = self.case["opening"]
        self._log("note", "The court", self._note)
        return self._observe(last_move=None)

    # -- moves --------------------------------------------------------------
    def _log(self, kind: str, who: str, text: str) -> None:
        self.transcript.append({"kind": kind, "who": who, "text": text})
        if len(self.transcript) > 10:
            self.transcript = self.transcript[-10:]

    def step(self, action: Any) -> tuple:
        if self.done:
            return self._observe(last_move=self.last_move), False, "The trial is over."
        move = getattr(action, "move", "") or ""
        move = move.strip()
        legal = self._legal_moves()
        if move not in legal:
            return self._observe(last_move=None), False, f"'{move}' is not available right now."
        self.last_move = move
        if move.startswith("call:"):
            wid = move.split(":", 1)[1]
            self._do_call(wid)
        elif move.startswith("ask:"):
            qid = move.split(":", 1)[1]
            self._do_ask(qid)
        elif move.startswith("offer:"):
            eid = move.split(":", 1)[1]
            self._do_offer(eid)
        elif move == "excuse":
            self._do_excuse()
        elif move == "rest":
            self._do_rest()
        elif move == "closing":
            self._do_closing()
        return self._observe(last_move=move), True, None

    # -- each action ---------------------------------------------------------
    def _do_call(self, wid: str) -> None:
        w = _witness(self.case, wid)
        self.on_stand = wid
        self.phase = "stand"
        self.judge_ruling = ""
        self.last_answer_by = w["name"]
        self.last_answer = f"{w['name']} takes the stand."
        self._log("note", w["name"], "takes the stand")

    def _do_excuse(self) -> None:
        wid = self.on_stand
        name = _witness(self.case, wid)["name"] if wid else "The witness"
        self.on_stand = None
        self.phase = "call"
        self.judge_ruling = ""
        self.last_answer = f"{name} steps down."
        self.last_answer_by = None
        self._log("note", name, "steps down")

    def _do_ask(self, qid: str) -> None:
        assert self.on_stand is not None
        w = _witness(self.case, self.on_stand)
        q = _question(w, qid)
        self.asked.add(qid)
        self.testified.add(self.on_stand)
        self.last_answer_by = w["name"]
        if q["admit"]:
            self.admitted.append(
                {
                    "kind": "testimony",
                    "source": w["name"],
                    "text": q["answer"],
                    "probative": q["probative"],
                }
            )
            self.judge_ruling = q["in_reason"]
            self._log("answer", w["name"], q["answer"])
            self._log("ruling", "The judge", "Admitted. " + q["in_reason"].split(".", 1)[0])
        else:
            self.excluded.append({"source": w["name"], "text": q["answer"]})
            self.judge_ruling = q["out_reason"]
            self._log("answer", w["name"], q["answer"])
            self._log("ruling", "The judge", "Excluded. " + q["out_reason"])
        self.last_answer = q["answer"]

    def _do_offer(self, eid: str) -> None:
        e = _exhibit(self.case, eid)
        if e["admit"] and e.get("needs") and e["needs"] not in self.testified:
            self.judge_ruling = e["out_reason"]
            self.last_answer_by = None
            self.last_answer = f"Offered: {e['name']} — {e['desc']}"
            self._log("ruling", "The judge", "Excluded (no foundation). " + e["out_reason"])
            return
        if e["admit"]:
            self.offered.add(eid)
            self.admitted.append(
                {
                    "kind": "exhibit",
                    "source": e["name"],
                    "text": e["desc"],
                    "probative": e["probative"],
                }
            )
            self.judge_ruling = e["in_reason"]
            self._log("ruling", "The judge", "Admitted. " + e["in_reason"])
        else:
            self.offered.add(eid)
            self.excluded.append({"source": e["name"], "text": e["desc"]})
            self.judge_ruling = e["out_reason"]
            self._log("ruling", "The judge", "Excluded. " + e["out_reason"])
        self.last_answer_by = None
        self.last_answer = f"Offered: {e['name']} — {e['desc']}"

    def _do_rest(self) -> None:
        self.on_stand = None
        self.phase = "closing"
        self.judge_ruling = ""
        self.last_answer = "The State rests. It is time for closing arguments."
        self.last_answer_by = None
        self._log("note", "The prosecutor", "The State rests.")

    def _do_closing(self) -> None:
        admitted = self.admitted
        bits = [a["text"] for a in admitted]
        if admitted:
            self.closing_text = (
                "Ladies and gentlemen of the jury — you heard that " +
                " and that ".join(bits) +
                ". On the record before you, the People ask you to convict."
            )
        else:
            self.closing_text = (
                "Ladies and gentlemen of the jury — the People presented no "
                "admitted evidence for you to weigh."
            )
        total = sum(a["probative"] for a in admitted)
        if total >= self.case["threshold"]:
            self.verdict = "guilty"
        else:
            self.verdict = "not guilty"
        self.phase = "verdict"
        self.done = True
        self.judge_ruling = ""
        self.last_answer_by = None
        self.last_answer = self._verdict_line()
        self._log("verdict", "The jury", self._verdict_line())

    def _verdict_line(self) -> str:
        if self.verdict == "guilty":
            return "Verdict: GUILTY of theft."
        return "Verdict: NOT GUILTY."

    # -- observation ---------------------------------------------------------
    def _legal_moves(self) -> List[str]:
        if self.done:
            return []
        if self.phase == "closing":
            return ["closing"]
        moves: List[str] = []
        for w in self.case["witnesses"]:
            if self.phase == "call":
                moves.append("call:" + w["id"])
        for e in self.case["exhibits"]:
            if e["id"] not in self.offered:
                moves.append("offer:" + e["id"])
        if self.phase == "stand" and self.on_stand:
            w = _witness(self.case, self.on_stand)
            for q in w["questions"]:
                if q["id"] not in self.asked:
                    moves.append("ask:" + q["id"])
            moves.append("excuse")
        moves.append("rest")
        return moves

    def _observe(self, last_move: Optional[str]) -> Dict[str, Any]:
        reward = 0.0
        if self.done:
            reward = 1.0 if self.verdict == "guilty" else -1.0
        witnesses = []
        for w in self.case["witnesses"]:
            if w["id"] == self.on_stand:
                status = "on the stand"
            elif w["id"] in self.testified:
                status = "testified"
            else:
                status = ""
            witnesses.append(
                {"id": w["id"], "label": w["name"], "role": w["role"], "status": status}
            )
        exhibits = []
        for e in self.case["exhibits"]:
            if e["id"] in self.offered:
                status = "admitted" if e["admit"] else "excluded"
            else:
                status = ""
            exhibits.append(
                {"id": e["id"], "label": e["name"], "desc": e["desc"], "status": status}
            )
        questions = []
        if self.phase == "stand" and self.on_stand:
            w = _witness(self.case, self.on_stand)
            for q in w["questions"]:
                questions.append(
                    {
                        "id": q["id"],
                        "label": q["text"],
                        "asked": q["id"] in self.asked,
                    }
                )
        on_stand = None
        if self.on_stand:
            w = _witness(self.case, self.on_stand)
            on_stand = {"id": w["id"], "name": w["name"], "role": w["role"]}
        admitted_record = [
            {"kind": a["kind"], "source": a["source"], "text": a["text"]}
            for a in self.admitted
        ]
        verdict_label = ""
        verdict_note = ""
        if self.done:
            verdict_label = "Guilty" if self.verdict == "guilty" else "Not guilty"
            n_admitted = len(self.admitted)
            verdict_note = (
                f"The jury weighed only the {n_admitted} item(s) the judge admitted. "
                + self._verdict_line()
            )
        return base_observation(
            reward=reward,
            done=self.done,
            legal_moves=self._legal_moves(),
            last_move=last_move,
            task_id=self.task_id,
            case_id=self.case["id"],
            case_name=self.case["name"],
            charge=self.case["charge"],
            defendant=self.case["defendant"],
            victim=self.case["victim"],
            goal=self.case["goal"],
            phase=self.phase,
            note=self._note,
            witnesses=witnesses,
            on_stand=on_stand,
            questions=questions,
            exhibits=exhibits,
            admitted_record=admitted_record,
            admitted_count=len(self.admitted),
            excluded_count=len(self.excluded),
            transcript=list(self.transcript),
            last_answer=self.last_answer,
            last_answer_by=self.last_answer_by,
            judge_ruling=self.judge_ruling,
            closing_text=self.closing_text if self.done else "",
            verdict=self.verdict if self.done else None,
            verdict_label=verdict_label,
            verdict_note=verdict_note,
        )