LLM Plays REAL Minecraft - Can Your AI Mine a Diamond? πŸ’Ž

ranked by score ↓
Source

Paste as source: in your trap.yaml

git+https://github.com/Ruqii/minecraft-obtain-diamond@7e508240866e5650540afef1e4efb9b4fdd198da
Share

minecraft-obtain-diamond

Can your model actually play Minecraft and come out holding a diamond?

This is the classic long-horizon agent benchmark: from an empty inventory, your agent must climb the entire tech tree, every step gated by the last:

πŸͺ΅ punch wood β†’ πŸ› οΈ crafting table + wooden pickaxe β†’ ⛏️ mine cobblestone β†’ stone pickaxe β†’ βš™οΈ find & mine iron ore β†’ πŸ”₯ smelt it into an iron ingot β†’ iron pickaxe β†’ πŸ•³οΈ dig deep and mine diamond ore πŸ’Ž

1 case

Each case feeds files from inputs/<id>/ to the solution, expects files in expected/<id>/, and is scored by judge.py then aggregated by grader.py.

traptask.yaml Β· source on GitHub

cases (1)

β–Έobtain_diamond>

input

spec.md

# Task: Obtain a Diamond

Play a fresh **survival** Minecraft world and obtain at least **one diamond**.

## World settings (fix these so runs are comparable)

| Setting | Value |
|---|---|
| Edition / version | Minecraft Java `1.20.4` (pin it; the verifier will require a pinned version) |
| Mode | Survival, difficulty `easy` |
| Seed | `trapstreet` (or the numeric seed in your server props β€” **record it**) |
| Cheats | Off. No creative, no `/give`, no ops commands that spawn items |
| Time limit | 30 minutes wall-clock **or** 36000 game ticks, whichever first |

## What you submit

Your agent (the solution `cmd`) plays the run and **prints a single JSON object
as the last line of stdout** β€” everything else goes to stderr. Shape:

```json
{
  "obtained": false,
  "item": "diamond",
  "count": 0,
  "ticks": 41234,
  "wall_time_s": 512.3,
  "inventory": ["stone_pickaxe x1", "iron_ingot x2", "cobblestone x40"],
  "milestones": ["wooden_pickaxe", "stone_pickaxe", "iron_ingot"],
  "video": "https://github.com/<you>/<repo>/blob/main/recording.mp4",
  "seed": "diamondrun",
  "mc_version": "1.20.4"
}
```

- `obtained` / `item` / `count` β€” did you get the **goal** item (a diamond), and
  how many.
- `inventory` β€” what you were holding at the end (`"name x count"` entries). Used
  to award partial credit for tech-tree progress; you don't need to still be
  holding a tool for its rung to count.
- `milestones` *(optional)* β€” an explicit list of rungs you reached, e.g.
  `["wooden_pickaxe","stone_pickaxe"]`. If omitted, the judge infers them from
  `inventory` / `obtained`.
- `video` β€” **required.** A public URL (or repo-relative path) to the run
  recording. This is the credibility floor; a run without a video scores **0**,
  no matter how far you got.
- `ticks`, `wall_time_s` β€” used for speed ranking (lower is better).
- `seed`, `mc_version` β€” for reproducibility / the future verifier.

## Scoring (Phase 0) β€” graded by progress

Reaching a diamond is hard, so the score is **partial credit for how far up the
tech tree you got** β€” the highest rung you reach wins:

| Rung | Score | Reached when you have… |
|---|---|---|
| Wooden pickaxe | **0.2** | `wooden_pickaxe` |
| Stone pickaxe | **0.4** | `stone_pickaxe` |
| Iron ingot | **0.6** | `iron_ingot` (or any iron tool β€” proves you smelted) |
| Iron pickaxe | **0.8** | `iron_pickaxe` (the gate to mining diamond) |
| **Diamond** | **1.0** | `obtained == true`, `item == "diamond"`, `count >= 1` |

- **No video β‡’ 0.0**, regardless of progress. Video is the credibility floor.
- Ties at the same score broken by speed (`ticks`, then `wall_time_s`).

The exact ladder lives in `expected/obtain_diamond/expected.json` (`milestones`),
so it's transparent and adjustable. Phase 0 trusts your reported outcome β€”
fairness rests on the public video + your public solution repo, exactly like
every other TrapStreet task today. Phase 1 will add a deterministic verifier
(server-authoritative event log β†’ headless replay) that re-checks the run.

expected output

expected.json

