extends SceneTree ## Do the game modes end, and end on the RIGHT thing? ## ## godot --headless --path . -s res://debug/game_mode_check.gd ## ## `GameMode.check_win` is a pure function of the stats and the clock, which is ## the point of writing it that way: a win condition that can only be exercised ## by playing a whole match is a win condition nobody tests, and this project's ## previous one — a five-minute timer that set `match_active = false` and did ## nothing else — was never exercised at all. ## ## The cases below are the ones that actually go wrong: a draw reported as a win ## for whoever happens to be first in a dictionary, a team mode decided on one ## player's score instead of the team's, and a team kill counting toward the ## killer. var _fails := 0 func _init() -> void: # Autoloads are not up during `_init`; LoadoutManager is needed below. await process_frame _check_table() _check_deathmatch() _check_team() _check_gun_game() _check_standings() _done() ## Every mode must be able to end. A mode with no score limit and no time limit ## runs forever, and that is the failure that produced this whole task. func _check_table() -> void: for id in GameMode.all_ids(): _expect(GameMode.is_valid(id), "'%s' can end (score %d, time %.0fs)" % [id, GameMode.score_limit(id), GameMode.time_limit(id)]) _expect(GameMode.display_name(id) != "", "'%s' has a display name" % id) _expect(GameMode.score_limit(GameMode.GUN_GAME) == GameMode.LADDER.size(), "Gun Game's limit IS its ladder length (%d), so adding a rung cannot make the match unwinnable" % GameMode.LADDER.size()) func _check_deathmatch() -> void: var id := GameMode.DEATHMATCH var limit := GameMode.score_limit(id) var mid := {1: _p("A", limit - 1), 2: _p("B", 3)} _expect(GameMode.check_win(id, mid, 120.0).is_empty(), "deathmatch keeps running one short of the limit") var done := {1: _p("A", limit), 2: _p("B", 3)} var r := GameMode.check_win(id, done, 120.0) _expect(r.get("reason", "") == "score" and r.get("name", "") == "A", "deathmatch ends when someone reaches %d, and A won" % limit) # On time, the leader takes it. var timed := GameMode.check_win(id, {1: _p("A", 7), 2: _p("B", 4)}, 0.0) _expect(timed.get("reason", "") == "time" and timed.get("name", "") == "A", "deathmatch on time goes to the leader") # A DRAW is a draw. Reporting a winner here means reporting whichever key # the dictionary happened to hand over first, which is not a rule. var drawn := GameMode.check_win(id, {1: _p("A", 5), 2: _p("B", 5)}, 0.0) _expect(drawn.get("name", "") == "DRAW", "a tie on time is a DRAW, not a win for whoever is first in the dictionary") func _check_team() -> void: var id := GameMode.TEAM_DEATHMATCH var limit := GameMode.score_limit(id) # Two on team 1 with half the limit each: the TEAM has won, though neither # player is close on their own. Deciding this on individual score — which is # what a per-player check would do — never ends a team match. var half := int(limit / 2.0) var stats := { 1: _p("A", half, 1), 2: _p("B", half, 1), 3: _p("C", 4, 2), 4: _p("D", 4, 2), } var r := GameMode.check_win(id, stats, 300.0) _expect(r.get("reason", "") == "score" and r.get("team", 0) == 1, "team deathmatch is decided by the TEAM's total, not one player's") var totals := GameMode.team_scores(id, stats) _expect(totals.get(1, 0) == half * 2 and totals.get(2, 0) == 8, "team totals add up (%d / %d)" % [totals.get(1, 0), totals.get(2, 0)]) # Balance on join, by count and not by turn — a 4v4 that loses three from one # side must refill the short side, which round-robin on join order does not. var lopsided := {1: _p("A", 0, 1), 2: _p("B", 0, 1), 3: _p("C", 0, 1)} _expect(GameMode.assign_team(id, lopsided) == 2, "a joining player goes to the SHORT team") var tied := GameMode.check_win(id, {1: _p("A", 5, 1), 2: _p("B", 5, 2)}, 0.0) _expect(tied.get("name", "") == "DRAW", "level teams on time is a DRAW") func _check_gun_game() -> void: var id := GameMode.GUN_GAME var top := GameMode.LADDER.size() _expect(GameMode.check_win(id, {1: _p("A", top - 1)}, 300.0).is_empty(), "gun game keeps running on the last rung") var r := GameMode.check_win(id, {1: _p("A", top)}, 300.0) _expect(r.get("reason", "") == "score", "gun game ends when someone finishes the ladder") # Every rung must name a weapon that exists, or a promotion hands the player # nothing and the mode softlocks on that rung. # # Reached through the tree rather than by the autoload's global identifier: a # `-s` tool script compiles its dependencies BEFORE autoloads register, so # naming `LoadoutManager` directly stops this file from loading at all. var loadouts = root.get_node_or_null("LoadoutManager") for i in GameMode.LADDER.size(): var w := GameMode.ladder_weapon(i) _expect(loadouts != null and loadouts.weapon_db.has(w), "ladder rung %d ('%s') is a real weapon" % [i, w]) # Past the end clamps rather than going out of bounds — the winning kill # promotes past the top rung before the match-end RPC lands. _expect(GameMode.ladder_weapon(999) == GameMode.LADDER[-1], "promoting past the top rung clamps instead of erroring") func _check_standings() -> void: var stats := { 1: _p("A", 5), 2: _p("B", 9), 3: _p("C", 5), } stats[1]["deaths"] = 2 stats[3]["deaths"] = 7 var order := GameMode.standings(stats) _expect(order[0] == 2, "standings put the highest score first") _expect(order[1] == 1 and order[2] == 3, "equal scores break on FEWEST DEATHS (kills are equal by definition there)") func _p(who: String, score: int, team: int = 0) -> Dictionary: return {"username": who, "score": score, "kills": score, "deaths": 0, "assists": 0, "team": team, "best_streak": 0, "rung": score} func _expect(ok: bool, what: String) -> void: if ok: print(" OK: ", what) else: print(" FAIL: ", what) _fails += 1 func _done() -> void: print("\n=== GAME MODE SUMMARY ===") print("Failures: %d" % _fails) quit(1 if _fails > 0 else 0)