ajhahn.de
← Theria
GDScript 241 lines
extends GutTest
## Geometry contracts for the 3v3 arena. These pin the map's mirror-fairness across the y = x
## axis and the lane orientation the creep layer depends on. Pure data checks — no engine or
## render coupling, the same discipline as the sim-core tests.


func test_mirror_swaps_components_and_is_its_own_inverse() -> void:
	var p := Vector2(120.0, -340.0)
	assert_eq(MapData.mirror(p), Vector2(-340.0, 120.0), "the mirror reflects across y = x")
	assert_eq(MapData.mirror(MapData.mirror(p)), p, "mirroring twice returns the original point")


func test_nexuses_are_axially_symmetric_and_in_bounds() -> void:
	var n0 := MapData.nexus_for_team(0)
	var n1 := MapData.nexus_for_team(1)
	assert_eq(n1, MapData.mirror(n0), "team 1's nexus must be team 0's nexus mirrored across y = x")
	assert_eq(MapData.clamp_to_bounds(n0), n0, "nexus 0 must sit inside the bounds")
	assert_eq(MapData.clamp_to_bounds(n1), n1, "nexus 1 must sit inside the bounds")


func test_team_fountains_sit_at_base_and_mirror_across_the_axis() -> void:
	var f0 := MapData.spawn_for_team(0)
	var f1 := MapData.spawn_for_team(1)
	assert_eq(f1, MapData.mirror(f0), "team 1's fountain must be team 0's fountain mirrored")
	assert_eq(MapData.clamp_to_bounds(f0), f0, "the fountain must sit inside the bounds")
	# It is at base: closer to its own nexus than to the map centre.
	var nexus := MapData.nexus_for_team(0)
	assert_true(
		f0.distance_to(nexus) < f0.length(),
		"a fountain spawns at its base, not out near the centre",
	)


func test_lane_path_orients_from_each_team_nexus_to_the_enemy_nexus() -> void:
	for lane in MapData.lane_count():
		var team0 := MapData.lane_path(lane, 0)
		var team1 := MapData.lane_path(lane, 1)
		assert_eq(team0[0], MapData.nexus_for_team(0), "team 0 starts at its own nexus")
		assert_eq(team0[team0.size() - 1], MapData.nexus_for_team(1), "team 0 ends at the enemy nexus")
		assert_eq(team1[0], MapData.nexus_for_team(1), "team 1 starts at its own nexus")
		assert_eq(team1[team1.size() - 1], MapData.nexus_for_team(0), "team 1 ends at the enemy nexus")


func test_lane_path_for_each_team_is_the_reverse_of_the_other() -> void:
	for lane in MapData.lane_count():
		var forward := MapData.lane_path(lane, 0)
		var reversed := MapData.lane_path(lane, 1)
		reversed.reverse()
		assert_eq(forward, reversed, "a corridor is one path walked in opposite directions")


func test_lane_path_returns_a_fresh_copy() -> void:
	var first := MapData.lane_path(0, 0)
	first.reverse()
	var second := MapData.lane_path(0, 0)
	assert_ne(first, second, "mutating a returned path must not alter the stored geometry")


func test_each_lane_is_its_own_axial_reflection() -> void:
	# Mirror-and-reverse must map each corridor onto itself, so the two teams walk a lane the
	# same way and neither has a shorter or safer route.
	for lane in MapData.lane_count():
		var path := MapData.lane_path(lane, 0)
		var mirrored := PackedVector2Array()
		for i in range(path.size() - 1, -1, -1):
			mirrored.append(MapData.mirror(path[i]))
		assert_eq(mirrored, path, "each lane must be its own reflection across the y = x axis")


func test_jungle_camps_are_closed_under_the_axis_mirror_and_in_bounds() -> void:
	var camps := MapData.JUNGLE_CAMPS
	for camp in camps:
		assert_eq(MapData.clamp_to_bounds(camp), camp, "every camp must sit inside the bounds")
		assert_true(camps.has(MapData.mirror(camp)), "every camp must have a mirror partner across y = x")


func test_two_jungle_camps_are_neutral_on_the_axis() -> void:
	# The camps sitting on the y = x axis are their own mirror — the shared, team-neutral camps.
	# The map has exactly two; the rest are off-axis mirror pairs, one per side.
	var neutral := 0
	for camp in MapData.JUNGLE_CAMPS:
		if is_equal_approx(camp.x, camp.y):
			neutral += 1
	assert_eq(neutral, 2, "exactly two jungle camps are neutral (on the y = x axis)")


func test_tower_positions_are_axially_symmetric_between_teams() -> void:
	var team0 := MapData.tower_positions(0)
	var team1 := MapData.tower_positions(1)
	assert_eq(team0.size(), team1.size(), "both teams field the same number of towers")
	for i in team0.size():
		assert_eq(team1[i], MapData.mirror(team0[i]), "team 1's towers must be team 0's mirrored")


func test_tower_positions_are_in_bounds() -> void:
	for team in MapData.NEXUS_POSITIONS.size():
		for tower in MapData.tower_positions(team):
			assert_eq(MapData.clamp_to_bounds(tower), tower, "every tower must sit inside the bounds")


func test_tower_positions_returns_a_fresh_copy() -> void:
	var first := MapData.tower_positions(0)
	first[0] = Vector2.ZERO
	var second := MapData.tower_positions(0)
	assert_ne(first[0], second[0], "mutating a returned tower list must not alter the stored slots")


func test_squad_spawn_of_one_falls_back_to_the_fountain() -> void:
	for team in MapData.NEXUS_POSITIONS.size():
		assert_eq(
			MapData.squad_spawn(team, 0, 1),
			MapData.spawn_for_team(team),
			"a squad of one spawns on the bare fountain",
		)