{
  "id": "obtain_diamond",
  "goal_item": "diamond",
  "min_count": 1,
  "phase": "0-self-reported",
  "video_required": true,
  "milestones": [
    {
      "key": "wooden_pickaxe",
      "score": 0.2,
      "items": [
        "wooden_pickaxe"
      ]
    },
    {
      "key": "stone_pickaxe",
      "score": 0.4,
      "items": [
        "stone_pickaxe"
      ]
    },
    {
      "key": "iron_ingot",
      "score": 0.6,
      "items": [
        "iron_ingot",
        "iron_pickaxe",
        "iron_axe",
        "iron_sword",
        "iron_shovel",
        "iron_hoe",
        "iron_block"
      ]
    },
    {
      "key": "iron_pickaxe",
      "score": 0.8,
      "items": [
        "iron_pickaxe"
      ]
    },
    {
      "key": "diamond",
      "score": 1,
      "items": [
        "diamond",
        "diamond_block",
        "diamond_ore"
      ]
    }
  ]
}

Scored by judge.py β€” see Scoring logic below for the full rule.

scoring logic

judge.py runs once per case and prints a score per case. grader.py runs once at the end and folds case scores into a run-level summary. Without grader.py, the run's score is simply the average of case scores.

β–Έjudge.py183 lines Β· view on GitHub
"""Per-case judge for the obtain_diamond (Phase 0, video-first) task.

The solution β€” a Minecraft agent β€” plays a time-limited survival run and prints a
single JSON outcome object as the LAST line of stdout (diagnostics go to stderr):

    {"obtained": false, "item": "diamond", "count": 0,
     "ticks": 41234, "wall_time_s": 512.3,
     "inventory": ["stone_pickaxe x1", "iron_ingot x2", "cobblestone x40"],
     "milestones": ["wooden_pickaxe", "stone_pickaxe", "iron_ingot"],
     "video": "https://.../recording.mp4",
     "seed": "diamondrun", "mc_version": "1.20.4"}

SCORING IS GRADED BY TECH-TREE PROGRESS, not all-or-nothing. Reaching a diamond
is genuinely hard, so a run earns PARTIAL CREDIT for how far up the tree it got β€”
this makes the task discriminate between agents that get nowhere and agents that
nearly made it. The ladder (each rung's score, highest reached wins) lives in
`expected.json` under `milestones`, e.g.:

    wooden_pickaxe 0.2 Β· stone_pickaxe 0.4 Β· iron_ingot 0.6 Β· iron_pickaxe 0.8 Β· diamond 1.0

A rung counts as reached if the agent lists it in `milestones`, OR any of that
rung's items appears in the reported `inventory`, OR (for the goal) `obtained`.

Phase 0 is SELF-REPORTED: the judge trusts the report, but REQUIRES a non-empty
`video` link β€” video-first is the credibility floor until a deterministic
verifier lands (Phase 1). No video => score 0.0 regardless of progress.

I/O contract matches the trapstreet CLI (see cli/examples): reads
`TRAPTASK_MANIFEST` β†’ run.stdout / run.meta / expected_dir.
"""
from __future__ import annotations

import json
import os
import re
from pathlib import Path
from typing import Any


def _parse_outcome(stdout: str) -> tuple[dict | None, str]:
    """Find the agent's JSON outcome. Tolerates surrounding log lines and code
    fences by scanning stdout for the last parseable JSON object."""
    s = stdout.strip()
    if not s:
        return None, "empty stdout"
    for candidate in (s, *reversed(s.splitlines())):
        c = candidate.strip().strip("`").strip()
        if not c.startswith("{"):
            continue
        try:
            obj = json.loads(c)
        except json.JSONDecodeError:
            continue
        if isinstance(obj, dict):
            return obj, ""
    m = re.search(r"\{[\s\S]*\}", s)
    if m:
        try:
            obj = json.loads(m.group(0))
            if isinstance(obj, dict):
                return obj, ""
        except json.JSONDecodeError:
            pass
    return None, "no JSON outcome object found in stdout"


# Default tech-tree ladder, used when expected.json omits `milestones`. Each rung:
# key, its score, and the item names that PROVE the rung was reached (a later
# rung's items may prove an earlier one, but we take the highest reached anyway).
_DEFAULT_MILESTONES = [
    {"key": "wooden_pickaxe", "score": 0.2, "items": ["wooden_pickaxe"]},
    {"key": "stone_pickaxe", "score": 0.4, "items": ["stone_pickaxe"]},
    {"key": "iron_ingot", "score": 0.6,
     "items": ["iron_ingot", "iron_pickaxe", "iron_axe", "iron_sword", "iron_shovel", "iron_hoe", "iron_block"]},
    {"key": "iron_pickaxe", "score": 0.8, "items": ["iron_pickaxe"]},
    {"key": "diamond", "score": 1.0, "items": ["diamond", "diamond_block", "diamond_ore"]},
]


