A robot vacuum (scripts/Roomba.gd, kind=steel) docks near spawn, lifeless, and deploys the moment the first thing in the site breaks: the cleaner arrives because there is mess. It patrols, sniffs out debris within 5 m, and eats it — each shard shrinks into the intake, beeps smugly, and takes 2 points off your score. Dent it (crowbar and up — the matrix already knew) and it panics and flees at 2.7x cruise. A sledgehammer genuinely kills it, at which point the payslip bills you for a destroyed Roomba, because it was company property and you were seen. Voice is three synthesized chirps, no assets. Not spawned in LEVEL 0 — nothing cute lives in the Backrooms. Docked = genuinely asleep, so probe_levels stays 0.00. dev/probe_roomba.gd measures the contract: docked at spawn, wakes on first smash, shard dropped in its path is eaten within 5 s and the score goes down. ROOMBA OK. probe_levels 6x 0.00 m/s - probe_overlap CLEAN. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
76 lines
2.4 KiB
GDScript
76 lines
2.4 KiB
GDScript
extends SceneTree
|
|
|
|
## The Roomba contract, measured:
|
|
## 1. docked at spawn — contributes nothing to residual motion (probe_levels covers
|
|
## the number; here we assert it is genuinely asleep and un-awake)
|
|
## 2. wakes on the first smash in the site
|
|
## 3. eats debris: a shard dropped in front of it is gone within a few seconds,
|
|
## and the score went DOWN for it
|
|
##
|
|
## Godot --headless --path game --script dev/probe_roomba.gd
|
|
|
|
func _initialize() -> void:
|
|
var main: Node = (load("res://main.tscn") as PackedScene).instantiate()
|
|
get_root().add_child(main)
|
|
await process_frame
|
|
for i in 70:
|
|
await physics_frame
|
|
|
|
var roombas := get_nodes_in_group("roomba")
|
|
if roombas.size() != 1:
|
|
print("FAIL: expected 1 roomba in scranton, got %d" % roombas.size())
|
|
quit(1)
|
|
return
|
|
var r: Roomba = roombas[0]
|
|
print("docked: awake=%s sleeping=%s at %s" % [r._awake, r.sleeping, r.global_position.snappedf(0.01)])
|
|
if r._awake:
|
|
print("FAIL: roomba should be docked until the first smash")
|
|
quit(1)
|
|
return
|
|
|
|
# smash the nearest smashable that isn't the roomba -> it must wake
|
|
var victim: Smashable = null
|
|
var best := INF
|
|
for n in get_nodes_in_group("smashable"):
|
|
if n is Roomba or not (n is Smashable) or (n as RigidBody3D).freeze:
|
|
continue
|
|
var d: float = (n.global_position - r.global_position).length_squared()
|
|
if d < best:
|
|
best = d
|
|
victim = n
|
|
victim.smash(Vector3(0, 2, 0), 999.0)
|
|
await physics_frame
|
|
if not r._awake:
|
|
print("FAIL: roomba did not wake on the first smash")
|
|
quit(1)
|
|
return
|
|
print("woke on first smash: OK")
|
|
|
|
# drop a fresh piece of debris directly in its path and count down
|
|
var score_before: int = main.get("_score")
|
|
var shard := RigidBody3D.new()
|
|
shard.add_to_group("debris")
|
|
var col := CollisionShape3D.new()
|
|
var box := BoxShape3D.new()
|
|
box.size = Vector3(0.06, 0.06, 0.06)
|
|
col.shape = box
|
|
shard.add_child(col)
|
|
main.get("_world").add_child(shard)
|
|
shard.global_position = r.global_position + Vector3(0.05, 0.12, 0.0)
|
|
|
|
var eaten := false
|
|
for i in 300: # up to 5 s
|
|
await physics_frame
|
|
if not is_instance_valid(shard) or shard.is_queued_for_deletion():
|
|
eaten = true
|
|
break
|
|
var score_after: int = main.get("_score")
|
|
print("shard eaten=%s score %d -> %d roomba _eaten=%d" % [
|
|
eaten, score_before, score_after, r._eaten])
|
|
if eaten and r._eaten >= 1:
|
|
print("ROOMBA OK")
|
|
quit(0)
|
|
else:
|
|
print("FAIL: shard survived the roomba for 5 s")
|
|
quit(1)
|