Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- import random
- from typing import Self
- import re
- from dataclasses import dataclass, field
- from copy import deepcopy as copy
- from cli import clear, pause, flags_in_args
- import sys
- from data_processing import try_cast, remove_items_from_list as remove_extremes
- @dataclass
- class Dice:
- """
- Represents a series of dice rolls for tabletop roleplaying games.
- """
- quantity: int = 1
- sides: int = 6
- modifier: int = 0
- down_the_line: bool = False
- keep_high: int = 0
- keep_low: int = 0
- explode: bool = False
- results: list[int] = field(default_factory=list)
- def __str__(self) -> str:
- string = f"{self.quantity}d{self.sides}"
- string += f"{'+' if self.modifier > 0 else ''}"
- string += f"{'-' if self.modifier < 0 else ''}"
- string += f"{self.modifier if self.modifier != 0 else ''}"
- string += " "
- string += f"{":dtl " if self.down_the_line else ""}"
- string += f"{f':kh{self.keep_high} ' if self.keep_high > 0 else ''}"
- string += f"{f':kl{self.keep_low} ' if self.keep_low > 0 else ''}"
- string += f"{':ex ' if self.explode else ''}"
- return string
- @staticmethod
- def _parse_options(dice: Dice, options: list[str]) -> Dice:
- for option in options:
- if option == 'dtl':
- dice.down_the_line = True
- continue
- if option.startswith("kh"):
- option = option.replace("kh", "")
- dice.keep_high = try_cast(option, int) or 0
- continue
- if option.startswith("kl"):
- option = option.replace("kl", "")
- dice.keep_low = try_cast(option, int) or 0
- continue
- # Exploding dice, only keeps rolls with maximum value
- if option.startswith("ex"):
- dice.explode = True
- continue
- return dice
- @staticmethod
- def _parse_dice(dice: Dice, string: str) -> Dice | None:
- pattern: str = r"^(?P<quantity>\d*)d(?P<sides>\d+)(?P<modifier>[+-]*\d*)$"
- match = re.search(pattern, string)
- if not match:
- return None
- quantity = match.group('quantity')
- sides = match.group('sides')
- modifier = match.group('modifier')
- dice.quantity = try_cast(quantity, int) or 1
- dice.sides = try_cast(sides, int) or 6
- dice.modifier = try_cast(modifier, int) or 0
- if dice.quantity < 1 or dice.sides < 2:
- return None
- return dice
- @staticmethod
- def from_string(string: str) -> Dice | None:
- """
- Format: # d# ±# :flag :flag
- - #1: Quantity of dice
- - #2: Sides of dice
- - #3: Modifier
- Flags:
- - kh#: keep highest # dice
- - kl#: keep lowest # dice
- - ex: explode dice (not compatible with dtl)
- - dtl: roll this dice 6 times in order for D&D scores (not compatible with ex)
- """
- dice: Dice = Dice()
- string = string.strip().replace(' ', '').lower()
- parts: list[str] = string.split(':')
- new_dice: Dice | None = Dice._parse_dice(dice, parts[0])
- if not new_dice:
- return None
- if len(parts) > 1:
- new_dice = Dice._parse_options(new_dice, parts[1::])
- return new_dice
- @staticmethod
- def from_strings(strings: list[str]) -> list[Dice]:
- queue: list[Dice] = []
- for string in strings:
- dice = Dice.from_string(string)
- if dice:
- queue.append(dice)
- return queue
- def roll(self) -> Self:
- dice = self
- dice.results = [random.randint(1, dice.sides) for _ in range(dice.quantity)]
- if dice.keep_low != 0:
- dice.results = remove_extremes(dice.results, dice.keep_low)
- if dice.keep_high != 0:
- dice.results = remove_extremes(dice.results, dice.keep_high, descending=True)
- if dice.explode:
- dice.results = list(filter(lambda x: x == dice.sides, dice.results))
- return self
- def total(self) -> int:
- return sum(self.results) + self.modifier
- def _print_exploding(self, pretty=False) -> bool:
- if self.explode and not pretty:
- print(len(self.results))
- return True
- if self.explode and pretty:
- print(f"Exploded dice: {len(self.results)} ({len(self.results)}/{self.quantity} dice rolled a {self.sides})")
- return True
- return False
- def _print_down_the_line(self, pretty=False) -> bool:
- if self.down_the_line and not pretty:
- results = ""
- for _ in range(1,6):
- results += f"{copy(self.roll()).total()} "
- print(results)
- return True
- if self.down_the_line and pretty:
- strength = copy(self.roll())
- dexterity = copy(self.roll())
- constitution = copy(self.roll())
- intelligence = copy(self.roll())
- wisdom = copy(self.roll())
- charisma = copy(self.roll())
- print(f"Strength: {strength.total()} {strength.results}")
- print(f"Dexterity: {dexterity.total()} {dexterity.results}")
- print(f"Constitution: {constitution.total()} {constitution.results}")
- print(f"Intelligence: {intelligence.total()} {intelligence.results}")
- print(f"Wisdom: {wisdom.total()} {wisdom.results}")
- print(f"Charisma: {charisma.total()} {charisma.results}")
- return True
- return False
- def print(self, pretty=False) -> None:
- if self._print_exploding(pretty) or self._print_down_the_line(pretty):
- return
- if pretty:
- print(f"{str(self)}: {self.total()} {self.results}")
- return
- print(self.total())
- return
- def help_text():
- print()
- print("Format: # d# [+-] # :flag :flag")
- print("- #1: Quantity of dice")
- print("- #2: Number of sides on dice")
- print("- [+-]: Add OR subtract")
- print("- #3: Modifier")
- print("")
- print("Flags:")
- print("- kh#: keep highest # dice")
- print("- kl#: keep lowest # dice")
- print("- ex: explode dice (not compatible with dtl)")
- print("- dtl: roll this dice 6 times in order for D&D scores (not compatible with ex)")
- print("")
- print("Example: 3d6 :dtl -> Roll 3d6 for each D&D ability score.")
- def interactive(debug=False):
- clear()
- text = input("> ")
- commands = text.strip().lower().split(" ")
- if flags_in_args(['-h', '--help', 'help', '?'], commands):
- help_text()
- pause()
- interactive(debug)
- if flags_in_args(['-d', '--debug', 'debug'], commands):
- print(f"Debug mode {'disabled' if debug else 'enabled'}.")
- pause()
- interactive(False) if debug else interactive(True)
- print()
- for die in Dice.from_strings(commands):
- die.roll().print(pretty=True)
- print(f"\nDEBUG: {str(die)}\n") if debug else None
- if flags_in_args(['-e', '--exit', 'exit'], commands):
- clear()
- exit(0)
- pause()
- interactive(debug)
- def main():
- if len(sys.argv) > 1:
- commands = [arg.lower() for arg in sys.argv]
- debug = False
- if flags_in_args(['-h', '--help', 'help', '?'], commands):
- help_text()
- return
- if flags_in_args(['-d', '--debug', 'debug'], commands):
- debug = True
- for die in Dice.from_strings(commands):
- die.roll().print(pretty=True)
- print(f"\nDEBUG: {str(die)}\n") if debug else None
- else:
- interactive()
- if __name__ == "__main__":
- main()
Add Comment
Please, Sign In to add comment