import random import sqlite3 from pathlib import Path import pytest from engine.character_creator import PLAYER_SELECTABLE_SPECIES, CharacterCreator from engine.character_library import CharacterLibrary from engine.defs import DefRegistry DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs" @pytest.fixture() def registry(): return DefRegistry.load(DEFS_DIR) @pytest.fixture() def library(): return CharacterLibrary(sqlite3.connect(":memory:")) # --- species / gender selection ----------------------------------------------------------- def test_set_species_defaults_gender_to_the_first_valid_option(registry): creator = CharacterCreator(registry) assert creator.set_species("human") is True assert creator.species_id == "human" assert creator.gender == "male" def test_set_species_rejects_unknown_species(registry): creator = CharacterCreator(registry) assert creator.set_species("dragon") is False assert creator.species_id is None def test_dog_is_not_a_player_selectable_species(registry): creator = CharacterCreator(registry) assert creator.set_species("dog") is False assert creator.species_id is None def test_available_species_excludes_dog(registry): creator = CharacterCreator(registry) assert "dog" not in creator.available_species() assert set(creator.available_species()) == {"human", "reptilian_quad", "alien_tri", "bot"} def test_selectable_species_is_configurable(registry): creator = CharacterCreator(registry, selectable_species=("dog",)) assert creator.set_species("dog") is True assert creator.set_species("human") is False def test_set_gender_accepts_a_valid_option(registry): creator = CharacterCreator(registry) creator.set_species("human") assert creator.set_gender("nonbinary") is True assert creator.gender == "nonbinary" def test_set_gender_rejects_option_not_valid_for_species(registry): creator = CharacterCreator(registry) creator.set_species("alien_tri") # genders: alpha/beta/gamma assert creator.set_gender("male") is False assert creator.gender == "alpha" # unchanged, still the auto-picked default def test_set_gender_before_species_is_a_no_op(registry): creator = CharacterCreator(registry) assert creator.set_gender("male") is False assert creator.gender is None def test_switching_species_resets_gender_if_no_longer_valid(registry): creator = CharacterCreator(registry) creator.set_species("human") creator.set_gender("nonbinary") creator.set_species("bot") # monogender, "none" - "nonbinary" isn't valid here assert creator.gender == "none" def test_switching_species_keeps_gender_if_still_valid(registry): creator = CharacterCreator(registry, selectable_species=PLAYER_SELECTABLE_SPECIES + ("dog",)) creator.set_species("human") creator.set_gender("female") creator.set_species("dog") # dog also has "male"/"female" assert creator.gender == "female" # --- body type / hair color selection --------------------------------------------------------- def test_set_species_auto_fills_body_type_and_hair_color(registry): creator = CharacterCreator(registry) creator.set_species("human") assert creator.body_type == "average" assert creator.hair_color == "black" def test_set_species_with_no_hair_colors_leaves_hair_color_none(registry): creator = CharacterCreator(registry) creator.set_species("reptilian_quad") assert creator.body_type == "average" assert creator.hair_color is None def test_set_body_type_accepts_a_valid_option(registry): creator = CharacterCreator(registry) creator.set_species("human") assert creator.set_body_type("hulk") is True assert creator.body_type == "hulk" def test_set_body_type_rejects_invalid_option(registry): creator = CharacterCreator(registry) creator.set_species("human") assert creator.set_body_type("gigantic") is False assert creator.body_type == "average" def test_set_hair_color_accepts_a_valid_option(registry): creator = CharacterCreator(registry) creator.set_species("human") assert creator.set_hair_color("red") is True assert creator.hair_color == "red" def test_set_hair_color_rejects_invalid_option_for_species_with_no_hair(registry): creator = CharacterCreator(registry) creator.set_species("bot") assert creator.set_hair_color("black") is False assert creator.hair_color is None def test_set_body_type_before_species_is_a_no_op(registry): creator = CharacterCreator(registry) assert creator.set_body_type("hulk") is False def test_switching_species_resets_body_type_and_hair_color_if_no_longer_valid(registry): creator = CharacterCreator(registry) creator.set_species("human") creator.set_body_type("hulk") creator.set_hair_color("red") creator.set_species("bot") # no body_type_options overlap, no hair at all assert creator.body_type == "compact" # bot's first body_type option assert creator.hair_color is None # --- starter clothing selection ----------------------------------------------------------------- def test_set_starter_clothing_accepts_a_compatible_item(registry): creator = CharacterCreator(registry) creator.set_species("human") assert creator.set_starter_clothing("torso", "leather_jacket") is True assert creator.starter_clothing["torso"] == "leather_jacket" def test_set_starter_clothing_rejects_incompatible_body_type(registry): creator = CharacterCreator(registry, selectable_species=PLAYER_SELECTABLE_SPECIES + ("dog",)) creator.set_species("dog") assert creator.set_starter_clothing("torso", "leather_jacket") is False assert "torso" not in creator.starter_clothing def test_set_starter_clothing_rejects_incompatible_arm_count(registry): creator = CharacterCreator(registry) creator.set_species("reptilian_quad") # 4 arms assert creator.set_starter_clothing("torso", "leather_jacket") is False # 2-sleeve jacket def test_set_starter_clothing_accepts_matching_species_gear(registry): creator = CharacterCreator(registry, selectable_species=PLAYER_SELECTABLE_SPECIES + ("dog",)) creator.set_species("dog") assert creator.set_starter_clothing("torso", "dog_harness") is True assert creator.set_starter_clothing("back", "dog_saddlebag") is True def test_set_starter_clothing_before_species_is_a_no_op(registry): creator = CharacterCreator(registry) assert creator.set_starter_clothing("torso", "leather_jacket") is False def test_remove_starter_clothing_clears_a_slot(registry): creator = CharacterCreator(registry) creator.set_species("human") creator.set_starter_clothing("torso", "leather_jacket") creator.remove_starter_clothing("torso") assert "torso" not in creator.starter_clothing def test_switching_species_drops_starter_clothing(registry): creator = CharacterCreator(registry, selectable_species=PLAYER_SELECTABLE_SPECIES + ("dog",)) creator.set_species("human") creator.set_starter_clothing("torso", "leather_jacket") creator.set_species("dog") assert creator.starter_clothing == {} # --- skill point budget --------------------------------------------------------------------- def test_starts_with_the_full_budget_unspent(registry): creator = CharacterCreator(registry) assert creator.spent_skill_points() == 0 assert creator.remaining_skill_points() == creator.skill_points_budget def test_increase_skill_spends_a_point(registry): creator = CharacterCreator(registry) assert creator.increase_skill("stealth") is True assert creator.bio["stealth"] == 1 assert creator.remaining_skill_points() == creator.skill_points_budget - 1 def test_increase_skill_fails_once_budget_exhausted(registry): creator = CharacterCreator(registry, skill_points_budget=2) creator.increase_skill("stealth") creator.increase_skill("stealth") assert creator.increase_skill("athletics") is False assert creator.bio["athletics"] == 0 def test_increase_skill_fails_past_max_level(registry): creator = CharacterCreator(registry, skill_points_budget=99, max_skill_level=2) creator.increase_skill("stealth") creator.increase_skill("stealth") assert creator.increase_skill("stealth") is False assert creator.bio["stealth"] == 2 def test_increase_skill_rejects_unknown_skill(registry): creator = CharacterCreator(registry) assert creator.increase_skill("lockpicking") is False def test_decrease_skill_refunds_a_point(registry): creator = CharacterCreator(registry) creator.increase_skill("stealth") assert creator.decrease_skill("stealth") is True assert creator.bio["stealth"] == 0 assert creator.remaining_skill_points() == creator.skill_points_budget def test_decrease_skill_below_zero_is_a_no_op(registry): creator = CharacterCreator(registry) assert creator.decrease_skill("stealth") is False # --- skill allocation modes ------------------------------------------------------------------ def test_defaults_to_point_buy_mode(registry): creator = CharacterCreator(registry) assert creator.skill_mode == "point_buy" def test_set_skill_mode_rejects_unknown_mode(registry): creator = CharacterCreator(registry) assert creator.set_skill_mode("vibes") is False assert creator.skill_mode == "point_buy" def test_switching_to_point_buy_resets_bio_and_clears_rolled_values(registry): creator = CharacterCreator(registry) creator.increase_skill("stealth") creator.set_skill_mode("point_buy") assert creator.bio["stealth"] == 0 assert creator.rolled_values == [] # roll_assign: roll a fixed pool, place each value on a skill of your choosing def test_roll_assign_generates_a_fixed_pool_of_values(registry): creator = CharacterCreator(registry) creator.set_skill_mode("roll_assign", rng=random.Random(1)) assert len(creator.rolled_values) == 6 assert all(0 <= v <= creator.max_skill_level for v in creator.rolled_values) assert all(v == 0 for v in creator.bio.values()) # nothing placed yet def test_assign_rolled_value_places_a_value_and_removes_it_from_the_pool(registry): creator = CharacterCreator(registry) creator.set_skill_mode("roll_assign") creator.rolled_values = [3, 1, 4] assert creator.assign_rolled_value(0, "stealth") is True assert creator.bio["stealth"] == 3 assert creator.rolled_values == [1, 4] # the placed value is gone, order preserved def test_assign_rolled_value_out_of_range_is_a_no_op(registry): creator = CharacterCreator(registry) creator.set_skill_mode("roll_assign") creator.rolled_values = [3] assert creator.assign_rolled_value(5, "stealth") is False assert creator.rolled_values == [3] def test_assign_rolled_value_rejects_unknown_skill(registry): creator = CharacterCreator(registry) creator.set_skill_mode("roll_assign") creator.rolled_values = [3] assert creator.assign_rolled_value(0, "lockpicking") is False assert creator.rolled_values == [3] def test_assign_rolled_value_only_works_in_roll_assign_mode(registry): creator = CharacterCreator(registry) # defaults to point_buy assert creator.assign_rolled_value(0, "stealth") is False def test_increase_skill_does_nothing_outside_point_buy_mode(registry): creator = CharacterCreator(registry) creator.set_skill_mode("roll_assign") assert creator.increase_skill("stealth") is False assert creator.decrease_skill("stealth") is False # manual: any skill, any value, no budget def test_set_skill_manual_sets_any_value(registry): creator = CharacterCreator(registry) creator.set_skill_mode("manual") assert creator.set_skill_manual("stealth", 5) is True assert creator.bio["stealth"] == 5 def test_set_skill_manual_rejects_value_out_of_range(registry): creator = CharacterCreator(registry) creator.set_skill_mode("manual") assert creator.set_skill_manual("stealth", 6) is False assert creator.set_skill_manual("stealth", -1) is False assert creator.bio["stealth"] == 0 def test_set_skill_manual_rejects_unknown_skill(registry): creator = CharacterCreator(registry) creator.set_skill_mode("manual") assert creator.set_skill_manual("lockpicking", 3) is False def test_set_skill_manual_only_works_in_manual_mode(registry): creator = CharacterCreator(registry) # defaults to point_buy assert creator.set_skill_manual("stealth", 3) is False def test_set_skill_manual_has_no_budget_limit(registry): creator = CharacterCreator(registry, skill_points_budget=1) creator.set_skill_mode("manual") for skill_id in creator.bio: assert creator.set_skill_manual(skill_id, 5) is True assert creator.spent_skill_points() == 5 * len(creator.bio) # far past the point-buy budget # random: every skill gets its own value immediately def test_random_mode_fills_every_skill_immediately(registry): creator = CharacterCreator(registry) creator.set_skill_mode("random", rng=random.Random(1)) assert all(0 <= v <= creator.max_skill_level for v in creator.bio.values()) assert creator.rolled_values == [] # nothing left to place - it's all already assigned def test_random_mode_is_deterministic_given_the_same_rng_seed(registry): a = CharacterCreator(registry) a.set_skill_mode("random", rng=random.Random(42)) b = CharacterCreator(registry) b.set_skill_mode("random", rng=random.Random(42)) assert a.bio == b.bio # --- body part preview ------------------------------------------------------------------------ def test_preview_body_parts_empty_before_species_chosen(registry): creator = CharacterCreator(registry) assert creator.preview_body_parts() == [] def test_preview_body_parts_matches_species_defaults(registry): creator = CharacterCreator(registry, selectable_species=PLAYER_SELECTABLE_SPECIES + ("dog",)) creator.set_species("dog") slots = {p.slot for p in creator.preview_body_parts()} assert slots == {"head", "torso", "front_left_leg", "front_right_leg", "back_left_leg", "back_right_leg"} # --- readiness / build_character ---------------------------------------------------------------- def test_not_ready_without_name_species_and_gender(registry): creator = CharacterCreator(registry) assert creator.is_ready() is False creator.set_name("Vex") assert creator.is_ready() is False creator.set_species("human") assert creator.is_ready() is True # species auto-fills a valid gender def test_build_character_returns_none_when_not_ready(registry): creator = CharacterCreator(registry) creator.set_name("Vex") assert creator.build_character() is None def test_build_character_produces_a_character_with_chosen_traits(registry): creator = CharacterCreator(registry) creator.set_name(" Vex ") creator.set_species("reptilian_quad") creator.increase_skill("athletics") character = creator.build_character() assert character is not None assert character.name == "Vex" # whitespace trimmed assert character.species_id == "reptilian_quad" assert character.gender == "none" assert character.body_type == "average" # auto-filled, reptilian has no hair to color assert character.hair_color is None assert character.bio["athletics"] == 1 assert {p.slot for p in character.default_body_parts} == { "head", "torso", "left_arm", "right_arm", "left_arm_2", "right_arm_2", "left_leg", "right_leg", } def test_build_character_includes_starter_clothing(registry): creator = CharacterCreator(registry, selectable_species=PLAYER_SELECTABLE_SPECIES + ("dog",)) creator.set_name("Rex") creator.set_species("dog") creator.set_starter_clothing("torso", "dog_harness") creator.set_starter_clothing("back", "dog_saddlebag") character = creator.build_character() assert character.get_clothing("torso").item_def_id == "dog_harness" assert character.get_clothing("back").item_def_id == "dog_saddlebag" # --- confirm: saves into the library ------------------------------------------------------------- def test_confirm_saves_into_the_library(registry, library): creator = CharacterCreator(registry) creator.set_name("Vex") creator.set_species("human") character = creator.confirm(library, "acct-1", "char-1") assert character is not None loaded = library.load_character("acct-1", "char-1", registry) assert loaded is not None assert loaded.name == "Vex" assert loaded.species_id == "human" def test_confirm_does_nothing_when_not_ready(registry, library): creator = CharacterCreator(registry) assert creator.confirm(library, "acct-1", "char-1") is None assert library.load_character("acct-1", "char-1", registry) is None