extends GutTest ## Deterministic checks on the authoritative simulation core. These run headless ## and stay free of any engine/render coupling — they exercise the exact step ## function the live client and (later) the netcode drive. func test_constant_input_advances_position_deterministically() -> void: var sim := SimCore.new() sim.spawn_creeps = false # isolate the movement assertion from the wave schedule var id := sim.add_entity(0, Vector2.ZERO, 300.0) var command := InputCommand.new() command.move_dir = Vector2.RIGHT var inputs := {id: command} for _i in SimCore.TICK_RATE: sim.step(inputs) var entity := sim.state.get_entity(id) # 60 ticks * (1/60 s) * 300 u/s = 300 units along +x. assert_almost_eq(entity.position.x, 300.0, 0.0001) assert_almost_eq(entity.position.y, 0.0, 0.0001) assert_eq(sim.state.tick, 60) func test_identical_input_replays_identically() -> void: var a := _run_scripted(120) var b := _run_scripted(120) assert_eq(a, b, "the simulation must be a pure function of state + input") func test_diagonal_input_is_not_faster() -> void: var sim := SimCore.new() sim.spawn_creeps = false var speed := 300.0 var id := sim.add_entity(0, Vector2.ZERO, speed) var command := InputCommand.new() command.move_dir = Vector2.ONE # length sqrt(2) -> must clamp to 1 sim.step({id: command}) var moved := sim.state.get_entity(id).position.length() assert_almost_eq(moved, speed * SimCore.TICK_DELTA, 0.0001) func test_entity_without_command_holds_still() -> void: var sim := SimCore.new() sim.spawn_creeps = false var id := sim.add_entity(0, Vector2(10.0, -5.0), 300.0) sim.step({}) assert_eq(sim.state.get_entity(id).position, Vector2(10.0, -5.0)) func _run_scripted(ticks: int) -> Vector2: var sim := SimCore.new() sim.spawn_creeps = false var id := sim.add_entity(0, Vector2.ZERO, 250.0) var command := InputCommand.new() for i in ticks: command.move_dir = Vector2(sin(float(i)), cos(float(i))) sim.step({id: command}) return sim.state.get_entity(id).position # --- Combat: towers, structures, and the win condition ---------------------- func test_structure_strikes_an_enemy_in_range() -> void: var sim := SimCore.new() sim.spawn_creeps = false sim.add_structure(0, Vector2.ZERO, 1000, 50, 200.0, 60) var enemy := sim.add_entity(1, Vector2(100.0, 0.0), 0.0, 600) sim.step({}) assert_eq(sim.state.get_entity(enemy).hp, 550, "an in-range enemy takes attack_damage") func test_structure_ignores_an_enemy_out_of_range() -> void: var sim := SimCore.new() sim.spawn_creeps = false sim.add_structure(0, Vector2.ZERO, 1000, 50, 200.0, 60) var enemy := sim.add_entity(1, Vector2(300.0, 0.0), 0.0, 600) sim.step({}) assert_eq(sim.state.get_entity(enemy).hp, 600, "an out-of-range enemy is untouched") func test_structure_does_not_strike_an_ally() -> void: var sim := SimCore.new() sim.spawn_creeps = false sim.add_structure(0, Vector2.ZERO, 1000, 50, 200.0, 60) var ally := sim.add_entity(0, Vector2(100.0, 0.0), 0.0, 600) sim.step({}) assert_eq(sim.state.get_entity(ally).hp, 600, "an attacker never hits its own team") func test_attack_respects_its_cooldown() -> void: var sim := SimCore.new() sim.spawn_creeps = false sim.add_structure(0, Vector2.ZERO, 1000, 50, 200.0, 60) var enemy := sim.add_entity(1, Vector2(100.0, 0.0), 0.0, 600) for _i in 60: sim.step({}) assert_eq(sim.state.get_entity(enemy).hp, 550, "one hit lands across a full cooldown window") sim.step({}) assert_eq(sim.state.get_entity(enemy).hp, 500, "the cooldown elapses next tick, second hit lands") func test_an_entity_dies_when_its_hp_reaches_zero() -> void: var sim := SimCore.new() sim.spawn_creeps = false sim.add_structure(0, Vector2.ZERO, 1000, 100, 200.0, 60) var enemy := sim.add_entity(1, Vector2(100.0, 0.0), 0.0, 100) sim.step({}) assert_null(sim.state.get_entity(enemy), "an entity at 0 hp is removed from the world") # --- Hero death & respawn --------------------------------------------------- func test_a_slain_hero_is_downed_not_erased() -> void: # Unlike a creep, a dead hero is kept in the world and put on the respawn clock, so its id and # countdown persist for the client's death screen and the revive step. var sim := SimCore.new() sim.spawn_creeps = false var hero := sim.add_hero(0, MapData.spawn_for_team(0), 320.0) sim.state.get_entity(hero).hp = 0 sim.step({}) var downed := sim.state.get_entity(hero) assert_not_null(downed, "a slain hero stays in the world rather than being erased") assert_true(downed.is_dead(), "the slain hero is marked dead") assert_eq(downed.respawn_ticks, SimCore.HERO_RESPAWN_TICKS, "its respawn clock is started") assert_eq(downed.hp, 0, "a downed hero sits at 0 hp") func test_a_downed_hero_respawns_full_at_its_spawn_point() -> void: var sim := SimCore.new() sim.spawn_creeps = false var spawn := MapData.spawn_for_team(0) var hero := sim.add_hero(0, spawn, 320.0) # Walk the hero off its spawn so the respawn-in-place is observable, then kill it. sim.state.get_entity(hero).position = spawn + Vector2(500.0, 0.0) sim.state.get_entity(hero).hp = 0 sim.step({}) # downs the hero, starting the HERO_RESPAWN_TICKS countdown for _i in SimCore.HERO_RESPAWN_TICKS - 1: sim.step({}) assert_true(sim.state.get_entity(hero).is_dead(), "the hero stays down until the timer elapses") sim.step({}) # the tick the timer reaches 0 var revived := sim.state.get_entity(hero) assert_false(revived.is_dead(), "the hero is alive once the timer elapses") assert_eq(revived.hp, SimCore.HERO_HP, "it returns at full health") assert_eq(revived.position, spawn, "it returns at its spawn point") func test_a_downed_hero_is_inert_and_untargetable() -> void: var sim := SimCore.new() sim.spawn_creeps = false var tower := sim.add_structure(1, Vector2.ZERO, 1000, 100, 300.0, 60) var hero := sim.add_hero(0, Vector2(100.0, 0.0), 320.0) sim.state.get_entity(hero).hp = 0 sim.step({}) # downs the hero assert_true(sim.state.get_entity(hero).is_dead()) var down_pos := sim.state.get_entity(hero).position # Untargetable: the only enemy in the tower's range is the corpse, so it finds nothing to hit. assert_null( sim._nearest_enemy_in_range(sim.state.get_entity(tower)), "a downed hero is not a valid attack target", ) # Inert: a move command on a downed hero is ignored — it holds where it fell. var command := InputCommand.new() command.move_dir = Vector2.RIGHT sim.step({hero: command}) assert_eq(sim.state.get_entity(hero).position, down_pos, "a downed hero does not move") func test_nexus_destruction_sets_the_winner_and_freezes_the_match() -> void: var sim := SimCore.new() sim.spawn_creeps = false sim.add_structure(0, Vector2.ZERO, 100, 0, 0.0, 0, true) # team 0 nexus # A team 1 attacker in range (a stand-in for the creeps that arrive next). sim.add_structure(1, Vector2(100.0, 0.0), 1000, 100, 200.0, 60) sim.step({}) assert_true(sim.state.is_match_over(), "a destroyed nexus ends the match") assert_eq(sim.state.winner, 1, "the other team wins") var frozen_tick := sim.state.tick sim.step({}) assert_eq(sim.state.tick, frozen_tick, "the simulation no-ops once the match is over") func test_spawn_structures_is_mirror_fair() -> void: var sim := SimCore.new() sim.spawn_structures() # Every team 0 structure must have a team 1 structure at the axially mirrored position # with the same role and health, so neither side starts ahead. for id in sim.state.entities: var s: SimEntity = sim.state.entities[id] if s.team != 0: continue var mirror := _structure_at(sim.state, 1, MapData.mirror(s.position)) assert_not_null(mirror, "team 0's structure must have a mirrored team 1 counterpart") if mirror != null: assert_eq(mirror.is_nexus, s.is_nexus, "the mirrored structure must share its role") assert_eq(mirror.max_hp, s.max_hp, "the mirrored structure must share its health") func test_spawn_structures_gives_each_team_a_nexus_and_four_towers() -> void: # A team's defences: one destructible nexus plus four towers — two ringing the nexus and # two forward down the lanes. var sim := SimCore.new() sim.spawn_structures() for team in MapData.NEXUS_POSITIONS.size(): var nexuses := 0 var towers := 0 for id in sim.state.entities: var s: SimEntity = sim.state.entities[id] if not s.is_structure or s.team != team: continue if s.is_nexus: nexuses += 1 else: towers += 1 assert_eq(nexuses, 1, "a team has exactly one nexus") assert_eq(towers, 4, "a team fields four towers — two guarding the nexus, two forward") func _structure_at(state: SimState, team: int, position: Vector2) -> SimEntity: for id in state.entities: var s: SimEntity = state.entities[id] if s.team == team and s.is_structure and s.position.is_equal_approx(position): return s return null func test_a_combat_run_replays_identically() -> void: var a := _run_combat() var b := _run_combat() assert_eq(a, b, "combat must be a pure function of state + input") func _run_combat() -> Array: var sim := SimCore.new() sim.spawn_structures() var hero := sim.add_hero(0, MapData.spawn_for_team(0), 320.0) var bot := sim.add_hero(1, MapData.spawn_for_team(1), 300.0) var march := InputCommand.new() march.move_dir = Vector2(1.0, -1.0) # walk both units toward the enemy base for _i in 600: sim.step({hero: march, bot: march}) return _snapshot(sim.state) ## A deterministic, comparable digest of the world: every surviving entity's id, ## hp, and rounded position, ordered by id. func _snapshot(state: SimState) -> Array: var ids := state.entities.keys() ids.sort() var rows: Array = [] for id in ids: var entity: SimEntity = state.entities[id] rows.append([id, entity.hp, entity.position.round()]) return rows # --- Creeps: lane marching, contact combat, and the wave schedule ----------- func test_a_creep_marches_its_lane_toward_the_enemy_nexus() -> void: var sim := SimCore.new() sim.spawn_creeps = false var path := MapData.lane_path(0, 0) var creep := sim.add_creep(0, 0, path[0]) var start := sim.state.get_entity(creep).position for _i in SimCore.TICK_RATE: sim.step({}) var here := sim.state.get_entity(creep).position assert_true(here.distance_to(start) > 0.0, "a creep with a clear lane keeps moving") assert_true( here.distance_to(path[1]) < start.distance_to(path[1]), "it advances toward its next waypoint", ) func test_a_creep_holds_position_to_fight_an_enemy_in_range() -> void: var sim := SimCore.new() sim.spawn_creeps = false var spawn := MapData.lane_path(0, 0)[0] var creep := sim.add_creep(0, 0, spawn) # An enemy parked just inside the creep's reach: the creep must stop to fight. var enemy := sim.add_entity(1, spawn + Vector2(SimCore.CREEP_RANGE - 10.0, 0.0), 0.0, 600) sim.step({}) assert_eq( sim.state.get_entity(creep).position, spawn, "a creep with an enemy in range holds to fight", ) assert_eq( sim.state.get_entity(enemy).hp, 600 - SimCore.CREEP_DAMAGE, "and strikes it through the shared combat primitive", ) func test_an_unopposed_creep_destroys_the_enemy_nexus_and_wins() -> void: var sim := SimCore.new() sim.spawn_creeps = false # A team-1 nexus weak enough to fall to two creep hits, and a lone team-0 creep # already in range — the win condition driven entirely by a creep. var nexus := sim.add_structure(1, Vector2.ZERO, SimCore.CREEP_DAMAGE * 2, 0, 0.0, 0, true) sim.add_creep(0, 0, Vector2(SimCore.CREEP_RANGE - 10.0, 0.0)) for _i in SimCore.CREEP_COOLDOWN_TICKS + 2: sim.step({}) assert_null(sim.state.get_entity(nexus), "the creep's strikes destroy the enemy nexus") assert_true(sim.state.is_match_over(), "felling the nexus ends the match") assert_eq(sim.state.winner, 0, "the creep's team wins") func test_creep_waves_spawn_on_the_wave_schedule() -> void: var sim := SimCore.new() # spawn_creeps defaults on var per_wave := SimCore.CREEP_PER_WAVE * MapData.lane_count() * MapData.NEXUS_POSITIONS.size() sim.step({}) # tick 0 -> the opening wave assert_eq( _count_creeps(sim.state), per_wave, "a full wave spawns for both teams on every lane at tick 0", ) # Clear the wave so the two teams' creeps can't clash and confound the count, # leaving the schedule the only thing that adds creeps. for id in sim.state.entities.keys(): if sim.state.entities[id].is_creep: sim.state.entities.erase(id) for _i in SimCore.CREEP_WAVE_INTERVAL_TICKS - 1: sim.step({}) assert_eq(_count_creeps(sim.state), 0, "no wave spawns between intervals") sim.step({}) # the next interval boundary assert_eq(_count_creeps(sim.state), per_wave, "the next wave spawns on the interval") func test_creep_waves_are_mirror_fair() -> void: var sim := SimCore.new() sim.step({}) # spawn and advance the opening waves one tick for id in sim.state.entities: var creep: SimEntity = sim.state.entities[id] if not creep.is_creep or creep.team != 0: continue assert_not_null( _creep_at(sim.state, 1, MapData.mirror(creep.position)), "every team-0 creep has a team-1 creep mirrored across the y = x axis", ) # --- Heroes: the player/bot combat unit ------------------------------------- func test_a_hero_strikes_an_enemy_in_range() -> void: var sim := SimCore.new() sim.spawn_creeps = false var hero := sim.add_hero(0, Vector2.ZERO, 320.0) var enemy := sim.add_entity(1, Vector2(SimCore.HERO_RANGE - 10.0, 0.0), 0.0, 600) sim.step({}) assert_eq( sim.state.get_entity(enemy).hp, 600 - SimCore.HERO_DAMAGE, "a hero auto-attacks an enemy in range through the shared combat primitive", ) # A hero out-hits a creep: its damage exceeds a creep's, so it clears waves. assert_true(SimCore.HERO_DAMAGE > SimCore.CREEP_DAMAGE, "a hero out-damages a creep") func _count_creeps(state: SimState) -> int: var n := 0 for id in state.entities: if state.entities[id].is_creep: n += 1 return n func _creep_at(state: SimState, team: int, position: Vector2) -> SimEntity: for id in state.entities: var creep: SimEntity = state.entities[id] if creep.is_creep and creep.team == team and creep.position.is_equal_approx(position): return creep return null