Files
transcience/tests/unit/test_map_grid.gd
T
claudeandClaude Opus 5 ef28970654 Close three holes an unmodified client could walk through
One account is now one session. Two peers signed in as the same account
each got their own player built from the SAME character record, so both
held a copy of one bag and whichever wrote last won: drop the single
potion from both copies and two potions hit the floor. Reconciling two
live mirrors of one record cannot be made to work, so the handshake
refuses the second connection -- the newcomer, not the incumbent, or
claiming an account would be a way to kick anyone off the server.

A muzzle offset is a teleport and the bullet pool never sees it: it
samples geometry once per tick. Forward that is 19px, but Doubleshot
adds up to 45px sideways, wider than a 32px wall plus the 6px hitbox
holding you off it. Six Doubleshots, stand flush against a wall, fire
along it, and live shots came out the far side. Shots now start from the
shooter when geometry stands in the way, which keeps the count honest.

Bullet-spawn events carry the firing actor's exact position, and they
were filtered by a flat 2900-unit radius against an 800-unit actor cull.
A depth-1 map's diagonal is about 2521, so that radius culled nothing:
every enemy that fired was pinpointed for every player, through fog, on
terrain the client was deliberately not streamed. The test is now
per bullet -- can THIS one reach ground you can see -- which keeps the
guarantee the wide radius existed for and drops the radar.

Found while fixing the above: a rejection never reached the client. The
RPC is queued and only leaves on the frame's flush, so kicking in the
same call threw the reason away, and _on_server_disconnected overwrote
last_error anyway. Both were true of protocol mismatch too, which is the
one message a player most needs. Smoke asserts the reason arrives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 17:04:05 +02:00

135 lines
5.6 KiB
GDScript

