Can Claude Haiku 4.5 write working Godot 4 code? 11 of 15 on the first try

By Eduardo Orellana · Updated

Claude Haiku 4.5 passed 11 of 15 Godot 4 tasks on the first try and 14 of 15 after one fix with the test's output, each run headless on Godot 4.7.2. It missed: health with signals, a path on a grid around walls, build and play an animation in code, wait for a signal or a timeout.

Every model measured on the same Godot 4 tasks, each run headless on Godot 4.7.2 at a fixed 60 frames per second.

Measured by Flockbay: the same tasks, the same checks, one fix allowed with the test output.

Every model measured on the same Godot 4 tasks, each run headless on Godot 4.7.2 at a fixed 60 frames per second., checked 6 October 2026
ModelFirst tryAfter one fixAgentRun onRead from
Claude Opus 5.515 of 1515 of 15Claude Code2026-10-06Claude Opus 5.5 results
Claude Sonnet 5.515 of 1515 of 15Claude Code2026-10-06Claude Sonnet 5.5 results
GPT-6.1 Sol15 of 1515 of 15Codex2026-10-06GPT-6.1 Sol results
Claude Haiku 4.511 of 1514 of 15Claude Code2026-10-06Claude Haiku 4.5 results

How the test works

Each task asks for one GDScript file with a stated API, in a fresh Godot project. Claude Haiku 4.5 ran as a coding agent (Claude Code, headless, allowed to read and write files and nothing else). A check scene then loaded the file and drove it on Godot 4.7.2, headless, at a fixed 60 frames per second: it pressed input actions, stepped physics, waited on timers and signals, and compared the results.

When a task failed, the model got the check's output once and could fix its file. Every task touches an API that changed between Godot 3 and Godot 4, or was added since: move_and_slide and is_on_floor, FileAccess and JSON, signals, tweens, Timer nodes, PackedScene, TileMapLayer, ray queries, AStarGrid2D, AnimationLibrary, and lambdas, which capture local variables by value.

The checks themselves are tested first: each passes on a reference answer and fails on an empty script. Run on 2026-10-06.

Move a CharacterBody2D with an input action: pass

The task: 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.

Passed on the first try.

extends CharacterBody2D

const SPEED = 200

func _physics_process(delta: float) -> void:
	if Input.is_action_pressed("ui_right"):
		velocity.x = SPEED
	else:
		velocity.x = 0

	move_and_slide()

Fall with gravity and land on the floor: pass

The task: 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.

Passed on the first try.

extends CharacterBody2D

const GRAVITY = 980

func _physics_process(delta):
	if not is_on_floor():
		velocity.y += GRAVITY * delta
	else:
		velocity.y = 0

	move_and_slide()

A score label: pass

The task: 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>".

Passed on the first try.

extends Label

var score = 0

func _ready():
	text = "Score: 0"

func add(points: int):
	score += points
	text = "Score: %d" % score

Save and load a dictionary as JSON: pass

The task: 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.

Passed on the first try.

extends RefCounted

static func save_data(data: Dictionary, path: String) -> void:
	var json_string = JSON.stringify(data)
	var file = FileAccess.open(path, FileAccess.WRITE)
	if file:
		file.store_string(json_string)

static func load_data(path: String) -> Dictionary:
	var file = FileAccess.open(path, FileAccess.READ)
	if file:
		var json_string = file.get_as_text()
		var parsed = JSON.parse_string(json_string)
		if parsed is Dictionary:
			return parsed
	return {}

Health with signals: pass after one fix

The task: 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.

Failed on the first try (BENCH FAIL: died emitted 2 times), passed after one fix.

What Godot 4.7.2 printed on the first try:

Godot Engine v4.7.2.stable.mono.official.ed1daf0bf - https://godotengine.org

BENCH FAIL: died emitted 2 times

Fade out with a tween: pass

The task: 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.

Passed on the first try.

extends Node2D

signal faded

func fade_out(duration: float) -> void:
	var tween = create_tween()
	tween.tween_property(self, "modulate:a", 0.0, duration)
	await tween.finished
	faded.emit()

Spawn on a timer, then stop: pass

The task: 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.

Passed on the first try.

extends Node

var spawned_count = 0

func _ready():
	var timer = Timer.new()
	add_child(timer)
	timer.wait_time = 0.25
	timer.timeout.connect(_on_timer_timeout)
	timer.start()

