spaceship/tests/test_planet_map_assignment.gd

142 lines
5.5 KiB
GDScript3
Raw Normal View History

Share one backdrop between game and editor, and fix three input/economy bugs The setup editor and the landed surface drew different backgrounds for the same planet. Each scene carried its own hand-written ShaderMaterial, and only the game side ever swapped in a profile's shader, so a gas giant authored in the editor showed recoloured craters while the game showed cloud bands. Both now instance one SurfaceBackground.tscn and configure it through one method, and every profile names its own shader - the old "null means keep whatever the scene already had" convention was the drift, since each scene "already had" something different. The shared material is resource_local_to_scene, or two live surfaces would repaint each other. The gas giant's bands broke into disjoint segments once per cell: the second turbulence octave doubled only the INTEGER half of a lattice coordinate, which is a different point in every cell rather than the same point at twice the frequency. Measured, the old form steps 0.153 at a cell boundary against 0.0002 now. A second discontinuity went with it - the camera cell index arrives pre-wrapped, so hashing it raw rearranged the whole sky every 32 cells; hashes are now wrapped per octave and the field is exactly periodic. Bands also gained low-frequency octaves and per-row differential drift. WorldMap swallowed every touch in _input(), which runs before the GUI system, so CloseButton never saw one: the map could be opened and not closed. Android and desktop hid it behind Godot's emulated mouse event, which this never swallowed; a phone browser gives no such second chance. Its test had been closing the map by calling the method the button was failing to reach. Construction now draws on the fleet hold as well as the bank. With an empty starting bank and ore bankable only at a mothership, charging construction to the bank alone made the first mothership cost ore that could not yet exist. Also: a conquered planet no longer re-spawns its garrison (guarded on having capture points, since a setup with none registers as vacuously conquered); planets keep the map they drew, with the candidate list sorted rather than left in filesystem order and assignments pruned rather than cleared on rescan; and the map editor lists setups at a tappable size and can delete one, as an explicit armed mode rather than a tap away from destroying work. Full suite 106/106. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:16:38 +02:00
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
func _initialize() -> void:
_remove_scratch()
PlanetSetups.rescan()
# --- E. deterministic ordering ------------------------------------------
var profile_id: String = "rocky"
var ordered: Array = PlanetSetups.setups_for_profile(profile_id)
if ordered.size() < 2:
# Not a failure of the code under test, but this file cannot say
# anything without a pool to draw from - say so rather than passing
# vacuously.
_fail("only %d '%s' setups exist, so nothing here can distinguish a stable draw from a fixed one" \
% [ordered.size(), profile_id])
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://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)
print("TEST_PLANET_MAP_ASSIGNMENT: PASS")
quit(0)
func _remove_scratch() -> void:
var path: String = ProjectSettings.globalize_path(SCRATCH_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:
print("TEST_PLANET_MAP_ASSIGNMENT: FAIL (%s)" % message)
quit(1)