extends GutTest
## The grid is the foundation for collision, bullets, fog and interest
## management, so its edge cases are worth pinning down directly rather than
## discovering them as strange behaviour three systems away.
var grid: MapGrid
func before_each() -> void:
# 10x10 room: solid border, open interior.
grid = MapGrid.new(10, 10, MapGrid.Kind.WALL)
grid.fill_rect(Rect2i(1, 1, 8, 8), MapGrid.Kind.FLOOR)
func test_out_of_bounds_reads_as_wall() -> void:
assert_eq(grid.at(-1, 5), MapGrid.Kind.WALL,
"the world has to be sealed without every caller bounds-checking")
assert_eq(grid.at(999, 999), MapGrid.Kind.WALL)
func test_tile_flags_are_independent() -> void:
# The whole reason for three flags rather than one "solid" bit.
assert_true(grid.BLOCKS_MOVE[MapGrid.Kind.PIT], "you cannot walk over a pit")
assert_false(grid.BLOCKS_BULLET[MapGrid.Kind.PIT], "but you can shoot over it")
assert_false(grid.BLOCKS_SIGHT[MapGrid.Kind.PIT], "and see over it")
assert_true(grid.BLOCKS_MOVE[MapGrid.Kind.BARRICADE])
assert_true(grid.BLOCKS_BULLET[MapGrid.Kind.BARRICADE])
assert_false(grid.BLOCKS_SIGHT[MapGrid.Kind.BARRICADE],
"chest height: you see over it but cannot shoot or walk through")
func test_world_and_tile_coordinates_round_trip() -> void:
assert_eq(grid.to_tile(Vector2(0.0, 0.0)), Vector2i(0, 0))
assert_eq(grid.to_tile(Vector2(MapGrid.TILE * 3.5, MapGrid.TILE * 2.5)), Vector2i(3, 2))
assert_eq(grid.tile_centre(3, 2), Vector2(MapGrid.TILE * 3.5, MapGrid.TILE * 2.5))
assert_eq(grid.world_size(), Vector2(10.0, 10.0) * MapGrid.TILE)
func test_negative_world_positions_map_outside_the_grid() -> void:
# floor(), not truncation -- int() would fold -0.5 onto tile 0 and let an
# actor stand half a tile outside the map.
assert_eq(grid.to_tile(Vector2(-1.0, -1.0)), Vector2i(-1, -1))
func test_circle_collision_against_a_wall() -> void:
var open := grid.tile_centre(4, 4)
assert_false(grid.circle_blocked(open, 6.0))
# Hard against the left wall: tile 0 is solid, so a circle at tile 1's
# centre minus most of a tile overlaps it.
assert_true(grid.circle_blocked(Vector2(MapGrid.TILE + 2.0, grid.tile_centre(1, 4).y), 6.0))
func test_sliding_along_a_wall_preserves_the_free_axis() -> void:
# Moving diagonally into the top wall should keep the horizontal motion.
var start := grid.tile_centre(4, 1)
var moved := grid.slide_circle(start, Vector2(8.0, -40.0), 6.0)
assert_almost_eq(moved.x, start.x + 8.0, 0.001, "x is unobstructed and must not be lost")
assert_lt(absf(moved.y - start.y), 40.0, "y is blocked by the wall")
func test_a_circle_cannot_be_pushed_through_a_wall() -> void:
var start := grid.tile_centre(4, 1)
for _i in 60:
start = grid.slide_circle(start, Vector2(0.0, -100.0), 6.0)
assert_gt(start.y, MapGrid.TILE, "no amount of shoving may cross a solid tile")
func test_bullets_stop_at_walls_but_cross_pits() -> void:
grid.set_tile(4, 4, MapGrid.Kind.PIT)
assert_false(grid.bullet_blocked(grid.tile_centre(4, 4)), "bullets fly over pits")
assert_true(grid.bullet_blocked(grid.tile_centre(0, 0)), "and stop at walls")
func test_line_of_sight_is_blocked_by_walls_and_not_by_pits() -> void:
var a := grid.tile_centre(1, 4)
var b := grid.tile_centre(8, 4)
assert_true(grid.has_line_of_sight(a, b), "clear floor between them")
grid.set_tile(4, 4, MapGrid.Kind.PIT)
assert_true(grid.has_line_of_sight(a, b), "a pit is a hole, not a screen")
grid.set_tile(4, 4, MapGrid.Kind.PILLAR)
assert_false(grid.has_line_of_sight(a, b), "a pillar blocks it")
grid.set_tile(4, 4, MapGrid.Kind.BARRICADE)
assert_true(grid.has_line_of_sight(a, b), "you can see over a barricade")
func test_line_of_sight_is_symmetric() -> void:
grid.set_tile(5, 4, MapGrid.Kind.WALL)
var a := grid.tile_centre(1, 4)
var b := grid.tile_centre(8, 4)
assert_eq(grid.has_line_of_sight(a, b), grid.has_line_of_sight(b, a),
"asymmetric sight would mean an enemy can shoot you from cover you " +
"cannot shoot back into")
func test_standing_inside_a_wall_can_still_see_out() -> void:
# Endpoints must not block, or an actor clipped into geometry goes blind
# and its aggro check silently fails.
var inside := grid.tile_centre(0, 0)
var outside := grid.tile_centre(1, 1)
assert_true(grid.has_line_of_sight(inside, outside))
## bullet_path_clear differs from has_line_of_sight in two ways that matter, and
## both are easy to get wrong: it consults BLOCKS_BULLET (so a barricade stops
## it and a pit does not), and the destination tile counts.
func test_a_bullet_path_is_blocked_by_what_stops_bullets_not_by_what_stops_sight() -> void:
var a := grid.tile_centre(1, 4)
var b := grid.tile_centre(8, 4)
assert_true(grid.bullet_path_clear(a, b), "clear floor between them")
grid.set_tile(4, 4, MapGrid.Kind.PIT)
assert_true(grid.bullet_path_clear(a, b), "bullets fly over a pit")
grid.set_tile(4, 4, MapGrid.Kind.BARRICADE)
assert_false(grid.bullet_path_clear(a, b),
"you can SEE over a barricade, which is exactly why sight is the wrong test")
assert_true(grid.has_line_of_sight(a, b), "and sight still passes it")
grid.set_tile(4, 4, MapGrid.Kind.WALL)
assert_false(grid.bullet_path_clear(a, b), "a wall stops both")
func test_a_bullet_path_ending_inside_a_wall_is_refused() -> void:
var from := grid.tile_centre(1, 4)
grid.set_tile(2, 4, MapGrid.Kind.WALL)
assert_false(grid.bullet_path_clear(from, grid.tile_centre(2, 4)),
"the destination counts: a muzzle inside a wall is the case this rules out")
# Sight deliberately does the opposite -- shooting at something embedded in
# a wall must still resolve -- so the two must not be collapsed into one.
assert_true(grid.has_line_of_sight(from, grid.tile_centre(2, 4)))