[
  {
    "id": "move",
    "name": "Move a CharacterBody2D with an input action",
    "prompt": "Write res://player.gd for Godot 4: a script for a CharacterBody2D that moves right at 200 pixels per second while the input action \"ui_right\" is held, and stands still otherwise.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://player.gd\")\n\tif script == null:\n\t\t_done(\"player.gd did not load\")\n\t\treturn\n\tvar body := CharacterBody2D.new()\n\tbody.set_script(script)\n\tadd_child(body)\n\tInput.action_press(\"ui_right\")\n\tfor i in 30:\n\t\tawait get_tree().physics_frame\n\tInput.action_release(\"ui_right\")\n\tvar moved: float = body.position.x\n\tfor i in 10:\n\t\tawait get_tree().physics_frame\n\tif moved < 90.0 or moved > 110.0:\n\t\t_done(\"after 0.5 s holding ui_right x is %.1f, expected about 100\" % moved)\n\telif absf(body.position.x - moved) > 1.0:\n\t\t_done(\"kept moving after ui_right was released\")\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "gravity",
    "name": "Fall with gravity and land on the floor",
    "prompt": "Write res://ball.gd for Godot 4: a script for a CharacterBody2D that falls with a gravity of 980 pixels per second squared and stops when it lands on the floor. The scene gives it its collision shape.",
    "check": "extends Node2D\n\nfunc _ready() -> void:\n\tvar script = load(\"res://ball.gd\")\n\tif script == null:\n\t\t_done(\"ball.gd did not load\")\n\t\treturn\n\tvar floor_body := StaticBody2D.new()\n\tvar floor_shape := CollisionShape2D.new()\n\tvar rect := RectangleShape2D.new()\n\trect.size = Vector2(1000, 20)\n\tfloor_shape.shape = rect\n\tfloor_body.add_child(floor_shape)\n\tfloor_body.position = Vector2(0, 200)\n\tadd_child(floor_body)\n\tvar ball := CharacterBody2D.new()\n\tvar ball_shape := CollisionShape2D.new()\n\tvar circle := CircleShape2D.new()\n\tcircle.radius = 10\n\tball_shape.shape = circle\n\tball.add_child(ball_shape)\n\tball.set_script(script)\n\tadd_child(ball)\n\tfor i in 120:\n\t\tawait get_tree().physics_frame\n\tif not ball.is_on_floor():\n\t\t_done(\"not on the floor after 2 s; y is %.1f\" % ball.position.y)\n\telif ball.position.y < 170.0 or ball.position.y > 191.0:\n\t\t_done(\"rests at y %.1f, expected about 180\" % ball.position.y)\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "score-label",
    "name": "A score label",
    "prompt": "Write res://score_label.gd for Godot 4: a script for a Label that shows \"Score: 0\" when it enters the scene, and has a method add(points: int) that adds to the score and updates the text to \"Score: <total>\".",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://score_label.gd\")\n\tif script == null:\n\t\t_done(\"score_label.gd did not load\")\n\t\treturn\n\tvar label := Label.new()\n\tlabel.set_script(script)\n\tadd_child(label)\n\tif label.text != \"Score: 0\":\n\t\t_done(\"starts as %s\" % label.text)\n\t\treturn\n\tlabel.add(3)\n\tlabel.add(4)\n\t_done(\"\" if label.text == \"Score: 7\" else \"after add(3) and add(4) shows %s\" % label.text)\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "save-load",
    "name": "Save and load a dictionary as JSON",
    "prompt": "Write res://save_game.gd for Godot 4: a script extending RefCounted with two static functions: save_data(data: Dictionary, path: String) -> void writes the dictionary to the file as JSON, and load_data(path: String) -> Dictionary reads it back.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://save_game.gd\")\n\tif script == null:\n\t\t_done(\"save_game.gd did not load\")\n\t\treturn\n\tvar path := \"user://bench_save.json\"\n\tscript.save_data({\"level\": 3, \"name\": \"Ada\", \"items\": [\"key\", \"map\"]}, path)\n\tvar back = script.load_data(path)\n\tif typeof(back) != TYPE_DICTIONARY:\n\t\t_done(\"load_data returned %s\" % type_string(typeof(back)))\n\telif int(back.get(\"level\", -1)) != 3 or back.get(\"name\") != \"Ada\" or back.get(\"items\") != [\"key\", \"map\"]:\n\t\t_done(\"read back %s\" % str(back))\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "health-signals",
    "name": "Health with signals",
    "prompt": "Write res://health.gd for Godot 4: a script extending Node with health starting at 100, a signal health_changed(old_value: int, new_value: int), a signal died, and a method take_damage(amount: int) that lowers health without going below 0, emits health_changed when health changes, and emits died once, when health reaches 0.",
    "check": "extends Node\n\nvar changes: Array = []\nvar deaths := 0\n\nfunc _ready() -> void:\n\tvar script = load(\"res://health.gd\")\n\tif script == null:\n\t\t_done(\"health.gd did not load\")\n\t\treturn\n\tvar health := Node.new()\n\thealth.set_script(script)\n\tadd_child(health)\n\thealth.health_changed.connect(func(old_value, new_value): changes.append([old_value, new_value]))\n\thealth.died.connect(func(): deaths += 1)\n\thealth.take_damage(30)\n\thealth.take_damage(80)\n\thealth.take_damage(10)\n\tif changes.size() < 2 or changes[0] != [100, 70] or changes[1] != [70, 0]:\n\t\t_done(\"health_changed gave %s\" % str(changes))\n\telif deaths != 1:\n\t\t_done(\"died emitted %d times\" % deaths)\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "fade-tween",
    "name": "Fade out with a tween",
    "prompt": "Write res://fader.gd for Godot 4: a script extending Node2D with a method fade_out(duration: float) that fades the node's modulate alpha from 1 to 0 over duration seconds with a tween, and a signal faded emitted when the fade ends.",
    "check": "extends Node\n\nvar faded_at := -1\n\nfunc _ready() -> void:\n\tvar script = load(\"res://fader.gd\")\n\tif script == null:\n\t\t_done(\"fader.gd did not load\")\n\t\treturn\n\tvar fader := Node2D.new()\n\tfader.set_script(script)\n\tadd_child(fader)\n\tvar start := Engine.get_process_frames()\n\tfader.faded.connect(func(): faded_at = Engine.get_process_frames() - start)\n\tfader.fade_out(0.5)\n\tfor i in 60:\n\t\tawait get_tree().process_frame\n\t\tif i == 15 and (fader.modulate.a > 0.75 or fader.modulate.a < 0.25):\n\t\t\t_done(\"alpha %.2f a quarter of a second in, expected about 0.5\" % fader.modulate.a)\n\t\t\treturn\n\tif faded_at < 0:\n\t\t_done(\"faded was not emitted within 1 s\")\n\telif absi(faded_at - 30) > 3:\n\t\t_done(\"faded emitted after %d frames, expected about 30\" % faded_at)\n\telif fader.modulate.a > 0.01:\n\t\t_done(\"alpha ends at %.2f\" % fader.modulate.a)\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "spawn-timer",
    "name": "Spawn on a timer, then stop",
    "prompt": "Write res://spawner.gd for Godot 4: a script extending Node that, once in the scene, adds a new child Node2D every 0.25 seconds, using a Timer, and stops after 4 children. It keeps the number spawned in a variable spawned_count.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://spawner.gd\")\n\tif script == null:\n\t\t_done(\"spawner.gd did not load\")\n\t\treturn\n\tvar spawner := Node.new()\n\tspawner.set_script(script)\n\tadd_child(spawner)\n\tfor i in 40:\n\t\tawait get_tree().process_frame\n\tvar early: int = spawner.spawned_count\n\tfor i in 80:\n\t\tawait get_tree().process_frame\n\tvar spawned := 0\n\tfor child in spawner.get_children():\n\t\tif child is Node2D:\n\t\t\tspawned += 1\n\tif early < 2 or early > 3:\n\t\t_done(\"%d spawned after 0.67 s, expected 2\" % early)\n\telif spawner.spawned_count != 4 or spawned != 4:\n\t\t_done(\"spawned_count %d and %d Node2D children after 2 s, expected 4\" % [spawner.spawned_count, spawned])\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "door-states",
    "name": "A door with states",
    "prompt": "Write res://door.gd for Godot 4: a script extending Node for a door with the states CLOSED, OPENING and OPEN. It starts CLOSED. A method interact() starts opening a closed door; 0.5 seconds of game time later it is OPEN. A method get_state_name() -> String returns \"CLOSED\", \"OPENING\" or \"OPEN\".",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://door.gd\")\n\tif script == null:\n\t\t_done(\"door.gd did not load\")\n\t\treturn\n\tvar door := Node.new()\n\tdoor.set_script(script)\n\tadd_child(door)\n\tif door.get_state_name() != \"CLOSED\":\n\t\t_done(\"starts %s\" % door.get_state_name())\n\t\treturn\n\tdoor.interact()\n\tfor i in 10:\n\t\tawait get_tree().process_frame\n\tif door.get_state_name() != \"OPENING\":\n\t\t_done(\"%s a sixth of a second after interact()\" % door.get_state_name())\n\t\treturn\n\tfor i in 30:\n\t\tawait get_tree().process_frame\n\t_done(\"\" if door.get_state_name() == \"OPEN\" else \"%s after 0.67 s\" % door.get_state_name())\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "inventory",
    "name": "An inventory with counts",
    "prompt": "Write res://inventory.gd for Godot 4: a script extending RefCounted with add_item(item_name: String, count: int), remove_item(item_name: String, count: int) -> bool that removes only when there are enough and returns whether it did, and get_count(item_name: String) -> int that is 0 for an item never added.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://inventory.gd\")\n\tif script == null:\n\t\t_done(\"inventory.gd did not load\")\n\t\treturn\n\tvar inventory = script.new()\n\tinventory.add_item(\"potion\", 3)\n\tinventory.add_item(\"potion\", 2)\n\tvar removed_too_many: bool = inventory.remove_item(\"potion\", 9)\n\tvar removed_two: bool = inventory.remove_item(\"potion\", 2)\n\tif removed_too_many:\n\t\t_done(\"removed 9 potions out of 5\")\n\telif not removed_two:\n\t\t_done(\"could not remove 2 potions out of 5\")\n\telif inventory.get_count(\"potion\") != 3:\n\t\t_done(\"%d potions left, expected 3\" % inventory.get_count(\"potion\"))\n\telif inventory.get_count(\"sword\") != 0:\n\t\t_done(\"an item never added counts %d\" % inventory.get_count(\"sword\"))\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "load-level",
    "name": "Load a level scene from a path",
    "prompt": "Write res://level_loader.gd for Godot 4: a script extending Node with a method load_level(path: String) -> Node that loads the scene file at path, creates an instance of it, adds it as a child, and returns it.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://level_loader.gd\")\n\tif script == null:\n\t\t_done(\"level_loader.gd did not load\")\n\t\treturn\n\tvar root := Node2D.new()\n\troot.name = \"LevelRoot\"\n\tvar marker := Node2D.new()\n\tmarker.name = \"Spawn\"\n\troot.add_child(marker)\n\tmarker.owner = root\n\tvar packed := PackedScene.new()\n\tpacked.pack(root)\n\tResourceSaver.save(packed, \"user://bench_level.tscn\")\n\tvar loader := Node.new()\n\tloader.set_script(script)\n\tadd_child(loader)\n\tvar level = loader.load_level(\"user://bench_level.tscn\")\n\tif level == null:\n\t\t_done(\"load_level returned null\")\n\telif level.name != \"LevelRoot\" or level.get_parent() != loader:\n\t\t_done(\"returned %s under %s\" % [str(level.name), str(level.get_parent())])\n\telif level.get_node_or_null(\"Spawn\") == null:\n\t\t_done(\"the level's child Spawn is missing\")\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "tilemap-layer",
    "name": "Paint cells on a TileMapLayer",
    "prompt": "Write res://grid_painter.gd for Godot 4.7: a script extending TileMapLayer with paint_row(y: int, from_x: int, to_x: int) that sets the cells from (from_x, y) to (to_x, y) inclusive to tile source 0, atlas coordinates (0, 0), and is_painted(cell: Vector2i) -> bool that says whether a cell has a tile.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://grid_painter.gd\")\n\tif script == null:\n\t\t_done(\"grid_painter.gd did not load\")\n\t\treturn\n\tvar tile_set := TileSet.new()\n\tvar source := TileSetAtlasSource.new()\n\tsource.texture = ImageTexture.create_from_image(Image.create(16, 16, false, Image.FORMAT_RGBA8))\n\tsource.texture_region_size = Vector2i(16, 16)\n\tsource.create_tile(Vector2i(0, 0))\n\ttile_set.add_source(source, 0)\n\tvar layer := TileMapLayer.new()\n\tlayer.tile_set = tile_set\n\tlayer.set_script(script)\n\tadd_child(layer)\n\tlayer.paint_row(2, 1, 4)\n\tvar used := layer.get_used_cells()\n\tif used.size() != 4:\n\t\t_done(\"%d cells used after painting (1..4, 2), expected 4\" % used.size())\n\telif layer.get_cell_source_id(Vector2i(3, 2)) != 0 or layer.get_cell_atlas_coords(Vector2i(3, 2)) != Vector2i(0, 0):\n\t\t_done(\"cell (3, 2) is source %d, atlas %s\" % [layer.get_cell_source_id(Vector2i(3, 2)), str(layer.get_cell_atlas_coords(Vector2i(3, 2)))])\n\telif not layer.is_painted(Vector2i(1, 2)) or layer.is_painted(Vector2i(5, 2)):\n\t\t_done(\"is_painted is wrong for (1, 2) or (5, 2)\")\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "ray-query",
    "name": "Cast a physics ray",
    "prompt": "Write res://line_of_sight.gd for Godot 4: a script extending Node2D with first_hit(from: Vector2, to: Vector2) -> Vector2 that casts a physics ray through the 2D world from `from` to `to` and returns the point where it first hits a collider, or `to` when it hits nothing. It is called from physics processing.",
    "check": "extends Node2D\n\nfunc _ready() -> void:\n\tvar script = load(\"res://line_of_sight.gd\")\n\tif script == null:\n\t\t_done(\"line_of_sight.gd did not load\")\n\t\treturn\n\tvar wall := StaticBody2D.new()\n\tvar shape := CollisionShape2D.new()\n\tvar rect := RectangleShape2D.new()\n\trect.size = Vector2(20, 200)\n\tshape.shape = rect\n\twall.add_child(shape)\n\twall.position = Vector2(100, 0)\n\tadd_child(wall)\n\tvar eye := Node2D.new()\n\teye.set_script(script)\n\tadd_child(eye)\n\tfor i in 3:\n\t\tawait get_tree().physics_frame\n\tvar hit: Vector2 = eye.first_hit(Vector2(0, 0), Vector2(300, 0))\n\tvar miss: Vector2 = eye.first_hit(Vector2(0, 500), Vector2(300, 500))\n\tif absf(hit.x - 90.0) > 1.0 or absf(hit.y) > 1.0:\n\t\t_done(\"a ray at the wall returned %s, expected (90, 0)\" % str(hit))\n\telif miss.distance_to(Vector2(300, 500)) > 0.5:\n\t\t_done(\"a ray past the wall returned %s, expected (300, 500)\" % str(miss))\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "grid-path",
    "name": "A path on a grid around walls",
    "prompt": "Write res://path_finder.gd for Godot 4: a script extending RefCounted with find_path(size: Vector2i, walls: Array[Vector2i], from: Vector2i, to: Vector2i) -> Array[Vector2i] that returns the shortest path of grid cells from `from` to `to`, both included, on a grid of `size` cells, never entering a wall cell and moving only up, down, left or right.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://path_finder.gd\")\n\tif script == null:\n\t\t_done(\"path_finder.gd did not load\")\n\t\treturn\n\tvar walls: Array[Vector2i] = [Vector2i(2, 0), Vector2i(2, 1), Vector2i(2, 2), Vector2i(2, 3)]\n\tvar path = script.new().find_path(Vector2i(5, 5), walls, Vector2i(0, 0), Vector2i(4, 0))\n\tif path == null or path.size() == 0:\n\t\t_done(\"no path returned\")\n\t\treturn\n\tif Vector2i(path[0]) != Vector2i(0, 0) or Vector2i(path[path.size() - 1]) != Vector2i(4, 0):\n\t\t_done(\"path runs %s to %s\" % [str(path[0]), str(path[path.size() - 1])])\n\t\treturn\n\tfor i in path.size():\n\t\tif walls.has(Vector2i(path[i])):\n\t\t\t_done(\"path enters the wall at %s\" % str(path[i]))\n\t\t\treturn\n\t\tif i > 0:\n\t\t\tvar step := Vector2i(path[i]) - Vector2i(path[i - 1])\n\t\t\tif absi(step.x) + absi(step.y) != 1:\n\t\t\t\t_done(\"step %s to %s is not one cell up, down, left or right\" % [str(path[i - 1]), str(path[i])])\n\t\t\t\treturn\n\t_done(\"\" if path.size() == 13 else \"%d cells, the shortest is 13\" % path.size())\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "animation-code",
    "name": "Build and play an animation in code",
    "prompt": "Write res://blinker.gd for Godot 4: a script extending Node2D that, when it enters the scene, creates an AnimationPlayer child and a looping animation named \"blink\", 1 second long, that keeps the node visible for the first half of each second and hidden for the second half, and plays it.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://blinker.gd\")\n\tif script == null:\n\t\t_done(\"blinker.gd did not load\")\n\t\treturn\n\tvar blinker := Node2D.new()\n\tblinker.set_script(script)\n\tadd_child(blinker)\n\tvar seen := []\n\tfor frame in [15, 45, 75, 105]:\n\t\twhile Engine.get_process_frames() < frame:\n\t\t\tawait get_tree().process_frame\n\t\tseen.append(blinker.visible)\n\t_done(\"\" if seen == [true, false, true, false] else \"visible at 0.25, 0.75, 1.25 and 1.75 s was %s, expected [true, false, true, false]\" % str(seen))\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  },
  {
    "id": "signal-or-timeout",
    "name": "Wait for a signal or a timeout",
    "prompt": "Write res://waiter.gd for Godot 4: a script extending Node with wait_for(sig: Signal, seconds: float) -> bool, a coroutine that waits until the signal is emitted or `seconds` of game time pass, whichever comes first, and returns true if the signal came first. The signal has no arguments.",
    "check": "extends Node\n\nfunc _ready() -> void:\n\tvar script = load(\"res://waiter.gd\")\n\tif script == null:\n\t\t_done(\"waiter.gd did not load\")\n\t\treturn\n\tvar waiter := Node.new()\n\twaiter.set_script(script)\n\tadd_child(waiter)\n\tvar quick := get_tree().create_timer(0.2)\n\tvar start := Engine.get_process_frames()\n\tvar got_quick: bool = await waiter.wait_for(quick.timeout, 1.0)\n\tvar quick_frames := Engine.get_process_frames() - start\n\tvar slow := get_tree().create_timer(3.0)\n\tstart = Engine.get_process_frames()\n\tvar got_slow: bool = await waiter.wait_for(slow.timeout, 0.5)\n\tvar slow_frames := Engine.get_process_frames() - start\n\tif not got_quick:\n\t\t_done(\"returned false for a signal 0.2 s away with a 1 s timeout\")\n\telif quick_frames > 16:\n\t\t_done(\"took %d frames for a signal 0.2 s away\" % quick_frames)\n\telif got_slow:\n\t\t_done(\"returned true for a signal 3 s away with a 0.5 s timeout\")\n\telif slow_frames < 27 or slow_frames > 34:\n\t\t_done(\"timed out after %d frames, expected about 30\" % slow_frames)\n\telse:\n\t\t_done(\"\")\n\nfunc _done(problem: String) -> void:\n\tprint(\"BENCH PASS\" if problem == \"\" else \"BENCH FAIL: \" + problem)\n\tget_tree().quit()\n"
  }
]
