"""Blocksworld: generator, verifier, prompts. Pure planning domain — no code syntax, no arithmetic. Plans are symbolically verifiable by simulation, so STaR bucketing works. Instances are generated (contamination-free by construction) with difficulty = blocks + moves. """ import json import random import re MOVE_RE = re.compile( r"move\s+([A-Z])\s+(?:onto|on top of|on)\s+(?:the\s+)?(table|[A-Z])", re.IGNORECASE) def gen_instance(n_blocks, rng): blocks = [chr(65 + i) for i in range(n_blocks)] def random_stacks(): bs = blocks[:] rng.shuffle(bs) stacks, i = [], 0 while i < len(bs): take = rng.randint(1, len(bs) - i) stacks.append(bs[i : i + take]) i += take return stacks init = random_stacks() goal = random_stacks() while goal == init: goal = random_stacks() return {"blocks": blocks, "init": init, "goal": goal} def fmt_state(stacks): out = [] for st in stacks: if len(st) == 1: out.append(f"{st[0]} is on the table") else: out.append(f"{st[0]} is on the table with " + " on top, then ".join( [f"{b}" for b in st[1:]]) + " on top") # clearer explicit form lines = [] for st in stacks: lines.append(f"stack: {' -> '.join(st)} (bottom -> top)") return "; ".join(lines) def question(inst): return ( "You are stacking blocks. Only the TOP block of a stack can be " "moved, one block at a time.\n" f"Blocks: {', '.join(inst['blocks'])}\n" f"Initial state: {fmt_state(inst['init'])}\n" f"Goal state: {fmt_state(inst['goal'])}\n" "Give a plan as a numbered list of moves, each exactly of the form " "'move X onto Y' or 'move X onto the table'.") def verify_plan(inst, text, max_moves=40): stacks = [st[:] for st in inst["init"]] def top_of(b): for st in stacks: if st and st[-1] == b: return st return None moves = MOVE_RE.findall(text) if not moves or len(moves) > max_moves: return False for b, tgt in moves: b = b.upper() tgt = tgt if tgt.lower() == "table" else tgt.upper() src = top_of(b) if src is None: return False # b not clear (or nonexistent) if tgt == "table" or tgt.lower() == "table": src.pop() stacks.append([b]) else: dst = top_of(tgt) if dst is None or b == tgt: return False src.pop() dst.append(b) stacks = [st for st in stacks if st] norm = sorted(tuple(st) for st in stacks) return norm == sorted(tuple(st) for st in inst["goal"]) def make_dataset(n_train=400, n_test=200, seed=0): rng = random.Random(seed) items = [] for split, n in (("train", n_train), ("test", n_test)): for i in range(n): nb = rng.choice([3, 3, 4, 4, 5]) inst = gen_instance(nb, rng) items.append({"split": split, "task_id": f"bw_{split}_{i}", "n_blocks": nb, **inst, "question": question(inst)}) return items if __name__ == "__main__": ds = make_dataset() print(json.dumps(ds[0], indent=1)) # verifier self-test: identity plan on trivial instance inst = {"blocks": ["A", "B"], "init": [["A"], ["B"]], "goal": [["A", "B"]]} assert verify_plan(inst, "1. move B onto A") assert not verify_plan(inst, "1. move A onto A") print("verifier self-test ok")