Last active
July 9, 2024 14:56
-
-
Save nerun/28c6d387026e77621bbef3b145821504 to your computer and use it in GitHub Desktop.
A dice roller with a gaussian distribution calculator. Written in Python 3.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# -*- coding: utf-8 -*- | |
# | |
# PyDice.py - version 2.0 - 08 july 2024 | |
# Copyright (c) 2024 Daniel Dias Rodrigues <[email protected]> | |
# | |
# This program is free software; you can redistribute it and/or modify it | |
# under the terms of the Creative Commons Zero 1.0 Universal (CC0 1.0) Public | |
# Domain Dedication (https://creativecommons.org/publicdomain/zero/1.0/). | |
# | |
import sys, re # for 're', see start() | |
from itertools import product # for 'product', see gauss() | |
from random import randint | |
args = [] | |
for arg in sys.argv[1:]: | |
# argv[0] is the python script file name that's why we exclude it with [1:] | |
if arg[0] == '-': | |
# eliminate dashes from arguments similar to --help and -h | |
# do not affect dice modifiers (e.g. 1d6-1) | |
arg = re.sub('-', '', arg) | |
# lower() lower case the string | |
# strip() removes spaces at the beginning and at the end of the string | |
args.append(arg.lower().strip()) | |
def span(txt='missing text', color='', style=''): | |
txt = str(txt) | |
color = str(color).lower() | |
style = str(style).lower() | |
colors = {'red':'1', 'green':'2', 'yellow':'3', 'blue':'4', 'magenta':'5', 'cyan':'6', 'white':'7', '':'\033[0', | |
'r':'1', 'g':'2', 'y':'3', 'b':'4', 'm':'5', 'c':'6', 'w':'7'} | |
styles = {'bold':'1', 'faint':'2', 'italic':'3', 'underline':'4', 'blink':'5', 'mark':'7', '':'m', | |
'b':'1', 'f':'2', 'i':'3', 'u':'4', 'k':'5', 'm':'7'} | |
if color not in colors.keys() or style not in styles.keys(): | |
print ("""Use: span(string, 'color code', 'style code') | |
Colors red (r), green (g), yellow (y), blue (b), magenta (m), cyan (c), | |
white (w) | |
Styles bold (b), italic (i), underline (u), faint (f), blink (k), | |
mark (m) | |
""") | |
return '\033[0;7m span(): bad formatted \033[0m' | |
else: | |
color = colors[color] | |
style = styles[style] | |
if color != '\033[0': | |
color = '\033[9' + color | |
if style != 'm': | |
style = ';' + style + 'm' | |
return color + style + txt + '\033[0m' | |
def roll(qty=1, sides=6, op='+', mods=0, drop_lowest=False): | |
def SumRolls(elements): | |
Sum = 0 | |
for i in elements: | |
Sum += i | |
return Sum | |
rolls = [] | |
i = 0 | |
while i < qty: | |
D = randint(1, sides) | |
rolls.append(D) | |
i += 1 | |
if drop_lowest == True and len(rolls) >= 2: | |
rolls.remove(min(rolls)) | |
res = SumRolls(rolls) | |
match op: | |
case '+': | |
return res + mods | |
case '-': | |
return res - mods | |
case '*' | 'x': | |
return res * mods | |
case '/': | |
return res / mods | |
case _: | |
print("ERROR: operator \"" + op + "\" is not valid.") | |
def analysis(diceroll, IsGauss=False, DropLowest=False): | |
diceroll = diceroll.lower() | |
def error(): | |
print("ERROR: \""+ diceroll +"\" doesn't look properly formatted.\n") | |
Op = "+" | |
# Is properly formatted? | |
validElements = ['0','1','2','3','4','5','6','7','8','9','d','+','-','*','x','/'] | |
operators = validElements[11:] # ['+','-','*','x','/'] | |
operatorsCount = 0 | |
for i in diceroll: # check if have valid elements | |
if i not in validElements: | |
return error() | |
if diceroll[0] == "d": # d+6 >> 1d+6 | |
diceroll = "1" + diceroll | |
elif diceroll[0] in operators: # +6 >> 1d+6 | |
diceroll = "1d" + diceroll | |
if len(diceroll) == 3: # + >> 1d+1 | |
diceroll = diceroll + "1" | |
if len(diceroll) == 1 and diceroll.isnumeric(): # 1 >> 1d | |
diceroll = diceroll + "d" | |
Dindex = diceroll.index("d") + 1 # 1d+6 >> 1d6+6 | |
diceroll_length = len(diceroll)-1 | |
if Dindex > diceroll_length or diceroll[Dindex].isnumeric() == False: | |
front = diceroll[:Dindex] | |
rear = diceroll[Dindex:] | |
diceroll = front + "6" + rear | |
print(diceroll) | |
for i in diceroll: # count qty of operators | |
if i in operators: | |
operatorsCount += 1 | |
Op = i # store the operator used in this dice roll | |
if operatorsCount == 0: | |
properlyFormatted = "OK" | |
elif operatorsCount == 1: | |
properlyFormatted = "OK+" # OK, but with modifiers to be applied | |
else: | |
return error() | |
a = diceroll.split("d") # 2d6 >> ['2', '6'] | |
# 2d6+6 >> ['2', '6+6'] | |
if properlyFormatted == "OK+": | |
b = a[1].split(Op) # split '6+6' in ['6', '6'] | |
SIDES = int(b[0]) | |
MODS = int(b[1]) | |
else: | |
SIDES = int(a[1]) | |
MODS = 0 | |
QTY = int(a[0]) | |
if IsGauss == False: | |
print(str(roll(QTY, SIDES, Op, MODS, DropLowest))) | |
else: # IsGauss == True | |
gauss(QTY, SIDES, Op, MODS, DropLowest) | |
def gauss(qty, sides, op, mods, drop): | |
# create a list of all elements in a dice roll, based upon dice number of "sides" | |
elements = [] | |
i = 0 | |
while i < sides: | |
i += 1 | |
elements.append(i) | |
# cartesian product [1,2,3,4,5,6] vs. itself a number of times equal to "qty" | |
PossComb = list(product(elements, repeat=qty)) # list of tuples | |
if drop == True: | |
PossComb = [list(el) for el in PossComb] # convert to list of lists | |
for fg in PossComb: | |
fg.remove(min(fg)) | |
tPossComb = len(PossComb) | |
Dic = {} | |
p = 0 | |
while p < tPossComb: | |
k = sum(PossComb[p]) | |
p += 1 | |
if k in Dic: | |
Dic[k] += 1 | |
else: | |
Dic[k] = 1 | |
print ("\n Total possible combinations:",tPossComb) | |
print (""" | |
Dice | Gaussian Distribution | % Less or | |
Sum | Qty. | % | Eq. Result | |
-----------------------------------------""") | |
LEq = 0 | |
for i in Dic: | |
LEq += (Dic[i]/float(tPossComb)) | |
match op: | |
case '+': | |
I = i + mods | |
case '-': | |
I = i - mods | |
case '*' | 'x': | |
I = i * mods | |
case '/': | |
I = i / mods | |
case _: | |
I = i | |
print ('{:>4} {:>11} {:>12} {:>11}'.format(I, '{:,}'.format(Dic[i]), '{:.4%}'.format(Dic[i]/float(tPossComb)), '{:.4%}'.format(LEq))) | |
print("") | |
def brp(): | |
print(""" | |
Compatible with: | |
Avalon Hill RuneQuest 3E | |
Chaosium Basic Roleplaying | |
Call of Cthulhu | |
Design Mechanism Mythras | |
Mongoose RuneQuest I/II/Legend | |
""") | |
print(" APP/CHA (3d6): ", roll(3,6)) | |
print(" CON (3d6): ", roll(3,6)) | |
print(" DEX (3d6): ", roll(3,6)) | |
print(" STR (3d6): ", roll(3,6)) | |
print(" INT (2d6+6): ", roll(2,6,"+",6)) | |
print(" POW (3d6): ", roll(3,6)) | |
print(" SIZ (2d6+6): ", roll(2,6,"+",6)) | |
print(" ------------------") | |
print(" EDU (3d6+3): ", roll(3,6,"+",3)) | |
print(" EDU (2d6+6): ", str(roll(2,6,"+",6))+"\n") | |
def helpme(): | |
print("""Usage: <command> | |
Commands: | |
brp Characteristics Generator for Basic Roleplaying and | |
similar systems. | |
help, h Show this help. | |
version, v Version and copyright information. | |
<option> [roll] Options are optional (dooh!). It is possible to use | |
more than one simultaneously, in any order, e.g.: | |
'-dg' or '-gd' or '-d -g' or '-g -d'. Options can be | |
provided before or after dice roll notation. Options | |
are as follow. | |
Roll is the standard dice roll notation used in | |
Tabletop RPG: [qty]d[sides][operator][modifier]. | |
Qty, sides and modifiers are integer numbers. | |
Operators: addition + | |
subtraction - | |
multiplication * (or x) | |
division / | |
Example: '1d6-1' means 'roll 1 six-sided dice and | |
subtract 1'. | |
Options: | |
--discard-lowest Discard the die with lowest result (e.g. "-d 4d6"). | |
-d | |
--gauss, -g Calculates gaussian distribution (e.g. "-g 3d6"). | |
But, please, avoid calculating more than 10 dice as | |
it will be CPU intensive and may even freeze your | |
computer for a very long time.""") | |
def version(): | |
print("""PyDice - version 2.0 - 08 july 2024 | |
Copyright (c) 2024 Daniel Dias Rodrigues <[email protected]> | |
This program is free software; you can redistribute it and/or modify it under | |
the terms of the Creative Commons Zero 1.0 Universal (CC0 1.0) Public Domain | |
Dedication (https://creativecommons.org/publicdomain/zero/1.0).""") | |
def start(): | |
command_executed = False | |
gauss = False | |
dropL = False | |
if len(args) == 0: | |
helpme() | |
else: | |
for arg in args: | |
if arg[0].isdigit(): | |
args.append(args.pop(args.index(arg))) | |
for arg in args: | |
if command_executed and len(args) > 1: | |
print(span("warning",'yellow','bold') + | |
": commands must be executed alone. Ignoring " + | |
span(args[args.index(arg)], 'magenta', 'b') + ".") | |
else: | |
match arg: | |
case 'brp': | |
brp() | |
command_executed = True | |
case 'help' | 'h': | |
helpme() | |
command_executed = True | |
case 'version' | 'v': | |
version() | |
command_executed = True | |
case 'gauss' | 'g': | |
gauss = True | |
case 'discardlowest' | 'd': | |
dropL = True | |
case 'dg' | 'gd': | |
gauss = True | |
dropL = True | |
case _: | |
if arg[0] == 'd' or arg[0].isdigit(): | |
if arg == args[-1]: | |
analysis(arg, IsGauss=gauss, DropLowest=dropL) | |
command_executed = True | |
else: | |
print(span("error", 'red', 'b') + | |
": there's no command or option called " + | |
span(arg,'magenta','b') + ".") | |
start() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment