extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_black_hole.gd # Forgejo #55: a rare landable body yielding dark matter, obtainable only # through an extractor - never a surface pickup. # # Covers: # A. WorldGen.planet_data_at_chunk() is a pure, deterministic function of # chunk coordinates, and black holes are noticeably rarer than gas # giants across a wide scan # B. dark matter is fully generic - in resource_types(), its own # ResourceDefinition/glyph/colour, distinct from every other resource # C. landing on a black hole spawns no resource provider and no capture # points at all (void's features_scene is null, and it has no # authored PlanetSetup) - yet the planet is immediately conquered # (a garrison of zero is vacuously already beaten), # so an extractor is buildable there right away # D. an extractor on a black hole mines dark matter locally exactly like # any other extractor, and a hub transporter banks it through the # normal collection path with no black-hole-specific code var game_state: Node var stage: int = 0 var elapsed: float = 0.0 var scene: Node var extractor_index: int = 0 var load_limit: int = 0 var baseline: Dictionary = {} const BLACK_HOLE_AT := Vector2(500.0, 0.0) const MOTHERSHIP_AT := Vector2.ZERO const HUB_AT := Vector2(150.0, 0.0) func _initialize() -> void: game_state = root.get_node("GameState") # --- A. deterministic and rare ----------------------------------------- var black_hole_count: int = 0 var gas_giant_count: int = 0 var total_planets: int = 0 for x in range(-40, 40): for y in range(-40, 40): var chunk: Vector2i = Vector2i(x, y) if not WorldGen.has_planet_at_chunk(chunk): continue total_planets += 1 var first: Dictionary = WorldGen.planet_data_at_chunk(chunk) var second: Dictionary = WorldGen.planet_data_at_chunk(chunk) if first.definition_id != second.definition_id or first.local_pos != second.local_pos \ or first.shape_seed != second.shape_seed or first.name != second.name: _fail("planet_data_at_chunk(%s) is not deterministic" % chunk) return if first.definition_id == "black_hole": black_hole_count += 1 elif first.definition_id == "gas_giant": gas_giant_count += 1 print("scanned %d planet chunks: %d black holes, %d gas giants" % [ total_planets, black_hole_count, gas_giant_count ]) if black_hole_count == 0: _fail("no black holes generated across a wide scan - the rare outcome never fires") return if black_hole_count >= gas_giant_count: _fail("black holes (%d) are not rarer than gas giants (%d)" % [black_hole_count, gas_giant_count]) return # --- B. dark matter is fully generic ----------------------------------- if not game_state.resource_types().has(GameState.RESOURCE_DARK_MATTER): _fail("dark_matter is not in resource_types() - every resource menu would miss it") return var definition: Resource = game_state.resource_definition(GameState.RESOURCE_DARK_MATTER) if definition == null or definition.glyph_scene == null: _fail("dark_matter has no ResourceDefinition or glyph_scene") return var dm_color: Color = game_state.resource_color(GameState.RESOURCE_DARK_MATTER) for other in [GameState.RESOURCE_ORE, GameState.RESOURCE_GAS, GameState.RESOURCE_ENERGY]: if dm_color == game_state.resource_color(other): _fail("dark_matter shares a colour with %s" % other) return print("dark matter is generic: in resource_types(), own definition, own colour %s" % dm_color) # --- structural: the black hole definition itself ----------------------- var black_hole_def: Resource = load("res://planets/PlanetDefinitions.gd").get_definition("black_hole") if black_hole_def.resource_id != "dark_matter": _fail("black_hole planet's resource_id is '%s', expected 'dark_matter'" % black_hole_def.resource_id) return if black_hole_def.surface_profile_id != "void": _fail("black_hole planet's surface_profile_id is '%s', expected 'void'" % black_hole_def.surface_profile_id) return var void_profile: Resource = load("res://surface/PlanetSurfaceProfiles.gd").get_profile("void") if void_profile.features_scene != null: _fail("void surface profile has a features_scene - dark matter must never be a surface pickup") return game_state.buildings.clear() game_state.extractors.clear() game_state.transporters.clear() game_state.planet_capture.clear() game_state.planet_id = "test_black_hole" game_state.planet_name = "Event Zero" game_state.planet_definition_id = "black_hole" scene = load("res://surface/PlanetSurface.tscn").instantiate() root.add_child(scene) current_scene = scene func _process(delta: float) -> bool: elapsed += delta match stage: 0: # --- C. no resource provider, no capture points, yet conquered --- if elapsed < 0.3: return false # What must never appear is a RESOURCE PROVIDER - dark matter is # not a surface pickup, which is this test's actual subject and is # guaranteed above by void's features_scene being null. # # This used to demand zero children outright, which quietly assumed # nobody would ever author a PlanetSetup for the void profile. That # is a fact about the content folder, not a rule about black holes, # and it stopped being true the moment someone saved a map under # the void profile in the editor. An authored setup here is legal; # a resource provider is not. var feature_host: Node = scene.get_node("SurfaceFeatures") for child in feature_host.get_children(): if not child.has_method("apply_setup"): return _fail("black hole surface spawned %s under SurfaceFeatures - only an authored PlanetSetupHost belongs here" % child.name) # Through the autoload NODE, not the bare GameState identifier: a # global class-name lookup inside a -s entry-point script is # resolved at compile time, before autoloads are registered. Same # hazard the header of test_planet_setups.gd documents. if not game_state.is_planet_conquered(game_state.planet_id) \ and int(game_state.garrison_total(game_state.planet_id)) == 0: return _fail("a black hole with no capture points required is not marked conquered") print("no resource provider; %d authored feature host(s)" % feature_host.get_child_count()) # Snapshot rather than assert > 0: bank_inventory starts with 500 # ore by default (a fresh game's starting stock), unrelated to # this planet - "nothing was picked up" has to mean "unchanged", # not "still zero". for resource_type in game_state.resource_types(): baseline[resource_type] = game_state.banked_resource(resource_type) + game_state.fleet_resource(resource_type) var fleet: Node = scene.get_node("Fleet") for ship in fleet.ships: ship.position += Vector2(300.0, 200.0) fleet.move_to(fleet.get_centroid()) elapsed = 0.0 stage = 1 return false 1: # --- C continued: flying around picks up nothing, ever ----------- if elapsed < 2.0: return false for resource_type in game_state.resource_types(): var total: int = game_state.banked_resource(resource_type) + game_state.fleet_resource(resource_type) if total != baseline.get(resource_type, 0): return _fail("something (%s) was picked up over a black hole surface" % resource_type) print("flew around for %.1fs, nothing picked up" % elapsed) # --- D: extractor mining + transporter banking -------------------- game_state.buildings.clear() game_state.extractors.clear() game_state.transporters.clear() game_state.reset_progression() for node in GameState.SKILL_TREE: game_state.unlocked.append(node.id) game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT) game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT) game_state.add_extractor("Event Zero", BLACK_HOLE_AT, GameState.RESOURCE_DARK_MATTER, "test_black_hole_extractor") extractor_index = game_state.extractor_index_at("test_black_hole_extractor") if extractor_index < 0: return _fail("add_extractor did not register the black hole extractor") load_limit = game_state.transporter_load_limit() elapsed = 0.0 stage = 2 return false 2: # Mining is local, exactly like ore - no black-hole-specific code. # DERIVED from the extraction rate, not a fixed 1.2s. That was # long enough only while an extractor produced 2 a second; the # rate has since been quartered, and a wait tuned to the old one # reported a broken extractor that was working perfectly. What is # under test is that dark matter accumulates LOCALLY rather than # banking itself - not how quickly it arrives. if elapsed < 1.0 / GameState.EXTRACTOR_RATE + 0.4: return false var mined: int = game_state.extractor_resource(extractor_index, GameState.RESOURCE_DARK_MATTER) if mined <= 0: return _fail("extractor mined no dark matter locally after %.1fs" % elapsed) print("extractor mined %d dark matter locally, bank still %d" % [ mined, game_state.banked_resource(GameState.RESOURCE_DARK_MATTER) ]) # Force a full load directly (bypasses waiting out the real # mining rate for the rest) so the transporter pickup dispatch # fires without a long extra wait. game_state.extractors[extractor_index].inventory.set_amount(GameState.RESOURCE_DARK_MATTER, load_limit) elapsed = 0.0 stage = 3 return false 3: # --- a transporter collects the full load and banks it ----------- if game_state.banked_resource(GameState.RESOURCE_DARK_MATTER) >= load_limit: print("transporter banked %d dark matter" % game_state.banked_resource(GameState.RESOURCE_DARK_MATTER)) print("TEST_BLACK_HOLE: PASS") quit(0) return true if elapsed > 10.0: return _fail("dark matter was never collected to the bank (extractor=%d bank=%d)" % [ game_state.extractor_resource(extractor_index, GameState.RESOURCE_DARK_MATTER), game_state.banked_resource(GameState.RESOURCE_DARK_MATTER), ]) return false return false func _fail(message: String) -> bool: print("TEST_BLACK_HOLE: FAIL (%s)" % message) quit(1) return true