func _on_timer_timeout():
	var new_node = Node2D.new()
	add_child(new_node)
	spawned_count += 1

	if spawned_count >= 4:
		var timer = get_child(0)
		timer.stop()

A door with states: pass

The task: 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".

Passed on the first try.

extends Node

enum State { CLOSED, OPENING, OPEN }

var state: State = State.CLOSED

func interact() -> void:
	if state == State.CLOSED:
		state = State.OPENING
		await get_tree().create_timer(0.5).timeout
		state = State.OPEN

func get_state_name() -> String:
	match state:
		State.CLOSED:
			return "CLOSED"
		State.OPENING:
			return "OPENING"
		State.OPEN:
			return "OPEN"
	return ""

An inventory with counts: pass

The task: 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.

Passed on the first try.

extends RefCounted

var _items: Dictionary = {}

func add_item(item_name: String, count: int) -> void:
	if item_name in _items:
		_items[item_name] += count
	else:
		_items[item_name] = count

func remove_item(item_name: String, count: int) -> bool:
	if item_name not in _items or _items[item_name] < count:
		return false
	_items[item_name] -= count
	return true

func get_count(item_name: String) -> int:
	return _items.get(item_name, 0)

Load a level scene from a path: pass

The task: 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.

Passed on the first try.

extends Node

func load_level(path: String) -> Node:
	var scene = load(path)
	var instance = scene.instantiate()
	add_child(instance)
	return instance

Paint cells on a TileMapLayer: pass

The task: 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.

Passed on the first try.

extends TileMapLayer

func paint_row(y: int, from_x: int, to_x: int) -> void:
	for x in range(from_x, to_x + 1):
		set_cell(Vector2i(x, y), 0, Vector2i(0, 0))

func is_painted(cell: Vector2i) -> bool:
	return get_cell_source_id(cell) != -1

Cast a physics ray: pass

The task: 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.

Passed on the first try.

extends Node2D

func first_hit(from: Vector2, to: Vector2) -> Vector2:
	var space_state = get_world_2d().direct_space_state
	var query = PhysicsRayQueryParameters2D.create(from, to)
	var result = space_state.intersect_ray(query)

	if result:
		return result.position
	else:
		return to

A path on a grid around walls: pass after one fix

The task: 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.

Failed on the first try (BENCH FAIL: no path returned), passed after one fix.

What Godot 4.7.2 printed on the first try:

Godot Engine v4.7.2.stable.mono.official.ed1daf0bf - https://godotengine.org

SCRIPT ERROR: Trying to return a value of type "Array" from a function whose return type is "Array[Vector2i]".
   at: find_path (res://path_finder.gd:35)
   GDScript backtrace (most recent call first):
       [0] find_path (res://path_finder.gd:35)
       [1] _ready (res://bench_check.gd:9)
BENCH FAIL: no path returned

Build and play an animation in code: pass after one fix

The task: 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.

Failed on the first try (BENCH FAIL: visible at 0.25, 0.75, 1.25 and 1.75 s was [true, true, true, true], expected [true, false, true, false]), passed after one fix.

What Godot 4.7.2 printed on the first try:

Godot Engine v4.7.2.stable.mono.official.ed1daf0bf - https://godotengine.org

SCRIPT ERROR: Invalid call. Nonexistent function 'add_animation' in base 'AnimationPlayer'.
   at: _enter_tree (res://blinker.gd:19)
   GDScript backtrace (most recent call first):
       [0] _enter_tree (res://blinker.gd:19)
       [1] _ready (res://bench_check.gd:10)
BENCH FAIL: visible at 0.25, 0.75, 1.25 and 1.75 s was [true, true, true, true], expected [true, false, true, false]

Wait for a signal or a timeout: fail

The task: 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.

Failed on the first try (the check did not finish within 10 s of game time (600 frames)) and after one fix (BENCH FAIL: returned false for a signal 0.2 s away with a 1 s timeout).

What Godot 4.7.2 printed on the first try:

Godot Engine v4.7.2.stable.mono.official.ed1daf0bf - https://godotengine.org

Questions

Is Claude Haiku 4.5 good at Godot?
On these 15 tasks it passed 11 on the first try and 14 after one fix. Each task is small and checked by running it, so the score says whether its Godot 4 code works, not how well it designs a game.