Mi2dRPGamEng/tests/test_character_creator.py

535 lines
19 KiB
Python

import random
import sqlite3
from pathlib import Path
import pytest
from engine.character_creator import NPC_SELECTABLE_SPECIES, 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", "hydraulibot", "motorbot"}
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_npc_selectable_species_includes_dog(registry):
creator = CharacterCreator(registry, selectable_species=NPC_SELECTABLE_SPECIES)
assert creator.set_species("dog") is True
assert set(creator.available_species()) == set(PLAYER_SELECTABLE_SPECIES) | {"dog"}
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("hydraulibot") # 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("hydraulibot")
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("hydraulibot") # no body_type_options overlap, no hair at all
assert creator.body_type == "compact" # hydraulibot'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
def test_intelligence_is_a_directly_buyable_bio_skill(registry):
creator = CharacterCreator(registry)
assert "intelligence" in creator.bio
assert creator.increase_skill("intelligence") is True
assert creator.bio["intelligence"] == 1
def test_tech_is_not_in_the_bio_skill_list(registry):
creator = CharacterCreator(registry)
assert "tech" not in creator.bio
assert creator.increase_skill("tech") is False
# --- 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"
# --- secret items password -------------------------------------------------------------------
def test_the_miro_password_grants_the_secret_cyberdeck(registry):
creator = CharacterCreator(registry)
creator.set_species("human")
creator.set_name("Vex")
creator.set_secret_password("The_miro")
character = creator.build_character()
assert character is not None
assert "cyberdeck_the_miro_model_3a" in character.implants
def test_password_match_is_case_sensitive(registry):
creator = CharacterCreator(registry)
creator.set_species("human")
creator.set_name("Vex")
creator.set_secret_password("the_miro")
character = creator.build_character()
assert character is not None
assert character.implants == []
def test_wrong_password_grants_nothing(registry):
creator = CharacterCreator(registry)
creator.set_species("human")
creator.set_name("Vex")
creator.set_secret_password("hunter2")
character = creator.build_character()
assert character is not None
assert character.implants == []
def test_empty_password_grants_nothing(registry):
creator = CharacterCreator(registry)
creator.set_species("human")
creator.set_name("Vex")
character = creator.build_character()
assert character is not None
assert character.implants == []
# --- 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