genesius

Advanced Dice Module

Oct 29th, 2025
542
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 7.81 KB | Source Code | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import random
  4. from typing import Self
  5. import re
  6. from dataclasses import dataclass, field
  7. from copy import deepcopy as copy
  8. from cli import clear, pause, flags_in_args
  9. import sys
  10. from data_processing import try_cast, remove_items_from_list as remove_extremes
  11.  
  12. @dataclass
  13. class Dice:
  14.     """
  15.    Represents a series of dice rolls for tabletop roleplaying games.
  16.    """
  17.     quantity: int = 1
  18.     sides: int = 6
  19.     modifier: int = 0
  20.     down_the_line: bool = False
  21.     keep_high: int = 0
  22.     keep_low: int = 0
  23.     explode: bool = False
  24.     results: list[int] = field(default_factory=list)
  25.  
  26.     def __str__(self) -> str:
  27.         string = f"{self.quantity}d{self.sides}"
  28.         string += f"{'+' if self.modifier > 0 else ''}"
  29.         string += f"{'-' if self.modifier < 0 else ''}"
  30.         string += f"{self.modifier if self.modifier != 0 else ''}"
  31.         string += " "
  32.         string += f"{":dtl " if self.down_the_line else ""}"
  33.         string += f"{f':kh{self.keep_high} ' if self.keep_high > 0 else ''}"
  34.         string += f"{f':kl{self.keep_low} ' if self.keep_low > 0 else ''}"
  35.         string += f"{':ex ' if self.explode else ''}"
  36.  
  37.         return string
  38.  
  39.     @staticmethod
  40.     def _parse_options(dice: Dice, options: list[str]) -> Dice:
  41.         for option in options:
  42.             if option == 'dtl':
  43.                 dice.down_the_line = True
  44.                 continue
  45.  
  46.             if option.startswith("kh"):
  47.                 option = option.replace("kh", "")
  48.                 dice.keep_high = try_cast(option, int) or 0
  49.                 continue
  50.  
  51.             if option.startswith("kl"):
  52.                 option = option.replace("kl", "")
  53.                 dice.keep_low = try_cast(option, int) or 0
  54.                 continue
  55.  
  56.             # Exploding dice, only keeps rolls with maximum value
  57.             if option.startswith("ex"):
  58.                 dice.explode = True
  59.                 continue
  60.  
  61.         return dice
  62.  
  63.     @staticmethod
  64.     def _parse_dice(dice: Dice, string: str) -> Dice | None:
  65.         pattern: str = r"^(?P<quantity>\d*)d(?P<sides>\d+)(?P<modifier>[+-]*\d*)$"
  66.  
  67.         match = re.search(pattern, string)
  68.         if not match:
  69.             return None
  70.  
  71.         quantity = match.group('quantity')
  72.         sides = match.group('sides')
  73.         modifier = match.group('modifier')
  74.  
  75.         dice.quantity = try_cast(quantity, int) or 1
  76.         dice.sides = try_cast(sides, int) or 6
  77.         dice.modifier = try_cast(modifier, int) or 0
  78.  
  79.         if dice.quantity < 1 or dice.sides < 2:
  80.             return None
  81.  
  82.         return dice
  83.  
  84.  
  85.     @staticmethod
  86.     def from_string(string: str) -> Dice | None:
  87.         """
  88.        Format: # d# ±# :flag :flag
  89.  
  90.        - #1: Quantity of dice
  91.        - #2: Sides of dice
  92.        - #3: Modifier
  93.  
  94.        Flags:
  95.  
  96.        - kh#: keep highest # dice
  97.        - kl#: keep lowest # dice
  98.        - ex: explode dice (not compatible with dtl)
  99.        - dtl: roll this dice 6 times in order for D&D scores (not compatible with ex)
  100.        """
  101.         dice: Dice = Dice()
  102.         string = string.strip().replace(' ', '').lower()
  103.         parts: list[str] = string.split(':')
  104.  
  105.         new_dice: Dice | None = Dice._parse_dice(dice, parts[0])
  106.  
  107.         if not new_dice:
  108.             return None
  109.  
  110.         if len(parts) > 1:
  111.             new_dice = Dice._parse_options(new_dice, parts[1::])
  112.  
  113.         return new_dice
  114.  
  115.     @staticmethod
  116.     def from_strings(strings: list[str]) -> list[Dice]:
  117.         queue: list[Dice] = []
  118.  
  119.         for string in strings:
  120.             dice = Dice.from_string(string)
  121.  
  122.             if dice:
  123.                 queue.append(dice)
  124.  
  125.         return queue
  126.  
  127.  
  128.     def roll(self) -> Self:
  129.         dice = self
  130.         dice.results = [random.randint(1, dice.sides) for _ in range(dice.quantity)]
  131.  
  132.         if dice.keep_low != 0:
  133.             dice.results = remove_extremes(dice.results, dice.keep_low)
  134.  
  135.         if dice.keep_high != 0:
  136.             dice.results = remove_extremes(dice.results, dice.keep_high, descending=True)
  137.  
  138.         if dice.explode:
  139.             dice.results = list(filter(lambda x: x == dice.sides, dice.results))
  140.  
  141.         return self
  142.  
  143.     def total(self) -> int:
  144.         return sum(self.results) + self.modifier
  145.  
  146.     def _print_exploding(self, pretty=False) -> bool:
  147.         if self.explode and not pretty:
  148.             print(len(self.results))
  149.             return True
  150.  
  151.         if self.explode and pretty:
  152.             print(f"Exploded dice: {len(self.results)} ({len(self.results)}/{self.quantity} dice rolled a {self.sides})")
  153.             return True
  154.  
  155.         return False
  156.  
  157.     def _print_down_the_line(self, pretty=False) -> bool:
  158.         if self.down_the_line and not pretty:
  159.             results = ""
  160.  
  161.             for _ in range(1,6):
  162.                 results += f"{copy(self.roll()).total()} "
  163.  
  164.             print(results)
  165.             return True
  166.  
  167.         if self.down_the_line and pretty:
  168.             strength = copy(self.roll())
  169.             dexterity = copy(self.roll())
  170.             constitution = copy(self.roll())
  171.             intelligence = copy(self.roll())
  172.             wisdom = copy(self.roll())
  173.             charisma = copy(self.roll())
  174.  
  175.             print(f"Strength: {strength.total()} {strength.results}")
  176.             print(f"Dexterity: {dexterity.total()} {dexterity.results}")
  177.             print(f"Constitution: {constitution.total()} {constitution.results}")
  178.             print(f"Intelligence: {intelligence.total()} {intelligence.results}")
  179.             print(f"Wisdom: {wisdom.total()} {wisdom.results}")
  180.             print(f"Charisma: {charisma.total()} {charisma.results}")
  181.             return True
  182.  
  183.         return False
  184.  
  185.     def print(self, pretty=False) -> None:
  186.         if self._print_exploding(pretty) or self._print_down_the_line(pretty):
  187.             return
  188.  
  189.         if pretty:
  190.             print(f"{str(self)}: {self.total()} {self.results}")
  191.             return
  192.  
  193.         print(self.total())
  194.         return
  195.  
  196.  
  197. def help_text():
  198.     print()
  199.     print("Format: # d# [+-] # :flag :flag")
  200.     print("- #1: Quantity of dice")
  201.     print("- #2: Number of sides on dice")
  202.     print("- [+-]: Add OR subtract")
  203.     print("- #3: Modifier")
  204.     print("")
  205.     print("Flags:")
  206.     print("- kh#: keep highest # dice")
  207.     print("- kl#: keep lowest # dice")
  208.     print("- ex: explode dice (not compatible with dtl)")
  209.     print("- dtl: roll this dice 6 times in order for D&D scores (not compatible with ex)")
  210.     print("")
  211.     print("Example: 3d6 :dtl -> Roll 3d6 for each D&D ability score.")
  212.  
  213.  
  214. def interactive(debug=False):
  215.     clear()
  216.     text = input("> ")
  217.     commands = text.strip().lower().split(" ")
  218.  
  219.     if flags_in_args(['-h', '--help', 'help', '?'], commands):
  220.         help_text()
  221.         pause()
  222.         interactive(debug)
  223.  
  224.     if flags_in_args(['-d', '--debug', 'debug'], commands):
  225.         print(f"Debug mode {'disabled' if debug else 'enabled'}.")
  226.         pause()
  227.         interactive(False) if debug else interactive(True)
  228.  
  229.     print()
  230.  
  231.     for die in Dice.from_strings(commands):
  232.         die.roll().print(pretty=True)
  233.         print(f"\nDEBUG: {str(die)}\n") if debug else None
  234.  
  235.     if flags_in_args(['-e', '--exit', 'exit'], commands):
  236.         clear()
  237.         exit(0)
  238.  
  239.     pause()
  240.     interactive(debug)
  241.  
  242.  
  243. def main():
  244.     if len(sys.argv) > 1:
  245.         commands = [arg.lower() for arg in sys.argv]
  246.         debug = False
  247.  
  248.         if flags_in_args(['-h', '--help', 'help', '?'], commands):
  249.             help_text()
  250.             return
  251.  
  252.         if flags_in_args(['-d', '--debug', 'debug'], commands):
  253.             debug = True
  254.  
  255.         for die in Dice.from_strings(commands):
  256.             die.roll().print(pretty=True)
  257.             print(f"\nDEBUG: {str(die)}\n") if debug else None
  258.  
  259.     else:
  260.         interactive()
  261.  
  262. if __name__ == "__main__":
  263.     main()
  264.  
Add Comment
Please, Sign In to add comment