Skip to content

Character Generator API

character_generator

Character generation logic for Basic Fantasy RPG.

This module implements a roll-first character creation flow including 3d6 ability rolls, race and class validation, hit points, armor class, saving throws, and starting money.

AbilityScores dataclass

Container for the six Basic Fantasy ability scores.

Attributes:

Name Type Description
CHA int

Charisma score.

CON int

Constitution score.

DEX int

Dexterity score.

INT int

Intelligence score.

STR int

Strength score.

WIS int

Wisdom score.

Source code in src/rpgcharacters/character_generator.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@dataclass
class AbilityScores:
    """Container for the six Basic Fantasy ability scores.

    Attributes:
        CHA: Charisma score.
        CON: Constitution score.
        DEX: Dexterity score.
        INT: Intelligence score.
        STR: Strength score.
        WIS: Wisdom score.
    """

    CHA: int
    CON: int
    DEX: int
    INT: int
    STR: int
    WIS: int

Character dataclass

Represent a fully generated level-1 character.

The object contains the character's rolled abilities along with all derived statistics such as hit points, armor class, attack bonus, saving throws, and starting wealth.

Attributes:

Name Type Description
abilities AbilityScores

Rolled or assigned ability scores.

ability_mods dict[str, int]

Ability modifiers keyed by ability name.

ac int

Final armor class value at the current state.

attack_bonus int

Attack bonus applied to attack rolls.

class_name str

Normalized class name (lowercase).

hp int

Current hit points.

inventory list[str]

Carried items as display names.

level int

Character level.

money_gp int

Current wealth in gold pieces.

name str | None

Optional character name.

race str

Normalized race name (lowercase).

saving_throws dict[str, int]

Saving throw targets keyed by saving throw name.

Source code in src/rpgcharacters/character_generator.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
@dataclass
class Character:
    """Represent a fully generated level-1 character.

    The object contains the character's rolled abilities along with all
    derived statistics such as hit points, armor class, attack bonus,
    saving throws, and starting wealth.

    Attributes:
        abilities: Rolled or assigned ability scores.
        ability_mods: Ability modifiers keyed by ability name.
        ac: Final armor class value at the current state.
        attack_bonus: Attack bonus applied to attack rolls.
        class_name: Normalized class name (lowercase).
        hp: Current hit points.
        inventory: Carried items as display names.
        level: Character level.
        money_gp: Current wealth in gold pieces.
        name: Optional character name.
        race: Normalized race name (lowercase).
        saving_throws: Saving throw targets keyed by saving throw name.
    """

    abilities: AbilityScores
    ability_mods: dict[str, int]
    ac: int
    attack_bonus: int
    class_name: str
    hp: int
    inventory: list[str]
    level: int
    money_gp: int
    name: str | None
    race: str
    saving_throws: dict[str, int]

    def to_dict(self) -> dict[str, Any]:
        """Serialize the character to a JSON-friendly dictionary.

        Returns:
            dict[str, Any]: Character data including abilities, combat values,
                money, and inventory.
        """
        return {
            "name": self.name,
            "race": self.race,
            "class": self.class_name,
            "level": self.level,
            "abilities": vars(self.abilities),
            "ability_mods": self.ability_mods,
            "hp": self.hp,
            "ac": self.ac,
            "attack_bonus": self.attack_bonus,
            "saving_throws": self.saving_throws,
            "money_gp": self.money_gp,
            "inventory": self.inventory,
        }

to_dict()

Serialize the character to a JSON-friendly dictionary.

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Character data including abilities, combat values, money, and inventory.

Source code in src/rpgcharacters/character_generator.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def to_dict(self) -> dict[str, Any]:
    """Serialize the character to a JSON-friendly dictionary.

    Returns:
        dict[str, Any]: Character data including abilities, combat values,
            money, and inventory.
    """
    return {
        "name": self.name,
        "race": self.race,
        "class": self.class_name,
        "level": self.level,
        "abilities": vars(self.abilities),
        "ability_mods": self.ability_mods,
        "hp": self.hp,
        "ac": self.ac,
        "attack_bonus": self.attack_bonus,
        "saving_throws": self.saving_throws,
        "money_gp": self.money_gp,
        "inventory": self.inventory,
    }

