Archives
Trending
Support
Login
clear text
XML
Django
JavaScript
MATLAB
C
C++
C#
Python
SQL
Shell
Bash
Markdown
YAML
JSON
HTML
CSS
PHP
Java
Ruby
Go
Rust
Swift
Kotlin
Arduino
TypeScript
Perl
Autohotkey
Lua
SQF
R
Scala
Haskell
Groovy
Dart
Clojure
VB.NET
Objective-C
PowerShell
Bash
CoffeeScript
Verilog
#!/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
\d*)d(?P
\d+)(?P
[+-]*\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()
Mark as private (unlisted)
for 30 minutes
for 6 hours
for 1 day
for 1 week
for 1 month
for 1 year
everlasting (like CCCP)