func test_squad_spawn_fans_a_team_into_distinct_in_bounds_seats() -> void:
	var count := 3
	for team in MapData.NEXUS_POSITIONS.size():
		var seen: Array[Vector2] = []
		for i in count:
			var seat := MapData.squad_spawn(team, i, count)
			assert_eq(MapData.clamp_to_bounds(seat), seat, "every squad seat sits inside the bounds")
			assert_false(seen.has(seat), "squadmates spawn on distinct points, not stacked")
			seen.append(seat)


func test_squad_spawn_is_axially_symmetric_between_teams() -> void:
	var count := 3
	for i in count:
		assert_eq(
			MapData.squad_spawn(1, i, count),
			MapData.mirror(MapData.squad_spawn(0, i, count)),
			"team 1's squad seat must be team 0's mirrored, so neither side has an edge",
		)


func test_squad_spawn_fan_is_centred_on_the_fountain() -> void:
	# The fan is symmetric about the fountain, so the seats average back to it.
	var count := 3
	var sum := Vector2.ZERO
	for i in count:
		sum += MapData.squad_spawn(0, i, count)
	var centre := sum / float(count)
	assert_almost_eq(
		centre, MapData.spawn_for_team(0), Vector2(0.01, 0.01), "the squad fan centres on the fountain"
	)


func test_river_is_axially_symmetric_and_in_bounds() -> void:
	# Mirror-and-reverse must map the river onto itself, so it divides the map evenly and
	# neither team has more water in its half.
	var river := MapData.river_polyline()
	var mirrored := PackedVector2Array()
	for i in range(river.size() - 1, -1, -1):
		mirrored.append(MapData.mirror(river[i]))
	assert_eq(mirrored, river, "the river must be its own reflection across the y = x axis")
	for point in river:
		assert_eq(MapData.clamp_to_bounds(point), point, "every river point sits inside the bounds")


func test_river_polyline_returns_a_fresh_copy() -> void:
	var first := MapData.river_polyline()
	first.reverse()
	var second := MapData.river_polyline()
	assert_ne(first, second, "mutating the returned river must not alter the stored course")


func test_each_team_holds_four_towers() -> void:
	# Two towers ring the nexus and two stand forward down the lanes — four a side, mirrored.
	assert_eq(MapData.tower_positions(0).size(), 4, "each team fields four towers")
	assert_eq(MapData.tower_positions(1).size(), 4, "both teams field the same count")


# --- collision obstacles ---------------------------------------------------------------------


func test_obstacles_are_mirror_symmetric() -> void:
	var obs := MapData.obstacles()
	assert_gt(obs.size(), 0, "the map has collision obstacles")
	for o in obs:
		var mirrored: Vector2 = MapData.mirror(o["center"])
		var found := false
		for q in obs:
			if q["center"].distance_to(mirrored) < 0.01 and absf(q["radius"] - o["radius"]) < 0.01:
				found = true
				break
		assert_true(found, "every obstacle has its y = x mirror in the set — collision is team-fair")


func test_vision_blockers_are_the_walls_only_and_mirror_symmetric() -> void:
	# Sight is blocked by terrain, not buildings: the vision blockers are the jungle walls — every
	# one a WALL_RADIUS circle on a wall point — and never a tower or the nexus.
	var blockers := MapData.vision_blockers()
	assert_eq(blockers.size(), MapData.jungle_wall_points().size(), "one sight blocker per wall point")
	for b in blockers:
		assert_eq(b["radius"], MapData.WALL_RADIUS, "a sight blocker is a wall-footprint circle")
	for team in MapData.NEXUS_POSITIONS.size():
		var structures := PackedVector2Array(MapData.tower_positions(team))
		structures.append(MapData.nexus_for_team(team))
		for s in structures:
			for b in blockers:
				assert_gt(b["center"].distance_to(s), 1.0, "a structure is not a sight blocker")
	for b in blockers:
		var mirrored: Vector2 = MapData.mirror(b["center"])
		var found := false
		for q in blockers:
			if q["center"].distance_to(mirrored) < 0.01:
				found = true
				break
		assert_true(found, "every sight blocker has its y = x mirror — vision is team-fair")


func test_spawns_and_camp_centres_are_walkable() -> void:
	for team in MapData.NEXUS_POSITIONS.size():
		assert_false(
			MapData.point_blocked(MapData.spawn_for_team(team), SimCore.UNIT_RADIUS),
			"a team spawns on free ground, never walled in",
		)
	for camp in MapData.JUNGLE_CAMPS:
		assert_false(
			MapData.point_blocked(camp, SimCore.UNIT_RADIUS),
			"a camp's interior stays clear of its own pocket rocks",
		)


func test_slide_pushes_a_unit_out_of_an_obstacle_and_leaves_free_ground_alone() -> void:
	var obs := MapData.obstacles()
	var center: Vector2 = obs[0]["center"]
	var radius: float = obs[0]["radius"]
	# Driven straight at the obstacle's centre, the unit is stopped outside it.
	var from := center + Vector2(radius + 500.0, 0.0)
	var resolved := MapData.slide(from, center, SimCore.UNIT_RADIUS)
	assert_gt(resolved.distance_to(center), radius, "slide keeps a unit out of the obstacle it enters")
	# A move that ends on open ground (the map centre is clear) is returned untouched.
	assert_eq(
		MapData.slide(Vector2.ZERO, Vector2(5.0, 0.0), SimCore.UNIT_RADIUS),
		Vector2(5.0, 0.0),
		"slide leaves a clear destination unchanged",
	)