ability_modifier(score)

Convert an ability score to its Basic Fantasy modifier.

Parameters:

Name Type Description Default
score int

Ability score value.

required

Returns:

Name Type Description
int int

Bonus or penalty for the score.

Raises:

Type Description
ValueError

If score is outside the supported 3 to 18 range.

Source code in src/rpgcharacters/character_generator.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def ability_modifier(score: int) -> int:
    """Convert an ability score to its Basic Fantasy modifier.

    Args:
        score (int): Ability score value.

    Returns:
        int: Bonus or penalty for the score.

    Raises:
        ValueError: If ``score`` is outside the supported 3 to 18 range.
    """
    for low, high, mod in ABILITY_MOD_TABLE:
        if low <= score <= high:
            return mod
    raise ValueError("Ability score must be between 3 and 18.")

calculate_ability_modifiers(abilities)

Calculate modifiers for each ability score.

Parameters:

Name Type Description Default
abilities AbilityScores

Character ability scores.

required

Returns:

Type Description
dict[str, int]

dict[str, int]: Mapping from ability name to modifier.

Source code in src/rpgcharacters/character_generator.py
148
149
150
151
152
153
154
155
156
157
158
159
160
def calculate_ability_modifiers(abilities: AbilityScores) -> dict[str, int]:
    """Calculate modifiers for each ability score.

    Args:
        abilities (AbilityScores): Character ability scores.

    Returns:
        dict[str, int]: Mapping from ability name to modifier.
    """
    return {
        field.name: ability_modifier(getattr(abilities, field.name))
        for field in fields(AbilityScores)
    }

calculate_armor_class(dex_modifier)

Calculate base Armor Class before equipment is applied.

Parameters:

Name Type Description Default
dex_modifier int

Dexterity modifier.

required

Returns:

Name Type Description
int int

Base AC from "none" armor plus Dexterity modifier.

Source code in src/rpgcharacters/character_generator.py
338
339
340
341
342
343
344
345
346
347
348
349
def calculate_armor_class(dex_modifier: int) -> int:
    """Calculate base Armor Class before equipment is applied.

    Args:
        dex_modifier (int): Dexterity modifier.

    Returns:
        int: Base AC from "none" armor plus Dexterity modifier.
    """
    armor_key = cast(ArmorName, "none")
    armor_data = ARMOR[armor_key]
    return armor_data["base_ac"] + dex_modifier

calculate_saving_throws(class_name, race)

Compute level-1 saving throws with racial modifiers.

Parameters:

Name Type Description Default
class_name str

Character class used for base saves.

required
race str

Character race used for save modifiers.

required

Returns:

Type Description
dict[str, int]

dict[str, int]: Saving throw names mapped to adjusted values.

Raises:

Type Description
ValueError

If class_name or race is unknown.

Source code in src/rpgcharacters/character_generator.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
def calculate_saving_throws(class_name: str, race: str) -> dict[str, int]:
    """Compute level-1 saving throws with racial modifiers.

    Args:
        class_name (str): Character class used for base saves.
        race (str): Character race used for save modifiers.

    Returns:
        dict[str, int]: Saving throw names mapped to adjusted values.

    Raises:
        ValueError: If ``class_name`` or ``race`` is unknown.
    """
    # TODO: refactor this block into a helper function
    normalized_class = class_name.lower()
    if normalized_class not in CLASSES:
        raise ValueError(f"Unknown class: {normalized_class}")
    class_key = cast(ClassName, normalized_class)
    class_data = CLASSES[class_key]

    # TODO: refactor this block into a helper function
    normalized_race = race.lower()
    if normalized_race not in RACES:
        raise ValueError(f"Unknown race: {normalized_race}")
    race_key = cast(RaceName, normalized_race)
    race_data = RACES[race_key]

    base_saves = class_data["saving_throws"]
    modifiers = race_data["saving_throw_modifiers"]
    return {
        name: base_saves[name] + modifiers.get(name, 0)
        for name in base_saves
    }

generate_character(race, class_name, rng, name=None, abilities=None)

Generate a complete level-1 character from race, class, and dice rolls.

