DUNGEONS & DRAGONS
& PYTHON
Epic Adventures with Prompt-Toolkit and Friends
BY MIKE PIRNAT
Twitter: @mpirnat
Email: [email protected]
Slide 2
Slide 2 text
Prologue
Slide 3
Slide 3 text
with a mysterious stranger...
It begins at a tavern...
...who seeks your aid
Slide 4
Slide 4 text
Preparing Encounters
• Read an encounter
• Look up the monsters
• Copy stat blocks to an encounter sheet
• Painstaking, time-consuming, repetitive!
Slide 5
Slide 5 text
Preparing Encounters
• Read an encounter
• Look up the monsters
• Copy stat blocks to an encounter sheet
• Painstaking, time-consuming, repetitive!
• A lot of work even for a shorter adventure
• Doesn’t scale to a big campaign
• Especially if it’s a “sandbox”
Slide 6
Slide 6 text
Running Encounters
• Everyone rolls for initiative (turn order)
• Copy names onto a numbered list
• Play goes from highest to lowest number
• High to low cycle repeats
• Can be a lot of extra rolling for the DM
• Easy – and embarrassing – to lose track
of whose turn it is!
Slide 7
Slide 7 text
No content
Slide 8
Slide 8 text
No content
Slide 9
Slide 9 text
No content
Slide 10
Slide 10 text
No content
Slide 11
Slide 11 text
No content
Slide 12
Slide 12 text
One: Prompt-Toolkit
Slide 13
Slide 13 text
Prompt-Toolkit
• Library for creating interactive command line and terminal applications in Python
• Supports many features...
- Getting & validating input
- Styled output, colors, syntax highlighting
- Auto suggestion & completion
- Customizable key bindings
- Multiline input
- And more!
Slide 14
Slide 14 text
Getting Started
from prompt_toolkit import prompt
Slide 15
Slide 15 text
Getting Started
from prompt_toolkit import prompt
user_input = prompt("> ")
print(f"You entered '{user_input}'")
Slide 16
Slide 16 text
Getting Started
from prompt_toolkit import prompt
while True:
user_input = prompt("> ")
print(f"You entered '{user_input}'")
Slide 17
Slide 17 text
Getting Started
from prompt_toolkit import PromptSession
session = PromptSession()
while True:
user_input = session.prompt("> ")
print(f"You entered '{user_input}'")
Slide 18
Slide 18 text
No content
Slide 19
Slide 19 text
Auto Suggestion
from prompt_toolkit import PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
session = PromptSession(
auto_suggest=AutoSuggestFromHistory()
)
while True:
user_input = session.prompt("> ")
print(f"You entered '{user_input}'")
Slide 20
Slide 20 text
No content
Slide 21
Slide 21 text
Auto Completion
from prompt_toolkit.completion import WordCompleter
Slide 22
Slide 22 text
Auto Completion
from prompt_toolkit.completion import WordCompleter
words = ["roll", "start", "next", "stop", "quit"]
Slide 23
Slide 23 text
Auto Completion
from prompt_toolkit.completion import WordCompleter
words = ["roll", "start", "next", "stop", "quit"]
completer = WordCompleter(words)
Slide 24
Slide 24 text
Auto Completion
from prompt_toolkit.completion import WordCompleter
words = ["roll", "start", "next", "stop", "quit"]
completer = WordCompleter(words)
session = PromptSession(
completer=completer
)
Slide 25
Slide 25 text
No content
Slide 26
Slide 26 text
Bottom Toolbar
def bottom_toolbar():
return "ctl-d or ctl-c to exit"
Slide 27
Slide 27 text
Bottom Toolbar
def bottom_toolbar():
return "ctl-d or ctl-c to exit"
session = PromptSession(
bottom_toolbar=bottom_toolbar
)
Slide 28
Slide 28 text
No content
Slide 29
Slide 29 text
Bottom Toolbar
def bottom_toolbar():
return "ctl-d or ctl-c to exit"
TOML
• Tom’s Obvious Minimal Language
• Data serialization language
• Designed for minimal config file format with obvious semantics
• Alternative to YAML and JSON
• Lets us define things in data without having to hard code them
• Because it’s plain text, we can keep it in git, version it, branch it, etc.
name = "Goblin Ambush"
location = "Triboar Trail"
notes = """
Four goblins are hiding in the woods, ...
"""
[groups.goblins]
monster = "goblin"
count = 4
Slide 46
Slide 46 text
Attrs
• Makes it easy to create classes without boilerplate
• Spares us from meaningless __init__ methods!
• When you want a data class that’s a little bit more
• Has a more minimal syntax and a more verbose syntax (I prefer the latter)
Slide 47
Slide 47 text
from attr import attrs, attrib
Slide 48
Slide 48 text
from attr import attrs, attrib
@attrs
class Character:
Slide 49
Slide 49 text
from attr import attrs, attrib
@attrs
class Character:
name = attrib(default="Alice")
race = attrib(default="Human")
cclass = attrib(default="Fighter")
level = attrib(default=1)
max_hp = attrib(default=10)
cur_hp = attrib(default=10)
ac = attrib(default=0)
Slide 50
Slide 50 text
from attr import Factory as attr_factory
@attrs
class Character:
...
senses = attrib(default=attr_factory(dict))
Slide 51
Slide 51 text
import toml
Slide 52
Slide 52 text
import toml
def load_party(party_file):
with open(party_file, "r") as fin:
Slide 53
Slide 53 text
import toml
def load_party(party_file):
with open(party_file, "r") as fin:
party = toml.load(fin)
Slide 54
Slide 54 text
import toml
def load_party(party_file):
with open(party_file, 'r') as fin:
party = toml.load(fin)
return {x["name"]: Character(**x) for x in party.values()}
Slide 55
Slide 55 text
Click
• Helps write beautiful command line interfaces
• Makes it super easy to wire up well-behaved parameters
• Makes nice help output
• Well-covered in other talks – go check one of those out!
Slide 56
Slide 56 text
while True:
user_input = session.prompt("> ")
print(f"You entered '{user_input}'")
Slide 57
Slide 57 text
def main_loop():
while True:
user_input = session.prompt("> ")
print(f"You entered '{user_input}'")
if __name__ == "__main__":
main_loop()
import re
DICE_EXPR = re.compile(r"^(\d+)d(\d+)\+?(\-?\d+)?$")
How many dice
How many sides
Plus or minus anything?
Slide 82
Slide 82 text
import re
DICE_EXPR = re.compile(r"^(\d+)d(\d+)\+?(\-?\d+)?$")
def roll_dice_expr(value):
m = DICE_EXPR.match(value)
Slide 83
Slide 83 text
import re
DICE_EXPR = re.compile(r"^(\d+)d(\d+)\+?(\-?\d+)?$")
def roll_dice_expr(value):
m = DICE_EXPR.match(value)
if not m:
raise ValueError(f"Invalid dice expression '{value}'")
Slide 84
Slide 84 text
import re
DICE_EXPR = re.compile(r"^(\d+)d(\d+)\+?(\-?\d+)?$")
def roll_dice_expr(value):
m = DICE_EXPR.match(value)
if not m:
raise ValueError(f"Invalid dice expression '{value}'")
times, sides, modifier = m.groups()
Slide 85
Slide 85 text
import re
DICE_EXPR = re.compile(r"^(\d+)d(\d+)\+?(\-?\d+)?$")
def roll_dice_expr(value):
m = DICE_EXPR.match(value)
if not m:
raise ValueError(f"Invalid dice expression '{value}'")
times, sides, modifier = m.groups()
times, sides = int(times), int(sides)
modifier = int(modifier or 0)
Slide 86
Slide 86 text
import re
DICE_EXPR = re.compile(r"^(\d+)d(\d+)\+?(\-?\d+)?$")
def roll_dice_expr(value):
m = DICE_EXPR.match(value)
if not m:
raise ValueError(f"Invalid dice expression '{value}'")
times, sides, modifier = m.groups()
times, sides = int(times), int(sides)
modifier = int(modifier or 0)
return roll_dice(times, sides, modifier=modifier)
Slide 87
Slide 87 text
def main_loop():
...
while True:
user_input = session.prompt("> ")
def main_loop():
...
while True:
user_input = session.prompt("> ").split()
if not user_input:
continue
command, args = user_input[0], user_input[1:]
Slide 90
Slide 90 text
def main_loop():
...
while True:
user_input = session.prompt("> ").split()
if not user_input:
continue
command, args = user_input[0], user_input[1:]
if command == "roll":
Slide 91
Slide 91 text
def main_loop():
...
while True:
user_input = session.prompt("> ").split()
if not user_input:
continue
command, args = user_input[0], user_input[1:]
if command == "roll":
results = [str(roll_dice_expr(dice_expr))
for dice_expr in args]
Slide 92
Slide 92 text
def main_loop():
...
while True:
user_input = session.prompt("> ").split()
if not user_input:
continue
command, args = user_input[0], user_input[1:]
if command == "roll":
results = [str(roll_dice_expr(dice_expr))
for dice_expr in args]
print(", ".join(results))
Slide 93
Slide 93 text
def main_loop():
...
while True:
user_input = session.prompt("> ").split()
if not user_input:
continue
command, args = user_input[0], user_input[1:]
if command == "roll":
results = [str(roll_dice_expr(dice_expr))
for dice_expr in args]
print(", ".join(results))
else:
print(f"Unknown command: {command}")
Slide 94
Slide 94 text
No content
Slide 95
Slide 95 text
Taking Turns with “Initiative”
• D&D uses a system called “initiative” to manage whose turn it is
• When an encounter starts, everyone rolls a d20
• Play proceeds in rounds from highest to lowest, and then repeats
def remove_combatant(combatant):
for combatants in initiative.values():
Slide 105
Slide 105 text
def remove_combatant(combatant):
for combatants in initiative.values():
combatants.remove(combatant)
Slide 106
Slide 106 text
def remove_combatant(combatant):
for combatants in initiative.values():
if combatant in combatants:
combatants.remove(combatant)
Slide 107
Slide 107 text
def generate_turns():
Slide 108
Slide 108 text
def generate_turns():
while initiative:
Slide 109
Slide 109 text
def generate_turns():
while initiative:
round_number += 1
Slide 110
Slide 110 text
def generate_turns():
while initiative:
round_number += 1
turn_order = list(reversed(sorted(initiative.items())))
Slide 111
Slide 111 text
def generate_turns():
while initiative:
round_number += 1
turn_order = list(reversed(sorted(initiative.items())))
for initiative_value, combatants in turn_order:
Slide 112
Slide 112 text
def generate_turns():
while initiative:
round_number += 1
turn_order = list(reversed(sorted(initiative.items())))
for initiative_value, combatants in turn_order:
for combatant in combatants:
Slide 113
Slide 113 text
def generate_turns():
while initiative:
round_number += 1
turn_order = list(reversed(sorted(initiative.items())))
for initiative_value, combatants in turn_order:
for combatant in combatants:
yield round_number, initiative_value, combatant
Slide 114
Slide 114 text
>>> turns = generate_turns()
Slide 115
Slide 115 text
>>> turns = generate_turns()
>>> for i in range(100):
Slide 116
Slide 116 text
>>> turns = generate_turns()
>>> for i in range(100):
next(turns)
Slide 117
Slide 117 text
>>> turns = generate_turns()
>>> for i in range(100):
next(turns)
(1, 20, "Sariel"),
(1, 13, "goblin1"),
(1, 13, "goblin3"),
...
(2, 20, "Sariel"),
...
(42, 20, "Sariel"),
...
Slide 118
Slide 118 text
def generate_turns():
while initiative:
round_number += 1
turn_order = list(reversed(sorted(initiative.items())))
for initiative_value, combatants in turn_order:
for combatant in combatants:
yield round_number, initiative_value, combatant
{
20: ["Sariel"],
13: ["goblin1", "goblin3"],
10: ["Lander", "goblin4", "Pip"],
5: ["goblin2"],
} WTH?
Slide 119
Slide 119 text
def generate_turns():
while initiative:
round_number += 1
turn_order = list(reversed(sorted(initiative.items())))
for initiative_value, combatants in turn_order:
for combatant in combatants[:]:
if combatant not in combatants:
continue
yield round_number, initiative_value, combatant
{
20: ["Sariel"],
13: ["goblin1", "goblin3"],
10: ["Lander", "goblin4", "Pip"],
5: ["goblin2"],
}
Yay!
Organization
dice.py ← put the dice-rolling code here
initiative.py ← put the turn-tracking code here
models.py ← attrs classes here
...
commands/
__init__.py ← put command base class here
do_thing.py ← put a "do thing" command here
next_turn.py ← put the turn-advancing command here
roll_dice.py ← put the dice-rolling command here
Slide 131
Slide 131 text
class Command:
Slide 132
Slide 132 text
class Command:
keywords = ["command"]
Slide 133
Slide 133 text
class Command:
keywords = ["command"]
def __init__(self, game):
self.game = game
Slide 134
Slide 134 text
class Command:
keywords = ["command"]
def __init__(self, game):
self.game = game
for kw in self.keywords:
game.commands[kw] = self
Slide 135
Slide 135 text
class Command:
keywords = ["command"]
def __init__(self, game):
self.game = game
for kw in self.keywords:
game.commands[kw] = self
print(f"Registered {self.__class__.__name__}")
Slide 136
Slide 136 text
class Command:
keywords = ["command"]
def __init__(self, game):
self.game = game
for kw in self.keywords:
game.commands[kw]= self
print(f"Registered {self.__class__.__name__}")
def do_command(self, *args):
Slide 137
Slide 137 text
class Command:
keywords = ["command"]
def __init__(self, game):
self.game = game
for kw in self.keywords:
game.commands[kw]= self
print(f"Registered {self.__class__.__name__}")
def do_command(self, *args):
print("Nothing happens.")
Slide 138
Slide 138 text
class Command:
keywords = ["command"]
def __init__(self, game):
self.game = game
for kw in self.keywords:
game.commands[kw]= self
print(f"Registered {self.__class__.__name__}")
def do_command(self, *args):
print("Nothing happens.")
def show_help_text(self, keyword):
Slide 139
Slide 139 text
class Command:
keywords = ["command"]
def __init__(self, game):
self.game = game
for kw in self.keywords:
game.commands[kw]= self
print(f"Registered {self.__class__.__name__}")
def do_command(self, *args):
print("Nothing happens.")
def show_help_text(self, keyword):
print(self.help_text.strip())
Slide 140
Slide 140 text
class Command:
keywords = ["command"]
def __init__(self, game):
self.game = game
for kw in self.keywords:
game.commands[kw]= self
print(f"Registered {self.__class__.__name__}")
def do_command(self, *args):
print("Nothing happens.")
def show_help_text(self, keyword):
if getattr(self, "help_text", None):
print(self.help_text.strip())
else:
print(f"No help text available for '{keyword}'")
Slide 141
Slide 141 text
from dndme.commands import Command
from dndme.dice import roll_dice_expr
Slide 142
Slide 142 text
from dndme.commands import Command
from dndme.dice import roll_dice_expr
class RollDice(Command):
Slide 143
Slide 143 text
from dndme.commands import Command
from dndme.dice import roll_dice_expr
class RollDice(Command):
keywords = ["roll", "dice"]
Slide 144
Slide 144 text
from dndme.commands import Command
from dndme.dice import roll_dice_expr
class RollDice(Command):
keywords = ["roll", "dice"]
help_text = """..."""
Slide 145
Slide 145 text
from dndme.commands import Command
from dndme.dice import roll_dice_expr
class RollDice(Command):
keywords = ["roll", "dice"]
help_text = """..."""
def do_command(self, *args):
Slide 146
Slide 146 text
from dndme.commands import Command
from dndme.dice import roll_dice_expr
class RollDice(Command):
keywords = ["roll", "dice"]
help_text = """..."""
def do_command(self, *args):
results = [str(roll_dice_expr(dice_expr)) for dice_expr in args]
print(", ".join(results))
Slide 147
Slide 147 text
from dndme.commands import Command
class NextTurn(Command):
keywords = ["next"]
help_text = """..."""
def do_command(self, *args):
turn = next(self.game.turn_manager.turns)
...
Slide 148
Slide 148 text
Great, now we have lots of classes...
• We could manually import all those commands and register them when our
program starts up...
- but that’s really tiresome
- and easy to forget to do when adding a new command
• Let’s find and register them dynamically!
import importlib, os, pkgutil, sys
def load_commands(game):
path = os.path.join(os.path.dirname(__file__), "commands")
modules = pkgutil.iter_modules(path=[path])
for loader, mod_name, ispkg in modules:
Slide 154
Slide 154 text
import importlib, os, pkgutil, sys
def load_commands(game):
path = os.path.join(os.path.dirname(__file__), "commands")
modules = pkgutil.iter_modules(path=[path])
for loader, mod_name, ispkg in modules:
if mod_name in sys.modules:
continue
Slide 155
Slide 155 text
import importlib, os, pkgutil, sys
def load_commands(game):
path = os.path.join(os.path.dirname(__file__), "commands")
modules = pkgutil.iter_modules(path=[path])
for loader, mod_name, ispkg in modules:
if mod_name in sys.modules:
continue
loaded_mod = importlib.import_module(
f"dndme.commands.{mod_name}")
Slide 156
Slide 156 text
import importlib, os, pkgutil, sys
def load_commands(game):
path = os.path.join(os.path.dirname(__file__), "commands")
modules = pkgutil.iter_modules(path=[path])
for loader, mod_name, ispkg in modules:
if mod_name in sys.modules:
continue
loaded_mod = importlib.import_module(
f"dndme.commands.{mod_name}")
class_name = "".join([x.title() for x in mod_name.split("_")])
Slide 157
Slide 157 text
import importlib, os, pkgutil, sys
def load_commands(game):
path = os.path.join(os.path.dirname(__file__), "commands")
modules = pkgutil.iter_modules(path=[path])
for loader, mod_name, ispkg in modules:
if mod_name in sys.modules:
continue
loaded_mod = importlib.import_module(
f"dndme.commands.{mod_name}")
class_name = "".join([x.title() for x in mod_name.split("_")])
loaded_class = getattr(loaded_mod, class_name, None)
Slide 158
Slide 158 text
import importlib, os, pkgutil, sys
def load_commands(game):
path = os.path.join(os.path.dirname(__file__), "commands")
modules = pkgutil.iter_modules(path=[path])
for loader, mod_name, ispkg in modules:
if mod_name in sys.modules:
continue
loaded_mod = importlib.import_module(
f"dndme.commands.{mod_name}")
class_name = "".join([x.title() for x in mod_name.split("_")])
loaded_class = getattr(loaded_mod, class_name, None)
if not loaded_class:
continue
Slide 159
Slide 159 text
import importlib, os, pkgutil, sys
def load_commands(game):
path = os.path.join(os.path.dirname(__file__), "commands")
modules = pkgutil.iter_modules(path=[path])
for loader, mod_name, ispkg in modules:
if mod_name in sys.modules:
continue
loaded_mod = importlib.import_module(f"dndme.commands.{mod_name}")
class_name = "".join([x.title() for x in mod_name.split("_")])
loaded_class = getattr(loaded_mod, class_name, None)
if not loaded_class:
continue
instance = loaded_class(game)
from prompt_toolkit.completion import Completer, Completion
Slide 174
Slide 174 text
from prompt_toolkit.completion import Completer, Completion
class CommandCompleter(Completer):
Slide 175
Slide 175 text
from prompt_toolkit.completion import Completer, Completion
class CommandCompleter(Completer):
def __init__(self, commands, ignore_case=False, match_middle=False):
Slide 176
Slide 176 text
from prompt_toolkit.completion import Completer, Completion
class CommandCompleter(Completer):
def __init__(self, commands, ignore_case=False, match_middle=False):
self.commands = commands
Slide 177
Slide 177 text
from prompt_toolkit.completion import Completer, Completion
class CommandCompleter(Completer):
def __init__(self, commands, ignore_case=False, match_middle=False):
self.commands = commands
self.base_commands = sorted(list(commands.keys()))
...
Slide 178
Slide 178 text
from prompt_toolkit.completion import Completer, Completion
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
Slide 179
Slide 179 text
from prompt_toolkit.completion import Completer, Completion
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
# 1. Find out what's been typed so far
# 2. Find out what we completions we *might* suggest
# 3. Figure out if a completion is valid
# 4. Yield each valid completion
Slide 180
Slide 180 text
from prompt_toolkit.completion import Completer, Completion
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
# 1. Find out what's been typed so far
word_before_cursor = document.get_word_before_cursor()
Slide 181
Slide 181 text
from prompt_toolkit.completion import Completer, Completion
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
# 1. Find out what's been typed so far
word_before_cursor = document.get_word_before_cursor()
if self.ignore_case:
word_before_cursor = word_before_cursor.lower()
Slide 182
Slide 182 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
# 2. Find out what we completions we *might* suggest
suggestions = []
Slide 183
Slide 183 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
# 2. Find out what we completions we *might* suggest
suggestions = []
document_text_list = document.text.split(" ")
Slide 184
Slide 184 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
# 2. Find out what we completions we *might* suggest
suggestions = []
document_text_list = document.text.split(" ")
if len(document_text_list) < 2:
suggestions = self.base_commands
Slide 185
Slide 185 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
# 2. Find out what we completions we *might* suggest
suggestions = []
document_text_list = document.text.split(' ')
if len(document_text_list) < 2:
suggestions = self.base_commands
elif document_text_list[0] in self.commands:
Slide 186
Slide 186 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
# 2. Find out what we completions we *might* suggest
suggestions = []
document_text_list = document.text.split(' ')
if len(document_text_list) < 2:
suggestions = self.base_commands
elif document_text_list[0] in self.commands:
command = self.commands[document_text_list[0]]
Slide 187
Slide 187 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
# 2. Find out what we completions we *might* suggest
suggestions = []
document_text_list = document.text.split(' ')
if len(document_text_list) < 2:
suggestions = self.base_commands
elif document_text_list[0] in self.commands:
command = self.commands[document_text_list[0]]
suggestions = command.get_suggestions(document_text_list)
Slide 188
Slide 188 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
...
# 3. Figure out if a completion is valid
def word_matches(suggestion):
Slide 189
Slide 189 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
...
# 3. Figure out if a completion is valid
def word_matches(suggestion):
if self.ignore_case:
suggestion = suggestion.lower()
Slide 190
Slide 190 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
...
# 3. Figure out if a completion is valid
def word_matches(suggestion):
if self.ignore_case:
suggestion = suggestion.lower()
if self.match_middle:
return word_before_cursor in suggestion
Slide 191
Slide 191 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
...
# 3. Figure out if a completion is valid
def word_matches(suggestion):
if self.ignore_case:
suggestion = suggestion.lower()
if self.match_middle:
return word_before_cursor in suggestion
else:
return suggestion.startswith(word_before_cursor)
Slide 192
Slide 192 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
...
...
# 4. Yield each valid completion
for suggestion in suggestions:
Slide 193
Slide 193 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
...
...
# 4. Yield each valid completion
for suggestion in suggestions:
if word_matches(suggestion):
Slide 194
Slide 194 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
...
...
# 4. Yield each valid completion
for suggestion in suggestions:
if word_matches(suggestion):
yield Completion(suggestion,
start_position=-len(word_before_cursor))
Slide 195
Slide 195 text
class CommandCompleter(Completer):
...
def get_completions(self, document, complete_event):
...
...
...
# 4. Yield each valid completion
for word in suggestions:
if word_matches(word):
yield Completion(word,
start_position=-len(word_before_cursor))
# All done -- did everyone pass their INT saves??
Slide 196
Slide 196 text
class Command:
...
def get_suggestions(self, words):
return []
Slide 197
Slide 197 text
from dndme.commands import Command
class DamageCombatant(Command):
keywords = ["hit", "damage"]
def do_command(self, *args):
...
Slide 198
Slide 198 text
from dndme.commands import Command
class DamageCombatant(Command):
keywords = ["hit", "damage"]
def get_suggestions(self, words):
def do_command(self, *args):
...
Slide 199
Slide 199 text
from dndme.commands import Command
class DamageCombatant(Command):
keywords = ["hit", "damage"]
def get_suggestions(self, words):
names_already_chosen = words[1:]
def do_command(self, *args):
...