extends TestCase ## Tests of the bridge: GwSession is the Rust core, seen from GDScript. var key := PackedByteArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) func _joined_session() -> GwSession: var session := GwSession.new() session.start_new(7) session.join(key, "Ada") session.step() return session func _first_soft_rock(session: GwSession) -> Vector2i: var terrain := session.terrain_codes() var width := session.map_width() for index in terrain.size(): if terrain[index] == GwSession.TERRAIN_SOFT_ROCK: @warning_ignore("integer_division") return Vector2i(index % width, index / width) return Vector2i(-1, -1) ## Hits `tile` three times, waiting out the cooldown, and returns all events. func _dig_three_times(session: GwSession, tile: Vector2i) -> Array[Dictionary]: var events: Array[Dictionary] = [] for hit in 3: session.dig(tile.x, tile.y) events.append_array(session.step()) for wait in GwSession.DIG_COOLDOWN_TICKS: session.step() return events func test_a_session_without_a_world_is_harmless() -> void: var session := GwSession.new() check(not session.is_running(), "nothing runs yet") check_eq(session.tick(), 0, "tick") check_eq(session.step().size(), 0, "no events") check_eq(session.save().size(), 0, "nothing to save") check_eq(session.local_player(), -1, "no local player") func test_joining_makes_the_player_local() -> void: var session := GwSession.new() session.start_new(7) check(session.join(key, "Ada"), "join accepted") check_eq(session.local_player(), -1, "not before the tick") var events := session.step() check_eq(events.size(), 1, "one event") check_eq(events[0]["type"], "player_joined", "event type") check_eq(session.local_player(), 0, "local player after the tick") check_eq(session.tick(), 1, "one tick ran") func test_a_key_needs_sixteen_bytes() -> void: var session := GwSession.new() session.start_new(7) check(not session.join(PackedByteArray([1, 2, 3]), "Ada"), "short key refused") check(not session.last_error().is_empty(), "the reason is given") func test_the_map_can_be_read() -> void: var session := _joined_session() check_eq(session.map_width(), 64, "width") check_eq(session.map_height(), 64, "height") check_eq(session.terrain_codes().size(), 64 * 64, "one code per tile") check_eq(session.damage_codes().size(), 64 * 64, "one damage per tile") var spawn := session.spawn_point() check_eq(spawn, Vector2i(32, 32), "spawn in the centre") check(session.is_walkable(spawn.x, spawn.y), "the spawn can be walked on") check_eq(session.terrain_at(0, 0), GwSession.TERRAIN_BEDROCK, "the border is bedrock") check_eq(session.terrain_at(-1, 0), -1, "outside the map") check(not session.is_walkable(500, 500), "outside the map is not walkable") func test_three_hits_break_soft_rock_and_give_a_stone() -> void: var session := _joined_session() var rock := _first_soft_rock(session) check(rock.x >= 0, "the map has soft rock") var events := _dig_three_times(session, rock) var types := events.map(func(event: Dictionary) -> String: return event["type"]) check_eq(types, ["tile_hit", "tile_hit", "tile_changed", "item_gained"], "event order") check_eq(session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_GROUND, "the rock is gone") check_eq(session.item_count(GwSession.ITEM_STONE), 1, "one stone") func test_digging_before_joining_does_nothing() -> void: var session := GwSession.new() session.start_new(7) var rock := _first_soft_rock(session) session.dig(rock.x, rock.y) check_eq(session.step().size(), 0, "no events") check_eq(session.damage_codes()[rock.y * session.map_width() + rock.x], 0, "no damage") func test_a_save_keeps_the_world_and_the_player_comes_back() -> void: var session := _joined_session() _dig_three_times(session, _first_soft_rock(session)) var save := session.save() check(save.size() > 0, "a save was made") var loaded := GwSession.new() check(loaded.start_loaded(save), "the save loads") check_eq(loaded.tick(), session.tick(), "same tick") check_eq(loaded.terrain_codes(), session.terrain_codes(), "same map") check_eq(loaded.local_player(), -1, "nobody has joined the loaded world yet") loaded.join(key, "Ada") loaded.step() check_eq(loaded.local_player(), 0, "the player is the same") check_eq(loaded.item_count(GwSession.ITEM_STONE), 1, "the stone is still there") func test_a_damaged_save_is_refused_and_the_running_world_stays() -> void: var session := _joined_session() var hash_before := session.state_hash_hex() var save := session.save() var middle := save.size() >> 1 save[middle] = save[middle] ^ 0xFF check(not session.start_loaded(save), "a damaged save is refused") check(session.last_error().contains("checksum"), "the reason names the checksum: %s" % session.last_error()) check(not session.start_loaded(PackedByteArray([1, 2, 3])), "random bytes are refused") check_eq(session.state_hash_hex(), hash_before, "the running world is untouched") func test_the_recording_of_a_session_is_available() -> void: var session := _joined_session() session.step() var recording := session.recording_bytes() check(recording.size() > 0, "a recording exists") check_eq(recording.slice(0, 4), "GRWR".to_ascii_buffer(), "it starts with the recording marker") func test_the_state_hash_changes_with_every_tick() -> void: var session := _joined_session() var first := session.state_hash_hex() session.step() check_eq(first.length(), 16, "sixteen hexadecimal digits") check(session.state_hash_hex() != first, "the tick is part of the state") var guest_key := PackedByteArray([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) ## Carries every waiting message between `host` and `guest`, which is ## connected to the host as peer 5, until none are left. Returns the notices ## the guest got. func _carry(host: GwSession, guest: GwSession) -> Array[Dictionary]: var notices: Array[Dictionary] = [] var moved := true while moved: moved = false for message in guest.take_messages(): check_eq(message["peer"], GwSession.HOST_PEER, "a guest writes to its host") host.receive(5, message["bytes"]) moved = true for message in host.take_messages(): if message["peer"] == 5: notices.append_array(guest.receive(GwSession.HOST_PEER, message["bytes"])) moved = true return notices ## A host whose player Ada is in the world, and the guest Grace at peer 5, ## whose join has run. func _host_and_guest() -> Array[GwSession]: var host := _joined_session() host.connect_peer(5) var guest := GwSession.new() guest.start_guest(guest_key, "Grace") _carry(host, guest) host.step() _carry(host, guest) while guest.pending_ticks() > 0: guest.step() return [host, guest] func test_a_guest_follows_its_host_through_the_bridge() -> void: var host := _joined_session() host.connect_peer(5) var guest := GwSession.new() check(guest.start_guest(guest_key, "Grace"), "the guest starts") check(not guest.is_host(), "as a guest") check(not guest.is_running(), "without a world before the host answers") check_eq(_carry(host, guest), [{"type": "world_received", "resync": false}], "the host's world arrives") check_eq(guest.tick(), host.tick(), "at the host's tick") for tick in 25: host.step() _carry(host, guest) check_eq(guest.pending_ticks(), 25, "the ticks wait until the guest runs them") while guest.pending_ticks() > 0: guest.step() check_eq(guest.state_hash_hex(), host.state_hash_hex(), "both worlds are the same") check_eq(guest.local_player(), 1, "the guest is player 1") check_eq(host.player_of_peer(5), 1, "the host knows whose connection it is") check_eq(host.guest_count(), 1, "one guest") check_eq(guest.player_name(0), "Ada", "the guest knows the host's player") check_eq(guest.online_players(), PackedInt64Array([0, 1]), "two players online") check_eq(guest.save().size(), 0, "a guest never saves the host's world") check(guest.recording_bytes().size() > 0, "but it records its session") func test_the_hit_of_a_guest_becomes_a_message_to_the_host() -> void: var pair := _host_and_guest() var host := pair[0] var guest := pair[1] var rock := _first_soft_rock(host) guest.dig(rock.x, rock.y) _carry(host, guest) host.step() _carry(host, guest) var events: Array[Dictionary] = [] while guest.pending_ticks() > 0: events.append_array(guest.step()) check_eq(events.map(func(event: Dictionary) -> String: return event["type"]), ["tile_hit"], "the guest sees its hit") check_eq(host.damage_codes()[rock.y * host.map_width() + rock.x], 1, "and so does the host") func test_a_guest_learns_why_it_is_turned_away() -> void: var host := _joined_session() host.connect_peer(5) var twin := GwSession.new() twin.start_guest(key, "Ada") check_eq(_carry(host, twin), [{"type": "refused", "reason": GwSession.REFUSED_ALREADY_PLAYING}], "Ada is in the world already") check_eq(host.take_closing(), PackedInt64Array([5]), "the host gives up the connection") func test_an_unreadable_message_from_the_host_is_reported() -> void: var guest := GwSession.new() guest.start_guest(guest_key, "Grace") check_eq(guest.receive(GwSession.HOST_PEER, PackedByteArray([0xEE])), [{"type": "protocol_error"}], "a notice") check(guest.last_error().contains("unreadable"), "with the reason: %s" % guest.last_error()) check(not guest.start_guest(PackedByteArray([1, 2]), "Grace"), "a short key is refused")