Skip to content

Instantly share code, notes, and snippets.

@bferguson3
Last active October 2, 2025 20:46
Show Gist options
  • Select an option

  • Save bferguson3/2159fd4b5a0a7f5a70e28a791185c311 to your computer and use it in GitHub Desktop.

Select an option

Save bferguson3/2159fd4b5a0a7f5a70e28a791185c311 to your computer and use it in GitHub Desktop.
Sword World dice bot
#!/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)
@bferguson3
Copy link
Author

Updated:

  • !strike and !defense can now have modifiers e.g. !str 20+5 for strike power 20 and +5 damage bonus
  • !roll supports * modifier (e.g. gold rolls, !roll 2d*200)

@bferguson3
Copy link
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

@bferguson3
Copy link
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