spaceship/tests/test_research_points.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

171 lines
9.2 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_research_points.gd
# Forgejo #63: a rare enemy planet type that yields no resources but grants
# one research point when conquered - a required currency for mid/late
# skill-tree nodes and vessel/lab research, gated alongside their existing
# ore/resource costs.
#
# Covers:
# A. research labs generate deterministically and are rare
# (structural) the definition yields no resources and its surface has no
# resource provider at all
# F. a fresh game can still reach a ship crafter and an extractor with
# zero research points
# D. a research-point-gated skill node is refused while unaffordable, and
# explains why
# E. conquering a research_lab awards what its MAP says it is worth, once.
# The reward used to be a flat 1 on the planet definition; it now comes
# entirely from the authored setup, so a hard lab can pay more than an
# easy one and the value is editable in the map editor. The planet type
# contributing nothing of its own is part of what is checked (guarded
# even against a redundant re-mark), emits the signal once, and a
# non-research_lab planet's conquest never awards one
# D continued: unlocking spends the points and the balance updates
# G. the ship-lab research path gates and spends research points the
# same way, independent of the skill tree
var game_state: Node
func _initialize() -> void:
game_state = root.get_node("GameState")
# --- A. deterministic and rare -------------------------------------------
var lab_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:
return _fail("planet_data_at_chunk(%s) is not deterministic" % chunk)
if first.definition_id == "research_lab":
lab_count += 1
print("scanned %d planet chunks: %d research labs" % [total_planets, lab_count])
if lab_count == 0:
return _fail("no research labs generated across a wide scan - the rare outcome never fires")
if lab_count * 3 >= total_planets:
return _fail("research labs (%d of %d) are not rare" % [lab_count, total_planets])
print("research labs are deterministic and rare (%d of %d planet chunks)" % [lab_count, total_planets])
# --- structural: yields nothing at all ------------------------------------
var lab_def: Resource = load("res://planets/PlanetDefinitions.gd").get_definition("research_lab")
if String(lab_def.resource_id) != "":
return _fail("research_lab planet's resource_id is '%s', expected empty" % lab_def.resource_id)
var profile: Resource = load("res://surface/PlanetSurfaceProfiles.gd").get_profile("research_lab")
if profile.features_scene != null:
return _fail("research_lab surface profile has a features_scene - it must yield nothing")
print("research_lab yields no resource id and no surface feature provider")
# --- F. a fresh game still reaches a crafter and an extractor -------------
game_state.reset_new_game()
# #108: reset_new_game() arms the prologue, and the prologue opens with a
# convoy rather than the game's own starting fleet. This test is about
# what the GAME starts from, which is where the prologue hands over -
# see test_intro_scene.gd for the prologue itself.
game_state.intro_active = false
# can_unlock() also checks the ore price, and a new game now banks
# nothing - the opening mining run is the point. Stock the ore so this
# checks what it says it checks: reachability at ZERO RESEARCH POINTS.
game_state.rocks = 500
if not game_state.can_unlock("shipyards"):
return _fail("shipyards is not reachable with 0 research points")
game_state.unlock("shipyards")
if not game_state.can_unlock("logistics"):
return _fail("logistics is not reachable with 0 research points after shipyards")
game_state.unlock("logistics")
print("fresh game reaches a ship crafter (shipyards) and an extractor (logistics) at 0 research points")
# --- D. a points-gated node is refused and explains why -------------------
if game_state.can_unlock("coordination"):
return _fail("coordination (costs research points) was unlockable with 0 research points")
print("a research-point-gated node (coordination) correctly refused at 0 points")
# --- E. earning exactly one point, once, with the signal ------------------
var totals_seen: Array = []
game_state.research_points_changed.connect(func(total): totals_seen.append(total))
game_state.planet_capture.clear()
# The planet type itself must no longer carry a reward - that is the
# delegation. If it crept back, a lab would pay its map's value PLUS a
# hidden one, or pay something even with no map authored.
var lab_definition: Resource = load("res://planets/PlanetDefinitions.gd").get_definition("research_lab")
if int(lab_definition.conquest_research_points) != 0:
return _fail("the research_lab planet type still grants %d point(s) of its own - the reward belongs to the authored map now" \
% int(lab_definition.conquest_research_points))
game_state.register_planet_garrison("test_lab_no_reward", 1, "research_lab")
game_state.mark_garrison_enemy_destroyed("test_lab_no_reward")
if game_state.research_points != 0:
return _fail("a lab whose map states no reward still awarded %d" % game_state.research_points)
print("the planet type grants nothing by itself; an unpriced map awards nothing")
# What an authored map is worth is passed through as the override, exactly
# as PlanetSetupHost does from the setup's research_point_reward.
game_state.register_planet_garrison("test_lab_planet", 2, "research_lab", 1)
if game_state.is_planet_conquered("test_lab_planet"):
return _fail("a lab with 2 real capture points started conquered")
game_state.mark_garrison_enemy_destroyed("test_lab_planet")
if game_state.research_points != 0:
return _fail("a research point was awarded before every point was captured")
game_state.mark_garrison_enemy_destroyed("test_lab_planet")
if game_state.research_points != 1:
return _fail("conquering a research_lab did not award the 1 point its map is worth: %d" % game_state.research_points)
if totals_seen != [1]:
return _fail("research_points_changed did not fire exactly once with the new total: %s" % [totals_seen])
print("conquering a research_lab awarded exactly what its map is worth, once")
game_state.mark_garrison_enemy_destroyed("test_lab_planet") # already beaten - redundant kill
if game_state.research_points != 1:
return _fail("re-marking an already-captured point re-awarded a research point: %d" % game_state.research_points)
print("double-award guarded against")
game_state.register_planet_garrison("test_rocky_planet", 1, "rocky")
game_state.mark_garrison_enemy_destroyed("test_rocky_planet")
if game_state.research_points != 1:
return _fail("conquering a non-research_lab planet awarded a research point: %d" % game_state.research_points)
print("a non-research_lab planet's conquest never awards a point")
# --- D continued: unlocking spends the points, balance updates ------------
if not game_state.can_unlock("coordination"):
return _fail("coordination is still not unlockable with 1 research point and enough ore")
if not game_state.unlock("coordination"):
return _fail("unlock(coordination) failed despite can_unlock() being true")
if game_state.research_points != 0:
return _fail("unlocking a 1-point node did not spend the point: %d remaining" % game_state.research_points)
print("unlocking a research-point-gated skill node spent the point, balance now 0")
# --- G. the ship-lab research path gates and spends the same currency -----
var carrier_def: Resource = game_state.vessel_definition(GameState.VESSEL_CARRIER)
var point_cost: int = int(carrier_def.research_point_cost)
if point_cost <= 0:
return _fail("carrier production data has no research-point cost")
game_state.vessel_levels.erase(carrier_def.unlock_id)
var lab_id: int = game_state.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO)
if not game_state.queue_lab_research(lab_id, carrier_def.unlock_id):
return _fail("could not queue carrier research")
game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE, 1000)
game_state._advance_lab_research(100.0)
if game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER):
return _fail("carrier research completed despite 0 research points")
print("lab research correctly stalled on a research-point shortfall")
game_state.research_points = point_cost
game_state._advance_lab_research(100.0)
if not game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER):
return _fail("carrier research did not complete once research points were available")
if game_state.research_points != 0:
return _fail("lab research did not spend the research points: %d remaining" % game_state.research_points)
print("lab research spent the research points once affordable, and completed")
print("TEST_RESEARCH_POINTS: PASS")
quit(0)
func _fail(message: String) -> void:
print("TEST_RESEARCH_POINTS: FAIL (%s)" % message)
quit(1)