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(race := "human", calling := "sellsword") -> CreationDraft: var d := CreationDraft.new() d.character_seed = 8675309 d.set_race(race) d.set_calling(calling) d.name = "Aldric" return d func test_calling_detail_reads_the_table_rather_than_repeating_it(): # The guard that fails if anyone types "d8" into a string literal. If someone # retunes the Cutpurse's hit die, this test keeps passing and the CARD FOLLOWS — # which is the whole point. A hardcoded line would go red here. # # The talent is asserted through the DERIVED phrase ("talent second wind"), not # the raw id ("second_wind") the card used to print — but it is still read from # Callings.talent() at assert time, so hardcoding a talent into the format string # still goes red (traps.md #7: anchor to the full derived phrase). for id in Callings.IDS: var line := CreationCopy.calling_detail(id) assert_string_contains(line, "d%d" % Callings.hit_die(id)) assert_string_contains(line, CreationCopy.armor_word(id)) assert_string_contains(line, "talent %s" % CreationCopy.talent_word(id)) assert_string_contains(line, "picks %d skills" % Callings.skill_count(id)) for s in Callings.saves(id): assert_string_contains(line, s.to_upper()) func test_the_talent_is_humanised_and_no_card_line_shows_a_raw_id(): # The player read "talent second_wind" off the Sellsword's card. Four of the seven # talents carry an underscore — the card printed the ID. assert_eq(CreationCopy.talent_word("sellsword"), "second wind") assert_eq(CreationCopy.talent_word("cutpurse"), "backstab", "an underscore-free talent is untouched") var underscored := 0 for id in Callings.IDS: if Callings.talent(id).contains("_"): underscored += 1 assert_false(CreationCopy.calling_detail(id).contains("_"), "%s's card line shows a raw snake_case id: %s" % [id, CreationCopy.calling_detail(id)]) assert_eq(underscored, 4, "four talents carry an underscore — if that ever hits zero this guard stops guarding") func test_casters_show_a_pool_and_martials_show_cooldowns(): assert_string_contains(CreationCopy.calling_detail("hedge_mage"), "MP (MAG)") assert_string_contains(CreationCopy.calling_detail("bonesetter"), "MP (FTH)") assert_string_contains(CreationCopy.calling_detail("sellsword"), "cooldowns") assert_false(CreationCopy.calling_detail("sellsword").contains("MP")) func test_race_trait_lines_are_derived_from_the_race_table(): assert_string_contains(CreationCopy.race_trait("human"), "every save") assert_string_contains(CreationCopy.race_trait("human"), "skill of choice") assert_string_contains(CreationCopy.race_trait("elf"), "Perception") assert_string_contains(CreationCopy.race_trait("elf"), "Nightsight") assert_string_contains(CreationCopy.race_trait("dwarf"), "Poison") assert_string_contains(CreationCopy.race_trait("dwarf"), "Nightsight") assert_string_contains(CreationCopy.race_trait("beastfolk"), "Claws") assert_string_contains(CreationCopy.race_trait("beastfolk"), "Keen scent") func test_every_race_has_a_non_empty_trait_line(): for id in Races.IDS: assert_ne(CreationCopy.race_trait(id).strip_edges(), "", "%s has no trait line" % id) func test_skill_labels_are_human_readable(): assert_eq(CreationCopy.skill_label("sleight_of_hand"), "sleight of hand") assert_eq(CreationCopy.skill_label("faith_lore"), "faith lore") assert_eq(CreationCopy.skill_label("stealth"), "stealth") func test_nothing_in_the_copy_mentions_luck(): # §7 — the player must never be able to CALCULATE that he is cursed. for id in Callings.IDS: assert_false(CreationCopy.calling_detail(id).to_lower().contains("luck")) assert_false(CreationCopy.calling_detail(id).to_lower().contains("lck")) for id in Races.IDS: assert_false(CreationCopy.race_trait(id).to_lower().contains("luck")) assert_false(CreationCopy.race_trait(id).to_lower().contains("lck")) # ------------------------------------------------------------------- §13: the nag func test_the_nag_is_authored_copy_not_the_validator_s_grammar(): # §13. The exact strings a human must be able to judge, pinned. Anchored to the # authored phrase itself — asserting only "no underscore" would pass on any # string that happens to have none, including a blank one. var human := _draft("human", "sellsword") human.name = "" assert_eq(CreationCopy.error_line("player name is required", human), "name yourself") # "sellsword picks 2 skills, got 0" -> the count is DERIVED from the rules table. assert_eq(Callings.skill_count("sellsword"), 2, "the sellsword picks 2, or the line below means nothing") assert_eq(CreationCopy.error_line("sellsword picks 2 skills, got 0", human), "choose two more proficiencies") human.toggle_skill("athletics") assert_eq(CreationCopy.error_line("sellsword picks 2 skills, got 1", human), "choose one more proficiency", "one is singular, and the number is never hardcoded") # The same error against a calling that picks FOUR must say four — the proof the # number comes from Callings.skill_count and not from a literal "two". var thief := _draft("dwarf", "cutpurse") assert_eq(Callings.skill_count("cutpurse"), 4, "the cutpurse picks 4, or the line below means nothing") assert_eq(CreationCopy.error_line("cutpurse picks 4 skills, got 0", thief), "choose four more proficiencies") assert_eq(CreationCopy.error_line("human must choose a bonus skill", human), "one more proficiency, any of them — take it") func test_an_unmapped_error_still_speaks_in_voice(): # §13: "write fallbacks as content, not as error handling." An error nobody # anticipated (broken content; a spend the screen cannot produce) must never put # the raw string on the label, and must never blank it either. var d := _draft() for raw in ["unresolved ref: item:ghost_blade", "seed is required and must be an integer", "spend exceeds the pool of 3 (got 5)", "cannot spend on 'lck' — not an attribute"]: var line := CreationCopy.error_line(raw, d) assert_eq(line, CreationCopy.UNSPOKEN, "an unmapped error degrades to the authored fallback: %s" % raw) assert_ne(line, raw) assert_ne(CreationCopy.UNSPOKEN.strip_edges(), "", "the fallback is CONTENT — it is never blank") assert_false(CreationCopy.UNSPOKEN.contains("_")) func test_no_error_the_validator_can_raise_reaches_the_player_raw(): # Sweep EVERY error NewGame.validate actually produces for a draft the screen can # be driven into (plus the hostile shapes only a saga could hand it), and assert # the player never sees the id, the underscore, or the validator's grammar. var d := _draft() var hostiles: Array = [ {"name": "", "skills": [], "bonus_skill": ""}, # nameless, unpicked {"skills": ["athletics"]}, # too few {"skills": ["athletics", "athletics"]}, # duplicate {"skills": ["athletics", "sorcery"]}, # not in the pool {"race_id": "elf", "skills": ["athletics", "perception"], "bonus_skill": ""}, # granted already {"bonus_skill": ""}, # human, no bonus {"bonus_skill": "athletics", "skills": ["athletics", "endurance"]}, # bonus == a pick {"bonus_skill": "swimming"}, # no such skill {"race_id": "dwarf", "bonus_skill": "stealth"}, # dwarf gets none {"race_id": "orc"}, {"calling_id": "paladin"}, # off the tables {"seed": "8675309"}, {"spend": {"lck": 3}}, {"spend": {"str": 9}}, ] var base := { "name": "Aldric", "race_id": "human", "calling_id": "sellsword", "seed": 8675309, "spend": {}, "skills": ["athletics", "endurance"], "bonus_skill": "perception", } var seen := 0 for h in hostiles: var creation := base.duplicate(true) for k in h: creation[k] = h[k] var errors: Array = NewGame.validate(_deserter(), world, creation) assert_gt(errors.size(), 0, "hostile creation %s must actually be invalid, or it guards nothing" % str(h)) for e in errors: seen += 1 var line: String = CreationCopy.error_line(str(e), d) assert_ne(line.strip_edges(), "", "'%s' put a BLANK line on the label" % e) assert_ne(line, str(e), "'%s' reached the player raw" % e) assert_false(line.contains("_"), "'%s' put a snake_case id on the label: %s" % [e, line]) assert_false(line.contains("got "), "'%s' put the validator's grammar on the label: %s" % [e, line]) assert_gt(seen, 12, "the sweep must actually reach a spread of errors, not one or two")