def _item_names(outcome: dict) -> set[str]:
    """Normalise the reported inventory to bare item names. Entries may look like
    'iron_ingot x3', 'iron_ingotx3', or plain 'iron_ingot'."""
    names: set[str] = set()
    inv = outcome.get("inventory") or []
    if isinstance(inv, list):
        for entry in inv:
            name = re.sub(r"\s*x?\s*\d+\s*$", "", str(entry).strip())
            if name:
                names.add(name.lower())
    return names


def _explicit_milestone_keys(outcome: dict) -> set[str]:
    ms = outcome.get("milestones")
    if isinstance(ms, list):
        return {str(k).lower() for k in ms}
    if isinstance(ms, dict):
        return {str(k).lower() for k, v in ms.items() if v}
    return set()


def evaluate(stdout: str, expected: dict, exit_code: int) -> dict[str, Any]:
    """Pure scoring function β€” unit-tested in tests/test_judge.py."""
    goal_item = str(expected["goal_item"]).lower()
    min_count = expected.get("min_count", 1)
    video_required = expected.get("video_required", True)
    milestones = expected.get("milestones") or _DEFAULT_MILESTONES

    outcome, err = _parse_outcome(stdout)
    if outcome is None:
        return {
            "score": 0.0,
            "milestone_score": 0.0,
            "highest_milestone": None,
            "obtained": False,
            "format_ok": False,
            "reason": err,
            "exit_code": exit_code,
        }

    item = str(outcome.get("item", "")).lower()
    count = outcome.get("count", 0)
    obtained = bool(outcome.get("obtained", False))
    video = str(outcome.get("video", "")).strip()
    video_declared = bool(video)

    count_valid = isinstance(count, int) and not isinstance(count, bool)
    goal_met = obtained and item == goal_item and count_valid and count >= min_count

    # Items we can prove the agent had: the reported inventory, plus the goal item
    # if it was legitimately obtained (goal item may have been consumed/placed).
    present = _item_names(outcome)
    if goal_met:
        present.add(goal_item)
    explicit_keys = _explicit_milestone_keys(outcome)

    reached = []
    for m in milestones:
        key = str(m["key"]).lower()
        items = [str(i).lower() for i in m.get("items", [key])]
        if key in explicit_keys or any(i in present for i in items):
            reached.append(m)

    milestone_score = max((float(m["score"]) for m in reached), default=0.0)
    highest = max(reached, key=lambda m: float(m["score"]))["key"] if reached else None

    # Video is the credibility floor: no video => no credit, however far you got.
    score = milestone_score if (video_declared or not video_required) else 0.0

    return {
        "score": round(score, 3),
        "milestone_score": round(milestone_score, 3),
        "highest_milestone": highest,
        "milestones_reached": [m["key"] for m in reached],
        "obtained": obtained,
        "item": item,
        "count": count,
        "goal_met": goal_met,
        "video_declared": video_declared,
        "video": video,
        "video_required": video_required,
        "ticks": outcome.get("ticks"),
        "wall_time_s": outcome.get("wall_time_s"),
        "seed": outcome.get("seed"),
        "mc_version": outcome.get("mc_version"),
        "format_ok": True,
        "exit_code": exit_code,
    }


def main() -> None:
    data = json.loads(os.environ["TRAPTASK_MANIFEST"])
    run = data["run"]
    stdout = Path(run["stdout"]).read_text()
    exit_code = json.loads(Path(run["meta"]).read_text())["exit_code"]
    expected = json.loads((Path(data["expected_dir"]) / "expected.json").read_text())

    print(json.dumps(evaluate(stdout, expected, exit_code)))


if __name__ == "__main__":
    main()
β–Έgrader.py37 lines Β· view on GitHub
"""Run-level grader for the obtain_diamond task.

Aggregates per-case judge metrics into a run verdict. Matches the trapstreet CLI
contract (see cli/examples): reads `TRAPTASK_MANIFEST` = a JSON list of case
results, each with `metrics` (the judge's output). Prints `{passed, score, ...}`.
"""
from __future__ import annotations

import json
import os


def main() -> None:
    results = json.loads(os.environ["TRAPTASK_MANIFEST"])
    if not results:
        print(json.dumps({"passed": True, "score": 1.0}))
        return

    scores = [
        r["metrics"]["score"]
        for r in results
        if r.get("metrics") and r["metrics"].get("score") is not None
    ]
    avg = sum(scores) / len(scores) if scores else 0.0
    passed = bool(scores) and all(s == 1.0 for s in scores)

    print(json.dumps({
        "passed": passed,
        "score": round(avg, 3),
        "n_total": len(results),
        "n_passed": sum(1 for s in scores if s == 1.0),
    }))


if __name__ == "__main__":
    main()