Last active
October 2, 2025 20:46
-
-
Save bferguson3/2159fd4b5a0a7f5a70e28a791185c311 to your computer and use it in GitHub Desktop.
Sword World dice bot
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
| #!/opt/homebrew/bin/python3 | |
| # # # # # # # # # # # # # # | |
| # | |
| # Sword World 1.0/2.5 dice bot | |
| # | |
| # commands: !h[elp], !r[oll], !c[rit], !s[trike], !d[efense], !n[ew] | |
| # | |
| # How to use: | |
| # 1. Login to Discord's developer home (https://discord.com/developers/applications) | |
| # 2. Select New Application and fill in your bot's name, description etc. as desired | |
| # 3. Copy the Public Key into "discord_bot_key.txt" in the same folder as this script | |
| # 4. Run with $ python3 ./swdicebot.py | |
| # | |
| # * To install discord for python: $ python3 -m pip install discord | |
| # * On Windows, use "python" instead of "python3" | |
| # | |
| # | |
| # Features: | |
| # - Auto-success and auto-fail for !roll checks | |
| # - 1/2D support (0.5d) | |
| # - Bursting criticals properly implemented | |
| # - Strike and def rolls 0-100 with modifiers | |
| # - Comment support for rolls (e.g. !roll 2d+2 goblin A) | |
| # - !new supports all 1.0 races and CR1 2.5 races | |
| # - Multiple commands supported (split by line) | |
| # | |
| # Examples: | |
| # !roll 2d+6 | |
| # !r 0.5d+3 | |
| # !r 2d*400 | |
| # !roll 2d this is a comment | |
| # !crit 11 | |
| # !str 22 + 3 | |
| # !s 10+2 monster A | |
| # !def 15 - 1 | |
| # !new human 2.5 | |
| # !n dwarf 1.0 | |
| # | |
| # (c)2025 bferguson3 @ github aka Bent | |
| # | |
| # # # # # # # # # # # | |
| import discord,os,random,re,math,time | |
| intents = discord.Intents.all() | |
| client = discord.Client(command_prefix="!", intents=intents) | |
| users_list = [] | |
| class DiceUser(): | |
| def __init__(self, username="", crit=12): | |
| self.username = username | |
| self.crit = crit | |
| ### | |
| ### | |
| strike_table=[ | |
| [0,0,0,1,2,2,3,3,4,4], | |
| [0,0,0,1,2,2,3,3,4,4], | |
| [0,0,0,1,2,3,4,4,4,4], | |
| [0,0,1,1,2,3,4,4,4,5], | |
| [0,0,1,2,2,3,4,4,5,5], | |
| [0,1,1,2,2,3,4,5,5,5], | |
| [0,1,1,2,3,3,4,5,5,5], | |
| [0,1,1,2,3,4,4,5,5,6], | |
| [0,1,2,2,3,4,4,5,6,6], | |
| [0,1,2,3,3,4,4,5,6,7], | |
| [1,1,2,3,3,4,5,5,6,7], | |
| [1,2,2,3,3,4,5,6,6,7], | |
| [1,2,2,3,4,4,5,6,6,7], | |
| [1,2,3,3,4,4,5,6,7,7], | |
| [1,2,3,4,4,4,5,6,7,8], | |
| [1,2,3,4,4,5,5,6,7,8], | |
| [1,2,3,4,4,5,6,7,7,8], | |
| [1,2,3,4,5,5,6,7,7,8], | |
| [1,2,3,4,5,6,6,7,7,8], | |
| [1,2,3,4,5,6,7,7,8,9], | |
| [1,2,3,4,5,6,7,8,9,10], | |
| [1,2,3,4,6,6,7,8,9,10], | |
| [1,2,3,5,6,6,7,8,9,10], | |
| [2,2,3,5,6,7,7,8,9,10], | |
| [2,3,4,5,6,7,7,8,9,10], | |
| [2,3,4,5,6,7,8,8,9,10], | |
| [2,3,4,5,6,8,8,9,9,10], | |
| [2,3,4,6,6,8,8,9,9,10], | |
| [2,3,4,6,6,8,9,9,10,10], | |
| [2,3,4,6,7,8,9,9,10,10], | |
| [2,4,4,6,7,8,9,10,10,10], | |
| [2,4,5,6,7,8,9,10,10,11], | |
| [3,4,5,6,7,8,10,10,10,11], | |
| [3,4,5,6,8,8,10,10,10,11], | |
| [3,4,5,6,8,9,10,10,11,11], | |
| [3,4,5,7,8,9,10,10,11,12], | |
| [3,5,5,7,8,9,10,11,11,12], | |
| [3,5,6,7,8,9,10,11,12,12], | |
| [3,5,6,7,8,10,10,11,12,13], | |
| [4,5,6,7,8,10,11,11,12,13], | |
| [4,5,6,7,9,10,11,11,12,13], | |
| [4,6,6,7,9,10,11,12,12,13], | |
| [4,6,7,7,9,10,11,12,13,13], | |
| [4,6,7,8,9,10,11,12,13,14], | |
| [4,6,7,8,10,10,11,12,13,14], | |
| [4,6,7,9,10,10,11,12,13,14], | |
| [4,6,7,9,10,10,12,13,13,14], | |
| [4,6,7,9,10,11,12,13,13,15], | |
| [4,6,7,9,10,12,12,13,13,15], | |
| [4,6,7,10,10,12,12,13,14,15], | |
| [4,6,8,10,10,12,12,13,15,15], | |
| [5,7,8,10,10,12,12,13,15,15], | |
| [5,7,8,10,11,12,12,13,15,15], | |
| [5,7,9,10,11,12,12,14,15,15], | |
| [5,7,9,10,11,12,13,14,15,16], | |
| [5,7,10,10,11,12,13,14,16,16], | |
| [5,8,10,10,11,12,13,15,16,16], | |
| [5,8,10,11,11,12,13,15,16,17], | |
| [5,8,10,11,12,12,13,15,16,17], | |
| [5,9,10,11,12,12,14,15,16,17], | |
| [5,9,10,11,12,13,14,15,16,18], | |
| [5,9,10,11,12,13,14,16,17,18], | |
| [5,9,10,11,13,13,14,16,17,18], | |
| [5,9,10,11,13,13,15,17,17,18], | |
| [5,9,10,11,13,14,15,17,17,18], | |
| [5,9,10,12,13,14,15,17,18,18], | |
| [5,9,10,12,13,15,15,17,18,19], | |
| [5,9,10,12,13,15,16,17,19,19], | |
| [5,9,10,12,14,15,16,17,19,19], | |
| [5,9,10,12,14,16,16,17,19,19], | |
| [5,9,10,12,14,16,17,18,19,19], | |
| [5,9,10,13,14,16,17,18,19,20], | |
| [5,9,10,13,15,16,17,18,19,20], | |
| [5,9,10,13,15,16,17,19,20,21], | |
| [6,9,10,13,15,16,18,19,20,21], | |
| [6,9,10,13,16,16,18,19,20,21], | |
| [6,9,10,13,16,17,18,19,20,21], | |
| [6,9,10,13,16,17,18,20,21,22], | |
| [6,9,10,13,16,17,19,20,22,23], | |
| [6,9,10,13,16,18,19,20,22,23], | |
| [6,9,10,13,16,18,20,21,22,23], | |
| [6,9,10,13,17,18,20,21,22,23], | |
| [6,9,10,14,17,18,20,21,23,24], | |
| [6,9,11,14,17,18,20,21,23,24], | |
| [6,9,11,14,17,19,20,21,23,24], | |
| [6,9,11,14,17,19,21,22,23,24], | |
| [7,10,11,14,17,19,21,22,23,25], | |
| [7,10,12,14,17,19,21,22,24,25], | |
| [7,10,12,14,18,19,21,22,24,25], | |
| [7,10,12,15,18,19,21,22,24,26], | |
| [7,10,12,15,18,19,21,23,25,26], | |
| [7,11,13,15,18,19,21,23,25,26], | |
| [7,11,13,15,18,20,21,23,25,27], | |
| [8,11,13,15,18,20,22,23,25,27], | |
| [8,11,13,16,18,20,22,23,25,28], | |
| [8,11,14,16,18,20,22,23,26,28], | |
| [8,11,14,16,19,20,22,23,26,28], | |
| [8,12,14,16,19,20,22,24,26,28], | |
| [8,12,15,16,19,20,22,24,27,28], | |
| [8,12,15,17,19,20,22,24,27,29], | |
| [8,12,15,18,19,20,22,24,27,30] | |
| ] | |
| @client.event | |
| async def on_ready(): | |
| print("We have logged in as {0.user}".format(client)) | |
| def RollDie(msg, message, _time, canFail = True): | |
| _arraymsg = "" | |
| _aut = message.author.display_name | |
| _chunks = msg.lower().split(" ") | |
| ## parse msg: | |
| die = '1' | |
| mod = 0 | |
| next_is_mod = False | |
| next_is_mult = False | |
| next_is_neg = False | |
| dieFound = False | |
| mult = 1 | |
| comment = '' | |
| for c in _chunks: | |
| _isc = True # special cases that get number and continue | |
| if next_is_mod == True: | |
| next_is_mod = False | |
| mod = int(c) | |
| continue | |
| if next_is_mult == True: | |
| next_is_mult = False | |
| mult = int(c) | |
| continue | |
| if next_is_neg == True: | |
| next_is_neg = False | |
| mod = int(c) * -1 | |
| continue | |
| if c.find("!") != -1: # skip command word | |
| continue | |
| if (c.find("d") != -1): # this chunk has the die count | |
| if (dieFound == False): | |
| die = c.split("d")[0] | |
| dieFound = True | |
| _isc = False | |
| if c.find("+") != -1: # mod is + | |
| _isc = False | |
| if len(c) > 1: | |
| mod = int(c.split("+")[1]) | |
| else: | |
| next_is_mod = True | |
| if c.find("-") != -1: # mod is - | |
| _isc = False | |
| if (len(c)) > 1: | |
| mod = int(c.split("-")[1]) | |
| else: | |
| next_is_neg = True | |
| if c.find("*") != -1: # multiplier | |
| _isc = False | |
| if(len(c)) > 1: | |
| mult = int(c.split("*")[1]) | |
| else: | |
| next_is_mult = True | |
| if _isc == True: # otherwise, add as comment | |
| comment += c + " " | |
| ## done parsing, should have mod, mult, die and comment | |
| roll = [] | |
| tot = 0 | |
| die = float(die) | |
| if(math.floor(die) != die): | |
| if msg.find(".5") == -1: # validation: ".5" strings only | |
| _arraymsg += "Error: only use 0.5 die increments!\n" | |
| print(_time, msg + " | ",": error") | |
| return _arraymsg | |
| roll.append(random.randint(1, 3)) | |
| tot += roll[len(roll)-1] | |
| if (math.floor(die) == die): | |
| die = int(die) # flatten if its an integer | |
| _msg = "[" + _aut + ": " + str(die) + "d+" + str(mod) + "] " | |
| die = math.floor(die) # cast | |
| while die > 0: | |
| roll.append(random.randint(1, 6)) | |
| tot += roll[len(roll)-1] | |
| die -= 1 | |
| if canFail == True: | |
| if(len(roll) == 2): | |
| if(tot == 2): | |
| _arraymsg += "[" + _aut + " ] Auto-fail!! (1, 1) " + comment + "\n" | |
| print(_time, msg + " | ","Auto-fail : 1, 1") | |
| return _arraymsg | |
| if(tot == 12): | |
| _arraymsg += "[" + _aut + " ] Auto-success!! (6, 6) " + comment + "\n" | |
| print(_time, msg + " | ","Auto-success : 6, 6") | |
| return _arraymsg | |
| _msg += "Result: **" + str((tot + mod)*mult) + "** (" | |
| for r in roll: | |
| _msg += str(r) + ", " | |
| _msg = _msg[:len(_msg)-2] | |
| _msg += " + " + str(mod) + ")" | |
| if len(comment) > 1: | |
| _msg += " | " + comment | |
| _arraymsg += _msg + "\n" | |
| return _arraymsg | |
| ### | |
| def RollStrike(msg, message, _time): | |
| _aut = message.author.display_name | |
| _arraymsg = "" | |
| bonus_dmg = 0 | |
| _chunks = msg.split(" ") | |
| comment = "" | |
| _allc = False | |
| for c in _chunks: # add all non-numbers as comments | |
| if _allc: | |
| comment += c + " " | |
| continue | |
| if c.find("!") != -1: | |
| continue | |
| if re.sub("\\D", "", c) != "": | |
| _allc = True | |
| continue | |
| comment += c + " " | |
| sp = msg | |
| if sp.find("+") != -1: # modifier + | |
| sp = sp.split("+") | |
| bonus_dmg = int(re.sub("\\D","",sp[1])) | |
| sp = sp[0] | |
| elif sp.find("-") != -1:# modifier - | |
| sp = sp.split("-") | |
| bonus_dmg = int(re.sub("\\D","",sp[1])) * -1 | |
| sp = sp[0] | |
| sp = int(re.sub('\\D','', sp)) | |
| ## sp : int strike power, | |
| if (sp < 0) or (sp > 100): | |
| _arraymsg += "Strike range is between 0-100\n" | |
| print(_time, msg + " | ",": error") | |
| return _arraymsg | |
| roll = random.randint(1, 6) | |
| roll += random.randint(1, 6) | |
| if roll == 2: # auto-fail | |
| _arraymsg += "[" + _aut + "] Miss!! (Auto-fail) " + comment + "\n" | |
| print(_time, msg + " | ",": Miss, auto-fail") | |
| return _arraymsg | |
| _msg = "" | |
| burst = False | |
| _fnd = False # find user's crit number, add 12 if doesn't exist | |
| for u in users_list: | |
| if message.author.display_name == u.username: | |
| _fnd = True | |
| crit_target = u.crit | |
| if _fnd == False: | |
| users_list.append(DiceUser(username=message.author.display_name, crit=12)) | |
| crit_target = 12 | |
| if roll >= crit_target: | |
| _msg += "Critical! " | |
| f = random.randint(1, 6) + random.randint(1, 6) | |
| bonus_dmg += strike_table[sp][f - 3] | |
| while(f >= crit_target): | |
| _msg += "Bursting!! " | |
| burst = True | |
| f = random.randint(1, 6) + random.randint(1, 6) | |
| if(f == 2): | |
| break | |
| bonus_dmg += strike_table[sp][f - 3] | |
| roll -= 3 # range of 0-9 | |
| v = strike_table[sp][roll] | |
| _finalmsg = "[" + _aut + "] Damage: **" + _msg + str(v + bonus_dmg) + "** (rolled " + str(roll+3) + ")" + " " + comment | |
| return _finalmsg | |
| ### | |
| def RollDefense(msg, message, _time): | |
| _aut = message.author.display_name | |
| _arraymsg = "" | |
| bonus_dmg = 0 | |
| _chunks = msg.split(" ") | |
| comment = "" | |
| _allc = False | |
| for c in _chunks: # get comments for def roll | |
| if _allc: | |
| comment += c + " " | |
| continue | |
| if c.find("!") != -1: | |
| continue | |
| if re.sub("\\D", "", c) != "": | |
| _allc = True | |
| continue | |
| comment += c + " " | |
| sp = msg | |
| if sp.find("+") != -1: # positive mod | |
| sp = sp.split("+") | |
| bonus_dmg = int(re.sub("\\D","",sp[1])) | |
| sp = sp[0] | |
| elif sp.find("-") != -1: # negative mod | |
| sp = sp.split("-") | |
| bonus_dmg = int(re.sub("\\D","",sp[1])) * -1 | |
| sp = sp[0] | |
| sp = int(re.sub('\\D','', sp)) | |
| if (sp < 0) or (sp > 100): | |
| _arraymsg += "[" + _aut + "] Defense range is between 0-100\n" | |
| print(_time, msg + " | ",": error") | |
| return _arraymsg | |
| roll = random.randint(1, 6) | |
| roll += random.randint(1, 6) | |
| if roll == 2: | |
| _arraymsg += "Zero!! (Auto-fail)\n" | |
| print(_time, msg + " | ",": 0, auto-fail") | |
| return _arraymsg | |
| _msg = "" | |
| roll -= 3 # range of 0-9 | |
| v = strike_table[sp][roll] | |
| _finalmsg = "[" + _aut + "] Defense: **" + _msg + str(v + bonus_dmg) + "** (rolled " + str(roll+3) + ")" + " " + comment | |
| #await message.channel.send(_finalmsg) | |
| return _finalmsg | |
| ### | |
| def SetCritical(msg, message, _time): | |
| crit_target = int(re.sub('\\D','',msg)) | |
| _aut = message.author.display_name | |
| _arraymsg = "" | |
| _found = False | |
| for u in users_list: | |
| if u.username == _aut: | |
| _found = True | |
| u.crit = crit_target | |
| break | |
| if _found == False: | |
| _new = DiceUser() | |
| _new.username = _aut | |
| _new.crit = crit_target | |
| users_list.append(_new) | |
| _finalmsg = _aut + ", your critical target is set to: **" + str(crit_target) + "**" | |
| _arraymsg += _finalmsg + "\n" | |
| return _finalmsg | |
| #await message.channel.send(_finalmsg) | |
| ### | |
| _10_races = [ | |
| ("human", [ "2d", "2d", "2d", "2d", "2d", "2d", "2d", "2d" ]), | |
| ("dwarf", [ "2d+6", "0.5d", "1d+4", "1d", "1d+4", "1d+6", "2d+4", "2d+4" ]), | |
| ("elf", [ "1d+6", "1d+6", "1d+6", "1d+6", "1d", "0.5d", "1d+4", "1d+6" ]), | |
| ("grassrunner", [ "1d+6", "2d+4", "1d+6", "1d", "0.5d", "0.5d", "2d+6", "2d+4" ]), | |
| ("half-elf", [ "1d+4", "1d+6", "1d+4", "1d+6", "1d+2", "1d+2", "1d+4", "2d" ]) | |
| ] | |
| # Dex, Agi, Int, Str, Life, Mental | |
| # AB. BC. CD. EF. FG. GH | |
| _25_races = [ | |
| ("human", [ "2d", "2d", "2d", "2d", "2d", "2d" ]), | |
| ("dwarf", [ "2d+6", "1d", "2d", "2d", "1d", "2d+6" ]), | |
| ("elf", [ "2d", "2d", "1d", "2d", "2d", "2d" ]), | |
| ("tabbit", [ "1d", "1d", "1d", "2d", "2d+6", "2d" ]), | |
| ("nightmare", [ "2d", "2d", "1d", "1d", "2d", "2d" ]), | |
| ("runefolk", [ "2d", "1d", "2d", "2d", "2d", "1d" ]), | |
| ("lycant", [ "1d", "1d+3", "2d", "2d", "1d+6", "1d" ]) | |
| ] | |
| def NewCharacter(msg, message, _time): | |
| _finalmsg = "" | |
| msg = msg.split(" ") | |
| mode = 2.5 # 1 or 2.5 | |
| race = "" | |
| for t in msg: | |
| if t == "2.5": | |
| mode = 2.5 | |
| if (t == "1") or (t == "1.0"): | |
| mode = 1 | |
| if (t == "dwarf") or (t == "human") or (t == "elf") or (t == "grassrunner") or (t == "runefolk") or (t == "nightmare") or (t=="tabbit"): | |
| race = t | |
| if (t == "half-elf") or (t == "halfelf"): | |
| race = "half-elf" | |
| if (t == "lykant") or (t == "lycant"): | |
| race = "lycant" | |
| races = [] | |
| if mode == 1: | |
| races = _10_races | |
| else: | |
| races = _25_races | |
| for r in races: | |
| if r[0] == race: | |
| _i = 0 | |
| for d in r[1]: | |
| _i += 1 | |
| _tm = "!roll " + d | |
| _finalmsg += chr(0x40 + _i) + ": " + RollDie(_tm, message, _time, canFail=False) | |
| break | |
| return _finalmsg | |
| ### | |
| @client.event | |
| async def on_message(message): | |
| random.seed() | |
| _arraymsg = "" | |
| if message.author == client.user: | |
| return | |
| commands = message.content.lower().split("\n") | |
| for msg in commands: | |
| _time = time.strftime("%H:%M:%S | ", time.gmtime()) | |
| if msg.startswith("!r"): | |
| try: # split at the "d": | |
| if msg.find("d") != -1: | |
| ## | |
| _msg = RollDie(msg, message, _time) | |
| _arraymsg += _msg # contains newline | |
| ## | |
| print(_time, msg + " | ",_msg) | |
| else: | |
| raise "No d string" | |
| except Exception as e: | |
| _arraymsg += "Use dice format: xd+n\n e.g. `!roll 2d-1`, `!r 2d*200`, `!r 0.5d+6`" | |
| print(_time, msg + " | ",": error", e) | |
| ### | |
| elif msg.startswith("!s"): | |
| try: | |
| ## | |
| _finalmsg = RollStrike(msg, message, _time) | |
| _arraymsg += _finalmsg + "\n" | |
| ## | |
| print(_time, msg + " | ",_finalmsg) | |
| except Exception as e: | |
| _arraymsg += "Usage: !strike *n* where n = strike power" + "\n e.g. `!str 20` or `!s 15+4`" | |
| print(_time, msg + " | ",": error", e) | |
| ### | |
| elif msg.startswith("!d"): | |
| try: | |
| ## | |
| _finalmsg = RollDefense(msg, message, _time) | |
| _arraymsg += _finalmsg + "\n" | |
| ## | |
| print(_time, msg + " | ",_finalmsg) | |
| except Exception as e: | |
| _arraymsg += "Usage: !def *n* where n = defense number" + "\n e.g. `!def 12` or `!d 10+2`" | |
| print(_time, msg + " | ",e) | |
| ### | |
| elif msg.startswith("!c"): | |
| try: | |
| ## | |
| _finalmsg = SetCritical(msg, message, _time) | |
| _arraymsg += _finalmsg | |
| ## | |
| print(_time, msg + " | ",_finalmsg) | |
| except Exception as e: | |
| #await message.channel.send("Not a valid number?") | |
| _arraymsg += "Usage: !crit *n* where n = target critical number\n" | |
| print(_time, msg + " | ",e) | |
| ### | |
| elif msg.startswith("!n"): | |
| try: | |
| ## | |
| _finalmsg = NewCharacter(msg, message, _time) | |
| if _finalmsg == "": | |
| raise "Empty string" | |
| _arraymsg += _finalmsg | |
| ## | |
| print(_time, msg + " | " + _finalmsg) | |
| except Exception as e: | |
| _arraymsg += "Usage: !new *race [edition]* where race = any 1.0 or CR1 2.5 race and edition = 1.0 or 2.5\n e.g. `!new human 2.5` or `!n half-elf 1.0`" | |
| print(_time, msg + " | ",e) | |
| ### | |
| elif msg.startswith("!h"): | |
| _arraymsg += "To use me:\n\n**!r** or **!roll** *x*d+*n* to roll dice\n You can also do e.g. `!roll 2d*200` for multipliers\n or add text for comments.\n\n**!s** or **!strike** *n* to do a strike check (**!d**/**efend** *n* for defense check)\n You can add or subtract values to this roll using +/-.\n\n**!c** or **!crit** *n* to set the critical number for your current display name\n\n**!n** or **!new** *race [1.0 / 2.5]* will roll base stats for the given race\n e.g. `!new dwarf 1.0` will roll A-H for a SW1.0 Dwarf.\n\n**!h** or **!help** to see this text\n\nYou can also send multiple commands at once by splitting them one per line." | |
| print(_time, message.author.display_name + " | ",": requested help") | |
| ### | |
| if _arraymsg != "": | |
| await message.channel.send(_arraymsg) | |
| ### | |
| f = open("discord_bot_key.txt", "r") | |
| key = f.read() | |
| f.close() | |
| client.run(key) |
Author
Author
Updated:
- Supports multiple commands in single message, spilt by newline
- Added
!new <race> <1.0 / 2.5>to generate PC stat blocks
supports all 1.0 races and CRI 2.5 races
Author
updated:
- !r, !s, !d now supports line comments
- incomplete or incorrect inputs will display "Usage: xxx" for all commands
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated:
!str 20+5for strike power 20 and +5 damage bonus!roll 2d*200)