139 lines
6.1 KiB
GDScript3
139 lines
6.1 KiB
GDScript3
|
|
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
|