Source code for main

#!/usr/bin/env python3
#
# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "polars>=1.41.2",
#   "pydantic>=2.13.4",
# ]
# ///
# PLEASE BE AWARE THAT SCRIPT METADATA WILL OVERRIDE THE PYPROJECT.TOML

# SCRIPT NAME
# 2026 (c) YOUR NAME
# https://github.com/username/
# your.mail@mail.com

r"""Battles two characters.

.. code-block:: text
   :caption: Example Usage

   usage: main.py [-h] -f FILE [-c1 CHARACTER_1] [-c2 CHARACTER_2] [-hp HEALTH] [--version]

   Battles two characters.

   options:
     -h, --help            show this help message and exit
     -f, --file FILE       character file to read characters from (str).
     -c1, --character-1 CHARACTER_1
                           index of the first character to use (int).
     -c2, --character-2 CHARACTER_2
                           index of the second character to use (int).
     -hp, --hit-points HEALTH
                           health of all characters (int).
     --version             show program's version number and exit

   (c) Micha Birklbauer, 2026
"""

from __future__ import annotations

import argparse
import random
import logging
import polars as pl
from pydantic import BaseModel, Field, ConfigDict, computed_field

from typing import Annotated, Optional, Literal, Any, override

__version = "2.0.0"
__date = "2026-06-05"

logger = logging.getLogger(__name__)

# these examples use the numpy docstring style
# https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard


