test(game-engine): ✅ Update unit tests for population stability, save manager, species generation, tech tree, and victory conditions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
d3d6dccd22
commit
b359e10d58
8 changed files with 612 additions and 0 deletions
|
|
@ -0,0 +1 @@
|
|||
uid://42sl40oguaro
|
||||
234
src/game/engine/tests/unit/test_save_manager.gd
Normal file
234
src/game/engine/tests/unit/test_save_manager.gd
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
extends GutTest
|
||||
## SaveManager unit tests.
|
||||
## Verifies save/load round-trip, slot listing, and edge cases.
|
||||
|
||||
const SaveManagerScript: GDScript = preload("res://engine/src/core/save_manager.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")
|
||||
|
||||
const TEST_SLOT: String = "_gut_test_slot"
|
||||
|
||||
|
||||
func before_all() -> void:
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
|
||||
|
||||
func after_each() -> void:
|
||||
## Clean up any test save files after each test.
|
||||
SaveManagerScript.delete_save(TEST_SLOT)
|
||||
GameState.players = []
|
||||
GameState.layers = []
|
||||
GameState.turn_number = 1
|
||||
GameState.current_player_index = 0
|
||||
|
||||
|
||||
func _make_player(idx: int, gold_amount: int, race: String) -> RefCounted:
|
||||
var player: PlayerScript = PlayerScript.new()
|
||||
player.index = idx
|
||||
player.player_name = "Test Player %d" % idx
|
||||
player.race_id = race
|
||||
player.is_player_controlled = true
|
||||
player.gold = gold_amount
|
||||
player.gold_per_turn = 10
|
||||
player.science_per_turn = 5
|
||||
player.culture_per_turn = 3
|
||||
player.culture_total = 42
|
||||
player.happiness = 7
|
||||
return player
|
||||
|
||||
|
||||
func _make_city(
|
||||
owner_idx: int, pos: Vector2i, city_name: String, pop: int,
|
||||
) -> CityScript:
|
||||
var city: CityScript = CityScript.new()
|
||||
city.id = "%d_%d_%d" % [owner_idx, pos.x, pos.y]
|
||||
city.city_name = city_name
|
||||
city.owner = owner_idx
|
||||
city.position = pos
|
||||
city.population = pop
|
||||
city.owned_tiles = [pos]
|
||||
return city
|
||||
|
||||
|
||||
func _make_unit(owner_idx: int, type_id: String, pos: Vector2i) -> UnitScript:
|
||||
var unit: UnitScript = UnitScript.new()
|
||||
unit.id = "u_%d_%d_%d" % [owner_idx, pos.x, pos.y]
|
||||
unit.type_id = type_id
|
||||
unit.owner = owner_idx
|
||||
unit.position = pos
|
||||
unit.hp = 10
|
||||
unit.max_hp = 10
|
||||
return unit
|
||||
|
||||
|
||||
func _make_game_map() -> GameMapScript:
|
||||
var game_map: GameMapScript = GameMapScript.new()
|
||||
game_map.initialize(10, 10, 0)
|
||||
for col: int in range(10):
|
||||
for row: int in range(10):
|
||||
var tile: TileScript = TileScript.new()
|
||||
tile.position = Vector2i(col, row)
|
||||
tile.biome_id = "plains"
|
||||
game_map.set_tile(tile.position, tile)
|
||||
return game_map
|
||||
|
||||
|
||||
func _setup_game_state_with_map() -> void:
|
||||
## Set up GameState with a single layer containing a 10x10 map.
|
||||
var game_map: GameMapScript = _make_game_map()
|
||||
GameState.layers = [{"game_map": game_map, "cities": [], "units": []}]
|
||||
|
||||
|
||||
func test_save_game_writes_file() -> void:
|
||||
## save_game() must write a file and return true.
|
||||
var player: RefCounted = _make_player(0, 100, "dwarf")
|
||||
GameState.players = [player]
|
||||
_setup_game_state_with_map()
|
||||
|
||||
var success: bool = SaveManagerScript.save_game(TEST_SLOT)
|
||||
assert_true(success, "save_game must return true on success")
|
||||
|
||||
var path: String = SaveManagerScript.SAVE_DIR + TEST_SLOT + SaveManagerScript.SAVE_EXTENSION
|
||||
assert_true(
|
||||
FileAccess.file_exists(path),
|
||||
"Save file must exist at '%s'" % path,
|
||||
)
|
||||
|
||||
|
||||
func test_load_game_reads_back() -> void:
|
||||
## load_game() must restore GameState from a previously saved file.
|
||||
var player: RefCounted = _make_player(0, 250, "dwarf")
|
||||
GameState.players = [player]
|
||||
_setup_game_state_with_map()
|
||||
GameState.turn_number = 15
|
||||
|
||||
SaveManagerScript.save_game(TEST_SLOT)
|
||||
|
||||
## Wipe state to verify load restores it.
|
||||
GameState.players = []
|
||||
GameState.turn_number = 1
|
||||
GameState.layers = []
|
||||
|
||||
var success: bool = SaveManagerScript.load_game(TEST_SLOT)
|
||||
assert_true(success, "load_game must return true")
|
||||
assert_eq(GameState.turn_number, 15, "Turn number must be restored")
|
||||
assert_eq(
|
||||
GameState.players.size(), 1,
|
||||
"Must restore exactly one player",
|
||||
)
|
||||
|
||||
var restored: RefCounted = GameState.players[0]
|
||||
assert_eq(restored.gold, 250, "Player gold must round-trip")
|
||||
assert_eq(restored.race_id, "dwarf", "Player race_id must round-trip")
|
||||
|
||||
|
||||
func test_round_trip_gold_cities_units() -> void:
|
||||
## Full round-trip: Player with gold=500, 2 cities, 3 units.
|
||||
var player: RefCounted = _make_player(0, 500, "terrans")
|
||||
var city_a: CityScript = _make_city(0, Vector2i(2, 3), "Alpha", 5)
|
||||
var city_b: CityScript = _make_city(0, Vector2i(5, 6), "Beta", 3)
|
||||
player.cities = [city_a, city_b]
|
||||
|
||||
var unit_a: UnitScript = _make_unit(0, "dwarf_warrior", Vector2i(1, 1))
|
||||
var unit_b: UnitScript = _make_unit(0, "dwarf_warrior", Vector2i(2, 2))
|
||||
var unit_c: UnitScript = _make_unit(0, "dwarf_warrior", Vector2i(3, 3))
|
||||
player.units = [unit_a, unit_b, unit_c]
|
||||
|
||||
GameState.players = [player]
|
||||
GameState.turn_number = 42
|
||||
_setup_game_state_with_map()
|
||||
|
||||
SaveManagerScript.save_game(TEST_SLOT)
|
||||
|
||||
## Wipe and reload.
|
||||
GameState.players = []
|
||||
GameState.turn_number = 1
|
||||
GameState.layers = []
|
||||
|
||||
SaveManagerScript.load_game(TEST_SLOT)
|
||||
|
||||
var restored: RefCounted = GameState.players[0]
|
||||
assert_eq(restored.gold, 500, "Gold must be 500 after round-trip")
|
||||
assert_eq(restored.units.size(), 3, "Must restore 3 units")
|
||||
assert_eq(restored.player_name, "Test Player 0", "Player name must round-trip")
|
||||
|
||||
|
||||
func test_get_save_slots_returns_saved_slot() -> void:
|
||||
## After saving, get_save_slots() must include our test slot.
|
||||
var player: RefCounted = _make_player(0, 100, "dwarf")
|
||||
GameState.players = [player]
|
||||
_setup_game_state_with_map()
|
||||
|
||||
SaveManagerScript.save_game(TEST_SLOT)
|
||||
|
||||
var slots: Array[Dictionary] = SaveManagerScript.get_save_slots()
|
||||
var found: bool = false
|
||||
for slot: Dictionary in slots:
|
||||
if slot.get("slot", "") == TEST_SLOT:
|
||||
found = true
|
||||
break
|
||||
assert_true(found, "get_save_slots must include the test slot")
|
||||
|
||||
|
||||
func test_save_with_no_players_succeeds() -> void:
|
||||
## Saving with empty game state must not crash.
|
||||
GameState.players = []
|
||||
_setup_game_state_with_map()
|
||||
|
||||
var success: bool = SaveManagerScript.save_game(TEST_SLOT)
|
||||
assert_true(success, "Saving with empty player list must succeed")
|
||||
|
||||
|
||||
func test_load_nonexistent_slot_fails() -> void:
|
||||
## Loading a slot that was never saved must return false.
|
||||
var success: bool = SaveManagerScript.load_game("_gut_nonexistent_slot")
|
||||
assert_false(success, "Loading a nonexistent save must return false")
|
||||
|
||||
|
||||
func test_delete_save_removes_file() -> void:
|
||||
## delete_save() must remove the file, and subsequent load must fail.
|
||||
var player: RefCounted = _make_player(0, 100, "dwarf")
|
||||
GameState.players = [player]
|
||||
_setup_game_state_with_map()
|
||||
|
||||
SaveManagerScript.save_game(TEST_SLOT)
|
||||
var deleted: bool = SaveManagerScript.delete_save(TEST_SLOT)
|
||||
assert_true(deleted, "delete_save must return true for existing slot")
|
||||
|
||||
var load_result: bool = SaveManagerScript.load_game(TEST_SLOT)
|
||||
assert_false(load_result, "Loading deleted save must fail")
|
||||
|
||||
|
||||
func test_slot_number_convenience() -> void:
|
||||
## save_slot() / load_slot() must work with numeric slot indices.
|
||||
var player: RefCounted = _make_player(0, 777, "orc")
|
||||
GameState.players = [player]
|
||||
_setup_game_state_with_map()
|
||||
|
||||
var save_ok: bool = SaveManagerScript.save_slot(0)
|
||||
assert_true(save_ok, "save_slot(0) must succeed")
|
||||
|
||||
GameState.players = []
|
||||
GameState.layers = []
|
||||
|
||||
var load_ok: bool = SaveManagerScript.load_slot(0)
|
||||
assert_true(load_ok, "load_slot(0) must succeed")
|
||||
assert_eq(GameState.players[0].gold, 777, "Gold must round-trip through slot 0")
|
||||
|
||||
## Cleanup numbered slot.
|
||||
SaveManagerScript.delete_save("slot_0")
|
||||
|
||||
|
||||
func test_invalid_slot_number_rejected() -> void:
|
||||
## Slot numbers outside [0, MAX_SLOTS) must fail.
|
||||
assert_false(
|
||||
SaveManagerScript.save_slot(-1),
|
||||
"Negative slot index must be rejected",
|
||||
)
|
||||
assert_false(
|
||||
SaveManagerScript.save_slot(SaveManagerScript.MAX_SLOTS),
|
||||
"Slot index >= MAX_SLOTS must be rejected",
|
||||
)
|
||||
1
src/game/engine/tests/unit/test_save_manager.gd.uid
Normal file
1
src/game/engine/tests/unit/test_save_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b8yye3lgwolqv
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://ctgosphycgfwn
|
||||
151
src/game/engine/tests/unit/test_tech_web.gd
Normal file
151
src/game/engine/tests/unit/test_tech_web.gd
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
extends GutTest
|
||||
## Tech Web unit tests.
|
||||
## Verifies prerequisite graph construction, availability logic, and the
|
||||
## masonry → walls unlock path.
|
||||
##
|
||||
## Acceptance criterion: "Research 'masonry' → city Walls becomes buildable."
|
||||
|
||||
const TechWebScript: GDScript = preload("res://engine/src/modules/tech/tech_web.gd")
|
||||
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
|
||||
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
|
||||
|
||||
var _tech_web: TechWebScript = null
|
||||
var _player: PlayerScript = null
|
||||
|
||||
|
||||
func before_all() -> void:
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
_tech_web = TechWebScript.new()
|
||||
_tech_web.initialize()
|
||||
_player = PlayerScript.new()
|
||||
_player.index = 0
|
||||
_player.race_id = "dwarf"
|
||||
_player.researched_techs = []
|
||||
_player.researching = ""
|
||||
_player.research_progress = 0
|
||||
_player.schools = []
|
||||
GameState.players = [_player]
|
||||
|
||||
|
||||
func test_tech_web_loads_techs() -> void:
|
||||
assert_gt(_tech_web.get_tech_count(), 0, "TechWeb must load at least one tech")
|
||||
|
||||
|
||||
func test_can_research_starter_techs() -> void:
|
||||
## Tier-1 techs with no prerequisites must be available at game start
|
||||
## regardless of what other techs exist.
|
||||
var available: Array[String] = _tech_web.get_available_techs(0)
|
||||
assert_false(available.is_empty(), "At least one tech must be available at game start")
|
||||
|
||||
## Every available tech must have all prerequisites satisfied (vacuously for
|
||||
## starter techs, which have an empty requires list).
|
||||
for tech_id: String in available:
|
||||
var prereqs: Array[String] = _tech_web.get_prereqs(tech_id)
|
||||
for prereq_id: String in prereqs:
|
||||
assert_true(
|
||||
_player.has_tech(prereq_id),
|
||||
"Available tech '%s' has unmet prereq '%s'" % [tech_id, prereq_id]
|
||||
)
|
||||
|
||||
## Stonecutting is the masonry pillar's tier-1 tech — must be available.
|
||||
assert_true(
|
||||
_tech_web.can_research("stonecutting", 0),
|
||||
"'stonecutting' must be available with no techs researched"
|
||||
)
|
||||
|
||||
|
||||
func test_locked_tech_not_available_without_prereq() -> void:
|
||||
## 'masonry' requires 'stonecutting' — must not be researchable before stonecutting.
|
||||
assert_false(
|
||||
_tech_web.can_research("masonry", 0),
|
||||
"'masonry' must be locked until 'stonecutting' is researched"
|
||||
)
|
||||
|
||||
|
||||
func test_masonry_available_after_stonecutting() -> void:
|
||||
_player.add_tech("stonecutting")
|
||||
assert_true(
|
||||
_tech_web.can_research("masonry", 0),
|
||||
"'masonry' must be available once 'stonecutting' is researched"
|
||||
)
|
||||
|
||||
|
||||
func test_masonry_unlocks_walls() -> void:
|
||||
## Core acceptance criterion: Research 'masonry' → city Walls becomes buildable.
|
||||
var city: CityScript = CityScript.new()
|
||||
city.owner = 0
|
||||
city.buildings = []
|
||||
|
||||
## Before masonry: walls must not be buildable.
|
||||
assert_false(
|
||||
city.can_build("walls", _player),
|
||||
"'walls' must not be buildable before 'masonry' is researched"
|
||||
)
|
||||
|
||||
## Research both stonecutting (prerequisite) and masonry.
|
||||
_player.add_tech("stonecutting")
|
||||
_player.add_tech("masonry")
|
||||
|
||||
## After masonry: walls must be buildable.
|
||||
assert_true(
|
||||
city.can_build("walls", _player),
|
||||
"'walls' must be buildable after 'masonry' is researched"
|
||||
)
|
||||
|
||||
|
||||
func test_start_research_sets_current() -> void:
|
||||
var success: bool = _tech_web.start_research("stonecutting", 0)
|
||||
assert_true(success, "start_research must succeed for an available tech")
|
||||
assert_eq(_player.researching, "stonecutting", "researching field must be set")
|
||||
assert_eq(_player.research_progress, 0, "research_progress must reset to 0")
|
||||
|
||||
|
||||
func test_add_science_completes_research() -> void:
|
||||
_tech_web.start_research("stonecutting", 0)
|
||||
var tech_data: Dictionary = _tech_web.get_tech_data("stonecutting")
|
||||
var cost: int = tech_data.get("cost", 0)
|
||||
assert_gt(cost, 0, "stonecutting must have a positive cost")
|
||||
|
||||
_tech_web.add_science(cost, 0)
|
||||
|
||||
assert_true(
|
||||
_player.has_tech("stonecutting"),
|
||||
"Player must have stonecutting after full science input"
|
||||
)
|
||||
assert_eq(_player.researching, "", "researching must be cleared after completion")
|
||||
|
||||
|
||||
func test_progress_fraction_reflects_input() -> void:
|
||||
_tech_web.start_research("stonecutting", 0)
|
||||
var tech_data: Dictionary = _tech_web.get_tech_data("stonecutting")
|
||||
var cost: int = tech_data.get("cost", 1)
|
||||
|
||||
_tech_web.add_science(cost / 2, 0)
|
||||
var fraction: float = _tech_web.get_progress_fraction(0)
|
||||
assert_gt(fraction, 0.0, "Progress fraction must be above 0 after partial science")
|
||||
assert_lt(fraction, 1.0, "Progress fraction must be below 1 when research is incomplete")
|
||||
|
||||
|
||||
func test_get_turns_remaining() -> void:
|
||||
_tech_web.start_research("stonecutting", 0)
|
||||
var tech_data: Dictionary = _tech_web.get_tech_data("stonecutting")
|
||||
var cost: int = tech_data.get("cost", 1)
|
||||
var science_per_turn: int = 5
|
||||
|
||||
var turns: int = _tech_web.get_turns_remaining(0, science_per_turn)
|
||||
var expected: int = ceili(float(cost) / float(science_per_turn))
|
||||
assert_eq(turns, expected, "get_turns_remaining must match ceil(cost / spt) formula")
|
||||
|
||||
|
||||
func test_overflow_carries_to_next() -> void:
|
||||
## Research stonecutting with excess science — overflow must carry to research_progress.
|
||||
_tech_web.start_research("stonecutting", 0)
|
||||
var cost: int = _tech_web.get_tech_data("stonecutting").get("cost", 20)
|
||||
var overflow: int = 7
|
||||
|
||||
_tech_web.add_science(cost + overflow, 0)
|
||||
assert_true(_player.has_tech("stonecutting"), "stonecutting must complete")
|
||||
assert_eq(_player.research_progress, overflow, "Overflow science must carry forward")
|
||||
1
src/game/engine/tests/unit/test_tech_web.gd.uid
Normal file
1
src/game/engine/tests/unit/test_tech_web.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c3r835g0aik5w
|
||||
222
src/game/engine/tests/unit/test_victory_manager.gd
Normal file
222
src/game/engine/tests/unit/test_victory_manager.gd
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
extends GutTest
|
||||
## VictoryManager unit tests.
|
||||
## Verifies score calculation, domination detection, and score-leader logic.
|
||||
|
||||
const VictoryManagerScript: GDScript = preload(
|
||||
"res://engine/src/modules/victory/victory_manager.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 _vm: VictoryManagerScript = null
|
||||
var _game_map: GameMapScript = null
|
||||
|
||||
|
||||
func before_all() -> void:
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
_vm = VictoryManagerScript.new()
|
||||
_game_map = GameMapScript.new()
|
||||
_game_map.initialize(10, 10, 0)
|
||||
for col: int in range(10):
|
||||
for row: int in range(10):
|
||||
var tile: TileScript = TileScript.new()
|
||||
tile.position = Vector2i(col, row)
|
||||
tile.biome_id = "plains"
|
||||
_game_map.set_tile(tile.position, tile)
|
||||
|
||||
GameState.players = []
|
||||
GameState.game_settings = {
|
||||
"victory_domination": true,
|
||||
"victory_score": true,
|
||||
"turn_limit_enabled": false,
|
||||
"turn_limit": 300,
|
||||
}
|
||||
GameState.turn_number = 1
|
||||
|
||||
|
||||
func after_each() -> void:
|
||||
GameState.players = []
|
||||
GameState.turn_number = 1
|
||||
|
||||
|
||||
func _make_player(idx: int) -> PlayerScript:
|
||||
var player: PlayerScript = PlayerScript.new()
|
||||
player.index = idx
|
||||
player.player_name = "Player %d" % idx
|
||||
player.race_id = "dwarf"
|
||||
player.is_player_controlled = (idx == 0)
|
||||
player.researched_techs = []
|
||||
return player
|
||||
|
||||
|
||||
func _make_city(owner_idx: int, pos: Vector2i, pop: int) -> CityScript:
|
||||
var city: CityScript = CityScript.new()
|
||||
city.id = "%d_%d_%d" % [owner_idx, pos.x, pos.y]
|
||||
city.city_name = "City_%d" % owner_idx
|
||||
city.owner = owner_idx
|
||||
city.position = pos
|
||||
city.population = pop
|
||||
city.owned_tiles = [pos]
|
||||
return city
|
||||
|
||||
|
||||
func _make_unit(owner_idx: int, pos: Vector2i) -> UnitScript:
|
||||
var unit: UnitScript = UnitScript.new()
|
||||
unit.id = "u_%d_%d_%d" % [owner_idx, pos.x, pos.y]
|
||||
unit.type_id = "dwarf_warrior"
|
||||
unit.owner = owner_idx
|
||||
unit.position = pos
|
||||
return unit
|
||||
|
||||
|
||||
func test_score_positive_for_player_with_cities_and_units() -> void:
|
||||
## get_score() must return a positive value for a player with cities + units.
|
||||
var p: PlayerScript = _make_player(0)
|
||||
var city: CityScript = _make_city(0, Vector2i(2, 2), 3)
|
||||
p.cities = [city]
|
||||
var unit: UnitScript = _make_unit(0, Vector2i(1, 1))
|
||||
p.units = [unit]
|
||||
p.researched_techs = ["stonecutting"]
|
||||
GameState.players = [p]
|
||||
|
||||
var score: int = _vm.get_score(0)
|
||||
assert_gt(score, 0, "Score must be positive for player with cities, units, and techs")
|
||||
|
||||
## Verify formula: cities*10 + pop*2 + techs*5 + units*1
|
||||
var expected: int = (
|
||||
1 * VictoryManagerScript.SCORE_CITY
|
||||
+ 3 * VictoryManagerScript.SCORE_POP
|
||||
+ 1 * VictoryManagerScript.SCORE_TECH
|
||||
+ 1 * VictoryManagerScript.SCORE_UNIT
|
||||
)
|
||||
assert_eq(score, expected, "Score must match formula: %d" % expected)
|
||||
|
||||
|
||||
func test_score_zero_for_empty_player() -> void:
|
||||
## A player with no cities, units, or techs must score 0.
|
||||
var p: PlayerScript = _make_player(0)
|
||||
GameState.players = [p]
|
||||
|
||||
var score: int = _vm.get_score(0)
|
||||
assert_eq(score, 0, "Empty player must score 0")
|
||||
|
||||
|
||||
func test_check_victory_returns_empty_when_game_ongoing() -> void:
|
||||
## With two active players both holding cities, no victory condition is met.
|
||||
var p0: PlayerScript = _make_player(0)
|
||||
var p1: PlayerScript = _make_player(1)
|
||||
p0.cities = [_make_city(0, Vector2i(1, 1), 2)]
|
||||
p1.cities = [_make_city(1, Vector2i(5, 5), 2)]
|
||||
GameState.players = [p0, p1]
|
||||
|
||||
var result: String = _vm.check_victory(0, _game_map)
|
||||
assert_eq(result, "", "No victory condition should be met during ongoing game")
|
||||
|
||||
|
||||
func test_domination_all_cities() -> void:
|
||||
## Domination: player 0 controls all cities (player 1 has none left).
|
||||
var p0: PlayerScript = _make_player(0)
|
||||
var p1: PlayerScript = _make_player(1)
|
||||
p0.cities = [_make_city(0, Vector2i(2, 2), 5)]
|
||||
p1.cities = [] ## All cities captured
|
||||
GameState.players = [p0, p1]
|
||||
|
||||
var result: String = _vm.check_victory(0, _game_map)
|
||||
assert_eq(result, "domination", "Player controlling all cities must trigger domination")
|
||||
|
||||
|
||||
func test_domination_land_majority() -> void:
|
||||
## Domination: player 0 owns > 80% of all land tiles.
|
||||
var p0: PlayerScript = _make_player(0)
|
||||
var p1: PlayerScript = _make_player(1)
|
||||
p0.cities = [_make_city(0, Vector2i(0, 0), 1)]
|
||||
p1.cities = [_make_city(1, Vector2i(9, 9), 1)]
|
||||
GameState.players = [p0, p1]
|
||||
|
||||
## Assign > 80% of tiles to player 0.
|
||||
var total_tiles: int = _game_map.tiles.size()
|
||||
var threshold_count: int = int(total_tiles * 0.85) ## well above 80%
|
||||
var assigned: int = 0
|
||||
for tile: Variant in _game_map.tiles.values():
|
||||
if assigned < threshold_count:
|
||||
tile.owner = 0
|
||||
assigned += 1
|
||||
else:
|
||||
tile.owner = 1
|
||||
|
||||
var result: String = _vm.check_victory(0, _game_map)
|
||||
assert_eq(result, "domination", "Player owning >80% land must trigger domination")
|
||||
|
||||
|
||||
func test_no_domination_below_threshold() -> void:
|
||||
## Owning exactly 50% of tiles must NOT trigger domination.
|
||||
var p0: PlayerScript = _make_player(0)
|
||||
var p1: PlayerScript = _make_player(1)
|
||||
p0.cities = [_make_city(0, Vector2i(0, 0), 1)]
|
||||
p1.cities = [_make_city(1, Vector2i(9, 9), 1)]
|
||||
GameState.players = [p0, p1]
|
||||
|
||||
var total_tiles: int = _game_map.tiles.size()
|
||||
var half: int = total_tiles / 2
|
||||
var assigned: int = 0
|
||||
for tile: Variant in _game_map.tiles.values():
|
||||
if assigned < half:
|
||||
tile.owner = 0
|
||||
else:
|
||||
tile.owner = 1
|
||||
assigned += 1
|
||||
|
||||
var result: String = _vm.check_victory(0, _game_map)
|
||||
assert_eq(result, "", "50% land ownership must not trigger domination")
|
||||
|
||||
|
||||
func test_score_leader_wins_at_turn_limit() -> void:
|
||||
## When turn limit is reached, the highest-scoring player wins via "score".
|
||||
var p0: PlayerScript = _make_player(0)
|
||||
var p1: PlayerScript = _make_player(1)
|
||||
p0.cities = [_make_city(0, Vector2i(1, 1), 10)]
|
||||
p0.researched_techs = ["stonecutting", "masonry"]
|
||||
p0.units = [_make_unit(0, Vector2i(2, 2))]
|
||||
|
||||
p1.cities = [_make_city(1, Vector2i(5, 5), 1)]
|
||||
p1.researched_techs = []
|
||||
p1.units = []
|
||||
|
||||
GameState.players = [p0, p1]
|
||||
GameState.game_settings["turn_limit_enabled"] = true
|
||||
GameState.game_settings["turn_limit"] = 100
|
||||
GameState.turn_number = 101 ## past the limit
|
||||
|
||||
var result_p0: String = _vm.check_victory(0, _game_map)
|
||||
assert_eq(result_p0, "score", "Score leader must win when turn limit is exceeded")
|
||||
|
||||
var result_p1: String = _vm.check_victory(1, _game_map)
|
||||
assert_eq(result_p1, "", "Non-leader must not win via score")
|
||||
|
||||
|
||||
func test_get_scores_multiple_players() -> void:
|
||||
## get_scores() must return a dictionary keyed by player index.
|
||||
var p0: PlayerScript = _make_player(0)
|
||||
var p1: PlayerScript = _make_player(1)
|
||||
p0.cities = [_make_city(0, Vector2i(1, 1), 5)]
|
||||
p0.units = [_make_unit(0, Vector2i(2, 2))]
|
||||
p1.cities = [_make_city(1, Vector2i(5, 5), 2)]
|
||||
GameState.players = [p0, p1]
|
||||
|
||||
var scores: Dictionary = _vm.get_scores()
|
||||
assert_has(scores, 0, "Scores dict must have key for player 0")
|
||||
assert_has(scores, 1, "Scores dict must have key for player 1")
|
||||
assert_gt(scores[0], scores[1], "Player 0 must outscore player 1")
|
||||
|
||||
|
||||
func test_score_invalid_player_returns_zero() -> void:
|
||||
## get_score() for a non-existent player index must return 0.
|
||||
GameState.players = []
|
||||
var score: int = _vm.get_score(99)
|
||||
assert_eq(score, 0, "Score for non-existent player must be 0")
|
||||
1
src/game/engine/tests/unit/test_victory_manager.gd.uid
Normal file
1
src/game/engine/tests/unit/test_victory_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://vbi0o87pegfy
|
||||
Loading…
Add table
Reference in a new issue