extends "res://tests/test_case.gd" # §6.1 match lifecycle state machine (task 5.1). The table is pure data, so # it can be checked exhaustively rather than by example — which is the point # of keeping it out of NetworkedMatch. const MatchStateScript = preload("res://scripts/match_state.gd") func test_wire_values_are_stable() -> void: # These integers ARE the snapshot's match_state byte. Renumbering an # existing state silently reinterprets every packet from an older peer, # so pin them: this test failing means a protocol break, not a typo. assert_eq(MatchStateScript.State.LOBBY, 0, "LOBBY") assert_eq(MatchStateScript.State.LOADING, 1, "LOADING") assert_eq(MatchStateScript.State.WARMUP, 2, "WARMUP") assert_eq(MatchStateScript.State.PLAYING, 3, "PLAYING") assert_eq(MatchStateScript.State.GOAL_PAUSE, 4, "GOAL_PAUSE") assert_eq(MatchStateScript.State.FULL_TIME, 5, "FULL_TIME") assert_eq(MatchStateScript.State.OVERTIME_WARMUP, 6, "OVERTIME_WARMUP") assert_eq(MatchStateScript.State.OVERTIME, 7, "OVERTIME") assert_eq(MatchStateScript.State.RESULTS, 8, "RESULTS") func test_every_state_fits_in_the_wire_byte() -> void: for value in MatchStateScript.State.values(): assert_true(value >= 0 and value <= 255, "state %d must fit a u8" % value) func test_the_documented_happy_path_is_walkable() -> void: # §6.1's own diagram, start to finish, including the goal loop. var path := [ MatchStateScript.State.LOBBY, MatchStateScript.State.LOADING, MatchStateScript.State.WARMUP, MatchStateScript.State.PLAYING, MatchStateScript.State.GOAL_PAUSE, MatchStateScript.State.WARMUP, MatchStateScript.State.PLAYING, MatchStateScript.State.FULL_TIME, MatchStateScript.State.OVERTIME_WARMUP, MatchStateScript.State.OVERTIME, MatchStateScript.State.RESULTS, MatchStateScript.State.LOBBY, ] for i in path.size() - 1: assert_true( MatchStateScript.can_transition(path[i], path[i + 1]), "%s -> %s must be legal" % [MatchStateScript.to_name(path[i]), MatchStateScript.to_name(path[i + 1])] ) func test_illegal_shortcuts_are_rejected() -> void: var illegal := [ [MatchStateScript.State.LOBBY, MatchStateScript.State.PLAYING], # must load first [MatchStateScript.State.LOADING, MatchStateScript.State.PLAYING], # must warm up first [MatchStateScript.State.WARMUP, MatchStateScript.State.GOAL_PAUSE], # cannot score before play [MatchStateScript.State.PLAYING, MatchStateScript.State.RESULTS], # must pass full time [MatchStateScript.State.RESULTS, MatchStateScript.State.PLAYING], # match is over [MatchStateScript.State.FULL_TIME, MatchStateScript.State.PLAYING], # regulation cannot resume ] for pair in illegal: assert_true( not MatchStateScript.can_transition(pair[0], pair[1]), "%s -> %s must be rejected" % [MatchStateScript.to_name(pair[0]), MatchStateScript.to_name(pair[1])] ) func test_abort_to_lobby_is_reachable_from_anywhere_but_lobby() -> void: # §6.4: "if the last human leaves, abort to LOBBY" can fire at any point. for state in MatchStateScript.State.values(): if state == MatchStateScript.State.LOBBY: assert_true(not MatchStateScript.can_transition(state, state), "LOBBY -> LOBBY is not a transition") continue assert_true( MatchStateScript.can_transition(state, MatchStateScript.State.LOBBY), "%s must be able to abort to LOBBY" % MatchStateScript.to_name(state) ) func test_no_state_transitions_to_itself() -> void: for state in MatchStateScript.State.values(): assert_true(not MatchStateScript.can_transition(state, state), "%s -> itself" % MatchStateScript.to_name(state)) func test_every_state_is_reachable_and_can_make_progress() -> void: # Guards against a state being added to the enum and forgotten in the # table — an orphan would be broadcastable but a dead end, or unreachable # but present on the wire. for state in MatchStateScript.State.values(): var has_exit := false var has_entry := false for other in MatchStateScript.State.values(): if other != state and MatchStateScript.can_transition(state, other): has_exit = true if other != state and MatchStateScript.can_transition(other, state): has_entry = true assert_true(has_exit, "%s has no legal exit" % MatchStateScript.to_name(state)) assert_true(has_entry, "%s is unreachable" % MatchStateScript.to_name(state)) func test_only_playing_and_overtime_are_live() -> void: # is_live() gates simulation in tasks 5.3/5.4; a warmup or a goal pause # must never read as live. assert_true(MatchStateScript.is_live(MatchStateScript.State.PLAYING), "PLAYING is live") assert_true(MatchStateScript.is_live(MatchStateScript.State.OVERTIME), "OVERTIME is live") for state in [ MatchStateScript.State.LOBBY, MatchStateScript.State.LOADING, MatchStateScript.State.WARMUP, MatchStateScript.State.GOAL_PAUSE, MatchStateScript.State.FULL_TIME, MatchStateScript.State.OVERTIME_WARMUP, MatchStateScript.State.RESULTS, ]: assert_true(not MatchStateScript.is_live(state), "%s must not be live" % MatchStateScript.to_name(state)) func test_unknown_values_are_rejected_rather_than_coerced() -> void: # A newer server can legitimately send a state this build has never heard # of; it must be refused, not clamped into a neighbouring valid state. for bogus in [-1, 9, 42, 255]: assert_true(not MatchStateScript.is_valid(bogus), "%d is not a valid state" % bogus) assert_true(not MatchStateScript.can_transition(MatchStateScript.State.PLAYING, bogus), "cannot enter %d" % bogus) assert_true(not MatchStateScript.can_transition(bogus, MatchStateScript.State.PLAYING), "cannot leave %d" % bogus)