Skip to content

Instantly share code, notes, and snippets.

@nerun
Last active July 9, 2024 14:56
Show Gist options
  • Save nerun/28c6d387026e77621bbef3b145821504 to your computer and use it in GitHub Desktop.
Save nerun/28c6d387026e77621bbef3b145821504 to your computer and use it in GitHub Desktop.
A dice roller with a gaussian distribution calculator. Written in Python 3.
#!/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