The flow implements core Basic Fantasy creation steps: 3d6 ability rolling (when not provided), race/class eligibility checks, hit points, armor class, attack bonus, saving throws, and starting money.

Parameters:

Name Type Description Default
race str

Selected race name.

required
class_name str

Selected class name.

required
rng DiceRoller

Dice roller used for all random generation.

required
name str | None

Optional character name.

None
abilities AbilityScores | None

Optional pre-rolled ability scores. If None, abilities are rolled with 3d6 per ability.

None

Returns:

Name Type Description
Character Character

Fully built level-1 character record.

Raises:

Type Description
ValueError

If race or class validation returns any messages.

Source code in src/rpgcharacters/character_generator.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
def generate_character(
    race: str,
    class_name: str,
    rng: DiceRoller,
    name: str | None = None,
    abilities: AbilityScores | None = None
) -> Character:
    """Generate a complete level-1 character from race, class, and dice rolls.

    The flow implements core Basic Fantasy creation steps: 3d6 ability rolling
    (when not provided), race/class eligibility checks, hit points, armor
    class, attack bonus, saving throws, and starting money.

    Args:
        race (str): Selected race name.
        class_name (str): Selected class name.
        rng (DiceRoller): Dice roller used for all random generation.
        name (str | None): Optional character name.
        abilities (AbilityScores | None): Optional pre-rolled ability scores.
            If ``None``, abilities are rolled with ``3d6`` per ability.

    Returns:
        Character: Fully built level-1 character record.

    Raises:
        ValueError: If race or class validation returns any messages.
    """
    # 1. Roll abilities
    # abilities = roll_abilities(rng)
    abilities = abilities if abilities is not None else roll_abilities(rng)

    # 2. Validate race
    race_errors = validate_race(abilities, race)
    if race_errors:
        raise ValueError("; ".join(race_errors))

    # 3. Validate class
    class_errors = validate_class(abilities, race, class_name)
    if class_errors:
        raise ValueError("; ".join(class_errors))

    # 4. Ability modifiers
    ability_mods = calculate_ability_modifiers(abilities)

    # 5. Hit points
    hp = roll_hit_points(
        class_name,
        race,
        ability_mods["CON"],
        rng
    )

    # 6. Armor class (no armor at creation)
    ac = calculate_armor_class(ability_mods["DEX"])

    # 7. Attack bonus
    attack_bonus = level_one_attack_bonus()

    # 8. Saving throws
    saving_throws = calculate_saving_throws(class_name, race)

    # 9. Starting money
    money = starting_money(rng)

    # 10. Return Character
    return Character(
        abilities=abilities,
        ability_mods=ability_mods,
        ac=ac,
        attack_bonus=attack_bonus,
        class_name=class_name.lower(),
        hp=hp,
        inventory=[],
        level=1,
        money_gp=money,
        name=name,
        race=race.lower(),
        saving_throws=saving_throws,
    )

level_one_attack_bonus()

Return the fixed Basic Fantasy level-1 attack bonus.

Returns:

Name Type Description
int int

Level-1 attack bonus.

Source code in src/rpgcharacters/character_generator.py
364
365
366
367
368
369
370
def level_one_attack_bonus() -> int:
    """Return the fixed Basic Fantasy level-1 attack bonus.

    Returns:
        int: Level-1 attack bonus.
    """
    return 1

roll_abilities(rng)

Roll ability scores using Basic Fantasy's 3d6 method.

Rolls one 3d6 result for each ability in ABILITY_ROLL_ORDER.

Parameters:

Name Type Description Default
rng DiceRoller

Dice roller used to generate each score.

required

Returns:

Name Type Description
AbilityScores AbilityScores

Rolled scores for all six abilities.

Source code in src/rpgcharacters/character_generator.py
134
135
136
137
138
139
140
141
142
143
144
145
146
def roll_abilities(rng: DiceRoller) -> AbilityScores:
    """Roll ability scores using Basic Fantasy's 3d6 method.

    Rolls one ``3d6`` result for each ability in ``ABILITY_ROLL_ORDER``.

    Args:
        rng (DiceRoller): Dice roller used to generate each score.

    Returns:
        AbilityScores: Rolled scores for all six abilities.
    """
    rolled = {name: rng.roll(ABILITY_ROLL) for name in ABILITY_ROLL_ORDER}
    return AbilityScores(**rolled)

