extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_fleet_growth.gd # A new game opens with a SINGLE vessel and three slots, and the fleet grows # from there through the doctrine branch of the skill tree. # # BASE_FLEET_SIZE used to be both "how many slots exist" and "how many ships # you start with", which forced a new game to open with a full fleet - there # was nowhere to put a vessel until a doctrine was researched, so the first # ship crafter had nothing to do. The two are now separate constants, and # this test pins the distinction. # # Covers: # A. a new game starts with one vessel and three slots # B. the fleet really spawns one ship, and can be grown into its slots # C. every doctrine raises the ceiling by its own stated bonus # D. the doctrine branch is a chain, in increasing cost order # E. the tree cannot promise a slot the formation has no room for - the # invariant that made extra doctrines silently worthless const DOCTRINE_CHAIN: Array = ["wing", "squadron", "armada", "battlegroup", "grand_fleet"] var game_state: Node var scene: Node var fleet: Node var stage: int = 0 var elapsed: float = 0.0 func _initialize() -> void: game_state = root.get_node("GameState") game_state.reset_new_game() func _process(delta: float) -> bool: elapsed += delta match stage: 0: # --- A. the opening position --------------------------------- if GameState.STARTING_SHIP_COUNT != 1: return _fail("a new game starts with %d vessels, expected 1" % GameState.STARTING_SHIP_COUNT) if GameState.BASE_FLEET_SIZE != 3: return _fail("the fleet has %d slots by default, expected 3" % GameState.BASE_FLEET_SIZE) if game_state.surviving_ship_count != GameState.STARTING_SHIP_COUNT: return _fail("reset_new_game left %d surviving ships" % game_state.surviving_ship_count) if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE: return _fail("an unresearched fleet caps at %d, expected %d" \ % [game_state.max_fleet_size(), GameState.BASE_FLEET_SIZE]) print("new game: %d vessel, %d slots" % [GameState.STARTING_SHIP_COUNT, GameState.BASE_FLEET_SIZE]) # --- C + D. the doctrine chain ------------------------------- var expected: int = GameState.BASE_FLEET_SIZE var previous_cost: int = -1 var previous_points: int = -1 var previous_id: String = "coordination" for id in DOCTRINE_CHAIN: var node: Dictionary = game_state.skill_node(id) if node.is_empty(): return _fail("no skill node '%s' - the doctrine chain is broken" % id) var bonus: int = int(node.get("fleet_bonus", 0)) if bonus <= 0: return _fail("'%s' is in the doctrine chain but grants no fleet_bonus" % id) if String(node.get("requires", "")) != previous_id: return _fail("'%s' requires '%s', expected it to follow '%s' - the branch is not one chain" \ % [id, node.get("requires", ""), previous_id]) # Later tiers must not be cheaper than earlier ones, or the # order the player meets them stops matching what they cost. var cost: int = int(node.get("cost", 0)) var points: int = int(node.get("research_points", 0)) if cost <= previous_cost: return _fail("'%s' costs %d ore, not more than the tier before it (%d)" % [id, cost, previous_cost]) if points < previous_points: return _fail("'%s' costs %d research points, fewer than the tier before it (%d)" \ % [id, points, previous_points]) previous_cost = cost previous_points = points previous_id = id game_state.unlocked.append(id) expected += bonus if game_state.max_fleet_size() != expected: return _fail("after researching '%s' the fleet caps at %d, expected %d" \ % [id, game_state.max_fleet_size(), expected]) print(" %-12s +%d -> %2d slots (%d ore, %d research)" % [id, bonus, expected, cost, points]) # --- E. no doctrine may be swallowed by the formation --------- # max_fleet_size() clamps to the formation's length, so a doctrine # added without extending DEFAULT_FORMATION would cost the player # real resources and grant nothing. if expected > GameState.DEFAULT_FORMATION.size(): return _fail("the doctrines promise %d slots but DEFAULT_FORMATION only defines %d - the last ones buy nothing" \ % [expected, GameState.DEFAULT_FORMATION.size()]) print("full doctrine chain reaches %d slots, with %d formation positions defined" \ % [expected, GameState.DEFAULT_FORMATION.size()]) # --- B. what actually flies ---------------------------------- game_state.reset_new_game() scene = load("res://Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.3: return false scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) if fleet.ships.size() != GameState.STARTING_SHIP_COUNT: return _fail("a fresh scene spawned %d vessels, expected %d" \ % [fleet.ships.size(), GameState.STARTING_SHIP_COUNT]) print("a fresh game really flies %d vessel" % fleet.ships.size()) # The two empty slots are what the opening is for, so they have to # be fillable without researching anything. for i in range(GameState.BASE_FLEET_SIZE - GameState.STARTING_SHIP_COUNT): if not fleet.add_ship(GameState.VESSEL_INTERCEPTOR): return _fail("could not crew slot %d of the starting fleet" % (i + 2)) if fleet.ships.size() != GameState.BASE_FLEET_SIZE: return _fail("the fleet holds %d after filling its slots, expected %d" \ % [fleet.ships.size(), GameState.BASE_FLEET_SIZE]) # And the cap has to actually bite past that. if fleet.add_ship(GameState.VESSEL_INTERCEPTOR): return _fail("a %dth vessel joined an unresearched fleet of %d slots" \ % [fleet.ships.size(), GameState.BASE_FLEET_SIZE]) print("the starting slots fill to %d and refuse a %dth" \ % [GameState.BASE_FLEET_SIZE, GameState.BASE_FLEET_SIZE + 1]) print("TEST_FLEET_GROWTH: PASS") quit(0) return true return false func _fail(message: String) -> bool: print("TEST_FLEET_GROWTH: FAIL (%s)" % message) quit(1) return true