Gododin Gododin Documentation

Linked repository source

examples/logistics_post/project/main.gd

This read-only view comes from the file in the Gododin repository. It retains its design, example or evidence context.

Download the original file · Return to the guide

extends Node2D

## Logistics Post (DX-8): a compact depot-logistics loop. Godot owns the map
## authoring (the ASCII layout below), input, selection, pacing, and
## presentation; the native LogisticsPost owns job assignment, routing,
## inventory, and the fixed simulation ticks. The scene imports the map once,
## submits jobs with stable ids, drives explicit ticks, and reads one bounded
## worker snapshot per render — no engine call or signal per agent per tick.
## The job_completed signal drives the completion log; deterministic replay is
## proven by verify_probe.gd against the GDScript oracle.

const TILE := 40.0
const MAP := [
	"################",
	"#S...R....R...D#",
	"#.R..R....R.R.R#",
	"#.R..RRRRRR.R.R#",
	"#.R........R.R.#",
	"#.RRRRRRR..R.R.#",
	"#......R...R.R.#",
	"#.RRRR.R.RRR.R.#",
	"#....R.R.....R.#",
	"#.RRRR.RRRRRRR.#",
	"#S...R.......RD#",
	"################",
]

const CELL_EMPTY := 0
const CELL_ROAD := 1
const CELL_SOURCE := 2
const CELL_DEST := 3

const WORKER_COLORS := [Color(0.35, 0.75, 1.0), Color(1.0, 0.75, 0.3), Color(0.55, 1.0, 0.45), Color(1.0, 0.5, 0.8)]

@onready var world: Node2D = $Map
@onready var worker_layer: Node2D = $Workers

var sim: Node = null
var paused := true
var tick_accum := 0.0
var selected := -1
var worker_nodes: Array[Node2D] = []
var source_cells: PackedInt32Array = []
var dest_cells: PackedInt32Array = []
var pending_source := -1

func _ready() -> void:
	if not ClassDB.class_exists("LogisticsPost"):
		push_error("LogisticsPost is not registered; run gododin build first.")
		return
	sim = $Sim
	var settings: Resource = load("res://logistics_settings.tres")
	_build_map()
	var cells := _pack_cells()
	var code: int = sim.call("configure", MAP[0].length(), MAP.size(), cells,
		int(settings.worker_count), int(settings.seed))
	if code != 0:
		push_error("configure failed with %d" % code)
		return
	for source in source_cells:
		for dest in dest_cells:
			if source % 7 == dest % 5:
				sim.call("submit_job", source, dest)
	_spawn_worker_sprites(int(settings.worker_count))
	%PauseButton.pressed.connect(func() -> void: paused = not paused)
	%StepButton.pressed.connect(_do_tick)
	%ResetButton.pressed.connect(_on_reset)

func _pack_cells() -> PackedByteArray:
	var cells := PackedByteArray()
	cells.resize(MAP[0].length() * MAP.size())
	source_cells = PackedInt32Array()
	dest_cells = PackedInt32Array()
	for y in MAP.size():
		for x in MAP[y].length():
			var kind := CELL_EMPTY
			match MAP[y][x]:
				".": kind = CELL_ROAD
				"R": kind = CELL_ROAD
				"S":
					kind = CELL_SOURCE
					source_cells.append(y * MAP[0].length() + x)
				"D":
					kind = CELL_DEST
					dest_cells.append(y * MAP[0].length() + x)
			cells[y * MAP[0].length() + x] = kind
	return cells

func _build_map() -> void:
	for y in MAP.size():
		for x in MAP[y].length():
			var rect := ColorRect.new()
			rect.position = Vector2(x * TILE, y * TILE)
			rect.size = Vector2(TILE, TILE)
			match MAP[y][x]:
				"#": rect.color = Color(0.12, 0.13, 0.16)
				"R": rect.color = Color(0.55, 0.5, 0.42)
				"S": rect.color = Color(0.95, 0.75, 0.2)
				"D": rect.color = Color(0.3, 0.85, 0.5)
				_: rect.color = Color(0.07, 0.08, 0.1)
			world.add_child(rect)