roll_hit_points(class_name, race, con_modifier, rng)

Roll level-1 hit points from class hit die and Constitution modifier.

Basic Fantasy uses class-based hit dice, with racial hit-die caps for some races. This function applies the cap (if any), adds the Constitution modifier, and enforces a minimum of 1 HP.

Parameters:

Name Type Description Default
class_name str

Character class.

required
race str

Character race.

required
con_modifier int

Constitution modifier.

required
rng DiceRoller

Dice roller used for the hit die.

required

Returns:

Name Type Description
int int

Final level-1 hit points, minimum 1.

Raises:

Type Description
ValueError

If class_name or race is unknown.

Source code in src/rpgcharacters/character_generator.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def roll_hit_points(class_name: str, race: str, con_modifier: int, rng: DiceRoller) -> int:
    """Roll level-1 hit points from class hit die and Constitution modifier.

    Basic Fantasy uses class-based hit dice, with racial hit-die caps for some
    races. This function applies the cap (if any), adds the Constitution
    modifier, and enforces a minimum of 1 HP.

    Args:
        class_name (str): Character class.
        race (str): Character race.
        con_modifier (int): Constitution modifier.
        rng (DiceRoller): Dice roller used for the hit die.

    Returns:
        int: Final level-1 hit points, minimum 1.

    Raises:
        ValueError: If ``class_name`` or ``race`` is unknown.
    """
    # TODO: refactor this block into a helper function
    normalized_class = class_name.lower()
    if normalized_class not in CLASSES:
        raise ValueError(f"Unknown class: {normalized_class}")
    class_key = cast(ClassName, normalized_class)
    class_data = CLASSES[class_key]

    # TODO: refactor this block into a helper function
    normalized_race = race.lower()
    if normalized_race not in RACES:
        raise ValueError(f"Unknown race: {normalized_race}")
    race_key = cast(RaceName, normalized_race)
    race_data = RACES[race_key]

    hit_die = class_data["hit_die"]
    hit_die_cap = race_data["hit_die_max"]
    dice_type = hit_die
    if hit_die_cap is not None:
        dice_type = min(hit_die, hit_die_cap)

    roll = rng.roll(f"1d{dice_type}")
    return max(1, roll + con_modifier)

starting_money(rng)

Roll starting gold using Basic Fantasy's 3d6 x 10 rule.

Parameters:

Name Type Description Default
rng DiceRoller

Dice roller used for the money roll.

required

Returns:

Name Type Description
int int

Starting money in gold pieces.

Source code in src/rpgcharacters/character_generator.py
352
353
354
355
356
357
358
359
360
361
def starting_money(rng: DiceRoller) -> int:
    """Roll starting gold using Basic Fantasy's 3d6 x 10 rule.

    Args:
        rng (DiceRoller): Dice roller used for the money roll.

    Returns:
        int: Starting money in gold pieces.
    """
    return rng.roll(STARTING_MONEY_ROLL) * 10

valid_classes_for_race(abilities, race)

List classes that are valid for a race and ability scores.

Parameters:

Name Type Description Default
abilities AbilityScores

Ability scores to evaluate.

required
race str

Race used for class compatibility checks.

required

Returns:

Type Description
list[str]

list[str]: Class names with no class-validation messages.

Source code in src/rpgcharacters/character_generator.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def valid_classes_for_race(abilities: AbilityScores, race: str) -> list[str]:
    """List classes that are valid for a race and ability scores.

    Args:
        abilities (AbilityScores): Ability scores to evaluate.
        race (str): Race used for class compatibility checks.

    Returns:
        list[str]: Class names with no class-validation messages.
    """
    return [
        class_name for class_name in CLASSES
        if not validate_class(abilities, race, class_name)
    ]

valid_races_for_abilities(abilities)

List races that satisfy ability-based racial requirements.

Parameters:

Name Type Description Default
abilities AbilityScores

Ability scores to evaluate.

required

Returns:

Type Description
list[str]

list[str]: Race names with no race-validation messages.

