Add two-stage charged-crystal demo and original art set.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
extends SceneTree
|
||||
|
||||
const RECIPE_PATH := "res://data/recipe-book.json"
|
||||
const CATALOG_PATH := "res://data/art-catalog.json"
|
||||
const MAIN_SCRIPT_PATH := "res://src/main.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
var recipe := _read_json(RECIPE_PATH)
|
||||
var catalog := _read_json(CATALOG_PATH)
|
||||
if recipe.is_empty():
|
||||
failures.append("Could not read recipe book.")
|
||||
if catalog.is_empty():
|
||||
failures.append("Could not read art catalog.")
|
||||
|
||||
if failures.is_empty():
|
||||
failures.append_array(_check_items(recipe, catalog))
|
||||
failures.append_array(_check_machines(recipe, catalog))
|
||||
failures.append_array(_check_files(catalog))
|
||||
failures.append_array(_check_main_uses_generated_art())
|
||||
|
||||
if failures.is_empty():
|
||||
print("ART_CATALOG_SMOKE_OK items=%d machines=%d logistics=%d materials=%d" % [
|
||||
(catalog.get("items", []) as Array).size(),
|
||||
(catalog.get("machines", []) as Array).size(),
|
||||
(catalog.get("logistics", []) as Array).size(),
|
||||
(catalog.get("materials", []) as Array).size(),
|
||||
])
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _check_items(recipe: Dictionary, catalog: Dictionary) -> Array[String]:
|
||||
var failures: Array[String] = []
|
||||
var by_id := _index_by_id(catalog.get("items", []))
|
||||
for item_variant in recipe.get("items", []):
|
||||
var item: Dictionary = item_variant
|
||||
var item_id := String(item.get("id", ""))
|
||||
if not by_id.has(item_id):
|
||||
failures.append("Recipe item %s has no art catalog entry." % item_id)
|
||||
continue
|
||||
var entry: Dictionary = by_id[item_id]
|
||||
if String(entry.get("icon", "")).is_empty() or String(entry.get("render", "")).is_empty():
|
||||
failures.append("Art catalog item %s is missing icon or render." % item_id)
|
||||
return failures
|
||||
|
||||
|
||||
func _check_machines(recipe: Dictionary, catalog: Dictionary) -> Array[String]:
|
||||
var failures: Array[String] = []
|
||||
var by_id := _index_by_id(catalog.get("machines", []))
|
||||
for machine_variant in recipe.get("machines", []):
|
||||
var machine: Dictionary = machine_variant
|
||||
var machine_id := String(machine.get("id", ""))
|
||||
if not by_id.has(machine_id):
|
||||
failures.append("Recipe machine %s has no art catalog entry." % machine_id)
|
||||
continue
|
||||
if String(by_id[machine_id].get("render", "")).is_empty():
|
||||
failures.append("Art catalog machine %s is missing a render." % machine_id)
|
||||
return failures
|
||||
|
||||
|
||||
func _check_files(catalog: Dictionary) -> Array[String]:
|
||||
var failures: Array[String] = []
|
||||
var paths: Array[String] = []
|
||||
for item_variant in catalog.get("items", []):
|
||||
var item: Dictionary = item_variant
|
||||
paths.append(String(item.get("icon", "")))
|
||||
paths.append(String(item.get("render", "")))
|
||||
for machine_variant in catalog.get("machines", []):
|
||||
paths.append(String((machine_variant as Dictionary).get("render", "")))
|
||||
for logistics_variant in catalog.get("logistics", []):
|
||||
paths.append(String((logistics_variant as Dictionary).get("render", "")))
|
||||
for material_variant in catalog.get("materials", []):
|
||||
paths.append(String((material_variant as Dictionary).get("albedo", "")))
|
||||
for path in paths:
|
||||
if path.is_empty() or not FileAccess.file_exists("res://" + path.trim_prefix("res://")):
|
||||
failures.append("Missing generated art file: %s" % path)
|
||||
return failures
|
||||
|
||||
|
||||
func _check_main_uses_generated_art() -> Array[String]:
|
||||
var failures: Array[String] = []
|
||||
var file := FileAccess.open(MAIN_SCRIPT_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("Could not read main scene script.")
|
||||
return failures
|
||||
var source := file.get_as_text()
|
||||
if source.find("kenney_factory_kit") >= 0:
|
||||
failures.append("Playable scene still references Kenney Factory Kit models.")
|
||||
if source.find("assets/generated/renders/") < 0:
|
||||
failures.append("Playable scene does not load generated renders.")
|
||||
return failures
|
||||
|
||||
|
||||
func _index_by_id(rows: Array) -> Dictionary:
|
||||
var lookup := {}
|
||||
for row_variant in rows:
|
||||
var row: Dictionary = row_variant
|
||||
lookup[String(row.get("id", ""))] = row
|
||||
return lookup
|
||||
|
||||
|
||||
func _read_json(path: String) -> Dictionary:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
return {}
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
return {}
|
||||
return parsed
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4fydcbgsr7v7
|
||||
@@ -0,0 +1,120 @@
|
||||
extends SceneTree
|
||||
|
||||
const WorldSimulation = preload("res://src/simulation/world_simulation.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var packed := load("res://scenes/main.tscn") as PackedScene
|
||||
var demo := packed.instantiate()
|
||||
root.add_child.call_deferred(demo)
|
||||
await process_frame
|
||||
await process_frame
|
||||
|
||||
demo.call("_start_game")
|
||||
_build_cells(demo, WorldSimulation.KIND_BELT, [
|
||||
Vector2i(-5, 0),
|
||||
Vector2i(-4, 0),
|
||||
Vector2i(-3, 0),
|
||||
Vector2i(-2, 0),
|
||||
Vector2i(0, 0),
|
||||
Vector2i(2, 0),
|
||||
Vector2i(3, 0),
|
||||
Vector2i(4, 0),
|
||||
Vector2i(5, 0),
|
||||
])
|
||||
_build_cells(demo, WorldSimulation.KIND_MANA_PIPE, [
|
||||
Vector2i(0, 4),
|
||||
Vector2i(0, 3),
|
||||
Vector2i(0, 2),
|
||||
Vector2i(0, 1),
|
||||
])
|
||||
var simulation: WorldSimulation = demo.get("simulation")
|
||||
var building_count_before_overlap := simulation.buildings.size()
|
||||
demo.set("hovered_cell", Vector2i(-5, 0))
|
||||
demo.set("hover_is_valid", true)
|
||||
demo.call("_try_place_selected")
|
||||
var overlap_was_rejected := simulation.buildings.size() == building_count_before_overlap
|
||||
demo.call("_try_remove_hovered")
|
||||
var removal_succeeded := simulation.get_building_at(Vector2i(-5, 0)).is_empty()
|
||||
_build_cells(demo, WorldSimulation.KIND_BELT, [Vector2i(-5, 0)])
|
||||
|
||||
demo.call("_begin_bridge_config")
|
||||
_click_bridge_cell(demo, Vector2i(-1, 1))
|
||||
_click_bridge_cell(demo, Vector2i(-2, 0))
|
||||
_click_bridge_cell(demo, Vector2i(0, 0))
|
||||
demo.call("_begin_bridge_config")
|
||||
_click_bridge_cell(demo, Vector2i(1, 1))
|
||||
_click_bridge_cell(demo, Vector2i(0, 0))
|
||||
_click_bridge_cell(demo, Vector2i(2, 0))
|
||||
|
||||
# Selecting a locked processor must expose inspection fields without
|
||||
# mutating the running factory.
|
||||
demo.set("hovered_cell", Vector2i(-1, 1))
|
||||
demo.set("hover_is_valid", true)
|
||||
demo.call("_try_place_selected")
|
||||
var selected_id := int(demo.get("selected_building_id"))
|
||||
var inspect_before := simulation.state_fingerprint()
|
||||
var inspect_report: Dictionary = simulation.inspect_building(selected_id)
|
||||
var inspect_mutated := inspect_before != simulation.state_fingerprint()
|
||||
|
||||
for _tick in range(1800):
|
||||
simulation.step()
|
||||
|
||||
var anchor_machine_id := int(demo.get("anchor_machine_id"))
|
||||
var charger_machine_id := int(demo.get("charger_machine_id"))
|
||||
var machine: Dictionary = simulation.buildings[anchor_machine_id]
|
||||
var charger: Dictionary = simulation.buildings[charger_machine_id]
|
||||
var failures: Array[String] = []
|
||||
if bool(demo.get("bridge_mode")):
|
||||
failures.append("Bridge setup did not leave configuration mode.")
|
||||
if not overlap_was_rejected:
|
||||
failures.append("Grid placement allowed an overlapping building.")
|
||||
if not removal_succeeded:
|
||||
failures.append("Player removal did not clear grid occupancy.")
|
||||
if not bool(machine["bridge_configured"]):
|
||||
failures.append("Anchor machine did not retain bridge endpoints.")
|
||||
if not bool(charger["bridge_configured"]):
|
||||
failures.append("Charger did not retain bridge endpoints.")
|
||||
if not bool(machine["mana_powered"]):
|
||||
failures.append("Player-built mana pipe route did not power the refinery.")
|
||||
if not bool(charger["mana_powered"]):
|
||||
failures.append("Player-built mana pipe route did not power the charger.")
|
||||
if selected_id != anchor_machine_id:
|
||||
failures.append("Clicking the refinery did not select it for inspection.")
|
||||
if inspect_report.is_empty() or inspect_report.get("recipe", {}).is_empty():
|
||||
failures.append("Selection inspection did not return a recipe payload.")
|
||||
if inspect_mutated:
|
||||
failures.append("Inspection mutated simulation state.")
|
||||
if simulation.get_charged_count() < 15:
|
||||
failures.append("Completed player flow did not sustain charged-crystal output.")
|
||||
if not simulation.is_demo_complete():
|
||||
failures.append("Sustained charged-crystal intake did not mark the demo complete.")
|
||||
|
||||
if failures.is_empty():
|
||||
print("DEMO_FLOW_OK charged=%d refine_bridge=%s->%s charger_bridge=%s->%s mana_network=%d" % [
|
||||
simulation.get_charged_count(),
|
||||
str(machine["input_cell"]),
|
||||
str(machine["output_cell"]),
|
||||
str(charger["input_cell"]),
|
||||
str(charger["output_cell"]),
|
||||
int(machine["mana_network"]),
|
||||
])
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _build_cells(demo: Node, kind: StringName, cells: Array[Vector2i]) -> void:
|
||||
demo.call("_select_tool", kind)
|
||||
for cell in cells:
|
||||
demo.set("hovered_cell", cell)
|
||||
demo.set("hover_is_valid", true)
|
||||
demo.call("_try_place_selected")
|
||||
|
||||
|
||||
func _click_bridge_cell(demo: Node, cell: Vector2i) -> void:
|
||||
demo.set("hovered_cell", cell)
|
||||
demo.set("hover_is_valid", true)
|
||||
demo.call("_handle_bridge_click")
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtptpsgl6k5vb
|
||||
@@ -0,0 +1,231 @@
|
||||
extends SceneTree
|
||||
|
||||
const WorldSimulation = preload("res://src/simulation/world_simulation.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
var reports: Dictionary = {}
|
||||
|
||||
reports["no_mana"] = _capture_no_mana()
|
||||
reports["missing_input"] = _capture_missing_input()
|
||||
reports["waiting_input"] = _capture_waiting_input()
|
||||
reports["working"] = _capture_working()
|
||||
reports["outputting"] = _capture_outputting()
|
||||
reports["blocked_output"] = _capture_blocked_output()
|
||||
reports["charger_no_mana"] = _capture_charger_no_mana()
|
||||
|
||||
for status in [
|
||||
&"no_mana",
|
||||
&"missing_input",
|
||||
&"waiting_input",
|
||||
&"working",
|
||||
&"outputting",
|
||||
&"blocked_output",
|
||||
]:
|
||||
if not reports.has(status):
|
||||
failures.append("Missing inspection capture for %s." % String(status))
|
||||
continue
|
||||
var capture: Dictionary = reports[status]
|
||||
if not bool(capture.get("ok", false)):
|
||||
failures.append(
|
||||
"Could not reach status %s (got %s)." % [
|
||||
String(status),
|
||||
String(capture.get("status", &"")),
|
||||
]
|
||||
)
|
||||
continue
|
||||
failures.append_array(_validate_payload(status, capture["report"], capture["unchanged"]))
|
||||
|
||||
var charger_capture: Dictionary = reports["charger_no_mana"]
|
||||
if not bool(charger_capture.get("ok", false)):
|
||||
failures.append("Charger did not emit a second-stage no_mana status.")
|
||||
else:
|
||||
var charger_report: Dictionary = charger_capture["report"]
|
||||
var recipe: Dictionary = charger_report.get("recipe", {})
|
||||
if recipe.get("id", &"") != WorldSimulation.RECIPE_CHARGE:
|
||||
failures.append("Charger inspection did not expose the charge recipe.")
|
||||
if recipe.get("input_kind", &"") != WorldSimulation.ITEM_REFINED:
|
||||
failures.append("Charger recipe input was not refined crystal.")
|
||||
if recipe.get("output_kind", &"") != WorldSimulation.ITEM_CHARGED:
|
||||
failures.append("Charger recipe output was not charged crystal.")
|
||||
failures.append_array(
|
||||
_validate_payload(&"no_mana", charger_report, charger_capture["unchanged"])
|
||||
)
|
||||
|
||||
if failures.is_empty():
|
||||
print("INSPECTION_SMOKE_OK statuses=%s" % ",".join([
|
||||
"no_mana",
|
||||
"missing_input",
|
||||
"waiting_input",
|
||||
"working",
|
||||
"outputting",
|
||||
"blocked_output",
|
||||
"charger_no_mana",
|
||||
]))
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _validate_payload(status: StringName, report: Dictionary, unchanged: bool) -> Array[String]:
|
||||
var failures: Array[String] = []
|
||||
if report.is_empty():
|
||||
failures.append("Inspection returned an empty payload for %s." % String(status))
|
||||
return failures
|
||||
for key in ["recipe", "progress", "inventory", "mana", "reason"]:
|
||||
if not report.has(key):
|
||||
failures.append("Inspection for %s missing field %s." % [String(status), key])
|
||||
var recipe: Dictionary = report.get("recipe", {})
|
||||
if recipe.is_empty() or not recipe.has("id"):
|
||||
failures.append("Inspection for %s missing recipe id." % String(status))
|
||||
var progress: Dictionary = report.get("progress", {})
|
||||
if not progress.has("ratio") or not progress.has("remaining_ticks"):
|
||||
failures.append("Inspection for %s missing progress fields." % String(status))
|
||||
var inventory: Dictionary = report.get("inventory", {})
|
||||
if not inventory.has("input_count") or not inventory.has("output_buffer"):
|
||||
failures.append("Inspection for %s missing inventory fields." % String(status))
|
||||
var mana: Dictionary = report.get("mana", {})
|
||||
if not mana.has("powered") or not mana.has("network"):
|
||||
failures.append("Inspection for %s missing mana fields." % String(status))
|
||||
if status != &"working" and String(report.get("reason", "")).is_empty():
|
||||
failures.append("Inspection for %s had an empty stop reason." % String(status))
|
||||
if not unchanged:
|
||||
failures.append("Inspection for %s mutated tick, inventory, or fingerprint." % String(status))
|
||||
return failures
|
||||
|
||||
|
||||
func _inspect_without_mutation(simulation: WorldSimulation, building_id: int) -> Dictionary:
|
||||
var before := {
|
||||
"tick": simulation.tick_index,
|
||||
"fingerprint": simulation.state_fingerprint(),
|
||||
"sink": simulation.sink_inventory.duplicate(),
|
||||
}
|
||||
var input_count := -1
|
||||
if simulation.buildings.has(building_id):
|
||||
input_count = int(simulation.buildings[building_id].get("input_count", -1))
|
||||
var report := simulation.inspect_building(building_id)
|
||||
var unchanged: bool = (
|
||||
int(before["tick"]) == simulation.tick_index
|
||||
and String(before["fingerprint"]) == simulation.state_fingerprint()
|
||||
and before["sink"] == simulation.sink_inventory
|
||||
and (
|
||||
input_count < 0
|
||||
or input_count == int(simulation.buildings[building_id].get("input_count", -2))
|
||||
)
|
||||
)
|
||||
return {
|
||||
"report": report,
|
||||
"unchanged": unchanged,
|
||||
"status": report.get("status", &""),
|
||||
"ok": not report.is_empty(),
|
||||
}
|
||||
|
||||
|
||||
func _capture_no_mana() -> Dictionary:
|
||||
var simulation := WorldSimulation.new()
|
||||
var machine_id := simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(0, 0), 0)
|
||||
simulation.step()
|
||||
var capture := _inspect_without_mutation(simulation, machine_id)
|
||||
capture["ok"] = String(capture["status"]) == "no_mana"
|
||||
return capture
|
||||
|
||||
|
||||
func _capture_missing_input() -> Dictionary:
|
||||
var simulation := WorldSimulation.new()
|
||||
var machine_id := simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(0, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 1), 0)
|
||||
simulation.step()
|
||||
var capture := _inspect_without_mutation(simulation, machine_id)
|
||||
capture["ok"] = String(capture["status"]) == "missing_input"
|
||||
return capture
|
||||
|
||||
|
||||
func _capture_waiting_input() -> Dictionary:
|
||||
var simulation := WorldSimulation.new()
|
||||
var machine_id := simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(0, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 1), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-1, 0), 0)
|
||||
simulation.step()
|
||||
var capture := _inspect_without_mutation(simulation, machine_id)
|
||||
capture["ok"] = String(capture["status"]) == "waiting_input"
|
||||
return capture
|
||||
|
||||
|
||||
func _capture_working() -> Dictionary:
|
||||
var simulation := _powered_line()
|
||||
var machine_id := _find_kind(simulation, WorldSimulation.KIND_MACHINE)
|
||||
for _tick in range(400):
|
||||
simulation.step()
|
||||
if String(simulation.buildings[machine_id]["status"]) == "working":
|
||||
var capture := _inspect_without_mutation(simulation, machine_id)
|
||||
capture["ok"] = String(capture["status"]) == "working"
|
||||
return capture
|
||||
return {"ok": false, "status": simulation.buildings[machine_id]["status"], "report": {}, "unchanged": true}
|
||||
|
||||
|
||||
func _capture_outputting() -> Dictionary:
|
||||
var simulation := _powered_line()
|
||||
var machine_id := _find_kind(simulation, WorldSimulation.KIND_MACHINE)
|
||||
var source_cell := Vector2i(-3, 0)
|
||||
for _tick in range(400):
|
||||
simulation.step()
|
||||
if int(simulation.buildings[_find_kind(simulation, WorldSimulation.KIND_SOURCE)].get("produced", 0)) >= 1:
|
||||
break
|
||||
simulation.remove_building(source_cell)
|
||||
for _tick in range(400):
|
||||
simulation.step()
|
||||
if String(simulation.buildings[machine_id]["status"]) == "outputting":
|
||||
var capture := _inspect_without_mutation(simulation, machine_id)
|
||||
capture["ok"] = String(capture["status"]) == "outputting"
|
||||
return capture
|
||||
return {"ok": false, "status": simulation.buildings[machine_id]["status"], "report": {}, "unchanged": true}
|
||||
|
||||
|
||||
func _capture_blocked_output() -> Dictionary:
|
||||
var simulation := WorldSimulation.new()
|
||||
simulation.place_building(WorldSimulation.KIND_SOURCE, Vector2i(-3, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-1, 0), 0)
|
||||
var machine_id := simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(0, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 1), 0)
|
||||
for _tick in range(400):
|
||||
simulation.step()
|
||||
if String(simulation.buildings[machine_id]["status"]) == "blocked_output":
|
||||
var capture := _inspect_without_mutation(simulation, machine_id)
|
||||
capture["ok"] = String(capture["status"]) == "blocked_output"
|
||||
return capture
|
||||
return {"ok": false, "status": simulation.buildings[machine_id]["status"], "report": {}, "unchanged": true}
|
||||
|
||||
|
||||
func _capture_charger_no_mana() -> Dictionary:
|
||||
var simulation := WorldSimulation.new()
|
||||
var charger_id := simulation.place_building(WorldSimulation.KIND_CHARGER, Vector2i(2, 0), 0)
|
||||
simulation.step()
|
||||
var capture := _inspect_without_mutation(simulation, charger_id)
|
||||
capture["ok"] = String(capture["status"]) == "no_mana"
|
||||
return capture
|
||||
|
||||
|
||||
func _powered_line() -> WorldSimulation:
|
||||
var simulation := WorldSimulation.new()
|
||||
simulation.place_building(WorldSimulation.KIND_SOURCE, Vector2i(-3, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-1, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(0, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 2), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_PIPE, Vector2i(0, 1), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(1, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_SINK, Vector2i(3, 0), 0)
|
||||
return simulation
|
||||
|
||||
|
||||
func _find_kind(simulation: WorldSimulation, kind: StringName) -> int:
|
||||
for id_variant in simulation.get_building_ids():
|
||||
var id := int(id_variant)
|
||||
if simulation.buildings[id]["kind"] == kind:
|
||||
return id
|
||||
return -1
|
||||
@@ -0,0 +1 @@
|
||||
uid://bemwej038tpgp
|
||||
@@ -0,0 +1,125 @@
|
||||
extends SceneTree
|
||||
|
||||
const WorldSimulation = preload("res://src/simulation/world_simulation.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var first := _run_factory()
|
||||
var second := _run_factory()
|
||||
var mana_pause := _run_mana_pause_resume()
|
||||
var bridge_result := _run_bridge_factory()
|
||||
var failures: Array[String] = []
|
||||
|
||||
if int(first["refined"]) < 20:
|
||||
failures.append("Expected at least 20 refined crystals, got %d." % int(first["refined"]))
|
||||
if first["fingerprint"] != second["fingerprint"]:
|
||||
failures.append("Identical fixed-tick runs produced different fingerprints.")
|
||||
if int(first["items"]) != int(second["items"]):
|
||||
failures.append("Identical runs produced different in-flight item counts.")
|
||||
if not bool(mana_pause["stopped_without_mana"]):
|
||||
failures.append("Machine produced output without a connected mana network.")
|
||||
if not bool(mana_pause["paused_exactly"]):
|
||||
failures.append("Disconnecting mana did not preserve the unfinished recipe.")
|
||||
if not bool(mana_pause["resumed"]):
|
||||
failures.append("Machine did not resume its unfinished recipe after mana reconnection.")
|
||||
if int(bridge_result["refined"]) < 20:
|
||||
failures.append("Bridge-bound machine did not sustain production.")
|
||||
|
||||
if failures.is_empty():
|
||||
print("SIMULATION_SMOKE_OK refined=%d fingerprint=%s items=%d" % [
|
||||
int(first["refined"]),
|
||||
String(first["fingerprint"]),
|
||||
int(first["items"]),
|
||||
])
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _run_factory() -> Dictionary:
|
||||
var simulation := WorldSimulation.new()
|
||||
simulation.place_building(WorldSimulation.KIND_SOURCE, Vector2i(-3, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-1, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(0, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 2), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_PIPE, Vector2i(0, 1), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(1, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_SINK, Vector2i(3, 0), 0)
|
||||
|
||||
for _tick in range(1800):
|
||||
simulation.step()
|
||||
|
||||
return {
|
||||
"refined": simulation.get_refined_count(),
|
||||
"fingerprint": simulation.state_fingerprint(),
|
||||
"items": simulation.items.size(),
|
||||
}
|
||||
|
||||
|
||||
func _run_mana_pause_resume() -> Dictionary:
|
||||
var simulation := WorldSimulation.new()
|
||||
simulation.place_building(WorldSimulation.KIND_SOURCE, Vector2i(-3, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-1, 0), 0)
|
||||
var machine_id := simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(0, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(1, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_SINK, Vector2i(3, 0), 0)
|
||||
|
||||
for _tick in range(200):
|
||||
simulation.step()
|
||||
var stopped_without_mana: bool = (
|
||||
simulation.get_refined_count() == 0
|
||||
and simulation.buildings[machine_id]["status"] == &"no_mana"
|
||||
)
|
||||
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 2), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_PIPE, Vector2i(0, 1), 0)
|
||||
for _tick in range(10):
|
||||
simulation.step()
|
||||
var remaining_before_disconnect: int = int(simulation.buildings[machine_id]["completion_tick"]) - simulation.tick_index
|
||||
simulation.remove_building(Vector2i(0, 1))
|
||||
var paused_remaining: int = int(simulation.buildings[machine_id]["remaining_ticks"])
|
||||
for _tick in range(100):
|
||||
simulation.step()
|
||||
var paused_exactly: bool = (
|
||||
simulation.get_refined_count() == 0
|
||||
and paused_remaining == remaining_before_disconnect
|
||||
and int(simulation.buildings[machine_id]["completion_tick"]) == -1
|
||||
)
|
||||
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_PIPE, Vector2i(0, 1), 0)
|
||||
for _tick in range(120):
|
||||
simulation.step()
|
||||
return {
|
||||
"stopped_without_mana": stopped_without_mana,
|
||||
"paused_exactly": paused_exactly,
|
||||
"resumed": simulation.get_refined_count() > 0,
|
||||
}
|
||||
|
||||
|
||||
func _run_bridge_factory() -> Dictionary:
|
||||
var simulation := WorldSimulation.new()
|
||||
simulation.place_building(WorldSimulation.KIND_SOURCE, Vector2i(-3, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(-1, 0), 0)
|
||||
var machine_id := simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(0, 1), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(1, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(2, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_SINK, Vector2i(3, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 3), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_PIPE, Vector2i(0, 2), 0)
|
||||
var configured := simulation.configure_machine_bridge(
|
||||
machine_id,
|
||||
Vector2i(-1, 0),
|
||||
Vector2i(1, 0)
|
||||
)
|
||||
if not configured:
|
||||
return {"refined": 0}
|
||||
for _tick in range(1800):
|
||||
simulation.step()
|
||||
return {"refined": simulation.get_refined_count()}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0ik3wd2ycn1h
|
||||
@@ -0,0 +1,182 @@
|
||||
extends SceneTree
|
||||
|
||||
const WorldSimulation = preload("res://src/simulation/world_simulation.gd")
|
||||
|
||||
const RUN_TICKS := 2000
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var first := _run_two_stage(RUN_TICKS)
|
||||
var second := _run_two_stage(RUN_TICKS)
|
||||
var first_item := _run_until_first_charged()
|
||||
var blocked := _run_blocked_output()
|
||||
var failures: Array[String] = []
|
||||
|
||||
if int(first["charged"]) <= 0:
|
||||
failures.append("Two-stage factory produced no charged crystals.")
|
||||
if int(first["charged"]) != int(second["charged"]):
|
||||
failures.append("Identical two-stage runs produced different charged counts.")
|
||||
if String(first["fingerprint"]) != String(second["fingerprint"]):
|
||||
failures.append("Identical two-stage runs produced different fingerprints.")
|
||||
if int(first["mass"]) != int(first["produced"]):
|
||||
failures.append(
|
||||
"Mass was not conserved: produced=%d mass=%d." % [
|
||||
int(first["produced"]),
|
||||
int(first["mass"]),
|
||||
]
|
||||
)
|
||||
if int(second["mass"]) != int(second["produced"]):
|
||||
failures.append("Second two-stage run did not conserve mass.")
|
||||
if bool(first_item["saw_item"]) == false:
|
||||
failures.append("Factory never delivered the first charged crystal.")
|
||||
if bool(first_item["complete_after_one"]):
|
||||
failures.append("Demo completed after a single charged crystal.")
|
||||
if not bool(first["complete"]):
|
||||
failures.append("Sustained charged-crystal intake did not complete the demo.")
|
||||
if bool(first_item["complete_before_any"]):
|
||||
failures.append("Demo reported complete before any charged crystal arrived.")
|
||||
if not bool(blocked["stopped_upstream"]):
|
||||
failures.append("Blocked charger output did not stop the upstream refinery.")
|
||||
if int(blocked["mass"]) != int(blocked["produced"]):
|
||||
failures.append(
|
||||
"Blocked-output mass was not conserved: produced=%d mass=%d." % [
|
||||
int(blocked["produced"]),
|
||||
int(blocked["mass"]),
|
||||
]
|
||||
)
|
||||
if int(blocked["mass_after"]) != int(blocked["mass"]):
|
||||
failures.append("Blocked factory lost or duplicated items while idle.")
|
||||
if int(blocked["produced_after"]) != int(blocked["produced"]):
|
||||
failures.append("Blocked factory kept spawning items after belts filled.")
|
||||
if int(blocked["charged"]) != 0:
|
||||
failures.append("Blocked charger leaked charged crystals into the sink.")
|
||||
if int(first["charged"]) <= int(blocked["charged"]):
|
||||
failures.append("Clearing the output-belt bottleneck did not increase charged throughput.")
|
||||
|
||||
if failures.is_empty():
|
||||
print("TWO_STAGE_SMOKE_OK charged=%d fingerprint=%s produced=%d mass=%d" % [
|
||||
int(first["charged"]),
|
||||
String(first["fingerprint"]),
|
||||
int(first["produced"]),
|
||||
int(first["mass"]),
|
||||
])
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _run_two_stage(ticks: int) -> Dictionary:
|
||||
var simulation := _make_two_stage_factory(true)
|
||||
for _tick in range(ticks):
|
||||
simulation.step()
|
||||
return {
|
||||
"charged": simulation.get_charged_count(),
|
||||
"refined": simulation.get_refined_count(),
|
||||
"fingerprint": simulation.state_fingerprint(),
|
||||
"produced": _count_produced(simulation),
|
||||
"mass": _count_mass(simulation),
|
||||
"complete": simulation.is_demo_complete(),
|
||||
}
|
||||
|
||||
|
||||
func _run_until_first_charged() -> Dictionary:
|
||||
var simulation := _make_two_stage_factory(true)
|
||||
var complete_before_any := simulation.is_demo_complete()
|
||||
var saw_item := false
|
||||
var complete_after_one := false
|
||||
for _tick in range(RUN_TICKS):
|
||||
simulation.step()
|
||||
if simulation.get_charged_count() >= 1:
|
||||
saw_item = true
|
||||
complete_after_one = simulation.is_demo_complete()
|
||||
break
|
||||
return {
|
||||
"saw_item": saw_item,
|
||||
"complete_after_one": complete_after_one,
|
||||
"complete_before_any": complete_before_any,
|
||||
}
|
||||
|
||||
|
||||
func _run_blocked_output() -> Dictionary:
|
||||
var simulation := _make_two_stage_factory(false)
|
||||
for _tick in range(RUN_TICKS):
|
||||
simulation.step()
|
||||
var charger_id := _find_kind(simulation, WorldSimulation.KIND_CHARGER)
|
||||
var refine_id := _find_kind(simulation, WorldSimulation.KIND_MACHINE)
|
||||
var charger: Dictionary = simulation.buildings[charger_id]
|
||||
var refine: Dictionary = simulation.buildings[refine_id]
|
||||
var stopped_upstream := (
|
||||
String(charger["status"]) == "blocked_output"
|
||||
and String(refine["status"]) in ["blocked_output", "working", "outputting"]
|
||||
)
|
||||
# After the mid belt and charger buffers fill, further ticks must not
|
||||
# create or destroy crystals.
|
||||
var mass := _count_mass(simulation)
|
||||
var produced := _count_produced(simulation)
|
||||
for _tick in range(200):
|
||||
simulation.step()
|
||||
return {
|
||||
"stopped_upstream": stopped_upstream,
|
||||
"mass": mass,
|
||||
"produced": produced,
|
||||
"mass_after": _count_mass(simulation),
|
||||
"produced_after": _count_produced(simulation),
|
||||
"charged": simulation.get_charged_count(),
|
||||
"charger_status": charger["status"],
|
||||
"refine_status": refine["status"],
|
||||
}
|
||||
|
||||
|
||||
func _make_two_stage_factory(with_output_belts: bool) -> WorldSimulation:
|
||||
var simulation := WorldSimulation.new()
|
||||
simulation.place_building(WorldSimulation.KIND_SOURCE, Vector2i(-6, 0), 0)
|
||||
for x in range(-5, -1):
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(x, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(0, 0), 0)
|
||||
# A single drop belt with no sink creates real backpressure: the charger
|
||||
# can emit one charged crystal, then the cell stays occupied.
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(2, 0), 0)
|
||||
if with_output_belts:
|
||||
for x in range(3, 6):
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(x, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_SINK, Vector2i(6, 0), 0)
|
||||
var refine_id := simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(-1, 1), 0)
|
||||
var charger_id := simulation.place_building(WorldSimulation.KIND_CHARGER, Vector2i(1, 1), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 5), 1)
|
||||
for z in range(1, 5):
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_PIPE, Vector2i(0, z), 1)
|
||||
simulation.configure_machine_bridge(refine_id, Vector2i(-2, 0), Vector2i(0, 0))
|
||||
simulation.configure_machine_bridge(charger_id, Vector2i(0, 0), Vector2i(2, 0))
|
||||
return simulation
|
||||
|
||||
|
||||
func _count_produced(simulation: WorldSimulation) -> int:
|
||||
var produced := 0
|
||||
for building: Dictionary in simulation.buildings.values():
|
||||
if building["kind"] == WorldSimulation.KIND_SOURCE:
|
||||
produced += int(building.get("produced", 0))
|
||||
return produced
|
||||
|
||||
|
||||
func _count_mass(simulation: WorldSimulation) -> int:
|
||||
var mass := simulation.items.size()
|
||||
for kind in simulation.sink_inventory.keys():
|
||||
mass += int(simulation.sink_inventory[kind])
|
||||
for building: Dictionary in simulation.buildings.values():
|
||||
if not WorldSimulation.is_processor(building["kind"]):
|
||||
continue
|
||||
mass += int(building.get("input_count", 0))
|
||||
mass += int(building.get("output_buffer", 0))
|
||||
if int(building.get("completion_tick", -1)) >= 0 or int(building.get("remaining_ticks", 0)) > 0:
|
||||
mass += 1
|
||||
return mass
|
||||
|
||||
|
||||
func _find_kind(simulation: WorldSimulation, kind: StringName) -> int:
|
||||
for id_variant in simulation.get_building_ids():
|
||||
var id := int(id_variant)
|
||||
if simulation.buildings[id]["kind"] == kind:
|
||||
return id
|
||||
return -1
|
||||
@@ -0,0 +1 @@
|
||||
uid://bpl287yrytxky
|
||||
Reference in New Issue
Block a user