[docs] class Character(BaseModel): r"""Core data structure representing a character. Bases Pydantic `BaseModel <https://pydantic.dev/docs/validation/latest/api/pydantic/base_model/#pydantic.BaseModel>`_. Attributes Summary ------------------ Here is a short summary about the class attributes, for more details on the specific Pydantic validation requirements please refer to the corresponding attributes themselves. Required ^^^^^^^^ The following attributes are required: name : str The name of the character. Optional ^^^^^^^^ The following attributes are optional: race : one of "Elf", "Half-Elf", "Human", or None, default = None The race of the character. Should be one of Elf, Half-Elf, or Human. min_damage : float, default = 0.0 Minimum damage the character deals. max_damage : float, default = 0.0 Maximum damage the character deals. Notes ----- Minimum and maximum damage are automatically switched depending on which is greater. Examples -------- >>> from main import Character >>> character = Character(name="John Baldur") """ name: Annotated[str, Field(frozen=True, description="Name of the character.")] r""" Name of the character. """ race: Annotated[ Optional[Literal["Elf", "Half-Elf", "Human"]], Field(frozen=True, description="Race of the character."), ] = None r""" Race of the character. Should be one of Elf, Half-Elf, or Human. """ min_damage: Annotated[ float, Field(frozen=False, description="Minimum damage the character deals.") ] = 0.0 r""" Minimum damage the character deals. Is automatically switched with max_damage if max_damage is smaller. """ max_damage: Annotated[ float, Field(frozen=False, description="Maximum damage the character deals.") ] = 0.0 r""" Maximum damage the character deals. Is automatically switched with min_damage if min_damage is greater. """ model_config = ConfigDict( validate_assignment=True, strict=True, str_strip_whitespace=True ) r""" Pydantic configuration for the underlying validation model. """ @computed_field(description="Average damage dealt by the character.") @property def avg_damage(self) -> float: r""" Average damage dealt by the character. """ return (self.min_damage + self.max_damage) / 2.0
[docs] @override def model_post_init(self, context: Any = None) -> None: r""" Performs extra validation and post init functions. Warnings -------- This method should not be called manually! """ if self.min_damage > self.max_damage: self.__dict__["min_damage"], self.__dict__["max_damage"] = ( # pyright: ignore[reportIndexIssue] self.max_damage, self.min_damage, )
def __getitem__(self, key: str) -> Any: r""" Support for dict-like access. """ try: return getattr(self, key) except AttributeError as e: raise KeyError(f"'{key}' is not a valid field!") from e def __contains__(self, key: str) -> bool: r""" Support for ``in`` operator. """ return hasattr(self, key)
[docs] def copy_with_update(self, update: dict[str, Any] = {}) -> Character: r"""Creates a deep copy of the class with optional attribute updates. Parameters ---------- update : dict of str, any, default = empty dict Dictionary mapping attribute names (str) to their updated values. The default (empty dict) will create a deep copy with the original attribute values. Returns ------- Character New character with optionally updated attributes. Examples -------- >>> from main import Character >>> character = Character(name="John Baldur") >>> new_character = character.copy_with_update(update={"race": "Human"}) """ return Character( name=update["name"] if "name" in update else self.name, race=update["race"] if "race" in update else self.race, min_damage=update["min_damage"] if "min_damage" in update else self.min_damage, max_damage=update["max_damage"] if "max_damage" in update else self.max_damage, )
[docs] def attack(self) -> float: r"""Get the attack damage of the next attack. Returns ------- float The attack damage of the attack. Examples -------- >>> from main import Character >>> character = Character(name="John Baldur") >>> character.attack() 0.0 """ return self.min_damage + (self.max_damage - self.min_damage) * random.random() # noqa: S311
[docs] def character_factory(filename: str) -> list[Character]: r"""Creates a list of characters from a file. Parameters ---------- filename : str The filename of the character ``csv`` file. Returns ------- lisf of Character The parsed list of characters. Examples -------- >>> from main import character_factory >>> characters = character_factory("data/characters.csv") >>> characters[0].name 'Astarion' """ df = pl.read_csv(filename) characters: list[Character] = list() for row in df.iter_rows(named=True): characters.append( Character( name=str(row["name"]), race=str(row["race"]) if "race" in row else None, # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] min_damage=float(row["min_damage"]), max_damage=float(row["max_damage"]), ) ) return characters
[docs] def battle( character_1: Character, character_2: Character, health: float = 100.0 ) -> Character: r"""Makes two characters fight. Parameters ---------- character_1 : Character One of the two characters that should battle. character_2 : Character One of the two characters that should battle. health : float, default = 100.0 The amount of hit points both characters have. Returns ------- Character The winner of the two characters. Examples -------- >>> from main import character_factory, battle >>> characters = character_factory("data/characters.csv") >>> winner = battle(characters[0], characters[1], health=10000) >>> winner.name 'Shadowheart' """ health_1 = health health_2 = health initiative = random.random() # noqa: S311 if initiative < 0.5: logger.info(f"Character {character_1.name} has initiative!") else: logger.info(f"Character {character_2.name} has initiative!") while True: if initiative < 0.5: attack: float = character_1.attack() logger.info(f"Character {character_1.name} deals {attack} damage!") health_2 -= attack if health_2 <= 0: break attack: float = character_2.attack() logger.info(f"Character {character_2.name} deals {attack} damage!") health_1 -= attack if health_1 <= 0: break else: attack: float = character_2.attack() logger.info(f"Character {character_2.name} deals {attack} damage!") health_1 -= attack if health_1 <= 0: break attack: float = character_1.attack() logger.info(f"Character {character_1.name} deals {attack} damage!") health_2 -= attack if health_2 <= 0: break if health_1 <= 0: logger.info(f"Character {character_2.name} won!") return character_2 logger.info(f"Character {character_1.name} won!") return character_1
##### MAIN FUNCTION #####
[docs] def main(argv: Optional[list[str]] = None) -> int: """Main function. Parameters ---------- argv : list or str, or None, default = None Arguments passed to argparse. Returns ------- int Exit status (zero is success). Examples -------- >>> from main import main >>> main(["-f", "data/characters.csv"]) INFO:main:Both characters have 130.0 hit points! The battle begins: INFO:main:Character Shadowheart has initiative! INFO:main:Character Shadowheart deals 311.13673321167755 damage! INFO:main:Character Shadowheart won! 0 """ parser = argparse.ArgumentParser( prog="main.py", description="Battles two characters.", epilog="(c) Micha Birklbauer, 2026", ) parser.add_argument( "-f", "--file", dest="file", required=True, help="character file to read characters from (str).", type=str, ) parser.add_argument( "-c1", "--character-1", dest="character_1", default=0, help="index of the first character to use (int).", type=int, ) parser.add_argument( "-c2", "--character-2", dest="character_2", default=1, help="index of the second character to use (int).", type=int, ) parser.add_argument( "-hp", "--hit-points", dest="health", default=130, help="health of all characters (int).", type=int, ) parser.add_argument("--version", action="version", version=__version) args = parser.parse_args(argv) logging.basicConfig(level=logging.INFO) try: characters = character_factory(args.file) character_1 = int(args.character_1) character_2 = int(args.character_2) health = float(args.health) logger.info(f"Both characters have {health} hit points! The battle begins:") if character_1 < 0 or character_1 >= len(characters): raise IndexError("Character 1 is not a valid index in the character file!") if character_2 < 0 or character_2 >= len(characters): raise IndexError("Character 1 is not a valid index in the character file!") _ = battle(characters[character_1], characters[character_2], health) except Exception as _e: logger.exception("An error occurred while running the script!") return 1 return 0
######## SCRIPT ######### if __name__ == "__main__": m = main()