test(city-systems): ✅ Add/update unit tests for bridge logic, site scoring, and improvement management
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
e5ce018ff1
commit
07b7bd4476
5 changed files with 596 additions and 0 deletions
113
src/game/engine/tests/unit/test_city_bridge.gd
Normal file
113
src/game/engine/tests/unit/test_city_bridge.gd
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
extends GutTest
|
||||
## Task #18 — verify the City entity's GdCity bridge end-to-end.
|
||||
##
|
||||
## When the GdCity GDExtension is registered, we exercise the full path:
|
||||
## create a city with a smithy, load iron_axe from the item registry,
|
||||
## stock 2 iron_ore, enqueue, tick, and assert EventBus.item_crafted
|
||||
## fires with the expected item_id. When the extension is not loaded
|
||||
## (headless test run with no .so in addons/), we verify the degraded
|
||||
## path: enqueue returns a clear error string and no signal fires.
|
||||
|
||||
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
|
||||
|
||||
var _crafted_events: Array[Dictionary] = []
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
_crafted_events.clear()
|
||||
if EventBus.item_crafted.is_connected(_on_item_crafted):
|
||||
EventBus.item_crafted.disconnect(_on_item_crafted)
|
||||
EventBus.item_crafted.connect(_on_item_crafted)
|
||||
|
||||
|
||||
func after_each() -> void:
|
||||
if EventBus.item_crafted.is_connected(_on_item_crafted):
|
||||
EventBus.item_crafted.disconnect(_on_item_crafted)
|
||||
|
||||
|
||||
func _on_item_crafted(item_id: String, city: Variant, player: Variant) -> void:
|
||||
_crafted_events.append({
|
||||
"item_id": item_id,
|
||||
"city": city,
|
||||
"player": player,
|
||||
})
|
||||
|
||||
|
||||
func test_bridge_construction_sets_id_and_buildings() -> void:
|
||||
var city: RefCounted = CityScript.new("khazad", ["smithy"] as Array[String])
|
||||
assert_eq(city.id, "khazad")
|
||||
assert_true(city.has_building("smithy"))
|
||||
assert_false(city.has_building("forge"))
|
||||
|
||||
|
||||
func test_add_building_syncs_mirror() -> void:
|
||||
var city: RefCounted = CityScript.new("khazad", [] as Array[String])
|
||||
city.add_building("smithy")
|
||||
assert_true(city.has_building("smithy"))
|
||||
# Duplicate add is a no-op.
|
||||
city.add_building("smithy")
|
||||
assert_eq(city.buildings.size(), 1)
|
||||
|
||||
|
||||
func test_enqueue_without_extension_returns_error_string() -> void:
|
||||
if ClassDB.class_exists("GdCity"):
|
||||
# Skip — extension is loaded; the happy-path test covers this.
|
||||
pass_test("GdCity extension present — covered by happy-path test")
|
||||
return
|
||||
var city: RefCounted = CityScript.new("khazad", ["smithy"] as Array[String])
|
||||
var err: String = city.enqueue_item(
|
||||
"iron_axe", null, [] as Array[String]
|
||||
)
|
||||
assert_ne(err, "", "enqueue should surface an error when extension missing")
|
||||
assert_eq(_crafted_events.size(), 0)
|
||||
|
||||
|
||||
func test_happy_path_enqueue_tick_emits_item_crafted() -> void:
|
||||
if not ClassDB.class_exists("GdCity"):
|
||||
pass_test("GdCity extension not loaded in this test run")
|
||||
return
|
||||
if not ClassDB.class_exists("GdStockpile"):
|
||||
pass_test("GdStockpile extension not loaded in this test run")
|
||||
return
|
||||
if not ClassDB.class_exists("GdTreasury"):
|
||||
pass_test("GdTreasury extension not loaded in this test run")
|
||||
return
|
||||
|
||||
var city: RefCounted = CityScript.new("khazad", ["smithy"] as Array[String])
|
||||
var items_json: String = JSON.stringify([
|
||||
{
|
||||
"id": "iron_axe",
|
||||
"production": {
|
||||
"building": "smithy",
|
||||
"hammer_cost": 30,
|
||||
"materials": [{"resource": "iron_ore", "amount": 2}],
|
||||
"requires_tech": "bronze_working",
|
||||
},
|
||||
},
|
||||
])
|
||||
var loaded: bool = city.load_items_json(items_json)
|
||||
assert_true(loaded, "load_items_json should succeed")
|
||||
|
||||
var stockpile: RefCounted = ClassDB.instantiate("GdStockpile") as RefCounted
|
||||
stockpile.call("add", "iron_ore", 2)
|
||||
|
||||
var techs: Array[String] = ["bronze_working"]
|
||||
var err: String = city.enqueue_item("iron_axe", stockpile, techs)
|
||||
assert_eq(err, "", "enqueue should succeed")
|
||||
assert_eq(
|
||||
stockpile.call("available", "iron_ore"), 0,
|
||||
"materials must be consumed on enqueue"
|
||||
)
|
||||
assert_eq(city.queue_len("smithy"), 1)
|
||||
|
||||
var treasury: RefCounted = ClassDB.instantiate("GdTreasury") as RefCounted
|
||||
var completed: int = city.tick_building("smithy", 30, treasury)
|
||||
assert_eq(completed, 1, "30 hammers should complete iron_axe")
|
||||
assert_eq(city.queue_len("smithy"), 0)
|
||||
|
||||
assert_eq(_crafted_events.size(), 1, "item_crafted should fire exactly once")
|
||||
assert_eq(_crafted_events[0]["item_id"], "iron_axe")
|
||||
assert_eq(_crafted_events[0]["city"], city)
|
||||
|
||||
assert_eq(treasury.call("total_count"), 1)
|
||||
assert_true(treasury.call("contains", "iron_axe"))
|
||||
192
src/game/engine/tests/unit/test_city_site_scorer.gd
Normal file
192
src/game/engine/tests/unit/test_city_site_scorer.gd
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
extends GutTest
|
||||
## CitySiteScorer unit tests.
|
||||
## Covers: founding validity, site scoring, founder target search.
|
||||
|
||||
const CityScorerScript: GDScript = preload(
|
||||
"res://engine/src/modules/management/city_site_scorer.gd"
|
||||
)
|
||||
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
|
||||
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
|
||||
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
|
||||
const GameMapScript: GDScript = preload("res://engine/src/map/game_map.gd")
|
||||
const TileScript: GDScript = preload("res://engine/src/map/tile.gd")
|
||||
|
||||
var _scorer: CityScorerScript = null
|
||||
var _player: PlayerScript = null
|
||||
var _game_map: GameMapScript = null
|
||||
|
||||
|
||||
func before_all() -> void:
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
_scorer = CityScorerScript.new()
|
||||
|
||||
_player = PlayerScript.new()
|
||||
_player.index = 0
|
||||
_player.race_id = "dwarf"
|
||||
_player.units = []
|
||||
_player.cities = []
|
||||
_player.researched_techs = []
|
||||
|
||||
GameState.players = [_player]
|
||||
GameState.diplomacy = {}
|
||||
|
||||
_game_map = _build_small_map()
|
||||
|
||||
|
||||
func after_each() -> void:
|
||||
GameState.players = []
|
||||
GameState.diplomacy = {}
|
||||
|
||||
|
||||
func _build_small_map() -> GameMapScript:
|
||||
var gmap: GameMapScript = GameMapScript.new()
|
||||
gmap.initialize(10, 10, 0)
|
||||
|
||||
for x: int in range(10):
|
||||
for y: int in range(10):
|
||||
var tile: TileScript = TileScript.new(Vector2i(x, y), "plains")
|
||||
tile.temperature = 0.45
|
||||
tile.moisture = 0.45
|
||||
tile.wind_speed = 0.2
|
||||
tile.wind_direction = 0
|
||||
tile.elevation = 0.5
|
||||
tile.is_coastal = false
|
||||
gmap.set_tile(Vector2i(x, y), tile)
|
||||
|
||||
return gmap
|
||||
|
||||
|
||||
func _make_city(owner_idx: int, pos: Vector2i) -> CityScript:
|
||||
var city: CityScript = CityScript.new()
|
||||
city.id = "city_%d_%d" % [pos.x, pos.y]
|
||||
city.owner = owner_idx
|
||||
city.position = pos
|
||||
city.population = 1
|
||||
city.buildings = []
|
||||
return city
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# can_found_city_at
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func test_can_found_city_at_valid_land_tile() -> void:
|
||||
assert_true(
|
||||
_scorer.can_found_city_at(Vector2i(5, 5), _game_map),
|
||||
"Must allow founding on a valid land tile with no nearby cities"
|
||||
)
|
||||
|
||||
|
||||
func test_cannot_found_city_at_null_tile() -> void:
|
||||
var empty_map: GameMapScript = GameMapScript.new()
|
||||
empty_map.initialize(10, 10, 0)
|
||||
assert_false(
|
||||
_scorer.can_found_city_at(Vector2i(5, 5), empty_map),
|
||||
"Must reject founding when tile is null"
|
||||
)
|
||||
|
||||
|
||||
func test_cannot_found_city_too_close_to_existing() -> void:
|
||||
_player.cities.append(_make_city(0, Vector2i(5, 5)))
|
||||
assert_false(
|
||||
_scorer.can_found_city_at(Vector2i(5, 6), _game_map),
|
||||
"Must reject founding within 3 hexes of existing city"
|
||||
)
|
||||
|
||||
|
||||
func test_can_found_city_exactly_3_away() -> void:
|
||||
_player.cities.append(_make_city(0, Vector2i(5, 5)))
|
||||
|
||||
var candidate: Vector2i = Vector2i(-9999, -9999)
|
||||
for dx: int in range(-5, 6):
|
||||
for dy: int in range(-5, 6):
|
||||
var pos: Vector2i = Vector2i(5 + dx, 5 + dy)
|
||||
var dist: float = (abs(dx) + abs(dy) + abs(dx + dy)) / 2.0
|
||||
if int(dist) == 3 and pos.x >= 0 and pos.y >= 0 \
|
||||
and pos.x < 10 and pos.y < 10:
|
||||
candidate = pos
|
||||
break
|
||||
if candidate != Vector2i(-9999, -9999):
|
||||
break
|
||||
|
||||
if candidate == Vector2i(-9999, -9999):
|
||||
pass # No ring-3 tile in bounds — skip
|
||||
else:
|
||||
assert_true(
|
||||
_scorer.can_found_city_at(candidate, _game_map),
|
||||
"Must allow founding exactly 3 hexes from existing city"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# score_city_site
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func test_score_city_site_returns_negative_for_null_tile() -> void:
|
||||
var empty_map: GameMapScript = GameMapScript.new()
|
||||
empty_map.initialize(10, 10, 0)
|
||||
assert_eq(
|
||||
_scorer.score_city_site(Vector2i(5, 5), empty_map),
|
||||
-1.0,
|
||||
"score_city_site must return -1.0 when tile is null"
|
||||
)
|
||||
|
||||
|
||||
func test_score_city_site_returns_positive_for_valid_plains() -> void:
|
||||
assert_gt(
|
||||
_scorer.score_city_site(Vector2i(5, 5), _game_map),
|
||||
0.0,
|
||||
"score_city_site must return > 0 for valid plains tile"
|
||||
)
|
||||
|
||||
|
||||
func test_score_city_site_returns_negative_when_too_close_to_city() -> void:
|
||||
_player.cities.append(_make_city(0, Vector2i(5, 5)))
|
||||
assert_eq(
|
||||
_scorer.score_city_site(Vector2i(5, 6), _game_map),
|
||||
-1.0,
|
||||
"score_city_site must return -1.0 when too close to existing city"
|
||||
)
|
||||
|
||||
|
||||
func test_higher_quality_tile_scores_higher() -> void:
|
||||
var low_tile: TileScript = _game_map.get_tile(Vector2i(3, 3)) as TileScript
|
||||
var high_tile: TileScript = _game_map.get_tile(Vector2i(7, 7)) as TileScript
|
||||
low_tile.quality = 1
|
||||
high_tile.quality = 5
|
||||
|
||||
var low_score: float = _scorer.score_city_site(Vector2i(3, 3), _game_map)
|
||||
var high_score: float = _scorer.score_city_site(Vector2i(7, 7), _game_map)
|
||||
assert_gt(high_score, low_score, "Higher quality tile must score higher")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# find_best_founder_target
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func test_find_best_founder_target_returns_valid_position() -> void:
|
||||
var founder: UnitScript = UnitScript.new()
|
||||
founder.position = Vector2i(5, 5)
|
||||
assert_ne(
|
||||
_scorer.find_best_founder_target(founder, _game_map),
|
||||
Vector2i(-9999, -9999),
|
||||
"Must find a valid target when map has land tiles"
|
||||
)
|
||||
|
||||
|
||||
func test_find_best_founder_target_returns_sentinel_on_empty_map() -> void:
|
||||
var empty_map: GameMapScript = GameMapScript.new()
|
||||
empty_map.initialize(10, 10, 0)
|
||||
var founder: UnitScript = UnitScript.new()
|
||||
founder.position = Vector2i(5, 5)
|
||||
assert_eq(
|
||||
_scorer.find_best_founder_target(founder, empty_map),
|
||||
Vector2i(-9999, -9999),
|
||||
"Must return sentinel on empty map"
|
||||
)
|
||||
1
src/game/engine/tests/unit/test_city_site_scorer.gd.uid
Normal file
1
src/game/engine/tests/unit/test_city_site_scorer.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://buq3qpgkvg68w
|
||||
289
src/game/engine/tests/unit/test_improvement_manager.gd
Normal file
289
src/game/engine/tests/unit/test_improvement_manager.gd
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
extends GutTest
|
||||
## Improvement manager unit tests.
|
||||
## Covers: get_buildable_improvements, start_improvement, cancel_improvement,
|
||||
## build completion after turns, pending improvement queries.
|
||||
|
||||
const ImprovementManagerScript: GDScript = preload(
|
||||
"res://engine/src/modules/management/improvement_manager.gd"
|
||||
)
|
||||
const ImprovementScript: GDScript = preload("res://engine/src/entities/improvement.gd")
|
||||
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
|
||||
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
|
||||
const TileScript: GDScript = preload("res://engine/src/map/tile.gd")
|
||||
const GameMapScript: GDScript = preload("res://engine/src/map/game_map.gd")
|
||||
|
||||
var _manager: ImprovementManagerScript = null
|
||||
var _player: PlayerScript = null
|
||||
var _game_map: GameMapScript = null
|
||||
|
||||
|
||||
func before_all() -> void:
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
_manager = ImprovementManagerScript.new()
|
||||
|
||||
_player = PlayerScript.new()
|
||||
_player.index = 0
|
||||
_player.race_id = "dwarf"
|
||||
_player.units = []
|
||||
_player.cities = []
|
||||
_player.researched_techs = []
|
||||
_player.pending_improvements = []
|
||||
|
||||
GameState.players = [_player]
|
||||
|
||||
_game_map = GameMapScript.new()
|
||||
_game_map.initialize(10, 10, 1)
|
||||
for x: int in range(10):
|
||||
for y: int in range(10):
|
||||
var tile: TileScript = TileScript.new()
|
||||
tile.position = Vector2i(x, y)
|
||||
tile.biome_id = "grassland"
|
||||
tile.quality = 3
|
||||
tile.owner = 0
|
||||
_game_map.set_tile(Vector2i(x, y), tile)
|
||||
|
||||
|
||||
func _make_engineer(pos: Vector2i) -> UnitScript:
|
||||
var unit: UnitScript = UnitScript.new()
|
||||
unit.id = "eng_%d_%d" % [pos.x, pos.y]
|
||||
unit.type_id = "dwarf_engineer"
|
||||
unit.owner = 0
|
||||
unit.position = pos
|
||||
unit.hp = 10
|
||||
unit.max_hp = 10
|
||||
unit.bonus_attack = 0
|
||||
unit.bonus_defense = 0
|
||||
unit.unit_type = "civilian"
|
||||
unit.can_build_improvements = true
|
||||
unit.movement_remaining = 2
|
||||
return unit
|
||||
|
||||
|
||||
func _make_military_unit(pos: Vector2i) -> UnitScript:
|
||||
var unit: UnitScript = UnitScript.new()
|
||||
unit.id = "mil_%d_%d" % [pos.x, pos.y]
|
||||
unit.type_id = "spearmen"
|
||||
unit.owner = 0
|
||||
unit.position = pos
|
||||
unit.hp = 10
|
||||
unit.max_hp = 10
|
||||
unit.bonus_attack = 5
|
||||
unit.bonus_defense = 3
|
||||
unit.unit_type = "military"
|
||||
unit.can_build_improvements = false
|
||||
unit.movement_remaining = 2
|
||||
return unit
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_buildable_improvements
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_engineer_gets_buildable_list() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
|
||||
var buildable: Array[Dictionary] = _manager.get_buildable_improvements(
|
||||
engineer, _game_map, _player
|
||||
)
|
||||
|
||||
## At minimum, grassland should allow some improvements (farm, road, etc.)
|
||||
assert_gt(
|
||||
buildable.size(), 0,
|
||||
"Engineer on grassland must have at least one buildable improvement"
|
||||
)
|
||||
|
||||
## Each entry must have required keys
|
||||
for entry: Dictionary in buildable:
|
||||
assert_true(entry.has("id"), "Buildable entry must have 'id'")
|
||||
assert_true(entry.has("name"), "Buildable entry must have 'name'")
|
||||
assert_true(entry.has("build_turns"), "Buildable entry must have 'build_turns'")
|
||||
|
||||
|
||||
func test_military_unit_cannot_build() -> void:
|
||||
var soldier: UnitScript = _make_military_unit(Vector2i(3, 3))
|
||||
|
||||
var buildable: Array[Dictionary] = _manager.get_buildable_improvements(
|
||||
soldier, _game_map, _player
|
||||
)
|
||||
|
||||
assert_eq(
|
||||
buildable.size(), 0,
|
||||
"Military unit must not have any buildable improvements"
|
||||
)
|
||||
|
||||
|
||||
func test_no_build_on_existing_improvement() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
var tile: TileScript = _game_map.get_tile(Vector2i(3, 3)) as TileScript
|
||||
tile.improvement = "farm"
|
||||
|
||||
var buildable: Array[Dictionary] = _manager.get_buildable_improvements(
|
||||
engineer, _game_map, _player
|
||||
)
|
||||
|
||||
assert_eq(
|
||||
buildable.size(), 0,
|
||||
"Must not allow building on tile with existing improvement"
|
||||
)
|
||||
|
||||
|
||||
func test_no_build_on_enemy_territory() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
var tile: TileScript = _game_map.get_tile(Vector2i(3, 3)) as TileScript
|
||||
tile.owner = 1 ## Belongs to another player
|
||||
|
||||
var buildable: Array[Dictionary] = _manager.get_buildable_improvements(
|
||||
engineer, _game_map, _player
|
||||
)
|
||||
|
||||
assert_eq(
|
||||
buildable.size(), 0,
|
||||
"Must not allow building on enemy-owned territory"
|
||||
)
|
||||
|
||||
|
||||
func test_build_allowed_on_unclaimed_territory() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
var tile: TileScript = _game_map.get_tile(Vector2i(3, 3)) as TileScript
|
||||
tile.owner = -1 ## Unclaimed
|
||||
|
||||
var buildable: Array[Dictionary] = _manager.get_buildable_improvements(
|
||||
engineer, _game_map, _player
|
||||
)
|
||||
|
||||
assert_gt(
|
||||
buildable.size(), 0,
|
||||
"Engineer must be able to build on unclaimed territory"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# start_improvement
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_start_improvement_adds_pending() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
|
||||
var success: bool = _manager.start_improvement(engineer, "farm", _player)
|
||||
assert_true(success, "start_improvement must succeed for valid engineer")
|
||||
assert_eq(
|
||||
_player.pending_improvements.size(), 1,
|
||||
"Must add one pending improvement"
|
||||
)
|
||||
|
||||
var pending: Dictionary = _player.pending_improvements[0] as Dictionary
|
||||
assert_eq(pending.get("type", ""), "farm", "Pending type must be 'farm'")
|
||||
assert_eq(pending.get("x", -1), 3, "Pending x must match unit position")
|
||||
assert_eq(pending.get("y", -1), 3, "Pending y must match unit position")
|
||||
|
||||
|
||||
func test_start_improvement_consumes_movement() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
assert_gt(engineer.movement_remaining, 0, "Engineer must have movement")
|
||||
|
||||
_manager.start_improvement(engineer, "farm", _player)
|
||||
assert_eq(
|
||||
engineer.movement_remaining, 0,
|
||||
"Starting improvement must consume all movement"
|
||||
)
|
||||
|
||||
|
||||
func test_start_fails_without_movement() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
engineer.movement_remaining = 0
|
||||
|
||||
var success: bool = _manager.start_improvement(engineer, "farm", _player)
|
||||
assert_false(success, "Must fail when engineer has no movement remaining")
|
||||
assert_eq(
|
||||
_player.pending_improvements.size(), 0,
|
||||
"Must not add pending improvement on failure"
|
||||
)
|
||||
|
||||
|
||||
func test_start_fails_for_non_builder() -> void:
|
||||
var soldier: UnitScript = _make_military_unit(Vector2i(3, 3))
|
||||
|
||||
var success: bool = _manager.start_improvement(soldier, "farm", _player)
|
||||
assert_false(success, "Must fail for unit that cannot build improvements")
|
||||
|
||||
|
||||
func test_pending_has_correct_turns() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
_manager.start_improvement(engineer, "farm", _player)
|
||||
|
||||
var pending: Dictionary = _player.pending_improvements[0] as Dictionary
|
||||
var expected_turns: int = ImprovementScript.get_build_time("farm")
|
||||
assert_eq(
|
||||
pending.get("turns_remaining", -1),
|
||||
expected_turns,
|
||||
"Pending turns must match improvement build time"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cancel_improvement
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_cancel_removes_pending() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
_manager.start_improvement(engineer, "farm", _player)
|
||||
assert_eq(_player.pending_improvements.size(), 1, "Precondition: 1 pending")
|
||||
|
||||
var cancelled: bool = _manager.cancel_improvement(Vector2i(3, 3), _player)
|
||||
assert_true(cancelled, "cancel_improvement must succeed for existing pending")
|
||||
assert_eq(
|
||||
_player.pending_improvements.size(), 0,
|
||||
"Pending must be empty after cancellation"
|
||||
)
|
||||
|
||||
|
||||
func test_cancel_returns_false_for_no_pending() -> void:
|
||||
var cancelled: bool = _manager.cancel_improvement(Vector2i(5, 5), _player)
|
||||
assert_false(
|
||||
cancelled,
|
||||
"cancel_improvement must return false when nothing pending at position"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_pending_at
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_get_pending_at_returns_matching() -> void:
|
||||
var engineer: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
_manager.start_improvement(engineer, "farm", _player)
|
||||
|
||||
var pending: Dictionary = _manager.get_pending_at(Vector2i(3, 3), _player)
|
||||
assert_false(pending.is_empty(), "Must return pending at position")
|
||||
assert_eq(pending.get("type", ""), "farm", "Pending type must match")
|
||||
|
||||
|
||||
func test_get_pending_at_returns_empty_for_none() -> void:
|
||||
var pending: Dictionary = _manager.get_pending_at(Vector2i(7, 7), _player)
|
||||
assert_true(
|
||||
pending.is_empty(),
|
||||
"Must return empty dict when no pending at position"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Duplicate build prevention
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_no_duplicate_build_at_same_tile() -> void:
|
||||
var eng1: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
_manager.start_improvement(eng1, "farm", _player)
|
||||
|
||||
var eng2: UnitScript = _make_engineer(Vector2i(3, 3))
|
||||
eng2.id = "eng_dup"
|
||||
var buildable: Array[Dictionary] = _manager.get_buildable_improvements(
|
||||
eng2, _game_map, _player
|
||||
)
|
||||
|
||||
assert_eq(
|
||||
buildable.size(), 0,
|
||||
"Must not allow building where a pending improvement exists"
|
||||
)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://b3wax66cs7lmy
|
||||
Loading…
Add table
Reference in a new issue