extends "res://addons/gut/test.gd" const ContentDB = preload("res://scripts/content/content_db.gd") const NewGame = preload("res://scripts/newgame/new_game.gd") var world func before_each(): world = ContentDB.new() world.load_from(ContentDB.default_content_root()) func _deserter() -> Dictionary: return ContentDB.load_json(ContentDB.origin_path("deserter")) func _draft() -> CreationDraft: # A fixed seed, so every assertion below is reproducible. var d := CreationDraft.new() d.character_seed = 8675309 d.set_race("human") d.set_calling("sellsword") d.name = "Aldric" return d func test_a_fresh_draft_has_a_seed_and_no_choices(): var d := CreationDraft.fresh() assert_ne(d.character_seed, 0, "randomize() gave us the one bit of non-determinism there is") assert_eq(d.spend, {}) assert_eq(d.skills, []) assert_eq(d.bonus_skill, "") func test_rolled_is_exactly_what_new_game_will_roll(): var d := _draft() assert_eq(d.rolled(), NewGame.roll_attributes(d.character_seed), "the screen and construct must reach the same five numbers") func test_final_is_rolled_plus_spend(): var d := _draft() d.increment("str") d.increment("str") assert_eq(int(d.final()["str"]), int(d.rolled()["str"]) + 2) assert_eq(int(d.final()["dex"]), int(d.rolled()["dex"]), "an unspent stat is untouched") func test_the_pool_is_three_and_the_fourth_point_does_not_exist(): var d := _draft() assert_eq(d.points_left(), 3) d.increment("str"); d.increment("dex"); d.increment("con") assert_eq(d.points_left(), 0) assert_false(d.can_increment("fth")) d.increment("fth") assert_eq(int(d.final()["fth"]), int(d.rolled()["fth"]), "the fourth point bought nothing") assert_eq(d.spent_total(), 3) func test_minus_removes_only_what_you_spent_and_floors_at_the_roll(): var d := _draft() d.increment("str") assert_true(d.can_decrement("str")) d.decrement("str") assert_eq(d.points_left(), 3) assert_false(d.can_decrement("str"), "you cannot go below the roll — additive only") d.decrement("str") assert_eq(int(d.final()["str"]), int(d.rolled()["str"])) func test_reroll_changes_the_numbers_and_clears_the_spend_and_nothing_else(): var d := _draft() d.set_calling("cutpurse") d.toggle_skill("stealth") d.increment("dex") var before := d.character_seed d.reroll() assert_ne(d.character_seed, before, "a re-roll IS a new seed — nothing else") assert_eq(d.spend, {}, "points spent against a roll that no longer exists are meaningless") assert_eq(d.calling_id, "cutpurse", "the calling survives") assert_eq(d.skills, ["stealth"], "the picks survive") assert_eq(d.name, "Aldric", "the name survives") func test_changing_calling_clears_the_picks(): var d := _draft() d.toggle_skill("athletics") assert_eq(d.skills, ["athletics"]) d.set_calling("hedge_mage") assert_eq(d.skills, [], "the pool changed underneath them — stale picks are an invalid draft the player did not cause") func test_changing_race_drops_a_pick_the_new_race_now_grants(): # The elf/cutpurse case. An Elf is GRANTED perception; a Cutpurse who had # picked it now holds a pick that "buys nothing" and construct rejects by name. var d := _draft() d.set_calling("cutpurse") d.toggle_skill("perception") assert_eq(d.skills, ["perception"]) d.set_race("elf") assert_eq(d.skills, [], "the elf already has perception — the pick is dropped, not left to 422") func test_changing_race_off_human_clears_the_bonus_skill(): var d := _draft() d.set_bonus_skill("stealth") assert_eq(d.bonus_skill, "stealth") d.set_race("dwarf") assert_eq(d.bonus_skill, "", "only a human gets a bonus skill") func test_a_granted_skill_cannot_be_picked(): var d := _draft() d.set_race("elf") d.set_calling("cutpurse") assert_true(d.is_granted("perception")) assert_false(d.can_pick("perception")) d.toggle_skill("perception") assert_eq(d.skills, [], "the chip is inert, not merely styled to look inert") func test_picks_stop_at_the_calling_s_count(): var d := _draft() d.set_calling("sellsword") # picks 2 d.toggle_skill("athletics") d.toggle_skill("endurance") assert_eq(d.picks_left(), 0) d.toggle_skill("perception") assert_eq(d.skills, ["athletics", "endurance"], "the third pick does not exist") func test_toggling_an_already_picked_skill_removes_it(): var d := _draft() d.toggle_skill("athletics") d.toggle_skill("athletics") assert_eq(d.skills, []) func test_the_bonus_skill_cannot_also_be_picked_from_the_pool(): # The guard was one-directional: can_take_bonus() refused a skill already PICKED, # but can_pick() did not consider the bonus. Human + Sellsword, take athletics as # the bonus, then press athletics in the Sellsword's pool — the pick landed and # the draft went invalid ("bonus skill athletics is already proficient"), which is # precisely the invalid draft "he did not cause and cannot see" that set_calling's # comment forbids. var d := _draft() # human + sellsword; athletics is in the pool d.set_bonus_skill("athletics") assert_eq(d.bonus_skill, "athletics") assert_true("athletics" in d.skill_pool(), "athletics must be in the sellsword's pool or this proves nothing") assert_false(d.can_pick("athletics"), "the chip must be INERT, not merely styled to look inert") d.toggle_skill("athletics") assert_eq(d.skills, [], "the pick must not land while athletics is the bonus skill") # ...and the draft the press would have produced is exactly the one construct rejects. d.skills = ["athletics", "endurance"] # what the bug let the player build assert_true("bonus skill athletics is already proficient" in d.errors(_deserter(), world), "this IS the invalid draft the guard exists to prevent") func test_the_other_direction_still_holds_a_picked_skill_cannot_become_the_bonus(): # The half of the guard that already worked. Kept beside its mirror so the two # can never drift apart again. var d := _draft() d.toggle_skill("athletics") assert_eq(d.skills, ["athletics"]) assert_false(d.can_take_bonus("athletics")) d.set_bonus_skill("athletics") assert_eq(d.bonus_skill, "", "a picked skill must not also be taken as the bonus") func test_handing_back_the_bonus_skill_makes_the_pool_chip_live_again(): # The inert chip is recoverable — the bonus row is right there. Without this the # fix above would be a dead end the player could walk into and not walk out of. var d := _draft() d.set_bonus_skill("athletics") assert_false(d.can_pick("athletics")) d.set_bonus_skill("athletics") # pressing the chosen bonus chip clears it assert_eq(d.bonus_skill, "") assert_true(d.can_pick("athletics"), "handing the bonus back must make the pool chip live") d.toggle_skill("athletics") assert_eq(d.skills, ["athletics"]) func test_to_creation_carries_exactly_the_seven_contract_keys(): # §2, stated as a test rather than as a comment. The screen emits a seed and four # choices; it never hands construct a NUMBER. test_pressing_enter_emits_exactly_ # the_draft_s_creation_dict compares the emitted dict to to_creation() — both # sides move together, so adding an "attributes" key there breaks nothing. This # pins the contract itself: seven keys, no more, no fewer. var d := _draft() var keys: Array = d.to_creation().keys() keys.sort() assert_eq(keys, ["bonus_skill", "calling_id", "name", "race_id", "seed", "skills", "spend"], "the creation contract is exactly seven keys — an eighth (an 'attributes' block, say) is the §2 breach") func test_to_creation_round_trips_through_construct(): var d := _draft() d.toggle_skill("athletics") d.toggle_skill("endurance") d.set_bonus_skill("perception") d.increment("str") var res := NewGame.construct(_deserter(), world, d.to_creation()) assert_true(res["ok"], str(res["errors"])) assert_eq(res["state"].sheet.attributes, d.final(), "TRAP 1: what the screen SHOWED is what the player GOT — asserted across the pipeline") func test_errors_are_construct_s_errors_not_a_second_copy_of_the_rules(): var d := _draft() # no skills picked yet assert_gt(d.errors(_deserter(), world).size(), 0) assert_false(d.is_complete(_deserter(), world)) d.toggle_skill("athletics") d.toggle_skill("endurance") d.set_bonus_skill("perception") assert_eq(d.errors(_deserter(), world), [], "a legal draft has nothing wrong with it") assert_true(d.is_complete(_deserter(), world)) func test_an_unnamed_draft_is_not_complete(): var d := _draft() d.toggle_skill("athletics") d.toggle_skill("endurance") d.set_bonus_skill("perception") d.name = " " assert_false(d.is_complete(_deserter(), world), "a nameless character cannot enter the world") func test_the_draft_never_exposes_luck(): # §7 — no accessor, no field, no leak. The screen cannot show what it cannot reach. var d := _draft() assert_false("lck" in d.to_creation().get("spend", {})) assert_false(d.to_creation().has("luck")) assert_false(d.final().has("lck")) assert_false(d.rolled().has("lck"))