spaceship/tests/test_planet_map_assignment.gd
Loïc Wisniewski 6e13b6397c Take planets by killing what defends them
#211 conquest is the fight now: a planet registers the garrison its setup
authors, each defender's own death counts against it, and the last one takes
the world. The capture points - stand in a ring, wait out a timer, once per
ring - are gone, along with the editor palette that placed them.
#212 conquest opens a dialog on the surface naming the research points
recovered; the award travels with the signal, after it is banked.
#209 the prologue's charted sectors go with the breach, so the map the
survivors keep is of where they now are - and "chart 30 sectors" is no
longer complete before they have flown anywhere.
#218 enemy variety is gated on the FLEET, not on wave strength: a starting
fleet meets chasers only at any wave number, and each vessel past three
opens one more kind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 13:31:25 +02:00

170 lines
6.4 KiB
GDScript

extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_planet_map_assignment.gd
# Every planet should draw a map of its own surface type at random, and then
# keep it - leaving a planet and coming back must land on the same layout, or
# the place stops feeling like a place.
#
# Two things could break that, and both are silent:
# - the candidate list is built by scanning a directory, and the order a
# filesystem lists files in is not something to seed a choice with. The
# pick indexes into that list, so an unstable order re-deals every planet
# - the pool itself changes the moment the map editor saves a new setup,
# which would re-deal planets mid-session even with a perfect seed
#
# Covers:
# A. the same planet resolves to the same setup, every time
# B. the setup it draws is always authored for that planet's own profile
# C. different planets do not all collapse onto one setup
# D. an assignment survives a new setup file appearing in the pool
# E. candidates come back in a deterministic order regardless of scan order
const SCRATCH_ID: String = "test_assignment_scratch"
const SCRATCH_PATH: String = "res://PlanetSetups/%s.tres" % SCRATCH_ID
# This test needs a pool of several maps of one type to draw from. It used to
# use whatever rocky setups the project happened to ship, which made it a test
# of the content folder as much as of the code - deleting a map in the editor
# dropped the pool to one and this failed, reporting a broken draw when
# nothing was broken. It now plants its own.
const POOL_IDS: Array = [
"test_assignment_pool_a", "test_assignment_pool_b", "test_assignment_pool_c",
]
func _initialize() -> void:
_remove_scratch()
if not _plant_pool():
return
PlanetSetups.rescan()
# --- E. deterministic ordering ------------------------------------------
var profile_id: String = "rocky"
var ordered: Array = PlanetSetups.setups_for_profile(profile_id)
if ordered.size() < 2:
_remove_scratch()
_fail("only %d '%s' setups exist even after planting %d - the pool is not seeing them" \
% [ordered.size(), profile_id, POOL_IDS.size()])
return
var ids: Array = []
for setup in ordered:
ids.append(String(setup.setup_id))
var sorted_ids: Array = ids.duplicate()
sorted_ids.sort()
if ids != sorted_ids:
_fail("setups_for_profile returned %s, which is not sorted - the draw indexes into this, so the order cannot be left to the filesystem" % str(ids))
return
print("'%s' pool is deterministic: %s" % [profile_id, str(ids)])
# --- A/B/C. stable, correctly typed, and actually varied -----------------
var first_pass: Dictionary = {}
var planet_ids: Array = []
for i in range(40):
planet_ids.append("planet_%d_%d" % [i * 7, i * 13])
for planet_id in planet_ids:
var setup: Resource = PlanetSetups.setup_for_planet(planet_id, profile_id)
if setup == null:
_fail("%s drew no setup at all" % planet_id)
return
if String(setup.surface_profile_id) != profile_id:
_fail("%s is a '%s' planet but drew a '%s' map" \
% [planet_id, profile_id, setup.surface_profile_id])
return
first_pass[planet_id] = setup
for planet_id in planet_ids:
for repeat in range(3):
if PlanetSetups.setup_for_planet(planet_id, profile_id) != first_pass[planet_id]:
_fail("%s drew a different map on a later visit" % planet_id)
return
print("40 planets each re-drew the same map on every revisit")
var distinct: Dictionary = {}
for planet_id in first_pass:
distinct[first_pass[planet_id].setup_id] = true
if distinct.size() < 2:
_fail("all 40 planets drew the same map (%s), so the draw is not random at all" % str(distinct.keys()))
return
print("across 40 planets the draw used %d of the %d available maps" % [distinct.size(), ids.size()])
# --- D. a new map appearing does not re-deal the planets already visited -
var scratch: Resource = load("res://surface/PlanetSetup.gd").new()
scratch.setup_id = SCRATCH_ID
scratch.display_name = "Assignment Scratch"
scratch.surface_profile_id = profile_id
var err: int = ResourceSaver.save(scratch, SCRATCH_PATH)
if err != OK:
_fail("could not write the scratch setup (error %d)" % err)
return
# Exactly what the map editor does after a save.
PlanetSetups.rescan()
var live: Dictionary = {}
var scratch_in_pool: bool = false
for setup in PlanetSetups.setups_for_profile(profile_id):
live[setup] = true
if String(setup.setup_id) == SCRATCH_ID:
scratch_in_pool = true
if not scratch_in_pool:
_remove_scratch()
_fail("the scratch setup did not enter the pool, so this case proves nothing")
return
var moved: int = 0
var checked: int = 0
for planet_id in planet_ids:
# Skipped if the map this planet drew is no longer in the pool. The
# suite runs tests in parallel against one project directory, so
# another test's scratch setup can appear and vanish underneath this
# one - and a planet losing an assignment because its MAP was deleted
# is correct behaviour, not the re-deal this case is looking for.
if not live.has(first_pass[planet_id]):
continue
checked += 1
if PlanetSetups.setup_for_planet(planet_id, profile_id) != first_pass[planet_id]:
moved += 1
if checked == 0:
_remove_scratch()
_fail("every planet's map disappeared from the pool mid-test, so nothing could be checked")
return
_remove_scratch()
PlanetSetups.rescan()
if moved > 0:
_fail("saving one new map re-dealt %d of %d already-visited planets" % [moved, checked])
return
print("saving a new map left all %d existing assignments alone" % checked)
_remove_scratch()
PlanetSetups.rescan()
print("TEST_PLANET_MAP_ASSIGNMENT: PASS")
quit(0)
func _plant_pool() -> bool:
for pool_id in POOL_IDS:
var setup: Resource = load("res://surface/PlanetSetup.gd").new()
setup.setup_id = pool_id
setup.display_name = pool_id
setup.surface_profile_id = "rocky"
if ResourceSaver.save(setup, "res://PlanetSetups/%s.tres" % pool_id) != OK:
_remove_scratch()
_fail("could not plant the scratch map pool")
return false
return true
func _remove_scratch() -> void:
var paths: Array = [SCRATCH_PATH]
for pool_id in POOL_IDS:
paths.append("res://PlanetSetups/%s.tres" % pool_id)
for res_path in paths:
var path: String = ProjectSettings.globalize_path(res_path)
if FileAccess.file_exists(path):
DirAccess.remove_absolute(path)
if FileAccess.file_exists(path + ".uid"):
DirAccess.remove_absolute(path + ".uid")
func _fail(message: String) -> void:
_remove_scratch()
print("TEST_PLANET_MAP_ASSIGNMENT: FAIL (%s)" % message)
quit(1)