Source code in src/rpgcharacters/character_generator.py
262
263
264
265
266
267
268
269
270
271
272
273
274
def valid_races_for_abilities(abilities: AbilityScores) -> list[str]:
    """List races that satisfy ability-based racial requirements.

    Args:
        abilities (AbilityScores): Ability scores to evaluate.

    Returns:
        list[str]: Race names with no race-validation messages.
    """
    return [
        race for race in RACES
        if not validate_race(abilities, race)
    ]

validate_class(abilities, race, class_name)

Validate class choice for race compatibility and prime requisite.

Basic Fantasy classes require a minimum prime requisite score and may be restricted by race.

Parameters:

Name Type Description Default
abilities AbilityScores

Rolled or assigned ability scores.

required
race str

Character race to check for allowed classes.

required
class_name str

Class name to validate.

required

Returns:

Type Description
list[str]

list[str]: Validation messages. Empty when the class is valid.

Raises:

Type Description
KeyError

If race or class_name is unknown after normalization.

Source code in src/rpgcharacters/character_generator.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def validate_class(abilities: AbilityScores, race: str, class_name: str) -> list[str]:
    """Validate class choice for race compatibility and prime requisite.

    Basic Fantasy classes require a minimum prime requisite score and may be
    restricted by race.

    Args:
        abilities (AbilityScores): Rolled or assigned ability scores.
        race (str): Character race to check for allowed classes.
        class_name (str): Class name to validate.

    Returns:
        list[str]: Validation messages. Empty when the class is valid.

    Raises:
        KeyError: If ``race`` or ``class_name`` is unknown after normalization.
    """
    errors: list[str] = []

    # TODO: refactor this block into a helper function
    normalized_race = race.lower()
    if normalized_race not in RACES:
        errors.append(f"Unknown race '{normalized_race}'.")
    race_key = cast(RaceName, normalized_race)
    race_data = RACES[race_key]

    # TODO: refactor this block into a helper function
    normalized_class = class_name.lower()
    if normalized_class not in CLASSES:
        errors.append(f"Unknown class: '{normalized_class}'")
    class_key = cast(ClassName, normalized_class)
    class_data = CLASSES[class_key]

    if race_data and class_data:
        allowed_classes = race_data["allowed_classes"] or []
        if normalized_class not in allowed_classes:
            errors.append(
                f"{race.title()} characters cannot be {class_name.title()}s."
            )
        prime = class_data["prime_requisite"]
        min_prime = class_data["min_prime"]
        score = getattr(abilities, prime, None)
        if score is not None and score < min_prime:
            errors.append(
                f"{class_name.title()} requires {prime} >= {min_prime}; found {score}."
            )

    return errors

validate_race(abilities, race)

Validate race selection against race ability limits.

Basic Fantasy races can define minimum and maximum values for specific abilities.

Parameters:

Name Type Description Default
abilities AbilityScores

Rolled or assigned ability scores.

required
race str

Race name to validate.

required

Returns:

Type Description
list[str]

list[str]: Validation messages. Empty when the race is valid.

Source code in src/rpgcharacters/character_generator.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def validate_race(abilities: AbilityScores, race: str) -> list[str]:
    """Validate race selection against race ability limits.

    Basic Fantasy races can define minimum and maximum values for specific
    abilities.

    Args:
        abilities (AbilityScores): Rolled or assigned ability scores.
        race (str): Race name to validate.

    Returns:
        list[str]: Validation messages. Empty when the race is valid.
    """
    errors: list[str] = []

    # TODO: refactor this block into a helper function
    normalized_race = race.lower()
    if normalized_race not in RACES:
        errors.append(f"Unknown race: '{normalized_race}'")
        return errors
    race_key = cast(RaceName, normalized_race)
    race_data = RACES[race_key]

    ability_min = race_data["ability_min"]
    ability_max = race_data["ability_max"]

    for ability, minimum in ability_min.items():
        score = getattr(abilities, ability, None)
        if score is None:
            continue
        if score < minimum:
            errors.append(
                f"{race.title()} requires {ability} >= {minimum}; found {score}."
            )

    for ability, maximum in ability_max.items():
        score = getattr(abilities, ability, None)
        if score is None:
            continue
        if score > maximum:
            errors.append(
                f"{race.title()} limits {ability} to <= {maximum}; found {score}."
            )

    return errors