func _spawn_worker_sprites(worker_count: int) -> void:
	for index in worker_count:
		var dot := Node2D.new()
		var body := ColorRect.new()
		body.size = Vector2(TILE * 0.5, TILE * 0.5)
		body.position = -body.size / 2
		body.color = WORKER_COLORS[index % WORKER_COLORS.size()]
		dot.add_child(body)
		worker_layer.add_child(dot)
		worker_nodes.append(dot)

func _process(delta: float) -> void:
	if sim == null:
		return
	if not paused:
		tick_accum += delta
		while tick_accum >= 0.25:
			tick_accum -= 0.25
			_do_tick()
	_present()

func _do_tick() -> void:
	var delivered: int = sim.call("tick")
	if delivered > 0:
		var total: int = sim.call("get_deliveries")
		feedback("delivery complete — %d total" % total)

func _present() -> void:
	var worker_count: int = sim.call("get_worker_count")
	var snapshot := PackedFloat32Array()
	snapshot.resize(worker_count * 5)
	var written: int = sim.call("worker_snapshot", snapshot)
	if written < 0:
		return
	for index in worker_count:
		var base := index * 5
		var node := worker_nodes[index]
		node.position = Vector2((snapshot[base+1] + 0.5) * TILE, (snapshot[base+2] + 0.5) * TILE)
		var state := int(snapshot[base+3])
		var job := int(snapshot[base+4])
		node.modulate = Color(1, 1, 1) if state == 0 else (Color(1.6, 1.6, 1.2) if state == 1 else Color(1.2, 1.8, 1.2))
		if index == selected:
			node.scale = Vector2(1.35, 1.35)
		else:
			node.scale = Vector2.ONE
	%StatsLabel.text = "tick %d · deliveries %d · jobs %d" % [
		int(sim.call("get_ticks")), int(sim.call("get_deliveries")), int(sim.call("get_job_count"))]
	if selected >= 0 and selected < worker_count:
		var base := selected * 5
		var job_state: int = sim.call("job_state", int(snapshot[base+4])) if snapshot[base+4] >= 0 else -1
		%InspectLabel.text = "worker %d · state %d · job %d (%s)" % [
			selected, int(snapshot[base+3]), int(snapshot[base+4]),
			["pending", "assigned", "delivered"][job_state] if job_state >= 0 and job_state < 3 else "none"]

func _unhandled_input(event: InputEvent) -> void:
	if sim == null or not event is InputEventMouseButton or not event.pressed:
		return
	if event.button_index != MOUSE_BUTTON_LEFT:
		return
	var world_mouse := get_global_mouse_position()
	var cell := Vector2i(world_mouse.floor())
	var index := cell.y * MAP[0].length() + cell.x
	# Select a worker within half a tile; otherwise queue a job from a source
	# to a destination (the explicit stable-id submission).
	for w_index in worker_nodes.size():
		if worker_nodes[w_index].position.distance_to(world_mouse) < TILE * 0.6:
			selected = w_index
			feedback("selected worker %d" % w_index)
			return
	selected = -1
	var is_source: bool = cell.x >= 0 and cell.y >= 0 and cell.y < MAP.size() and cell.x < MAP[0].length() and MAP[cell.y][cell.x] == "S"
	var is_dest: bool = cell.x >= 0 and cell.y >= 0 and cell.y < MAP.size() and cell.x < MAP[0].length() and MAP[cell.y][cell.x] == "D"
	if is_source:
		pending_source = index
		feedback("source %d picked — click a destination" % index)
	elif is_dest and pending_source >= 0:
		var job_id: int = sim.call("submit_job", pending_source, index)
		feedback("job #%d queued" % job_id if job_id > 0 else "job refused")
		pending_source = -1

func _on_reset() -> void:
	var settings: Resource = load("res://logistics_settings.tres")
	sim.call("configure", MAP[0].length(), MAP.size(), _pack_cells(),
		int(settings.worker_count), int(settings.seed))
	for node in worker_nodes:
		node.queue_free()
	worker_nodes.clear()
	_spawn_worker_sprites(int(settings.worker_count))
	pending_source = -1
	selected = -1
	feedback("reset — same seed, same spread")

func feedback(text: String) -> void:
	%FeedbackLabel.text = text