From c39ef611096cc132caf439d251d7d76c85bedec5 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 16 Jan 2023 21:35:02 -0500 Subject: [PATCH 001/113] Ignore extra whitespace in load command Ignore leading or trailing whitespace when attempting to load in a custom bestiary. --- src/encounter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index c56bfb5..61fbd8e 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -121,7 +121,7 @@ def execute(self, args = []): if numArgs == 1: try: - bestiaryFile = open(args[0]) + bestiaryFile = open(args[0].strip()) except FileNotFoundError: print("Selected bestiary file could not be found.") if len(self.bestiary.data) == 0: From 52bd28b03b4942946b33c0608e1f403545d1f1a2 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 16 Jan 2023 21:46:23 -0500 Subject: [PATCH 002/113] Allow capital letters in arguments Make command couldn't create names with capital letters in them since all arguments were sanitized so early. Commands will have to sanitize their input manually to avoid issues later down the line. --- src/encounter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 61fbd8e..f0ae2e1 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -584,12 +584,12 @@ def main(): # command loop while True: print() - usrRequest = input("Type a command: ").lower().split(" ") + usrRequest = input("Type a command: ").split(" ") action = None if usrRequest != ['']: - action = usrRequest[0] + action = usrRequest[0].lower() if action in ['quit', 'q', 'exit']: break From 473e6f5f4cea374bde6dc31309da32fc0aa65e99 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 16 Jan 2023 23:09:05 -0500 Subject: [PATCH 003/113] Add nickname command Allows users to display unique names for each NPC in their encounter. If no nickname is specified the NPC's original name is used. Because of this it's more appropriate to print an NPC's nickname in most cases so the unique name is used. The bestiary name (essentially the type of NPC) is still shown in parenthesis in all menus for clarity. --- src/encounter.py | 79 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index f0ae2e1..612cbcc 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -2,7 +2,7 @@ class NPC: - def __init__(self, name: str, maxHP: int, ac: int): + def __init__(self, name: str, maxHP: int, ac: int, nick: str|None = None): # Type assertions if type(name) != str: raise TypeError("Name must be a string.") @@ -21,14 +21,24 @@ def __init__(self, name: str, maxHP: int, ac: int): # Value assignment self.name = name + if nick is None: + self.nick = name + else: + self.nick = nick self.maxHP = self.currentHP = maxHP self.ac = int(ac) def __str__(self): if self.currentHP > 0: - return self.name + if self.nick is not self.name: + return self.nick + " (" + self.name + ")" + else: + return self.name else: - return self.name + " [X]" + if self.nick is not self.name: + return self.nick + " (" + self.name + ") [X]" + else: + return self.name + " [X]" def equals(self, other): if self == other: @@ -37,6 +47,8 @@ def equals(self, other): return False if self.name != other.name: return False + if self.nick != other.nick: + return False if self.maxHP != other.maxHP: return False if self.currentHP != other.currentHP: @@ -46,10 +58,21 @@ def equals(self, other): return True def combatStatus(self) -> str: + status = "" if self.currentHP > 0: - return self.name + " [" + str(self.currentHP) + "/" + str(self.maxHP) + "]" + if self.name is not self.nick: + status += self.nick + " (" + self.name + ")" + else: + status += self.name + status += " [" + str(self.currentHP) + "/" + str(self.maxHP) + "]" else: - return self.name + " [Dead]" + if self.name is not self.nick: + status += self.nick + " (" + self.name + ")" + else: + status += self.name + status += " [Dead]" + + return status def detailedInfo(self) -> str: info = "" @@ -362,9 +385,9 @@ def execute(self, args = []): if int(args[1]) >= npc.ac: npc.currentHP = max(0, npc.currentHP - int(args[2])) - print(npc.name + " took " + args[2] + " damage.") + print(npc.nick + " took " + args[2] + " damage.") else: - print("Attack misses " + npc.name + ".") + print("Attack misses " + npc.nick + ".") elif lenArgs == 2: if not isInt(args[1]): self.usage() @@ -375,11 +398,11 @@ def execute(self, args = []): if damage.isnumeric() is True: amt = int(damage) npc.currentHP = max(0, npc.currentHP - amt) - print(npc.name + " took " + damage + " damage.") + print(npc.nick + " took " + damage + " damage.") else: print("Damage must be a number.") else: - print("Attack misses " + npc.name + ".") + print("Attack misses " + npc.nick + ".") elif lenArgs == 1: accuracy = input("Roll for hit: ") if accuracy.isnumeric() is True: @@ -389,17 +412,17 @@ def execute(self, args = []): if damage.isnumeric() is True: amt = int(damage) npc.currentHP = max(0, npc.currentHP - amt) - print(npc.name + " took " + damage + " damage.") + print(npc.nick + " took " + damage + " damage.") else: print("Damage must be a number.") else: - print("Attack misses " + npc.name + ".") + print("Attack misses " + npc.nick + ".") else: print("Accuracy must be a number.") if npc is not None: if npc.currentHP <= 0: - print(npc.name + " has been defeated.") + print(npc.nick + " has been defeated.") if areAllDefeated(self.encounter.data): print("Party has defeated all enemies.") @@ -426,7 +449,7 @@ def execute(self, args = []): npc.currentHP = max(0, npc.currentHP - int(args[1])) if npc.currentHP <= 0: - print(npc.name + " has been defeated.") + print(npc.nick + " has been defeated.") if areAllDefeated(self.encounter.data): print("Party has defeated all enemies.") else: @@ -451,7 +474,7 @@ def execute(self, args = []): return else: npc.currentHP = 0 - print(npc.name + " was defeated.") + print(npc.nick + " was defeated.") if areAllDefeated(self.encounter.data): print("Party has defeated all enemies.") @@ -485,7 +508,7 @@ def execute(self, args = []): healedAmt = npc.currentHP - origHP - print(npc.name + " was healed " + str(healedAmt) + " points.") + print(npc.nick + " was healed " + str(healedAmt) + " points.") else: self.usage() @@ -537,7 +560,7 @@ def execute(self, args = []): class make(Command): def __init__(self, bestiary): super().__init__() - self.names = ["make"] + self.names = ['make'] self.bestiary = bestiary self.description = "Creates an NPC for the bestiary" self.usageStr = "make " @@ -553,6 +576,27 @@ def execute(self, args=[]) -> None: self.usage() +class name(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['name', 'nick'] + self.encounter = encounter + self.description = "Gives a specific name to an NPC in the encounter" + self.usageStr = "name " + + def execute(self, args=[]) -> None: + if len(args) == 2: + if not args[0].isnumeric(): + self.usage() + return + if isValidInt(args[0], self.encounter.data) is True: + self.encounter.data[int(args[0]) - 1].nick = args[1] + else: + self.usage() + else: + self.usage() + + def main(): bestiary = NPCList(['bestiary', 'book', 'b']) encounter = NPCList(['encounter', 'e', "combat", "c"]) @@ -571,7 +615,8 @@ def main(): heal(encounter), status(encounter), info(bestiary), - make(bestiary) + make(bestiary), + name(encounter) ] commands.append(displayHelp(commands)) From ef4761a2777154c4313d8c2f2ecc3edf1a797c39 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Tue, 24 Jan 2023 15:49:57 -0500 Subject: [PATCH 004/113] Allow bestiary to contain blank lines --- bestiary.txt | 19 ++++++++++--------- src/encounter.py | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/bestiary.txt b/bestiary.txt index 3e4b773..66020b5 100644 --- a/bestiary.txt +++ b/bestiary.txt @@ -1,15 +1,16 @@ -#Example bestiary file for encounter -#Lines beginning with "#" are comments -#Format to create custom NPC instances is as follows -#Name, Max HP, AC -#Extra values, descriptions, and notes separated with a comma will be ignored -# -#Nature +# Example bestiary file for encounter +# Lines beginning with "#" are comments +# Blank lines are ignored +# Format for custom NPC types: +# Name, Max HP, AC +# Extra values, descriptions, and notes separated with a comma will be ignored + +# Nature Wolf, 11, 13 Fox, 3, 12, this note will be ignored Spider, 1, 12, A tiny creepy crawley creature -#Fantasy +# Fantasy Skeleton, 13, 13 Zombie, 16, 15 -#Human +# Human Townsfolk, 9, 12 \ No newline at end of file diff --git a/src/encounter.py b/src/encounter.py index 612cbcc..5186ce2 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -155,7 +155,7 @@ def execute(self, args = []): else: self.bestiary.data.clear() for line in bestiaryFile: - if not line.startswith("#"): + if not (line.startswith("#") or line.isspace()): line = line.rstrip("\n").split(",") npc = NPC(line[0], int(line[1]), int(line[2])) self.bestiary.data.append(npc) From feb0863dd7be022a35e1314385b5d96d25c853d1 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 26 Jan 2023 12:01:41 -0500 Subject: [PATCH 005/113] Include flake8 styling preferences Including the flake8 config file works more consistently than trying to configure preferences in VSCode. Correct styling errors caught by flake8. --- .flake8 | 3 +++ src/encounter.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..bdd7bab --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +extend-ignore = E251 +max-line-length = 120 diff --git a/src/encounter.py b/src/encounter.py index 5186ce2..56284c6 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -2,7 +2,7 @@ class NPC: - def __init__(self, name: str, maxHP: int, ac: int, nick: str|None = None): + def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): # Type assertions if type(name) != str: raise TypeError("Name must be a string.") From d8cab1c217b4e0aafc911f89fefad8dc9bad3299 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 26 Jan 2023 12:04:41 -0500 Subject: [PATCH 006/113] Update metadata to reflect Python requirements Most recent type hinting requires Python 3.11. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b03c56..f35cdbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "encounter" version = "4.17.0" -requires-python = ">=3.10" +requires-python = ">=3.11" [tool.setuptools] package-dir = {"" = "src"} From 80b6645a58eb4601bfd0568f4541f633d5280f62 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 26 Jan 2023 12:06:28 -0500 Subject: [PATCH 007/113] Update version number to reflect nickname command Increment patch number to reflect patch to bestiary file reading. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f35cdbf..b1b67e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "encounter" -version = "4.17.0" +version = "4.18.1" requires-python = ">=3.11" [tool.setuptools] From bb21603af0a5911d1db7d80d8fb3eebb84b56979 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 26 Jan 2023 12:22:00 -0500 Subject: [PATCH 008/113] Initial implementation of mark command Allows user to specially notate any NPC in their encounter. --- src/encounter.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 56284c6..b2d525c 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -597,6 +597,50 @@ def execute(self, args=[]) -> None: self.usage() +class mark(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['mark', 'note'] + self.encounter = encounter + self.description = "Mark an NPC with a symbol and note" + self.usageStr = "mark [note]" + + def execute(self, args=[]) -> None: + if len(args) == 2: + if not args[0].isnumeric(): + self.usage() + return + if isValidInt(args[0], self.encounter.data) is True: + self.encounter.data[int(args[0]) - 1].marked = True + self.encounter.data[int(args[0]) - 1].note = args[1] + else: + self.usage() + else: + self.usage() + + +class unmark(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['unmark'] + self.encounter = encounter + self.description = "Remove mark and symbol from an NPC" + self.usageStr = "unmark " + + def execute(self, args=[]) -> None: + if len(args) == 1: + if not args[0].isnumeric(): + self.usage() + return + if isValidInt(args[0], self.encounter.data) is True: + self.encounter.data[int(args[0]) - 1].marked = False + self.encounter.data[int(args[0]) - 1].note = "" + else: + self.usage() + else: + self.usage() + + def main(): bestiary = NPCList(['bestiary', 'book', 'b']) encounter = NPCList(['encounter', 'e', "combat", "c"]) @@ -616,7 +660,9 @@ def main(): status(encounter), info(bestiary), make(bestiary), - name(encounter) + name(encounter), + mark(encounter), + unmark(encounter) ] commands.append(displayHelp(commands)) From 3172047d1e263911bc4a46f15096f1bf27a339d8 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 26 Jan 2023 12:25:05 -0500 Subject: [PATCH 009/113] Correct quotation styling inconsistency --- src/encounter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index b2d525c..35e7b40 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -643,7 +643,7 @@ def execute(self, args=[]) -> None: def main(): bestiary = NPCList(['bestiary', 'book', 'b']) - encounter = NPCList(['encounter', 'e', "combat", "c"]) + encounter = NPCList(['encounter', 'e', 'combat', 'c']) referenceLists = [bestiary, encounter] # Instantiate commands From 83f73cb9c383e4acb8d0028a519052042abc0191 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 26 Jan 2023 12:30:15 -0500 Subject: [PATCH 010/113] Disallow empty names or nicknames for NPCs --- src/encounter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 35e7b40..ec6f63c 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -14,6 +14,12 @@ def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): # Value assertions if len(name) < 1: raise ValueError("Name must be at least length 1.") + if name.isspace(): + raise ValueError("Name must not be blank.") + if nick is not None and len(nick) < 1: + raise ValueError("Nickname must be at least length 1.") + if nick is not None and nick.isspace(): + raise ValueError("Nickname must not be blank.") if maxHP < 1: raise ValueError("HP must be at least 1.") if ac < 0: From 4637d45b316d7db19b6aabd5a56c5eaff6e351b6 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 28 Jan 2023 23:46:59 -0500 Subject: [PATCH 011/113] Provide default values for NPC notes --- src/encounter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index ec6f63c..717a3dc 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -26,6 +26,8 @@ def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): raise ValueError("AC must be at least 0.") # Value assignment + self.marked = False + self.note = "" self.name = name if nick is None: self.nick = name From ae987fc903db7567d5092022d81c48301c612d02 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 28 Jan 2023 23:48:41 -0500 Subject: [PATCH 012/113] Include NPC note when determining NPC equality --- src/encounter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 717a3dc..1eace02 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -63,6 +63,8 @@ def equals(self, other): return False if self.ac != other.ac: return False + if self.note != other.note: + return False return True def combatStatus(self) -> str: From 1855c400d427f982b8a2adea67566f477e211d1b Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 28 Jan 2023 23:56:17 -0500 Subject: [PATCH 013/113] Refactor NPC string representation for code reuse Change made in preparation for more easily adding mark annotation. --- src/encounter.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 1eace02..c7c1244 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -37,16 +37,16 @@ def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): self.ac = int(ac) def __str__(self): - if self.currentHP > 0: - if self.nick is not self.name: - return self.nick + " (" + self.name + ")" - else: - return self.name + output = "" + if self.nick is not self.name: + output += self.nick + " (" + self.name + ")" else: - if self.nick is not self.name: - return self.nick + " (" + self.name + ") [X]" - else: - return self.name + " [X]" + output += self.name + + if self.currentHP <= 0: + output += " [X]" + + return output def equals(self, other): if self == other: From 8e804d6d21b43d942e1cbad03f771d3c68b4afac Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 00:05:20 -0500 Subject: [PATCH 014/113] Include asterisk in list screen by marked NPCs --- src/encounter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index c7c1244..4b7de65 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -43,6 +43,9 @@ def __str__(self): else: output += self.name + if self.marked: + output += "*" + if self.currentHP <= 0: output += " [X]" From a8f261800c00d691de3c390dfeb07e7d4825b388 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 00:13:43 -0500 Subject: [PATCH 015/113] Make note optional for mark command --- src/encounter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 4b7de65..04ebe4d 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -619,13 +619,14 @@ def __init__(self, encounter): self.usageStr = "mark [note]" def execute(self, args=[]) -> None: - if len(args) == 2: + if len(args) == 1 or len(args) == 2: if not args[0].isnumeric(): self.usage() return if isValidInt(args[0], self.encounter.data) is True: self.encounter.data[int(args[0]) - 1].marked = True - self.encounter.data[int(args[0]) - 1].note = args[1] + if len(args) == 2: + self.encounter.data[int(args[0]) - 1].note = args[1] else: self.usage() else: From c86520ded2abe47ec004d4271ea3dfc325f80a40 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 14:28:19 -0500 Subject: [PATCH 016/113] Simplify toMenu output for readability Simplifying this code in preparation for the merge between the info and status commands. Important that each output be more understandable so I can pick the best of each moving forward. --- src/encounter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 04ebe4d..cb6dd96 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -114,12 +114,12 @@ def toMenu(self): if len(self.data) == 0: info += "EMPTY" + return info else: - for i in self.data[:-1]: + for i in self.data: info += str(self.data.index(i) + 1) + " " + str(i) + "\n" - info += str(self.data.index(self.data[-1]) + 1) + " " + str(self.data[-1]) - return info + return info[:-1] def findList(name: str, referenceLists: list[NPCList]) -> NPCList | None: From 3c11866174e1661ab3bbf744aedc9d70457ef6d2 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 14:52:43 -0500 Subject: [PATCH 017/113] Display notes in status menu --- src/encounter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index cb6dd96..0eec867 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -85,6 +85,13 @@ def combatStatus(self) -> str: status += self.name status += " [Dead]" + if self.marked: + status += "\nNote:\n" + if not self.note.isspace(): + status += self.note + else: + status += "EMPTY" + return status def detailedInfo(self) -> str: From 636cbcb526c55957e9514246ae0c23488482bbdf Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 14:57:33 -0500 Subject: [PATCH 018/113] Allow notes of any length when marking an NPC --- src/encounter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 0eec867..7189c53 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -626,14 +626,14 @@ def __init__(self, encounter): self.usageStr = "mark [note]" def execute(self, args=[]) -> None: - if len(args) == 1 or len(args) == 2: + if len(args) >= 1: if not args[0].isnumeric(): self.usage() return if isValidInt(args[0], self.encounter.data) is True: self.encounter.data[int(args[0]) - 1].marked = True - if len(args) == 2: - self.encounter.data[int(args[0]) - 1].note = args[1] + if len(args) > 1: + self.encounter.data[int(args[0]) - 1].note = " ".join(args[1:]) else: self.usage() else: From 0d04c64bc771bcee8131058fd9f58d8238814ee0 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:14:03 -0500 Subject: [PATCH 019/113] Increment version number for mark/unmark commands The past series of commits have been focused on allowing users to add custom notes to NPCs in their encounters with the mark, or note, command. Notes can be removed with the unmark command. Notes take the form of a mark, an asterisk, next to the NPC name. Marks can be accompanied by a custom note viewable in the status menu of the marked NPC. It was my intention to merge the status and info commands in this series of commits, but I'm waiting on some feedback before going through with that. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b1b67e8..41e4319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "encounter" -version = "4.18.1" +version = "4.19.0" requires-python = ">=3.11" [tool.setuptools] From eb9748941152391b86b11a95e7b94775ba95948f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:29:14 -0500 Subject: [PATCH 020/113] Allow removing notes without removing an NPC mark This is done by calling the mark command on an NPC but leaving the note section blank. This sets the note to empty and retains the NPC's marked status. The mark can be removed with the unmark command. --- src/encounter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 7189c53..b9baccf 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -634,6 +634,8 @@ def execute(self, args=[]) -> None: self.encounter.data[int(args[0]) - 1].marked = True if len(args) > 1: self.encounter.data[int(args[0]) - 1].note = " ".join(args[1:]) + else: + self.encounter.data[int(args[0]) - 1].note = "" else: self.usage() else: From 89aacc7e18c93ace35fdacf28334613133e8174f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:32:11 -0500 Subject: [PATCH 021/113] Display empty message for blank notes Display the message EMPTY on marked NPCs that have no note to make it clear that the mark was explicitly given no accompanying message. issspace assumes there at least one character in the string. If the string was empty the EMPTY message was not displayed. --- src/encounter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index b9baccf..faeb6fc 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -87,7 +87,7 @@ def combatStatus(self) -> str: if self.marked: status += "\nNote:\n" - if not self.note.isspace(): + if not self.note.isspace() and len(self.note) > 0: status += self.note else: status += "EMPTY" From a2cde28325661a9bdad0f10bbe59588d95f1a843 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:38:34 -0500 Subject: [PATCH 022/113] Include macOS and Python 3.11 in testing Include macOS as a testing target since users I've shared the project use it. Include Python 3.11 as a testing target since I am building the project with it. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 04a64c7..55d4f0a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,8 +9,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest] - python-version: ['3.10'] + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.10', '3.11'] steps: - uses: actions/checkout@v2 From f1a37951f5b65ab898b5afeb33a23408d80e58f5 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:51:59 -0500 Subject: [PATCH 023/113] [CI] Don't run tests on every push event Most important that tests are run on each PR. Convenient to have them rerun when a new commit is made to an open PR. Run CI when pushing to main to help catch breaking errors. --- .github/workflows/tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 55d4f0a..c591dda 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,8 +1,10 @@ name: Tests on: - - push - - pull_request + pull_request: + push: + branches: + - main jobs: test: From e7e8c4736a5260515e97d97d6dafd63916551e15 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:53:18 -0500 Subject: [PATCH 024/113] Implement unmark all Removes markings and notes from every NPC in the encounter. --- src/encounter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index faeb6fc..c1f7339 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -653,7 +653,12 @@ def __init__(self, encounter): def execute(self, args=[]) -> None: if len(args) == 1: if not args[0].isnumeric(): - self.usage() + if args[0].lower() == "all": + for npc in self.encounter.data: + npc.marked = False + npc.note = "" + else: + self.usage() return if isValidInt(args[0], self.encounter.data) is True: self.encounter.data[int(args[0]) - 1].marked = False From 20aac83f7cbcf240e8d27a0b2a4b530e35440b27 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:58:36 -0500 Subject: [PATCH 025/113] Implement mark all --- src/encounter.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index c1f7339..b29320e 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -628,7 +628,15 @@ def __init__(self, encounter): def execute(self, args=[]) -> None: if len(args) >= 1: if not args[0].isnumeric(): - self.usage() + if args[0].lower() == "all": + for npc in self.encounter.data: + npc.marked = True + if len(args) > 1: + npc.note = " ".join(args[1:]) + else: + npc.note = "" + else: + self.usage() return if isValidInt(args[0], self.encounter.data) is True: self.encounter.data[int(args[0]) - 1].marked = True From c14ff30090087051b24ffd75a62a8f5f95158b81 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 11:09:06 -0500 Subject: [PATCH 026/113] Display usage when smiting an invalid index --- src/encounter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index b29320e..8f64813 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -498,6 +498,8 @@ def execute(self, args = []): if areAllDefeated(self.encounter.data): print("Party has defeated all enemies.") + else: + self.usage() else: self.usage() else: From dc45fbe47c3673cded5ce5cb5fe931523fc791bb Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 19:57:39 -0500 Subject: [PATCH 027/113] Implement smite all --- src/encounter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 8f64813..b392621 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -487,6 +487,12 @@ def __init__(self, encounter): def execute(self, args = []): if len(self.encounter.data) > 0: if len(args) == 1: + if args[0] == "all": + for npc in self.encounter.data: + if npc.currentHP > 0: + npc.currentHP = 0 + print("All enemies have been defeated.") + return if isValidInt(args[0], self.encounter.data) is True: npc = self.encounter.data[int(args[0]) - 1] if npc.currentHP <= 0: From 9910db5d725c605e6a0f9082f4e340beab5c542d Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 20:50:17 -0500 Subject: [PATCH 028/113] Handle potential type mismatch in addNPC Since findList may return None it is best to handle this case somewhere. This code doesn't stop this from crashing the program but rather acknowledges the specifics of the failure state. --- src/encounter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index b392621..a4c9e1d 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -291,7 +291,11 @@ def __init__(self, referenceLists): def execute(self, args = []): bestiary = findList("bestiary", self.referenceLists) + if bestiary is None: + raise TypeError("Bestiary list must be an NPCList.") encounter = findList("encounter", self.referenceLists) + if encounter is None: + raise TypeError("Encounter list must be an NPCList.") if len(args) == 1: selected = args[0].split(",") From 2b7a93f9fc7185d963a04e02538e857a58dffbb3 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 20:53:11 -0500 Subject: [PATCH 029/113] Implement multiple selection for smite command Allows the user to select multiple NPCs to act on with the smite command in the same way they already can with the add command. --- src/encounter.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index a4c9e1d..a8f5bb2 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -496,20 +496,25 @@ def execute(self, args = []): if npc.currentHP > 0: npc.currentHP = 0 print("All enemies have been defeated.") - return - if isValidInt(args[0], self.encounter.data) is True: - npc = self.encounter.data[int(args[0]) - 1] - if npc.currentHP <= 0: - print("Enemy already defeated.") - return - else: - npc.currentHP = 0 - print(npc.nick + " was defeated.") - - if areAllDefeated(self.encounter.data): - print("Party has defeated all enemies.") else: - self.usage() + selected = args[0].split(",") + + for index in selected: + if isValidInt(args[0], self.encounter.data) is False: + self.usage() + return + + for index in selected: + npc = self.encounter.data[int(index) - 1] + if npc.currentHP <= 0: + print("Enemy already defeated.") + return + else: + npc.currentHP = 0 + print(npc.nick + " was defeated.") + + if areAllDefeated(self.encounter.data): + print("Party has defeated all enemies.") else: self.usage() else: From 6f321b4def11ab5e2e759406179b52713af4d0ca Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 21:01:27 -0500 Subject: [PATCH 030/113] Display error when heal target it not valid Previously failed silently. Trying to prefer succeeding silently and failing with output. --- src/encounter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index a8f5bb2..9b9970b 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -546,6 +546,8 @@ def execute(self, args = []): healedAmt = npc.currentHP - origHP print(npc.nick + " was healed " + str(healedAmt) + " points.") + else: + self.usage() else: self.usage() From 7168394283e212e924d42bfa4d6e1efd0d559513 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 21:03:53 -0500 Subject: [PATCH 031/113] Bound heal amount over 0 Previously allowed to heal by a negative amount. Perhaps nichely useful, but overall counter to the intent of the command. It is more clear to not allow this. --- src/encounter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 9b9970b..47980cd 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -534,6 +534,9 @@ def execute(self, args = []): if not isInt(args[1]): self.usage() return + if int(args[1]) < 1: + print("Amount must be more than zero.") + return if isValidInt(args[0], self.encounter.data): npc = self.encounter.data[int(args[0]) - 1] origHP = npc.currentHP From 188986e975b85a2e545b87f380e19e6cde8524e9 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 21:09:07 -0500 Subject: [PATCH 032/113] More specific error reporting for damage command Fixes the same issues previously present in the heal command. The command now provides output when selecting a number that's out of range of the encounter list. Additionally, damage amounts must be more than 0. Like the heal command, it was previously possible to damage by a negative amount. --- src/encounter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 47980cd..91bae76 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -464,6 +464,9 @@ def execute(self, args = []): if not isInt(args[1]): self.usage() return + if int(args[1]) < 1: + print("Amount must be more than zero.") + return if isValidInt(args[0], self.encounter.data) is True: npc = self.encounter.data[int(args[0]) - 1] @@ -476,6 +479,8 @@ def execute(self, args = []): print(npc.nick + " has been defeated.") if areAllDefeated(self.encounter.data): print("Party has defeated all enemies.") + else: + self.usage() else: self.usage() From 658e7fd94ce8389585833da6407bc4a1806e7920 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 1 Feb 2023 21:14:46 -0500 Subject: [PATCH 033/113] Correct smite help string Incorrectly hinted that you selected from the bestiary indices. Now hints at the new multi select feature. --- src/encounter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 91bae76..cde92d3 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -491,7 +491,7 @@ def __init__(self, encounter): self.names = ['smite', 'kill'] self.encounter = encounter self.description = "Immediately kills an NPC." - self.usageStr = "smite " + self.usageStr = "smite " def execute(self, args = []): if len(self.encounter.data) > 0: From 1571e6f4702151a3ee402f577ca0700551e702f2 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 2 Feb 2023 14:56:39 -0500 Subject: [PATCH 034/113] Create private helper method for healing NPCs Should help reduce code repetition for different command selectors. --- src/encounter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index cde92d3..b106837 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -534,6 +534,12 @@ def __init__(self, encounter): self.description = "Directly adds to an NPC's health." self.usageStr = "heal " + def __healNPC(self, npc: NPC, amount: int) -> int: + originalHP = npc.currentHP + npc.currentHP = originalHP + amount + npc.currentHP = min(npc.maxHP, npc.currentHP) + return npc.currentHP - originalHP + def execute(self, args = []): if len(args) == 2: if not isInt(args[1]): From 3dc5528d51267b2e41045897a2f66cbe69c4cfb8 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 2 Feb 2023 14:58:30 -0500 Subject: [PATCH 035/113] Implement heal all --- src/encounter.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index b106837..48d01af 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -548,7 +548,16 @@ def execute(self, args = []): if int(args[1]) < 1: print("Amount must be more than zero.") return - if isValidInt(args[0], self.encounter.data): + if args[0] == "all": + if len(self.encounter.data) > 0: + for npc in self.encounter.data: + healedAmt = self.__healNPC(npc, int(args[1])) + output = "{} was healed {} points.".format(npc.nick, healedAmt) + print(output) + else: + print("Encounter is empty. There is noone to heal.") + return + elif isValidInt(args[0], self.encounter.data): npc = self.encounter.data[int(args[0]) - 1] origHP = npc.currentHP From 073e654fa1ce5b6f1c0d5ce13d074911ed774ca7 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 2 Feb 2023 15:01:37 -0500 Subject: [PATCH 036/113] Use new helper method in single heal --- src/encounter.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 48d01af..af2d52c 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -559,15 +559,7 @@ def execute(self, args = []): return elif isValidInt(args[0], self.encounter.data): npc = self.encounter.data[int(args[0]) - 1] - origHP = npc.currentHP - - npc.currentHP = npc.currentHP + int(args[1]) - - if npc.currentHP > npc.maxHP: - npc.currentHP = npc.maxHP - - healedAmt = npc.currentHP - origHP - + healedAmt = self.__healNPC(npc, int(args[1])) print(npc.nick + " was healed " + str(healedAmt) + " points.") else: self.usage() From e9991adceda2b59a01138ef336505d672dfbd50a Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 3 Feb 2023 00:48:06 -0500 Subject: [PATCH 037/113] Implement damage all --- src/encounter.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index af2d52c..4efbe6f 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -467,7 +467,18 @@ def execute(self, args = []): if int(args[1]) < 1: print("Amount must be more than zero.") return - if isValidInt(args[0], self.encounter.data) is True: + if args[0] == "all": + if len(self.encounter.data) < 1: + print("Encoutner is empty. Noone to damage.") + return + for npc in self.encounter.data: + if npc.currentHP > 0: + npc.currentHP = max(0, npc.currentHP - int(args[1])) + if npc.currentHP <= 0: + print(npc.nick + " has been defeated.") + if areAllDefeated(self.encounter.data): + print("Party has defeated all enemies.") + elif isValidInt(args[0], self.encounter.data) is True: npc = self.encounter.data[int(args[0]) - 1] if npc.currentHP <= 0: From 84ee54cdee886f79bc954cea5b4b506c11fd1e1b Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 3 Feb 2023 00:52:01 -0500 Subject: [PATCH 038/113] Remove duplicate selections when calling smite Ignores selecting the same NPC multiple times which could lead to unexpected / unintended behaviors. --- src/encounter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/encounter.py b/src/encounter.py index 4efbe6f..21057ea 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -514,6 +514,7 @@ def execute(self, args = []): print("All enemies have been defeated.") else: selected = args[0].split(",") + selected = list(set(selected)) # Remove duplicates from the selection for index in selected: if isValidInt(args[0], self.encounter.data) is False: From 70593dd6cf9a7faeb41857eb93541a0e2f8fffa1 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:33:03 -0500 Subject: [PATCH 039/113] Allow multi selection in heal command --- src/encounter.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 21057ea..0b619e7 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -569,12 +569,18 @@ def execute(self, args = []): else: print("Encounter is empty. There is noone to heal.") return - elif isValidInt(args[0], self.encounter.data): - npc = self.encounter.data[int(args[0]) - 1] - healedAmt = self.__healNPC(npc, int(args[1])) - print(npc.nick + " was healed " + str(healedAmt) + " points.") else: - self.usage() + if not isValidInt(args[0], self.encounter.data): + self.usage() + return + + selected = args[0].split(",") + selected = list(set(selected)) + + for index in selected: + npc = self.encounter.data[int(index) - 1] + healedAmt = self.__healNPC(npc, int(args[1])) + print(npc.nick + " was healed " + str(healedAmt) + " points.") else: self.usage() From d6122a1667f47a2e3ccbff28733ef0290b85d009 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:33:35 -0500 Subject: [PATCH 040/113] Utilize isValidInt helper method in add command --- src/encounter.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 0b619e7..b526bcf 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -298,12 +298,11 @@ def execute(self, args = []): raise TypeError("Encounter list must be an NPCList.") if len(args) == 1: - selected = args[0].split(",") + if not isValidInt(args[0], bestiary.data): + self.usage() + return - for index in selected: - if not isInt(index) or int(index) > len(bestiary.data) or int(index) <= 0: - self.usage() - return + selected = args[0].split(",") for index in selected: copyNPC(bestiary.data, int(index), encounter.data) From 7530f758a45a8739e35b55c142d68d865e1bdcfa Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:34:12 -0500 Subject: [PATCH 041/113] Don't display output on valid use of add command Avoid output unless a failure occurs. --- src/encounter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index b526bcf..b923ca3 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -306,7 +306,6 @@ def execute(self, args = []): for index in selected: copyNPC(bestiary.data, int(index), encounter.data) - print(encounter.toMenu()) else: self.usage() From c07dcb96bad3b5ae41bed8e023faeb61bb90b050 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:35:53 -0500 Subject: [PATCH 042/113] Implement dunder len for NPCList Should allow directly determining the length of an NPCList without having to interact with its fields. Feels more natural and makes the NPCList object more useful. Will require some refactoring to make use of. --- src/encounter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index b923ca3..f89bba0 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -116,6 +116,9 @@ def __init__(self, names: list[str]): self.name = names[0] self.data: list[NPC] = [] + def __len__(self): + return len(self.data) + def toMenu(self): info = self.name.upper() + ":\n" From 7bb059731b7a5bf42d05042e965ce63c206bf28a Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 4 Feb 2023 15:58:14 -0500 Subject: [PATCH 043/113] Implement multi select for damage command --- src/encounter.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index f89bba0..7ad95e0 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -465,7 +465,7 @@ def execute(self, args = []): if not isInt(args[1]): self.usage() return - if int(args[1]) < 1: + elif int(args[1]) < 1: print("Amount must be more than zero.") return if args[0] == "all": @@ -479,20 +479,27 @@ def execute(self, args = []): print(npc.nick + " has been defeated.") if areAllDefeated(self.encounter.data): print("Party has defeated all enemies.") - elif isValidInt(args[0], self.encounter.data) is True: - npc = self.encounter.data[int(args[0]) - 1] - - if npc.currentHP <= 0: - print("Enemy already defeated.") + else: + if not isValidInt(args[0], self.encounter.data): + self.usage() return - npc.currentHP = max(0, npc.currentHP - int(args[1])) - if npc.currentHP <= 0: - print(npc.nick + " has been defeated.") - if areAllDefeated(self.encounter.data): - print("Party has defeated all enemies.") - else: - self.usage() + selected = args[0].split(",") + selected = list(set(selected)) + + for index in selected: + npc = self.encounter.data[int(index) - 1] + + if npc.currentHP <= 0: + print(npc.nick + " already defeated.") + + npc.currentHP = max(0, npc.currentHP - int(args[1])) + + if npc.currentHP <= 0: + print(npc.nick + " has been defeated.") + if areAllDefeated(self.encounter.data): + print("Party has defeated all enemies.") + return else: self.usage() From b48ccc4780dceb5122fab3fcff9efd655125455b Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:33:40 -0500 Subject: [PATCH 044/113] Implement multiselect for mark --- src/encounter.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 7ad95e0..9a0fca4 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -687,25 +687,28 @@ def __init__(self, encounter): def execute(self, args=[]) -> None: if len(args) >= 1: - if not args[0].isnumeric(): - if args[0].lower() == "all": - for npc in self.encounter.data: - npc.marked = True - if len(args) > 1: - npc.note = " ".join(args[1:]) - else: - npc.note = "" - else: - self.usage() - return - if isValidInt(args[0], self.encounter.data) is True: - self.encounter.data[int(args[0]) - 1].marked = True - if len(args) > 1: - self.encounter.data[int(args[0]) - 1].note = " ".join(args[1:]) - else: - self.encounter.data[int(args[0]) - 1].note = "" + if args[0].lower() == "all": + for npc in self.encounter.data: + npc.marked = True + if len(args) > 1: + npc.note = " ".join(args[1:]) + else: + npc.note = "" else: - self.usage() + if not isValidInt(args[0], self.encounter.data): + self.usage() + return + + selected = args[0].split(",") + selected = list(set(selected)) + + for index in selected: + npc = self.encounter.data[int(index) - 1] + npc.marked = True + if len(args) > 1: + npc.note = " ".join(args[1:]) + else: + npc.note = "" else: self.usage() From 953ccea6521a10086de876238e57ccf0d566a74d Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:36:35 -0500 Subject: [PATCH 045/113] Print error if marking all and encounter is empty --- src/encounter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 9a0fca4..9228a96 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -688,6 +688,10 @@ def __init__(self, encounter): def execute(self, args=[]) -> None: if len(args) >= 1: if args[0].lower() == "all": + if (len(self.encounter) < 1): + print("Encounter is empty. Noone to mark.") + return + for npc in self.encounter.data: npc.marked = True if len(args) > 1: From 7605839c459bbedfead1218ec5907e55dd492c1c Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:41:01 -0500 Subject: [PATCH 046/113] Implement multi select for unmark --- src/encounter.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 9228a96..149acd8 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -727,19 +727,26 @@ def __init__(self, encounter): def execute(self, args=[]) -> None: if len(args) == 1: - if not args[0].isnumeric(): - if args[0].lower() == "all": - for npc in self.encounter.data: - npc.marked = False - npc.note = "" - else: - self.usage() - return - if isValidInt(args[0], self.encounter.data) is True: - self.encounter.data[int(args[0]) - 1].marked = False - self.encounter.data[int(args[0]) - 1].note = "" + if args[0].lower() == "all": + if (len(self.encounter) < 1): + print("Encounter is empty. Noone to unmark.") + return + + for npc in self.encounter.data: + npc.marked = False + npc.note = "" else: - self.usage() + if not isValidInt(args[0], self.encounter.data): + self.usage() + return + + selected = args[0].split(",") + selected = list(set(selected)) + + for index in selected: + npc = self.encounter.data[int(index) - 1] + npc.marked = False + npc.note = "" else: self.usage() From 39ba9825fda47d3beb0ff899f722b68e648e9261 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:47:45 -0500 Subject: [PATCH 047/113] Fuzzy check for all selector "All" (and any other variation) should be treated the same as "all". This commit makes this behavior consistent across all commands that include this selector. --- src/encounter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 149acd8..41cae35 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -249,7 +249,7 @@ def execute(self, args = []): else: print("Unknown list selected.") else: - if numArgs == 0 or args[0] == "all": + if numArgs == 0 or args[0].lower() == "all": for list in self.referenceLists: print(list.toMenu()) if list is not self.referenceLists[-1]: @@ -351,7 +351,7 @@ def __init__(self, encounter): def execute(self, args = []): if len(args) == 1: - if args[0] == "all": + if args[0].lower() == "all": self.encounter.data.clear() print(self.encounter.toMenu()) else: @@ -468,7 +468,7 @@ def execute(self, args = []): elif int(args[1]) < 1: print("Amount must be more than zero.") return - if args[0] == "all": + if args[0].lower() == "all": if len(self.encounter.data) < 1: print("Encoutner is empty. Noone to damage.") return @@ -515,7 +515,7 @@ def __init__(self, encounter): def execute(self, args = []): if len(self.encounter.data) > 0: if len(args) == 1: - if args[0] == "all": + if args[0].lower() == "all": for npc in self.encounter.data: if npc.currentHP > 0: npc.currentHP = 0 @@ -568,7 +568,7 @@ def execute(self, args = []): if int(args[1]) < 1: print("Amount must be more than zero.") return - if args[0] == "all": + if args[0].lower() == "all": if len(self.encounter.data) > 0: for npc in self.encounter.data: healedAmt = self.__healNPC(npc, int(args[1])) @@ -603,7 +603,7 @@ def __init__(self, encounter): def execute(self, args = []): if len(args) == 1: - if isinstance(args[0], str) and args[0].lower() == "all": + if args[0].lower() == "all": print("Status:") for npc in self.encounter.data: print(npc.combatStatus()) From b7ee9ee74423c720193a39c781b243710dd8dd0a Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:59:36 -0500 Subject: [PATCH 048/113] Implement multi select for status Fixes a bug that would crash the program if attempting to perform status on a valid list of indices. --- src/encounter.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 41cae35..1656ac8 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -599,18 +599,28 @@ def __init__(self, encounter): self.names = ['status'] self.encounter = encounter self.description = "Displays an NPC's current stats." - self.usageStr = "status " + self.usageStr = "status " def execute(self, args = []): if len(args) == 1: if args[0].lower() == "all": + if len(self.encounter) < 1: + print("Encounter is empty. Noone's status to display.") + return + print("Status:") for npc in self.encounter.data: print(npc.combatStatus()) elif isValidInt(args[0], self.encounter.data): - npc = self.encounter.data[int(args[0]) - 1] - print("Status:") - print(npc.combatStatus()) + selected = args[0].split(",") + selected = list(set(selected)) + + if len(self.encounter) > 0: + print("Status:") + for index in selected: + npc = self.encounter.data[int(index) - 1] + + print(npc.combatStatus()) else: self.usage() else: From 86c71a16793658beccaaca486727132f65e4634f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:00:05 -0500 Subject: [PATCH 049/113] Update help hints to reflect multi select --- src/encounter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 1656ac8..a15e9a3 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -458,7 +458,7 @@ def __init__(self, encounter): self.names = ['damage'] self.encounter = encounter self.description = "Directly subtracts from an NPC's health." - self.usageStr = "damage " + self.usageStr = "damage " def execute(self, args = []): if len(args) == 2: @@ -552,7 +552,7 @@ def __init__(self, encounter): self.names = ['heal'] self.encounter = encounter self.description = "Directly adds to an NPC's health." - self.usageStr = "heal " + self.usageStr = "heal " def __healNPC(self, npc: NPC, amount: int) -> int: originalHP = npc.currentHP @@ -693,7 +693,7 @@ def __init__(self, encounter): self.names = ['mark', 'note'] self.encounter = encounter self.description = "Mark an NPC with a symbol and note" - self.usageStr = "mark [note]" + self.usageStr = "mark [note]" def execute(self, args=[]) -> None: if len(args) >= 1: @@ -733,7 +733,7 @@ def __init__(self, encounter): self.names = ['unmark'] self.encounter = encounter self.description = "Remove mark and symbol from an NPC" - self.usageStr = "unmark " + self.usageStr = "unmark " def execute(self, args=[]) -> None: if len(args) == 1: From 7414189d9cb7460ed93004935aa1d94d638e63a0 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:14:31 -0500 Subject: [PATCH 050/113] End all help entries with a '.' Makes line endings for all help messages consistent. --- src/encounter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index a15e9a3..32aa413 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -652,7 +652,7 @@ def __init__(self, bestiary): super().__init__() self.names = ['make'] self.bestiary = bestiary - self.description = "Creates an NPC for the bestiary" + self.description = "Creates an NPC for the bestiary." self.usageStr = "make " def execute(self, args=[]) -> None: @@ -671,7 +671,7 @@ def __init__(self, encounter): super().__init__() self.names = ['name', 'nick'] self.encounter = encounter - self.description = "Gives a specific name to an NPC in the encounter" + self.description = "Gives a specific name to an NPC in the encounter." self.usageStr = "name " def execute(self, args=[]) -> None: @@ -692,7 +692,7 @@ def __init__(self, encounter): super().__init__() self.names = ['mark', 'note'] self.encounter = encounter - self.description = "Mark an NPC with a symbol and note" + self.description = "Mark an NPC with a symbol and note." self.usageStr = "mark [note]" def execute(self, args=[]) -> None: @@ -732,7 +732,7 @@ def __init__(self, encounter): super().__init__() self.names = ['unmark'] self.encounter = encounter - self.description = "Remove mark and symbol from an NPC" + self.description = "Remove mark and symbol from an NPC." self.usageStr = "unmark " def execute(self, args=[]) -> None: From 07baaed7da046d325cf9efe12eb8a77be4153a75 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:16:28 -0500 Subject: [PATCH 051/113] Don't display output on remove command success Makes output more consistent across the board. Successful output shouldn't display a message while errors should. --- src/encounter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 32aa413..63701cf 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -353,7 +353,6 @@ def execute(self, args = []): if len(args) == 1: if args[0].lower() == "all": self.encounter.data.clear() - print(self.encounter.toMenu()) else: selected = args[0].split(",") @@ -366,7 +365,6 @@ def execute(self, args = []): for index in selected: self.encounter.data.pop(int(index) - 1) - print(self.encounter.toMenu()) else: self.usage() From fecb58f075cc2f4285d91f9ebb7477ed5ecf554f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:18:40 -0500 Subject: [PATCH 052/113] Utilize isValidInt correctly in remove logic The isValidInt method checks an entire list automatically, so adding extra logic to do so was redundant and confusing. --- src/encounter.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 63701cf..d1b1e92 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -354,12 +354,11 @@ def execute(self, args = []): if args[0].lower() == "all": self.encounter.data.clear() else: - selected = args[0].split(",") - - for index in selected: - if not isValidInt(index, self.encounter.data): - return + if not isValidInt(args[0], self.encounter.data): + self.usage() + return + selected = args[0].split(",") # Remove duplicates and reverse sort the input selected = sorted(list(set(selected)), reverse = True) From 636c30e69dc4bfd9e45f2003eaa004d1e3c6d9fb Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:21:34 -0500 Subject: [PATCH 053/113] Implement empty encounter error helper method This helper method displays one single error message intended to be used when a command that acts on the encounter list is empty. --- src/encounter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index d1b1e92..106c802 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -149,6 +149,9 @@ def __init__(self): def usage(self) -> None: print("Usage: " + self.usageStr) + def encounterEmpty(self) -> None: + print("The encounter is empty. Add some NPCs to it and try again.") + @abstractmethod def execute(self, args = []) -> None: raise NotImplementedError("This command has not been implemented yet.") From f8bb5d48565cfcd3af582090ef16f897af427c6a Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:23:10 -0500 Subject: [PATCH 054/113] Utilize encounterEmpty method Refactor commands to make empty checks before processing the command. Starting with this check allows for refactoring to make the logic and control flow simpler for commands that were attempting to check for empty lists on their own. --- src/encounter.py | 117 ++++++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 106c802..cbfb144 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -353,6 +353,10 @@ def __init__(self, encounter): self.usageStr = "remove " def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + if len(args) == 1: if args[0].lower() == "all": self.encounter.data.clear() @@ -387,6 +391,10 @@ def __init__(self, encounter): self.usageStr = "attack [hit] [damage]" def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + lenArgs = len(args) npc = None @@ -461,6 +469,10 @@ def __init__(self, encounter): self.usageStr = "damage " def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + if len(args) == 2: if not isInt(args[1]): self.usage() @@ -469,9 +481,6 @@ def execute(self, args = []): print("Amount must be more than zero.") return if args[0].lower() == "all": - if len(self.encounter.data) < 1: - print("Encoutner is empty. Noone to damage.") - return for npc in self.encounter.data: if npc.currentHP > 0: npc.currentHP = max(0, npc.currentHP - int(args[1])) @@ -513,37 +522,38 @@ def __init__(self, encounter): self.usageStr = "smite " def execute(self, args = []): - if len(self.encounter.data) > 0: - if len(args) == 1: - if args[0].lower() == "all": - for npc in self.encounter.data: - if npc.currentHP > 0: - npc.currentHP = 0 - print("All enemies have been defeated.") - else: - selected = args[0].split(",") - selected = list(set(selected)) # Remove duplicates from the selection + if (len(self.encounter) < 1): + self.encounterEmpty() + return - for index in selected: - if isValidInt(args[0], self.encounter.data) is False: - self.usage() - return + if len(args) == 1: + if args[0].lower() == "all": + for npc in self.encounter.data: + if npc.currentHP > 0: + npc.currentHP = 0 + print("All enemies have been defeated.") + else: + selected = args[0].split(",") + selected = list(set(selected)) # Remove duplicates from the selection - for index in selected: - npc = self.encounter.data[int(index) - 1] - if npc.currentHP <= 0: - print("Enemy already defeated.") - return - else: - npc.currentHP = 0 - print(npc.nick + " was defeated.") + for index in selected: + if isValidInt(args[0], self.encounter.data) is False: + self.usage() + return - if areAllDefeated(self.encounter.data): - print("Party has defeated all enemies.") - else: - self.usage() + for index in selected: + npc = self.encounter.data[int(index) - 1] + if npc.currentHP <= 0: + print("Enemy already defeated.") + return + else: + npc.currentHP = 0 + print(npc.nick + " was defeated.") + + if areAllDefeated(self.encounter.data): + print("Party has defeated all enemies.") else: - print("Encounter is empty. There is no one to smite.") + self.usage() class heal(Command): @@ -561,6 +571,10 @@ def __healNPC(self, npc: NPC, amount: int) -> int: return npc.currentHP - originalHP def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + if len(args) == 2: if not isInt(args[1]): self.usage() @@ -569,14 +583,10 @@ def execute(self, args = []): print("Amount must be more than zero.") return if args[0].lower() == "all": - if len(self.encounter.data) > 0: - for npc in self.encounter.data: - healedAmt = self.__healNPC(npc, int(args[1])) - output = "{} was healed {} points.".format(npc.nick, healedAmt) - print(output) - else: - print("Encounter is empty. There is noone to heal.") - return + for npc in self.encounter.data: + healedAmt = self.__healNPC(npc, int(args[1])) + output = "{} was healed {} points.".format(npc.nick, healedAmt) + print(output) else: if not isValidInt(args[0], self.encounter.data): self.usage() @@ -602,12 +612,12 @@ def __init__(self, encounter): self.usageStr = "status " def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + if len(args) == 1: if args[0].lower() == "all": - if len(self.encounter) < 1: - print("Encounter is empty. Noone's status to display.") - return - print("Status:") for npc in self.encounter.data: print(npc.combatStatus()) @@ -615,8 +625,7 @@ def execute(self, args = []): selected = args[0].split(",") selected = list(set(selected)) - if len(self.encounter) > 0: - print("Status:") + print("Status:") for index in selected: npc = self.encounter.data[int(index) - 1] @@ -675,6 +684,10 @@ def __init__(self, encounter): self.usageStr = "name " def execute(self, args=[]) -> None: + if (len(self.encounter) < 1): + self.encounterEmpty() + return + if len(args) == 2: if not args[0].isnumeric(): self.usage() @@ -696,12 +709,12 @@ def __init__(self, encounter): self.usageStr = "mark [note]" def execute(self, args=[]) -> None: + if (len(self.encounter) < 1): + self.encounterEmpty() + return + if len(args) >= 1: if args[0].lower() == "all": - if (len(self.encounter) < 1): - print("Encounter is empty. Noone to mark.") - return - for npc in self.encounter.data: npc.marked = True if len(args) > 1: @@ -736,12 +749,12 @@ def __init__(self, encounter): self.usageStr = "unmark " def execute(self, args=[]) -> None: + if (len(self.encounter) < 1): + self.encounterEmpty() + return + if len(args) == 1: if args[0].lower() == "all": - if (len(self.encounter) < 1): - print("Encounter is empty. Noone to unmark.") - return - for npc in self.encounter.data: npc.marked = False npc.note = "" From 76b111e5ede8e513a216348e2ad34d6a10717b6b Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:41:26 -0500 Subject: [PATCH 055/113] Add spacing to note display This formatting makes reading the status all output more manageable. --- src/encounter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index cbfb144..55c4e4f 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -86,8 +86,9 @@ def combatStatus(self) -> str: status += " [Dead]" if self.marked: - status += "\nNote:\n" + status += "\n Note:\n" if not self.note.isspace() and len(self.note) > 0: + status += " " status += self.note else: status += "EMPTY" From 4ab4123108d51e7827e72d78303454d92d6ac766 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 12:01:16 -0500 Subject: [PATCH 056/113] Implement out of bounds error method Provides a more descriptive and helpful error message in the case the user selects an invalid index for a command. --- src/encounter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 55c4e4f..cfc0381 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -153,6 +153,10 @@ def usage(self) -> None: def encounterEmpty(self) -> None: print("The encounter is empty. Add some NPCs to it and try again.") + def OOBSelection(self, referenceList: NPCList) -> None: + print("Your selection contains values out of range for the " + referenceList.name) + print("Adjust your selection and try again.") + @abstractmethod def execute(self, args = []) -> None: raise NotImplementedError("This command has not been implemented yet.") From b527a48fc4d5be4787ad7f92b247df89a0a4e11f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 12:04:29 -0500 Subject: [PATCH 057/113] Utilize OOBSelection helper where relevant Utilizes the new helper method to provide better error reporting typically where only the generic usage was displayed. --- src/encounter.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index cfc0381..c914098 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -310,7 +310,7 @@ def execute(self, args = []): if len(args) == 1: if not isValidInt(args[0], bestiary.data): - self.usage() + self.OOBSelection(bestiary) return selected = args[0].split(",") @@ -367,7 +367,7 @@ def execute(self, args = []): self.encounter.data.clear() else: if not isValidInt(args[0], self.encounter.data): - self.usage() + self.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -408,7 +408,7 @@ def execute(self, args = []): return if not isValidInt(args[0], self.encounter.data): - self.usage() + self.OOBSelection(self.encounter) return npc = self.encounter.data[int(args[0]) - 1] @@ -495,7 +495,7 @@ def execute(self, args = []): print("Party has defeated all enemies.") else: if not isValidInt(args[0], self.encounter.data): - self.usage() + self.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -543,7 +543,7 @@ def execute(self, args = []): for index in selected: if isValidInt(args[0], self.encounter.data) is False: - self.usage() + self.OOBSelection(self.encounter) return for index in selected: @@ -594,7 +594,7 @@ def execute(self, args = []): print(output) else: if not isValidInt(args[0], self.encounter.data): - self.usage() + self.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -636,7 +636,7 @@ def execute(self, args = []): print(npc.combatStatus()) else: - self.usage() + self.OOBSelection(self.encounter) else: self.usage() @@ -655,6 +655,8 @@ def execute(self, args = []): if isValidInt(args[0], self.bestiary.data): print("INFO:") print(self.bestiary.data[int(args[0]) - 1].detailedInfo()) + else: + self.OOBSelection(self.bestiary) else: self.usage() else: @@ -700,7 +702,7 @@ def execute(self, args=[]) -> None: if isValidInt(args[0], self.encounter.data) is True: self.encounter.data[int(args[0]) - 1].nick = args[1] else: - self.usage() + self.OOBSelection(self.encounter) else: self.usage() @@ -728,7 +730,7 @@ def execute(self, args=[]) -> None: npc.note = "" else: if not isValidInt(args[0], self.encounter.data): - self.usage() + self.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -765,7 +767,7 @@ def execute(self, args=[]) -> None: npc.note = "" else: if not isValidInt(args[0], self.encounter.data): - self.usage() + self.OOBSelection(self.encounter) return selected = args[0].split(",") From 93cfb127587c3161be2a9f12e589b4c32239abcd Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 12:08:06 -0500 Subject: [PATCH 058/113] Refactor isValidInt to use NPCList class directly Makes code intent more clear and makes using the NPCList class less obtuse. --- src/encounter.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index c914098..b700fcc 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -278,10 +278,10 @@ def isInt(string: str) -> bool: return True -def isValidInt(selector: str, list: list[NPC]) -> bool: +def isValidInt(selector: str, referencList: NPCList) -> bool: selected = selector.split(",") for index in selected: - if not isInt(index) or int(index) <= 0 or int(index) > len(list): + if not isInt(index) or int(index) <= 0 or int(index) > len(referencList): return False return True @@ -309,7 +309,7 @@ def execute(self, args = []): raise TypeError("Encounter list must be an NPCList.") if len(args) == 1: - if not isValidInt(args[0], bestiary.data): + if not isValidInt(args[0], bestiary): self.OOBSelection(bestiary) return @@ -366,7 +366,7 @@ def execute(self, args = []): if args[0].lower() == "all": self.encounter.data.clear() else: - if not isValidInt(args[0], self.encounter.data): + if not isValidInt(args[0], self.encounter): self.OOBSelection(self.encounter) return @@ -407,7 +407,7 @@ def execute(self, args = []): self.usage() return - if not isValidInt(args[0], self.encounter.data): + if not isValidInt(args[0], self.encounter): self.OOBSelection(self.encounter) return @@ -494,7 +494,7 @@ def execute(self, args = []): if areAllDefeated(self.encounter.data): print("Party has defeated all enemies.") else: - if not isValidInt(args[0], self.encounter.data): + if not isValidInt(args[0], self.encounter): self.OOBSelection(self.encounter) return @@ -542,7 +542,7 @@ def execute(self, args = []): selected = list(set(selected)) # Remove duplicates from the selection for index in selected: - if isValidInt(args[0], self.encounter.data) is False: + if not isValidInt(args[0], self.encounter): self.OOBSelection(self.encounter) return @@ -593,7 +593,7 @@ def execute(self, args = []): output = "{} was healed {} points.".format(npc.nick, healedAmt) print(output) else: - if not isValidInt(args[0], self.encounter.data): + if not isValidInt(args[0], self.encounter): self.OOBSelection(self.encounter) return @@ -626,7 +626,7 @@ def execute(self, args = []): print("Status:") for npc in self.encounter.data: print(npc.combatStatus()) - elif isValidInt(args[0], self.encounter.data): + elif isValidInt(args[0], self.encounter): selected = args[0].split(",") selected = list(set(selected)) @@ -652,7 +652,7 @@ def __init__(self, bestiary): def execute(self, args = []): if len(args) == 1: if isInt(args[0]): - if isValidInt(args[0], self.bestiary.data): + if isValidInt(args[0], self.bestiary): print("INFO:") print(self.bestiary.data[int(args[0]) - 1].detailedInfo()) else: @@ -699,7 +699,7 @@ def execute(self, args=[]) -> None: if not args[0].isnumeric(): self.usage() return - if isValidInt(args[0], self.encounter.data) is True: + if isValidInt(args[0], self.encounter): self.encounter.data[int(args[0]) - 1].nick = args[1] else: self.OOBSelection(self.encounter) @@ -729,7 +729,7 @@ def execute(self, args=[]) -> None: else: npc.note = "" else: - if not isValidInt(args[0], self.encounter.data): + if not isValidInt(args[0], self.encounter): self.OOBSelection(self.encounter) return @@ -766,7 +766,7 @@ def execute(self, args=[]) -> None: npc.marked = False npc.note = "" else: - if not isValidInt(args[0], self.encounter.data): + if not isValidInt(args[0], self.encounter): self.OOBSelection(self.encounter) return From d116948051b0175d3589d4b00f1902c47d438478 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 18:51:39 -0500 Subject: [PATCH 059/113] Utilize NPCList len in load --- src/encounter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index b700fcc..d522b5c 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -178,7 +178,7 @@ def execute(self, args = []): bestiaryFile = open(args[0].strip()) except FileNotFoundError: print("Selected bestiary file could not be found.") - if len(self.bestiary.data) == 0: + if len(self.bestiary) == 0: print("Loading placeholder bestiary.") self.bestiary.data.append(NPC("Human", 5, 12)) self.bestiary.data.append(NPC("Animal", 3, 10)) From b22db0c1086209b65f006c6143584071ee7165f2 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 18:53:53 -0500 Subject: [PATCH 060/113] Utilize NPCList class in NPC defeat message helper Hides some interactions with NPCList's data parameter. Simplifies the interaction with the helper method. --- src/encounter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index d522b5c..79e2192 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -380,8 +380,8 @@ def execute(self, args = []): self.usage() -def areAllDefeated(encounter): - for npc in encounter: +def areAllDefeated(encounter: NPCList): + for npc in encounter.data: if npc.currentHP > 0: return False return True @@ -461,7 +461,7 @@ def execute(self, args = []): if npc is not None: if npc.currentHP <= 0: print(npc.nick + " has been defeated.") - if areAllDefeated(self.encounter.data): + if areAllDefeated(self.encounter): print("Party has defeated all enemies.") @@ -491,7 +491,7 @@ def execute(self, args = []): npc.currentHP = max(0, npc.currentHP - int(args[1])) if npc.currentHP <= 0: print(npc.nick + " has been defeated.") - if areAllDefeated(self.encounter.data): + if areAllDefeated(self.encounter): print("Party has defeated all enemies.") else: if not isValidInt(args[0], self.encounter): @@ -511,7 +511,7 @@ def execute(self, args = []): if npc.currentHP <= 0: print(npc.nick + " has been defeated.") - if areAllDefeated(self.encounter.data): + if areAllDefeated(self.encounter): print("Party has defeated all enemies.") return else: @@ -555,7 +555,7 @@ def execute(self, args = []): npc.currentHP = 0 print(npc.nick + " was defeated.") - if areAllDefeated(self.encounter.data): + if areAllDefeated(self.encounter): print("Party has defeated all enemies.") else: self.usage() From d469c9a62f5fb6204942dae58ccec9487928b89c Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:10:50 -0500 Subject: [PATCH 061/113] Change capitalization for Command class names These names serve as examples. These entries must be all lowercase to ensure compatibility with existing systems. --- src/encounter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 79e2192..1d3f698 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -143,8 +143,8 @@ def findList(name: str, referenceLists: list[NPCList]) -> NPCList | None: class Command(ABC): def __init__(self): - self.names: list[str] = ['Command', 'Test'] - self.description: str = "This Command has no defined description yet." + self.names: list[str] = ['command', 'test'] + self.description: str = "This command has no defined description yet." self.usageStr: str = "This command has no defined usage yet." def usage(self) -> None: From 85b48f990662311b956149cdfc1e9d49cbc47133 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:12:48 -0500 Subject: [PATCH 062/113] Implement detailed help descriptions Allow commands to define a details string. This will be used for providing more specifics about commands that aren't described in their description text. This should be used for making known to the user alternate command names or how to use special actions like the 'all' selector when available. --- src/encounter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 1d3f698..fa233f3 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -145,6 +145,7 @@ class Command(ABC): def __init__(self): self.names: list[str] = ['command', 'test'] self.description: str = "This command has no defined description yet." + self.details: str | None = None self.usageStr: str = "This command has no defined usage yet." def usage(self) -> None: @@ -217,6 +218,9 @@ def execute(self, args = []): if args[0].lower() in command.names: print(command.description) command.usage() + if command.details is not None: + print() + print(command.details) found = True break From a4f78ca5d71df73a6a6969a7d3df6a886369dc6f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 8 Feb 2023 11:29:07 -0500 Subject: [PATCH 063/113] Improve list documentation Describe aliases for the list command in the detailed help menu. --- src/encounter.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index fa233f3..fd38fad 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from textwrap import dedent class NPC: @@ -247,7 +248,14 @@ def __init__(self, referenceLists: list[NPCList]): super().__init__() self.names = ['list', 'display', 'show'] self.referenceLists = referenceLists - self.description = "Displays a list of NPCs." + self.description = "Displays the selected list of NPCs." + self.details = dedent("""\ + The list command can be called using the aliases "display" and "show". + + The selected list can be any valid alias for their respective list. + Allowed aliases for "bestiary" are "book" and "b". + Allowed aliases for "encounter" are "e", "combat", and "c". + """).strip() self.usageStr = "list [all | bestiary | encounter]" def execute(self, args = []): From b7824e870be69c783b67507e7014dfe3315f3f45 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Wed, 8 Feb 2023 11:31:40 -0500 Subject: [PATCH 064/113] Invalidate list command with extra arguments Display usage menu when provided too many arguments for the list command. Still allows having too few arguments. --- src/encounter.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index fd38fad..650b9ba 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -261,21 +261,21 @@ def __init__(self, referenceLists: list[NPCList]): def execute(self, args = []): numArgs = len(args) - if numArgs == 1 and args[0] != "all": - list = findList(args[0], self.referenceLists) - - if list is not None: - print(list.toMenu()) - else: - print("Unknown list selected.") - else: + if numArgs <= 1: if numArgs == 0 or args[0].lower() == "all": for list in self.referenceLists: print(list.toMenu()) if list is not self.referenceLists[-1]: - print() + print() # Print newline between all lists else: - self.usage() + list = findList(args[0], self.referenceLists) + + if list is not None: + print(list.toMenu()) + else: + print("Unknown list selected.") + else: + self.usage() def isInt(string: str) -> bool: From 170f3e82586f76ecf3ffad50273ea098317eb5b3 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 10 Feb 2023 17:51:40 -0500 Subject: [PATCH 065/113] Differentiate description and details for commands --- src/encounter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 650b9ba..483f59d 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -220,7 +220,7 @@ def execute(self, args = []): print(command.description) command.usage() if command.details is not None: - print() + print("\nNote:") print(command.details) found = True break From 1dfa0e55308206c8992b18a5e0c9d7893153a662 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 10 Feb 2023 17:52:53 -0500 Subject: [PATCH 066/113] Improve load command help details * Makes short description more accurate. * Describes the fallback process for loading files. --- src/encounter.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 483f59d..4751312 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -169,7 +169,15 @@ def __init__(self, bestiary): super().__init__() self.names = ['load'] self.bestiary = bestiary - self.description = "Replaces the default bestiary." + self.description = "Replaces the loaded bestiary." + self.details = dedent("""\ + Searches the absolute address provided for a valid bestiary file. + The correct format for a file is provided in an example file "bestiary.txt". + + If the provided file cannot be loaded the current list will be kept. + If the current list is empty and a new list cannot be found + then some primitive entries will be generated. + """).strip() self.usageStr = "load " def execute(self, args = []): From 3daf8cdcbbb967d298e84e8cc7b30434b061bf1b Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Fri, 10 Feb 2023 17:58:25 -0500 Subject: [PATCH 067/113] Don't display output on load success Part of making command output consistent and only output extra info on error. Obvious that the list is loaded once the user can input another command. --- src/encounter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 4751312..b57d25c 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -201,7 +201,6 @@ def execute(self, args = []): npc = NPC(line[0], int(line[1]), int(line[2])) self.bestiary.data.append(npc) bestiaryFile.close() - print("Bestiary loaded.") else: self.usage() From b5f56c53ad06c10e0145a6447bb328e73d56be81 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 11 Feb 2023 12:36:06 -0500 Subject: [PATCH 068/113] Improve add command help details --- src/encounter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index b57d25c..a80ba28 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -317,6 +317,11 @@ def __init__(self, referenceLists): self.names = ['add'] self.referenceLists = referenceLists self.description = "Adds an NPC to the encounter." + self.details = dedent("""\ + Reference entries in the bestiary by number. + Multiple NPCs (even multiple of the same type) can be added at the same time + in a comma separated list without spaces. + """).strip() self.usageStr = "add " def execute(self, args = []): From ada2ee452d6ace8532e6bf68ab6de896c9a0aba9 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 11 Feb 2023 15:35:18 -0500 Subject: [PATCH 069/113] Improve some command descriptions Better clarifies the effects of these commands and better hints at multi-selection. --- src/encounter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index a80ba28..457f007 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -350,7 +350,7 @@ def __init__(self, referenceLists): super().__init__() self.names = ['clear'] self.referenceLists = referenceLists - self.description = "Clears a list of NPCs." + self.description = "Removes all NPCs from a list." self.usageStr = "clear {all | bestiary | encounter}" def execute(self, args = []): @@ -378,7 +378,7 @@ def __init__(self, encounter): super().__init__() self.names = ['remove'] self.encounter = encounter - self.description = "Removes an NPC from the encounter." + self.description = "Removes selected NPC(s) from the encounter." self.usageStr = "remove " def execute(self, args = []): @@ -494,7 +494,7 @@ def __init__(self, encounter): super().__init__() self.names = ['damage'] self.encounter = encounter - self.description = "Directly subtracts from an NPC's health." + self.description = "Directly subtracts from selected NPCs' health." self.usageStr = "damage " def execute(self, args = []): From f748d2e650fa0483a532d45e458d6f33f8f99fef Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Tue, 21 Feb 2023 16:40:17 -0500 Subject: [PATCH 070/113] Improve smite command details Uses the all selector in a way that's slightly different than something like the list command, so it's reasonable to assume some may not realize all can be used here. --- src/encounter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 457f007..50c0d39 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -548,6 +548,11 @@ def __init__(self, encounter): self.names = ['smite', 'kill'] self.encounter = encounter self.description = "Immediately kills an NPC." + self.details = dedent("""\ + The smite command can be called using the alias "kill". + + Supports the all selector, i.e. "kill all" will smite all NPCs in the encounter. + """).strip() self.usageStr = "smite " def execute(self, args = []): From e2b519d30083ba3f1bdfd8d90d0912997d638a80 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:06:36 -0500 Subject: [PATCH 071/113] Improve help details for attack --- src/encounter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/encounter.py b/src/encounter.py index 50c0d39..81c7c5b 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -417,6 +417,10 @@ def __init__(self, encounter): self.names = ['attack'] self.encounter = encounter self.description = "Initiantiates D&D like combat with an NPC." + self.details = dedent("""\ + The attack command is interactive meaning if you leave out + a required field you will be asked for the data instead of + the command throwing an error state.""").strip() self.usageStr = "attack [hit] [damage]" def execute(self, args = []): From c00fbb6183820085b63659054c150a4a41fb67c4 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:12:29 -0500 Subject: [PATCH 072/113] Remove line breaks for extra help text In most places these line breaks were unwanted to begin with, so it was preferable to remove them. --- src/encounter.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 81c7c5b..193415f 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -173,11 +173,10 @@ def __init__(self, bestiary): self.details = dedent("""\ Searches the absolute address provided for a valid bestiary file. The correct format for a file is provided in an example file "bestiary.txt". - If the provided file cannot be loaded the current list will be kept. If the current list is empty and a new list cannot be found - then some primitive entries will be generated. - """).strip() + then some primitive entries will be generated.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "load " def execute(self, args = []): @@ -258,11 +257,10 @@ def __init__(self, referenceLists: list[NPCList]): self.description = "Displays the selected list of NPCs." self.details = dedent("""\ The list command can be called using the aliases "display" and "show". - The selected list can be any valid alias for their respective list. Allowed aliases for "bestiary" are "book" and "b". - Allowed aliases for "encounter" are "e", "combat", and "c". - """).strip() + Allowed aliases for "encounter" are "e", "combat", and "c".\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "list [all | bestiary | encounter]" def execute(self, args = []): @@ -320,8 +318,8 @@ def __init__(self, referenceLists): self.details = dedent("""\ Reference entries in the bestiary by number. Multiple NPCs (even multiple of the same type) can be added at the same time - in a comma separated list without spaces. - """).strip() + in a comma separated list without spaces.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "add " def execute(self, args = []): @@ -420,7 +418,8 @@ def __init__(self, encounter): self.details = dedent("""\ The attack command is interactive meaning if you leave out a required field you will be asked for the data instead of - the command throwing an error state.""").strip() + the command throwing an error state.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "attack [hit] [damage]" def execute(self, args = []): @@ -554,9 +553,8 @@ def __init__(self, encounter): self.description = "Immediately kills an NPC." self.details = dedent("""\ The smite command can be called using the alias "kill". - - Supports the all selector, i.e. "kill all" will smite all NPCs in the encounter. - """).strip() + Supports the all selector, i.e. "kill all" will smite all NPCs in the encounter.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "smite " def execute(self, args = []): From b7e9a45d6ddb89774cdc82d38c511111a910482c Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:29:13 -0500 Subject: [PATCH 073/113] Remove success output for make command Prefer silence on command success. User should be able to be confident their command worked and be notified if they did not. --- src/encounter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 193415f..04738d2 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -708,7 +708,6 @@ def execute(self, args=[]) -> None: self.usage() return self.bestiary.data.append(NPC(args[0], int(args[1]), int(args[2]))) - print(self.bestiary.toMenu()) else: self.usage() From 2a58f2ecbf2ad71a77292a4e1dfba93a6852aec0 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:37:26 -0500 Subject: [PATCH 074/113] Improve remaining command descriptions This commit adds extra details to the remaining commands that still have undescribed capabilities. The usage string may be a more concise way to highlight the availability of the all selector, but this solution should be more understandable to the vast majority of users if they decide to look it up. --- src/encounter.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 04738d2..26c9151 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -377,6 +377,9 @@ def __init__(self, encounter): self.names = ['remove'] self.encounter = encounter self.description = "Removes selected NPC(s) from the encounter." + self.details = dedent("""\ + Can be used with the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "remove " def execute(self, args = []): @@ -498,6 +501,9 @@ def __init__(self, encounter): self.names = ['damage'] self.encounter = encounter self.description = "Directly subtracts from selected NPCs' health." + self.details = dedent("""\ + Can be used with the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "damage " def execute(self, args = []): @@ -598,6 +604,9 @@ def __init__(self, encounter): self.names = ['heal'] self.encounter = encounter self.description = "Directly adds to an NPC's health." + self.details = dedent("""\ + Can be used with the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "heal " def __healNPC(self, npc: NPC, amount: int) -> int: @@ -645,6 +654,10 @@ def __init__(self, encounter): self.names = ['status'] self.encounter = encounter self.description = "Displays an NPC's current stats." + self.details = dedent("""\ + Displays the current health of the selected NPC in the encounter. + Supports the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "status " def execute(self, args = []): @@ -699,7 +712,11 @@ def __init__(self, bestiary): super().__init__() self.names = ['make'] self.bestiary = bestiary - self.description = "Creates an NPC for the bestiary." + self.description = "Creates an NPC and adds it to the bestiary." + self.details = dedent("""\ + Entries added in this manner are temporary and will + not persist across reloads of the program.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "make " def execute(self, args=[]) -> None: @@ -718,6 +735,11 @@ def __init__(self, encounter): self.names = ['name', 'nick'] self.encounter = encounter self.description = "Gives a specific name to an NPC in the encounter." + self.details = dedent("""\ + Nicknames work on a per NPC basis. Multiple NPCs may have the + same nickname. The nickname does not replace the NPCs original + name and will still be displayed alongside it.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "name " def execute(self, args=[]) -> None: @@ -743,6 +765,12 @@ def __init__(self, encounter): self.names = ['mark', 'note'] self.encounter = encounter self.description = "Mark an NPC with a symbol and note." + self.details = dedent("""\ + Can be used with the all selector. Will place an '*' next to the + NPC's name in any list display of NPCs. If this command is run on the same + NPC again the new note will overwrite their old note. This can be used + to delete a note entirely by replacing it with an empty note. + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "mark [note]" def execute(self, args=[]) -> None: @@ -783,6 +811,9 @@ def __init__(self, encounter): self.names = ['unmark'] self.encounter = encounter self.description = "Remove mark and symbol from an NPC." + self.details = dedent("""\ + Can be used with the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "unmark " def execute(self, args=[]) -> None: From b9cab4becfaba211705dbec8d888ced326a71417 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:39:47 -0500 Subject: [PATCH 075/113] Improve readme detail Guides users more clearly and updates python requirements. --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 468d0dc..e019ee4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ # encounter -A script that manages a combat encounter for tabletop rpgs. -Run with python command in your shell environment. -Enter help in the program to view a list of available commands and their functions. +An interactive command-line script that manages a combat encounter for tabletop rpgs. +Run from the highest level directory (the one containing bestiary.txt) using the python command in your terminal. + +* Install with pip to run from anywhere. + +Enter `help` once the program has started to view a list of available commands and their functions. + +* Create your own NPCs to add to your encounter. See the make command to create them dynamically or check the provided bestiary.txt file for the csv-like file format. ## Dependencies -* Python 3.10 or higher -* Recommended to have a "bestiary.txt" file. An example bestiary file with its associated formatting is included. - * If no bestiary file is found the program loads some generic entries. +* Python 3.11 or higher From 594334c9ae0ee2a83a4582b392affa7caa92e0b8 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Tue, 28 Feb 2023 15:02:27 -0500 Subject: [PATCH 076/113] Improve error for bestiary entry missing arguments Describes how many arguments are missing and on which line the first instance of missing parameters appeared. Does not fix the case in which a user forgets to put a value after placing a comma or if they provide the wrong type of information for a field. --- src/encounter.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 26c9151..6e190f5 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -3,6 +3,8 @@ class NPC: + REQUIRED_PARAMETERS = 3 + def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): # Type assertions if type(name) != str: @@ -196,8 +198,12 @@ def execute(self, args = []): self.bestiary.data.clear() for line in bestiaryFile: if not (line.startswith("#") or line.isspace()): - line = line.rstrip("\n").split(",") - npc = NPC(line[0], int(line[1]), int(line[2])) + parameters = line.rstrip("\n").split(",") + if len(parameters) < NPC.REQUIRED_PARAMETERS: + raise AttributeError("Missing parameters for line below. Expected " + + str(NPC.REQUIRED_PARAMETERS) + " but got " + + str(len(parameters)) + "\n" + line + "") + npc = NPC(parameters[0], int(parameters[1]), int(parameters[2])) self.bestiary.data.append(npc) bestiaryFile.close() else: From 93fffb19c180c44232bf902724016a575bd069e0 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Tue, 28 Feb 2023 22:17:50 -0500 Subject: [PATCH 077/113] Stop damage processing for defeated NPCs Avoids displaying a defeated message for an already defeated NPC. Displays just the error message instead of error and a new defeated message. --- src/encounter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/encounter.py b/src/encounter.py index 6e190f5..db2835a 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -545,6 +545,7 @@ def execute(self, args = []): if npc.currentHP <= 0: print(npc.nick + " already defeated.") + continue npc.currentHP = max(0, npc.currentHP - int(args[1])) From bcb872237ac65fa40f9fd348bcc6f6c871f7d99f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Tue, 28 Feb 2023 22:41:31 -0500 Subject: [PATCH 078/113] Describe status interaction with mark command --- src/encounter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/encounter.py b/src/encounter.py index db2835a..102a7e6 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -663,6 +663,7 @@ def __init__(self, encounter): self.description = "Displays an NPC's current stats." self.details = dedent("""\ Displays the current health of the selected NPC in the encounter. + Additionally displays the contents of notes if any. Supports the all selector.\ """).strip().replace('\n', ' ').replace('\r', '') self.usageStr = "status " From 30440214f868442279cab2795b37193498635eb1 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Tue, 28 Feb 2023 22:45:48 -0500 Subject: [PATCH 079/113] Prefer not to display obvious command output User should be able to expect the smite command will smite the selected NPC. Abnormal states like smiting a dead NPC might hint at using an unintended command so are helpful to log. --- src/encounter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 102a7e6..4d7d93d 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -580,7 +580,6 @@ def execute(self, args = []): for npc in self.encounter.data: if npc.currentHP > 0: npc.currentHP = 0 - print("All enemies have been defeated.") else: selected = args[0].split(",") selected = list(set(selected)) # Remove duplicates from the selection @@ -597,7 +596,6 @@ def execute(self, args = []): return else: npc.currentHP = 0 - print(npc.nick + " was defeated.") if areAllDefeated(self.encounter): print("Party has defeated all enemies.") From 5f82b54cdf6c4afa0831067d6cd46b4822744414 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Tue, 28 Feb 2023 22:46:36 -0500 Subject: [PATCH 080/113] List NPC name in smite error states --- src/encounter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 4d7d93d..8660ffd 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -592,7 +592,7 @@ def execute(self, args = []): for index in selected: npc = self.encounter.data[int(index) - 1] if npc.currentHP <= 0: - print("Enemy already defeated.") + print(npc.nick + " already defeated.") return else: npc.currentHP = 0 From c78a12b880f86fc661620d725bf1671fe863cc83 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Thu, 16 Mar 2023 19:10:07 -0400 Subject: [PATCH 081/113] Version bump to 4.19.1 All the changes to commands constitute a noticeable amount of polish without adding any new commands. Overall the changes improve consistency across commands (i.e. more commands come with multi or all selectors) and help descriptions for commands are far more detailed. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 41e4319..4139a4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "encounter" -version = "4.19.0" +version = "4.19.1" requires-python = ">=3.11" [tool.setuptools] From 7a48a7337ce3ab30ab3bd0fa34b970ba1f7653f3 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 13:14:02 -0400 Subject: [PATCH 082/113] Use NPCList as copyNPC parameter Makes it simpler to use the copyNPC method by allowing the direct passing of NPCLists. --- src/encounter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 8660ffd..0494e08 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -309,10 +309,10 @@ def isValidInt(selector: str, referencList: NPCList) -> bool: return True -def copyNPC(bestiaryList, index: int, list: list[NPC]) -> None: - npc = bestiaryList[index - 1] +def copyNPC(bestiary: NPCList, index: int, other: NPCList) -> None: + npc = bestiary.data[index - 1] copy = NPC(npc.name, npc.maxHP, npc.ac) - list.append(copy) + other.data.append(copy) class addNPC(Command): @@ -344,7 +344,7 @@ def execute(self, args = []): selected = args[0].split(",") for index in selected: - copyNPC(bestiary.data, int(index), encounter.data) + copyNPC(bestiary, int(index), encounter) else: self.usage() From 9f48d326825072ebe57b1f4d3ec0725cc4f29b9b Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 16:03:19 -0400 Subject: [PATCH 083/113] Split main file up by class Helps to make reading through each portion much easier. Main file was getting too long to modify easily. --- src/commands.py | 712 ++++++++++++++++++++++++++++++++++++++ src/encounter.py | 882 +---------------------------------------------- src/npc.py | 145 ++++++++ 3 files changed, 875 insertions(+), 864 deletions(-) create mode 100644 src/commands.py create mode 100644 src/npc.py diff --git a/src/commands.py b/src/commands.py new file mode 100644 index 0000000..d843b1f --- /dev/null +++ b/src/commands.py @@ -0,0 +1,712 @@ +from abc import ABC, abstractmethod +from textwrap import dedent +from npc import NPC, NPCList, findList + + +class Command(ABC): + def __init__(self): + self.names: list[str] = ['command', 'test'] + self.description: str = "This command has no defined description yet." + self.details: str | None = None + self.usageStr: str = "This command has no defined usage yet." + + def usage(self) -> None: + print("Usage: " + self.usageStr) + + def encounterEmpty(self) -> None: + print("The encounter is empty. Add some NPCs to it and try again.") + + def OOBSelection(self, referenceList: NPCList) -> None: + print("Your selection contains values out of range for the " + referenceList.name) + print("Adjust your selection and try again.") + + @abstractmethod + def execute(self, args = []) -> None: + raise NotImplementedError("This command has not been implemented yet.") + + +class load(Command): + def __init__(self, bestiary): + super().__init__() + self.names = ['load'] + self.bestiary = bestiary + self.description = "Replaces the loaded bestiary." + self.details = dedent("""\ + Searches the absolute address provided for a valid bestiary file. + The correct format for a file is provided in an example file "bestiary.txt". + If the provided file cannot be loaded the current list will be kept. + If the current list is empty and a new list cannot be found + then some primitive entries will be generated.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "load " + + def execute(self, args = []): + numArgs = len(args) + + if numArgs == 1: + try: + bestiaryFile = open(args[0].strip()) + except FileNotFoundError: + print("Selected bestiary file could not be found.") + if len(self.bestiary) == 0: + print("Loading placeholder bestiary.") + self.bestiary.data.append(NPC("Human", 5, 12)) + self.bestiary.data.append(NPC("Animal", 3, 10)) + self.bestiary.data.append(NPC("Enemy", 10, 13)) + else: + self.bestiary.data.clear() + for line in bestiaryFile: + if not (line.startswith("#") or line.isspace()): + parameters = line.rstrip("\n").split(",") + if len(parameters) < NPC.REQUIRED_PARAMETERS: + raise AttributeError("Missing parameters for line below. Expected " + + str(NPC.REQUIRED_PARAMETERS) + " but got " + + str(len(parameters)) + "\n" + line + "") + npc = NPC(parameters[0], int(parameters[1]), int(parameters[2])) + self.bestiary.data.append(npc) + bestiaryFile.close() + else: + self.usage() + + +class displayHelp(Command): + def __init__(self, commands: list[Command]): + super().__init__() + self.names = ['help', '?'] + self.commands = commands + self.description = "Prints a list of availible commands." + self.usageStr = "help [command_name]" + + def execute(self, args = []): + numArgs = len(args) + + if numArgs == 1: + if args[0].lower() in ["quit", "q", "exit"]: + print("Exits the program.") + print("Usage: {quit | q | exit}") + else: + found = False + for command in self.commands: + if args[0].lower() in command.names: + print(command.description) + command.usage() + if command.details is not None: + print("\nNote:") + print(command.details) + found = True + break + + if not found: + print("Unrecognized command.") + print("Type help or ? to learn how to use availible commands.") + else: + if numArgs == 0: + spacing = 0 + for command in self.commands: + if len(command.names[0]) > spacing: + spacing = len(command.names[0]) + print("quit".ljust(spacing) + ": " + "Exits the program.") + for command in self.commands: + print(command.names[0].ljust(spacing) + ": " + command.description) + print("") + print("For more detailed information > Usage: " + self.usageStr) + else: + self.usage() + + +class displayMenu(Command): + def __init__(self, referenceLists: list[NPCList]): + super().__init__() + self.names = ['list', 'display', 'show'] + self.referenceLists = referenceLists + self.description = "Displays the selected list of NPCs." + self.details = dedent("""\ + The list command can be called using the aliases "display" and "show". + The selected list can be any valid alias for their respective list. + Allowed aliases for "bestiary" are "book" and "b". + Allowed aliases for "encounter" are "e", "combat", and "c".\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "list [all | bestiary | encounter]" + + def execute(self, args = []): + numArgs = len(args) + + if numArgs <= 1: + if numArgs == 0 or args[0].lower() == "all": + for list in self.referenceLists: + print(list.toMenu()) + if list is not self.referenceLists[-1]: + print() # Print newline between all lists + else: + list = findList(args[0], self.referenceLists) + + if list is not None: + print(list.toMenu()) + else: + print("Unknown list selected.") + else: + self.usage() + + +def isInt(string: str) -> bool: + if string.isnumeric(): + return True + else: + try: + int(string) + except ValueError: + return False + else: + return True + + +def isValidInt(selector: str, referencList: NPCList) -> bool: + selected = selector.split(",") + for index in selected: + if not isInt(index) or int(index) <= 0 or int(index) > len(referencList): + return False + return True + + +def copyNPC(bestiary: NPCList, index: int, other: NPCList) -> None: + npc = bestiary.data[index - 1] + copy = NPC(npc.name, npc.maxHP, npc.ac) + other.data.append(copy) + + +class addNPC(Command): + def __init__(self, referenceLists): + super().__init__() + self.names = ['add'] + self.referenceLists = referenceLists + self.description = "Adds an NPC to the encounter." + self.details = dedent("""\ + Reference entries in the bestiary by number. + Multiple NPCs (even multiple of the same type) can be added at the same time + in a comma separated list without spaces.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "add " + + def execute(self, args = []): + bestiary = findList("bestiary", self.referenceLists) + if bestiary is None: + raise TypeError("Bestiary list must be an NPCList.") + encounter = findList("encounter", self.referenceLists) + if encounter is None: + raise TypeError("Encounter list must be an NPCList.") + + if len(args) == 1: + if not isValidInt(args[0], bestiary): + self.OOBSelection(bestiary) + return + + selected = args[0].split(",") + + for index in selected: + copyNPC(bestiary, int(index), encounter) + else: + self.usage() + + +class clearNPCList(Command): + def __init__(self, referenceLists): + super().__init__() + self.names = ['clear'] + self.referenceLists = referenceLists + self.description = "Removes all NPCs from a list." + self.usageStr = "clear {all | bestiary | encounter}" + + def execute(self, args = []): + if len(args) == 1: + if args[0].lower() == "all": + for list in self.referenceLists: + list.data.clear() + print(list.toMenu()) + if list is not self.referenceLists[-1]: + print() + else: + list = findList(args[0], self.referenceLists) + + if list is not None: + list.data.clear() + print(list.toMenu()) + else: + print("Unknown list selected.") + else: + self.usage() + + +class removeNPC(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['remove'] + self.encounter = encounter + self.description = "Removes selected NPC(s) from the encounter." + self.details = dedent("""\ + Can be used with the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "remove " + + def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) == 1: + if args[0].lower() == "all": + self.encounter.data.clear() + else: + if not isValidInt(args[0], self.encounter): + self.OOBSelection(self.encounter) + return + + selected = args[0].split(",") + # Remove duplicates and reverse sort the input + selected = sorted(list(set(selected)), reverse = True) + + for index in selected: + self.encounter.data.pop(int(index) - 1) + else: + self.usage() + + +def areAllDefeated(encounter: NPCList): + for npc in encounter.data: + if npc.currentHP > 0: + return False + return True + + +class attack(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['attack'] + self.encounter = encounter + self.description = "Initiantiates D&D like combat with an NPC." + self.details = dedent("""\ + The attack command is interactive meaning if you leave out + a required field you will be asked for the data instead of + the command throwing an error state.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "attack [hit] [damage]" + + def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + lenArgs = len(args) + npc = None + + if lenArgs > 3 or lenArgs < 1: + self.usage() + return + + if not isValidInt(args[0], self.encounter): + self.OOBSelection(self.encounter) + return + + npc = self.encounter.data[int(args[0]) - 1] + if npc.currentHP <= 0: + print("Enemy already defeated.") + return + + if lenArgs == 3: + if not isInt(args[1]) or not isInt(args[2]): + self.usage() + return + + if int(args[1]) >= npc.ac: + npc.currentHP = max(0, npc.currentHP - int(args[2])) + print(npc.nick + " took " + args[2] + " damage.") + else: + print("Attack misses " + npc.nick + ".") + elif lenArgs == 2: + if not isInt(args[1]): + self.usage() + return + + if int(args[1]) >= npc.ac: + damage = input("Roll for damage: ") + if damage.isnumeric() is True: + amt = int(damage) + npc.currentHP = max(0, npc.currentHP - amt) + print(npc.nick + " took " + damage + " damage.") + else: + print("Damage must be a number.") + else: + print("Attack misses " + npc.nick + ".") + elif lenArgs == 1: + accuracy = input("Roll for hit: ") + if accuracy.isnumeric() is True: + accuracy = int(accuracy) + if accuracy >= npc.ac: + damage = input("Roll for damage: ") + if damage.isnumeric() is True: + amt = int(damage) + npc.currentHP = max(0, npc.currentHP - amt) + print(npc.nick + " took " + damage + " damage.") + else: + print("Damage must be a number.") + else: + print("Attack misses " + npc.nick + ".") + else: + print("Accuracy must be a number.") + + if npc is not None: + if npc.currentHP <= 0: + print(npc.nick + " has been defeated.") + if areAllDefeated(self.encounter): + print("Party has defeated all enemies.") + + +class damage(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['damage'] + self.encounter = encounter + self.description = "Directly subtracts from selected NPCs' health." + self.details = dedent("""\ + Can be used with the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "damage " + + def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) == 2: + if not isInt(args[1]): + self.usage() + return + elif int(args[1]) < 1: + print("Amount must be more than zero.") + return + if args[0].lower() == "all": + for npc in self.encounter.data: + if npc.currentHP > 0: + npc.currentHP = max(0, npc.currentHP - int(args[1])) + if npc.currentHP <= 0: + print(npc.nick + " has been defeated.") + if areAllDefeated(self.encounter): + print("Party has defeated all enemies.") + else: + if not isValidInt(args[0], self.encounter): + self.OOBSelection(self.encounter) + return + + selected = args[0].split(",") + selected = list(set(selected)) + + for index in selected: + npc = self.encounter.data[int(index) - 1] + + if npc.currentHP <= 0: + print(npc.nick + " already defeated.") + continue + + npc.currentHP = max(0, npc.currentHP - int(args[1])) + + if npc.currentHP <= 0: + print(npc.nick + " has been defeated.") + if areAllDefeated(self.encounter): + print("Party has defeated all enemies.") + return + else: + self.usage() + + +class smite(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['smite', 'kill'] + self.encounter = encounter + self.description = "Immediately kills an NPC." + self.details = dedent("""\ + The smite command can be called using the alias "kill". + Supports the all selector, i.e. "kill all" will smite all NPCs in the encounter.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "smite " + + def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) == 1: + if args[0].lower() == "all": + for npc in self.encounter.data: + if npc.currentHP > 0: + npc.currentHP = 0 + else: + selected = args[0].split(",") + selected = list(set(selected)) # Remove duplicates from the selection + + for index in selected: + if not isValidInt(args[0], self.encounter): + self.OOBSelection(self.encounter) + return + + for index in selected: + npc = self.encounter.data[int(index) - 1] + if npc.currentHP <= 0: + print(npc.nick + " already defeated.") + return + else: + npc.currentHP = 0 + + if areAllDefeated(self.encounter): + print("Party has defeated all enemies.") + else: + self.usage() + + +class heal(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['heal'] + self.encounter = encounter + self.description = "Directly adds to an NPC's health." + self.details = dedent("""\ + Can be used with the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "heal " + + def __healNPC(self, npc: NPC, amount: int) -> int: + originalHP = npc.currentHP + npc.currentHP = originalHP + amount + npc.currentHP = min(npc.maxHP, npc.currentHP) + return npc.currentHP - originalHP + + def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) == 2: + if not isInt(args[1]): + self.usage() + return + if int(args[1]) < 1: + print("Amount must be more than zero.") + return + if args[0].lower() == "all": + for npc in self.encounter.data: + healedAmt = self.__healNPC(npc, int(args[1])) + output = "{} was healed {} points.".format(npc.nick, healedAmt) + print(output) + else: + if not isValidInt(args[0], self.encounter): + self.OOBSelection(self.encounter) + return + + selected = args[0].split(",") + selected = list(set(selected)) + + for index in selected: + npc = self.encounter.data[int(index) - 1] + healedAmt = self.__healNPC(npc, int(args[1])) + print(npc.nick + " was healed " + str(healedAmt) + " points.") + else: + self.usage() + + +class status(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['status'] + self.encounter = encounter + self.description = "Displays an NPC's current stats." + self.details = dedent("""\ + Displays the current health of the selected NPC in the encounter. + Additionally displays the contents of notes if any. + Supports the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "status " + + def execute(self, args = []): + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) == 1: + if args[0].lower() == "all": + print("Status:") + for npc in self.encounter.data: + print(npc.combatStatus()) + elif isValidInt(args[0], self.encounter): + selected = args[0].split(",") + selected = list(set(selected)) + + print("Status:") + for index in selected: + npc = self.encounter.data[int(index) - 1] + + print(npc.combatStatus()) + else: + self.OOBSelection(self.encounter) + else: + self.usage() + + +class info(Command): + def __init__(self, bestiary): + super().__init__() + self.names = ['info', 'details'] + self.bestiary = bestiary + self.description = "Displays detailed stats for a bestiary entry." + self.usageStr = "info " + + def execute(self, args = []): + if len(args) == 1: + if isInt(args[0]): + if isValidInt(args[0], self.bestiary): + print("INFO:") + print(self.bestiary.data[int(args[0]) - 1].detailedInfo()) + else: + self.OOBSelection(self.bestiary) + else: + self.usage() + else: + self.usage() + + +class make(Command): + def __init__(self, bestiary): + super().__init__() + self.names = ['make'] + self.bestiary = bestiary + self.description = "Creates an NPC and adds it to the bestiary." + self.details = dedent("""\ + Entries added in this manner are temporary and will + not persist across reloads of the program.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "make " + + def execute(self, args=[]) -> None: + if len(args) == 3: + if args[0].isnumeric() or not isInt(args[1]) or not isInt(args[2]): + self.usage() + return + self.bestiary.data.append(NPC(args[0], int(args[1]), int(args[2]))) + else: + self.usage() + + +class name(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['name', 'nick'] + self.encounter = encounter + self.description = "Gives a specific name to an NPC in the encounter." + self.details = dedent("""\ + Nicknames work on a per NPC basis. Multiple NPCs may have the + same nickname. The nickname does not replace the NPCs original + name and will still be displayed alongside it.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "name " + + def execute(self, args=[]) -> None: + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) == 2: + if not args[0].isnumeric(): + self.usage() + return + if isValidInt(args[0], self.encounter): + self.encounter.data[int(args[0]) - 1].nick = args[1] + else: + self.OOBSelection(self.encounter) + else: + self.usage() + + +class mark(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['mark', 'note'] + self.encounter = encounter + self.description = "Mark an NPC with a symbol and note." + self.details = dedent("""\ + Can be used with the all selector. Will place an '*' next to the + NPC's name in any list display of NPCs. If this command is run on the same + NPC again the new note will overwrite their old note. This can be used + to delete a note entirely by replacing it with an empty note. + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "mark [note]" + + def execute(self, args=[]) -> None: + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) >= 1: + if args[0].lower() == "all": + for npc in self.encounter.data: + npc.marked = True + if len(args) > 1: + npc.note = " ".join(args[1:]) + else: + npc.note = "" + else: + if not isValidInt(args[0], self.encounter): + self.OOBSelection(self.encounter) + return + + selected = args[0].split(",") + selected = list(set(selected)) + + for index in selected: + npc = self.encounter.data[int(index) - 1] + npc.marked = True + if len(args) > 1: + npc.note = " ".join(args[1:]) + else: + npc.note = "" + else: + self.usage() + + +class unmark(Command): + def __init__(self, encounter): + super().__init__() + self.names = ['unmark'] + self.encounter = encounter + self.description = "Remove mark and symbol from an NPC." + self.details = dedent("""\ + Can be used with the all selector.\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "unmark " + + def execute(self, args=[]) -> None: + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) == 1: + if args[0].lower() == "all": + for npc in self.encounter.data: + npc.marked = False + npc.note = "" + else: + if not isValidInt(args[0], self.encounter): + self.OOBSelection(self.encounter) + return + + selected = args[0].split(",") + selected = list(set(selected)) + + for index in selected: + npc = self.encounter.data[int(index) - 1] + npc.marked = False + npc.note = "" + else: + self.usage() + + +if __name__ == "__main__": + print("Something seems wrong, this file is not meant to be executed.") + print("Did you mean to run encounter instead?") diff --git a/src/encounter.py b/src/encounter.py index 0494e08..f4620f6 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -1,851 +1,5 @@ -from abc import ABC, abstractmethod -from textwrap import dedent - - -class NPC: - REQUIRED_PARAMETERS = 3 - - def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): - # Type assertions - if type(name) != str: - raise TypeError("Name must be a string.") - if type(maxHP) != int: - raise TypeError("HP must be an integer.") - if type(ac) != int: - raise TypeError("AC must be an integer.") - - # Value assertions - if len(name) < 1: - raise ValueError("Name must be at least length 1.") - if name.isspace(): - raise ValueError("Name must not be blank.") - if nick is not None and len(nick) < 1: - raise ValueError("Nickname must be at least length 1.") - if nick is not None and nick.isspace(): - raise ValueError("Nickname must not be blank.") - if maxHP < 1: - raise ValueError("HP must be at least 1.") - if ac < 0: - raise ValueError("AC must be at least 0.") - - # Value assignment - self.marked = False - self.note = "" - self.name = name - if nick is None: - self.nick = name - else: - self.nick = nick - self.maxHP = self.currentHP = maxHP - self.ac = int(ac) - - def __str__(self): - output = "" - if self.nick is not self.name: - output += self.nick + " (" + self.name + ")" - else: - output += self.name - - if self.marked: - output += "*" - - if self.currentHP <= 0: - output += " [X]" - - return output - - def equals(self, other): - if self == other: - return True - if other is None: - return False - if self.name != other.name: - return False - if self.nick != other.nick: - return False - if self.maxHP != other.maxHP: - return False - if self.currentHP != other.currentHP: - return False - if self.ac != other.ac: - return False - if self.note != other.note: - return False - return True - - def combatStatus(self) -> str: - status = "" - if self.currentHP > 0: - if self.name is not self.nick: - status += self.nick + " (" + self.name + ")" - else: - status += self.name - status += " [" + str(self.currentHP) + "/" + str(self.maxHP) + "]" - else: - if self.name is not self.nick: - status += self.nick + " (" + self.name + ")" - else: - status += self.name - status += " [Dead]" - - if self.marked: - status += "\n Note:\n" - if not self.note.isspace() and len(self.note) > 0: - status += " " - status += self.note - else: - status += "EMPTY" - - return status - - def detailedInfo(self) -> str: - info = "" - info += "NAME: " + str(self.name) + "\n" - info += "MAX HP: " + str(self.maxHP) + "\n" - info += "AC: " + str(self.ac) - return info - - -class NPCList: - def __init__(self, names: list[str]): - # Error Checking - if type(names) != list: - raise TypeError("Names must be a list of strings.") - - if len(names) < 1: - raise ValueError("List must contain at least one entry.") - - # Value assignment - self.names = names - self.name = names[0] - self.data: list[NPC] = [] - - def __len__(self): - return len(self.data) - - def toMenu(self): - info = self.name.upper() + ":\n" - - if len(self.data) == 0: - info += "EMPTY" - return info - else: - for i in self.data: - info += str(self.data.index(i) + 1) + " " + str(i) + "\n" - - return info[:-1] - - -def findList(name: str, referenceLists: list[NPCList]) -> NPCList | None: - name = name.lower() - for list in referenceLists: - if name in list.names: - return list - return None - - -class Command(ABC): - def __init__(self): - self.names: list[str] = ['command', 'test'] - self.description: str = "This command has no defined description yet." - self.details: str | None = None - self.usageStr: str = "This command has no defined usage yet." - - def usage(self) -> None: - print("Usage: " + self.usageStr) - - def encounterEmpty(self) -> None: - print("The encounter is empty. Add some NPCs to it and try again.") - - def OOBSelection(self, referenceList: NPCList) -> None: - print("Your selection contains values out of range for the " + referenceList.name) - print("Adjust your selection and try again.") - - @abstractmethod - def execute(self, args = []) -> None: - raise NotImplementedError("This command has not been implemented yet.") - - -class load(Command): - def __init__(self, bestiary): - super().__init__() - self.names = ['load'] - self.bestiary = bestiary - self.description = "Replaces the loaded bestiary." - self.details = dedent("""\ - Searches the absolute address provided for a valid bestiary file. - The correct format for a file is provided in an example file "bestiary.txt". - If the provided file cannot be loaded the current list will be kept. - If the current list is empty and a new list cannot be found - then some primitive entries will be generated.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "load " - - def execute(self, args = []): - numArgs = len(args) - - if numArgs == 1: - try: - bestiaryFile = open(args[0].strip()) - except FileNotFoundError: - print("Selected bestiary file could not be found.") - if len(self.bestiary) == 0: - print("Loading placeholder bestiary.") - self.bestiary.data.append(NPC("Human", 5, 12)) - self.bestiary.data.append(NPC("Animal", 3, 10)) - self.bestiary.data.append(NPC("Enemy", 10, 13)) - else: - self.bestiary.data.clear() - for line in bestiaryFile: - if not (line.startswith("#") or line.isspace()): - parameters = line.rstrip("\n").split(",") - if len(parameters) < NPC.REQUIRED_PARAMETERS: - raise AttributeError("Missing parameters for line below. Expected " - + str(NPC.REQUIRED_PARAMETERS) + " but got " - + str(len(parameters)) + "\n" + line + "") - npc = NPC(parameters[0], int(parameters[1]), int(parameters[2])) - self.bestiary.data.append(npc) - bestiaryFile.close() - else: - self.usage() - - -class displayHelp(Command): - def __init__(self, commands: list[Command]): - super().__init__() - self.names = ['help', '?'] - self.commands = commands - self.description = "Prints a list of availible commands." - self.usageStr = "help [command_name]" - - def execute(self, args = []): - numArgs = len(args) - - if numArgs == 1: - if args[0].lower() in ["quit", "q", "exit"]: - print("Exits the program.") - print("Usage: {quit | q | exit}") - else: - found = False - for command in self.commands: - if args[0].lower() in command.names: - print(command.description) - command.usage() - if command.details is not None: - print("\nNote:") - print(command.details) - found = True - break - - if not found: - print("Unrecognized command.") - print("Type help or ? to learn how to use availible commands.") - else: - if numArgs == 0: - spacing = 0 - for command in self.commands: - if len(command.names[0]) > spacing: - spacing = len(command.names[0]) - print("quit".ljust(spacing) + ": " + "Exits the program.") - for command in self.commands: - print(command.names[0].ljust(spacing) + ": " + command.description) - print("") - print("For more detailed information > Usage: " + self.usageStr) - else: - self.usage() - - -class displayMenu(Command): - def __init__(self, referenceLists: list[NPCList]): - super().__init__() - self.names = ['list', 'display', 'show'] - self.referenceLists = referenceLists - self.description = "Displays the selected list of NPCs." - self.details = dedent("""\ - The list command can be called using the aliases "display" and "show". - The selected list can be any valid alias for their respective list. - Allowed aliases for "bestiary" are "book" and "b". - Allowed aliases for "encounter" are "e", "combat", and "c".\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "list [all | bestiary | encounter]" - - def execute(self, args = []): - numArgs = len(args) - - if numArgs <= 1: - if numArgs == 0 or args[0].lower() == "all": - for list in self.referenceLists: - print(list.toMenu()) - if list is not self.referenceLists[-1]: - print() # Print newline between all lists - else: - list = findList(args[0], self.referenceLists) - - if list is not None: - print(list.toMenu()) - else: - print("Unknown list selected.") - else: - self.usage() - - -def isInt(string: str) -> bool: - if string.isnumeric(): - return True - else: - try: - int(string) - except ValueError: - return False - else: - return True - - -def isValidInt(selector: str, referencList: NPCList) -> bool: - selected = selector.split(",") - for index in selected: - if not isInt(index) or int(index) <= 0 or int(index) > len(referencList): - return False - return True - - -def copyNPC(bestiary: NPCList, index: int, other: NPCList) -> None: - npc = bestiary.data[index - 1] - copy = NPC(npc.name, npc.maxHP, npc.ac) - other.data.append(copy) - - -class addNPC(Command): - def __init__(self, referenceLists): - super().__init__() - self.names = ['add'] - self.referenceLists = referenceLists - self.description = "Adds an NPC to the encounter." - self.details = dedent("""\ - Reference entries in the bestiary by number. - Multiple NPCs (even multiple of the same type) can be added at the same time - in a comma separated list without spaces.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "add " - - def execute(self, args = []): - bestiary = findList("bestiary", self.referenceLists) - if bestiary is None: - raise TypeError("Bestiary list must be an NPCList.") - encounter = findList("encounter", self.referenceLists) - if encounter is None: - raise TypeError("Encounter list must be an NPCList.") - - if len(args) == 1: - if not isValidInt(args[0], bestiary): - self.OOBSelection(bestiary) - return - - selected = args[0].split(",") - - for index in selected: - copyNPC(bestiary, int(index), encounter) - else: - self.usage() - - -class clearNPCList(Command): - def __init__(self, referenceLists): - super().__init__() - self.names = ['clear'] - self.referenceLists = referenceLists - self.description = "Removes all NPCs from a list." - self.usageStr = "clear {all | bestiary | encounter}" - - def execute(self, args = []): - if len(args) == 1: - if args[0].lower() == "all": - for list in self.referenceLists: - list.data.clear() - print(list.toMenu()) - if list is not self.referenceLists[-1]: - print() - else: - list = findList(args[0], self.referenceLists) - - if list is not None: - list.data.clear() - print(list.toMenu()) - else: - print("Unknown list selected.") - else: - self.usage() - - -class removeNPC(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['remove'] - self.encounter = encounter - self.description = "Removes selected NPC(s) from the encounter." - self.details = dedent("""\ - Can be used with the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "remove " - - def execute(self, args = []): - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - if len(args) == 1: - if args[0].lower() == "all": - self.encounter.data.clear() - else: - if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) - return - - selected = args[0].split(",") - # Remove duplicates and reverse sort the input - selected = sorted(list(set(selected)), reverse = True) - - for index in selected: - self.encounter.data.pop(int(index) - 1) - else: - self.usage() - - -def areAllDefeated(encounter: NPCList): - for npc in encounter.data: - if npc.currentHP > 0: - return False - return True - - -class attack(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['attack'] - self.encounter = encounter - self.description = "Initiantiates D&D like combat with an NPC." - self.details = dedent("""\ - The attack command is interactive meaning if you leave out - a required field you will be asked for the data instead of - the command throwing an error state.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "attack [hit] [damage]" - - def execute(self, args = []): - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - lenArgs = len(args) - npc = None - - if lenArgs > 3 or lenArgs < 1: - self.usage() - return - - if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) - return - - npc = self.encounter.data[int(args[0]) - 1] - if npc.currentHP <= 0: - print("Enemy already defeated.") - return - - if lenArgs == 3: - if not isInt(args[1]) or not isInt(args[2]): - self.usage() - return - - if int(args[1]) >= npc.ac: - npc.currentHP = max(0, npc.currentHP - int(args[2])) - print(npc.nick + " took " + args[2] + " damage.") - else: - print("Attack misses " + npc.nick + ".") - elif lenArgs == 2: - if not isInt(args[1]): - self.usage() - return - - if int(args[1]) >= npc.ac: - damage = input("Roll for damage: ") - if damage.isnumeric() is True: - amt = int(damage) - npc.currentHP = max(0, npc.currentHP - amt) - print(npc.nick + " took " + damage + " damage.") - else: - print("Damage must be a number.") - else: - print("Attack misses " + npc.nick + ".") - elif lenArgs == 1: - accuracy = input("Roll for hit: ") - if accuracy.isnumeric() is True: - accuracy = int(accuracy) - if accuracy >= npc.ac: - damage = input("Roll for damage: ") - if damage.isnumeric() is True: - amt = int(damage) - npc.currentHP = max(0, npc.currentHP - amt) - print(npc.nick + " took " + damage + " damage.") - else: - print("Damage must be a number.") - else: - print("Attack misses " + npc.nick + ".") - else: - print("Accuracy must be a number.") - - if npc is not None: - if npc.currentHP <= 0: - print(npc.nick + " has been defeated.") - if areAllDefeated(self.encounter): - print("Party has defeated all enemies.") - - -class damage(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['damage'] - self.encounter = encounter - self.description = "Directly subtracts from selected NPCs' health." - self.details = dedent("""\ - Can be used with the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "damage " - - def execute(self, args = []): - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - if len(args) == 2: - if not isInt(args[1]): - self.usage() - return - elif int(args[1]) < 1: - print("Amount must be more than zero.") - return - if args[0].lower() == "all": - for npc in self.encounter.data: - if npc.currentHP > 0: - npc.currentHP = max(0, npc.currentHP - int(args[1])) - if npc.currentHP <= 0: - print(npc.nick + " has been defeated.") - if areAllDefeated(self.encounter): - print("Party has defeated all enemies.") - else: - if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) - return - - selected = args[0].split(",") - selected = list(set(selected)) - - for index in selected: - npc = self.encounter.data[int(index) - 1] - - if npc.currentHP <= 0: - print(npc.nick + " already defeated.") - continue - - npc.currentHP = max(0, npc.currentHP - int(args[1])) - - if npc.currentHP <= 0: - print(npc.nick + " has been defeated.") - if areAllDefeated(self.encounter): - print("Party has defeated all enemies.") - return - else: - self.usage() - - -class smite(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['smite', 'kill'] - self.encounter = encounter - self.description = "Immediately kills an NPC." - self.details = dedent("""\ - The smite command can be called using the alias "kill". - Supports the all selector, i.e. "kill all" will smite all NPCs in the encounter.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "smite " - - def execute(self, args = []): - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - if len(args) == 1: - if args[0].lower() == "all": - for npc in self.encounter.data: - if npc.currentHP > 0: - npc.currentHP = 0 - else: - selected = args[0].split(",") - selected = list(set(selected)) # Remove duplicates from the selection - - for index in selected: - if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) - return - - for index in selected: - npc = self.encounter.data[int(index) - 1] - if npc.currentHP <= 0: - print(npc.nick + " already defeated.") - return - else: - npc.currentHP = 0 - - if areAllDefeated(self.encounter): - print("Party has defeated all enemies.") - else: - self.usage() - - -class heal(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['heal'] - self.encounter = encounter - self.description = "Directly adds to an NPC's health." - self.details = dedent("""\ - Can be used with the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "heal " - - def __healNPC(self, npc: NPC, amount: int) -> int: - originalHP = npc.currentHP - npc.currentHP = originalHP + amount - npc.currentHP = min(npc.maxHP, npc.currentHP) - return npc.currentHP - originalHP - - def execute(self, args = []): - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - if len(args) == 2: - if not isInt(args[1]): - self.usage() - return - if int(args[1]) < 1: - print("Amount must be more than zero.") - return - if args[0].lower() == "all": - for npc in self.encounter.data: - healedAmt = self.__healNPC(npc, int(args[1])) - output = "{} was healed {} points.".format(npc.nick, healedAmt) - print(output) - else: - if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) - return - - selected = args[0].split(",") - selected = list(set(selected)) - - for index in selected: - npc = self.encounter.data[int(index) - 1] - healedAmt = self.__healNPC(npc, int(args[1])) - print(npc.nick + " was healed " + str(healedAmt) + " points.") - else: - self.usage() - - -class status(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['status'] - self.encounter = encounter - self.description = "Displays an NPC's current stats." - self.details = dedent("""\ - Displays the current health of the selected NPC in the encounter. - Additionally displays the contents of notes if any. - Supports the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "status " - - def execute(self, args = []): - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - if len(args) == 1: - if args[0].lower() == "all": - print("Status:") - for npc in self.encounter.data: - print(npc.combatStatus()) - elif isValidInt(args[0], self.encounter): - selected = args[0].split(",") - selected = list(set(selected)) - - print("Status:") - for index in selected: - npc = self.encounter.data[int(index) - 1] - - print(npc.combatStatus()) - else: - self.OOBSelection(self.encounter) - else: - self.usage() - - -class info(Command): - def __init__(self, bestiary): - super().__init__() - self.names = ['info', 'details'] - self.bestiary = bestiary - self.description = "Displays detailed stats for a bestiary entry." - self.usageStr = "info " - - def execute(self, args = []): - if len(args) == 1: - if isInt(args[0]): - if isValidInt(args[0], self.bestiary): - print("INFO:") - print(self.bestiary.data[int(args[0]) - 1].detailedInfo()) - else: - self.OOBSelection(self.bestiary) - else: - self.usage() - else: - self.usage() - - -class make(Command): - def __init__(self, bestiary): - super().__init__() - self.names = ['make'] - self.bestiary = bestiary - self.description = "Creates an NPC and adds it to the bestiary." - self.details = dedent("""\ - Entries added in this manner are temporary and will - not persist across reloads of the program.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "make " - - def execute(self, args=[]) -> None: - if len(args) == 3: - if args[0].isnumeric() or not isInt(args[1]) or not isInt(args[2]): - self.usage() - return - self.bestiary.data.append(NPC(args[0], int(args[1]), int(args[2]))) - else: - self.usage() - - -class name(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['name', 'nick'] - self.encounter = encounter - self.description = "Gives a specific name to an NPC in the encounter." - self.details = dedent("""\ - Nicknames work on a per NPC basis. Multiple NPCs may have the - same nickname. The nickname does not replace the NPCs original - name and will still be displayed alongside it.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "name " - - def execute(self, args=[]) -> None: - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - if len(args) == 2: - if not args[0].isnumeric(): - self.usage() - return - if isValidInt(args[0], self.encounter): - self.encounter.data[int(args[0]) - 1].nick = args[1] - else: - self.OOBSelection(self.encounter) - else: - self.usage() - - -class mark(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['mark', 'note'] - self.encounter = encounter - self.description = "Mark an NPC with a symbol and note." - self.details = dedent("""\ - Can be used with the all selector. Will place an '*' next to the - NPC's name in any list display of NPCs. If this command is run on the same - NPC again the new note will overwrite their old note. This can be used - to delete a note entirely by replacing it with an empty note. - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "mark [note]" - - def execute(self, args=[]) -> None: - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - if len(args) >= 1: - if args[0].lower() == "all": - for npc in self.encounter.data: - npc.marked = True - if len(args) > 1: - npc.note = " ".join(args[1:]) - else: - npc.note = "" - else: - if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) - return - - selected = args[0].split(",") - selected = list(set(selected)) - - for index in selected: - npc = self.encounter.data[int(index) - 1] - npc.marked = True - if len(args) > 1: - npc.note = " ".join(args[1:]) - else: - npc.note = "" - else: - self.usage() - - -class unmark(Command): - def __init__(self, encounter): - super().__init__() - self.names = ['unmark'] - self.encounter = encounter - self.description = "Remove mark and symbol from an NPC." - self.details = dedent("""\ - Can be used with the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') - self.usageStr = "unmark " - - def execute(self, args=[]) -> None: - if (len(self.encounter) < 1): - self.encounterEmpty() - return - - if len(args) == 1: - if args[0].lower() == "all": - for npc in self.encounter.data: - npc.marked = False - npc.note = "" - else: - if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) - return - - selected = args[0].split(",") - selected = list(set(selected)) - - for index in selected: - npc = self.encounter.data[int(index) - 1] - npc.marked = False - npc.note = "" - else: - self.usage() +import commands as cmd +from npc import NPCList def main(): @@ -855,24 +9,24 @@ def main(): # Instantiate commands commands = [ - load(bestiary), - displayMenu(referenceLists), - addNPC(referenceLists), - removeNPC(encounter), - clearNPCList(referenceLists), - smite(encounter), - damage(encounter), - attack(encounter), - heal(encounter), - status(encounter), - info(bestiary), - make(bestiary), - name(encounter), - mark(encounter), - unmark(encounter) + cmd.load(bestiary), + cmd.displayMenu(referenceLists), + cmd.addNPC(referenceLists), + cmd.removeNPC(encounter), + cmd.clearNPCList(referenceLists), + cmd.smite(encounter), + cmd.damage(encounter), + cmd.attack(encounter), + cmd.heal(encounter), + cmd.status(encounter), + cmd.info(bestiary), + cmd.make(bestiary), + cmd.name(encounter), + cmd.mark(encounter), + cmd.unmark(encounter) ] - commands.append(displayHelp(commands)) + commands.append(cmd.displayHelp(commands)) # Load default bestiary commands[0].execute(["bestiary.txt"]) diff --git a/src/npc.py b/src/npc.py new file mode 100644 index 0000000..d106683 --- /dev/null +++ b/src/npc.py @@ -0,0 +1,145 @@ +class NPC: + REQUIRED_PARAMETERS = 3 + + def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): + # Type assertions + if type(name) != str: + raise TypeError("Name must be a string.") + if type(maxHP) != int: + raise TypeError("HP must be an integer.") + if type(ac) != int: + raise TypeError("AC must be an integer.") + + # Value assertions + if len(name) < 1: + raise ValueError("Name must be at least length 1.") + if name.isspace(): + raise ValueError("Name must not be blank.") + if nick is not None and len(nick) < 1: + raise ValueError("Nickname must be at least length 1.") + if nick is not None and nick.isspace(): + raise ValueError("Nickname must not be blank.") + if maxHP < 1: + raise ValueError("HP must be at least 1.") + if ac < 0: + raise ValueError("AC must be at least 0.") + + # Value assignment + self.marked = False + self.note = "" + self.name = name + if nick is None: + self.nick = name + else: + self.nick = nick + self.maxHP = self.currentHP = maxHP + self.ac = int(ac) + + def __str__(self): + output = "" + if self.nick is not self.name: + output += self.nick + " (" + self.name + ")" + else: + output += self.name + + if self.marked: + output += "*" + + if self.currentHP <= 0: + output += " [X]" + + return output + + def equals(self, other): + if self == other: + return True + if other is None: + return False + if self.name != other.name: + return False + if self.nick != other.nick: + return False + if self.maxHP != other.maxHP: + return False + if self.currentHP != other.currentHP: + return False + if self.ac != other.ac: + return False + if self.note != other.note: + return False + return True + + def combatStatus(self) -> str: + status = "" + if self.currentHP > 0: + if self.name is not self.nick: + status += self.nick + " (" + self.name + ")" + else: + status += self.name + status += " [" + str(self.currentHP) + "/" + str(self.maxHP) + "]" + else: + if self.name is not self.nick: + status += self.nick + " (" + self.name + ")" + else: + status += self.name + status += " [Dead]" + + if self.marked: + status += "\n Note:\n" + if not self.note.isspace() and len(self.note) > 0: + status += " " + status += self.note + else: + status += "EMPTY" + + return status + + def detailedInfo(self) -> str: + info = "" + info += "NAME: " + str(self.name) + "\n" + info += "MAX HP: " + str(self.maxHP) + "\n" + info += "AC: " + str(self.ac) + return info + + +class NPCList: + def __init__(self, names: list[str]): + # Error Checking + if type(names) != list: + raise TypeError("Names must be a list of strings.") + + if len(names) < 1: + raise ValueError("List must contain at least one entry.") + + # Value assignment + self.names = names + self.name = names[0] + self.data: list[NPC] = [] + + def __len__(self): + return len(self.data) + + def toMenu(self): + info = self.name.upper() + ":\n" + + if len(self.data) == 0: + info += "EMPTY" + return info + else: + for i in self.data: + info += str(self.data.index(i) + 1) + " " + str(i) + "\n" + + return info[:-1] + + +def findList(name: str, referenceLists: list[NPCList]) -> NPCList | None: + name = name.lower() + for list in referenceLists: + if name in list.names: + return list + return None + + +if __name__ == "__main__": + print("Something seems wrong, this file is not meant to be executed.") + print("Did you mean to run encounter instead?") From 18d44a31aeccda1a1d40be7df300b78a677fee1f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 16:44:00 -0400 Subject: [PATCH 084/113] Abstract command initialization Makes reading the program's main loop much easier by abstracting it to its own helper method. --- src/encounter.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index f4620f6..8d8e6c9 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -2,38 +2,37 @@ from npc import NPCList -def main(): +def initialize_commands() -> list[cmd.Command]: bestiary = NPCList(['bestiary', 'book', 'b']) encounter = NPCList(['encounter', 'e', 'combat', 'c']) referenceLists = [bestiary, encounter] - # Instantiate commands - commands = [ - cmd.load(bestiary), - cmd.displayMenu(referenceLists), - cmd.addNPC(referenceLists), - cmd.removeNPC(encounter), - cmd.clearNPCList(referenceLists), - cmd.smite(encounter), - cmd.damage(encounter), - cmd.attack(encounter), - cmd.heal(encounter), - cmd.status(encounter), - cmd.info(bestiary), - cmd.make(bestiary), - cmd.name(encounter), - cmd.mark(encounter), - cmd.unmark(encounter) - ] - + commands = [] + commands.append(cmd.load(bestiary)) + commands.append(cmd.displayMenu(referenceLists)) + commands.append(cmd.addNPC(referenceLists)) + commands.append(cmd.removeNPC(encounter)) + commands.append(cmd.clearNPCList(referenceLists)) + commands.append(cmd.smite(encounter)) + commands.append(cmd.damage(encounter)) + commands.append(cmd.attack(encounter)) + commands.append(cmd.heal(encounter)) + commands.append(cmd.status(encounter)) + commands.append(cmd.info(bestiary)) + commands.append(cmd.make(bestiary)) + commands.append(cmd.name(encounter)) + commands.append(cmd.mark(encounter)) + commands.append(cmd.unmark(encounter)) commands.append(cmd.displayHelp(commands)) + return commands # Load default bestiary commands[0].execute(["bestiary.txt"]) print("Type help or ? to get a list of availible commands.") +def main(): + commands = initialize_commands() - # command loop while True: print() usrRequest = input("Type a command: ").split(" ") From e56de2e40a4a35066ac7854c3e025c50b2ba7a3b Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 16:49:49 -0400 Subject: [PATCH 085/113] Search for load command on startup While this technically is more robust in that it would always find the load command if it exists in the command list it's unlikely that will become necessary. Personally I think this is just far more descriptive and just happens to be more resilient to failure. Could potentially ensure the load command existed and fail if not found, but that's unlikely to be a real failure state. --- src/encounter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 8d8e6c9..1e6564b 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -26,13 +26,15 @@ def initialize_commands() -> list[cmd.Command]: commands.append(cmd.displayHelp(commands)) return commands - # Load default bestiary - commands[0].execute(["bestiary.txt"]) print("Type help or ? to get a list of availible commands.") def main(): commands = initialize_commands() + for command in commands: + if "load" in command.names: + command.execute("bestiary.txt") + break while True: print() usrRequest = input("Type a command: ").split(" ") From 5d7cd1b62a672645cd8c86ab31519f4f082163db Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 17:01:10 -0400 Subject: [PATCH 086/113] Change prompt after first command Makes the command prompt less obtrusive after the user has used a command once. The prompt could be changed to always be the less obtrusive one instead, but I like the feeling of the more inviting prompt, just not all the time. --- src/encounter.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index 1e6564b..105bf6d 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -27,7 +27,6 @@ def initialize_commands() -> list[cmd.Command]: return commands - print("Type help or ? to get a list of availible commands.") def main(): commands = initialize_commands() @@ -35,9 +34,13 @@ def main(): if "load" in command.names: command.execute("bestiary.txt") break + + prompt = "\nType a command: " + print("Type help or ? to get a list of availible commands.") + while True: - print() - usrRequest = input("Type a command: ").split(" ") + usrRequest = input(prompt).split(" ") + prompt = "\ncmd: " action = None From 730e1293a1c952c28e198171e321ec98341dd9fd Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 17:29:22 -0400 Subject: [PATCH 087/113] Correct imports for automatic testing Change the import paths for automated tests to match new program structure. --- tests/test_encounter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_encounter.py b/tests/test_encounter.py index 43e0ae1..3e054f3 100644 --- a/tests/test_encounter.py +++ b/tests/test_encounter.py @@ -1,6 +1,6 @@ import unittest -from src.encounter import isInt -from src.encounter import NPC +from src.commands import isInt +from src.npc import NPC class TestSubMethods(unittest.TestCase): From 40477fdc7a5e4b9119e79fb5cf4c986fed326f0a Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 18:51:12 -0400 Subject: [PATCH 088/113] Utilize list for executing initial load The load command required a list of arguments but was instead given the argument by itself. As such, the load command was not working properly. --- src/encounter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encounter.py b/src/encounter.py index 105bf6d..48a2867 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -32,7 +32,7 @@ def main(): for command in commands: if "load" in command.names: - command.execute("bestiary.txt") + command.execute(["bestiary.txt"]) break prompt = "\nType a command: " From 4973dc6522c613abe10987be8ad6e8b3cdfb0f67 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 19:03:59 -0400 Subject: [PATCH 089/113] Include rank field for NPCs The rank field is meant as a generic stand-in for the table top concept of initiative. This includes two values so that the rank can be temporarily modified by status effects or death and be restored to its original value when these effects pass. --- src/npc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/npc.py b/src/npc.py index d106683..e119184 100644 --- a/src/npc.py +++ b/src/npc.py @@ -34,6 +34,7 @@ def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): self.nick = nick self.maxHP = self.currentHP = maxHP self.ac = int(ac) + self.maxRank = self.currentRank = 0 def __str__(self): output = "" @@ -67,6 +68,10 @@ def equals(self, other): return False if self.note != other.note: return False + if self.maxRank != other.maxRank: + return False + if self.currentRank != other.currentRank: + return False return True def combatStatus(self) -> str: From f23504fc1d9bbb436204f043d7d36956ab64717a Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 19:04:40 -0400 Subject: [PATCH 090/113] Type hint NPC equals method Requires some unique rules defined here: https://peps.python.org/pep-0484/#forward-references --- src/npc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/npc.py b/src/npc.py index e119184..edbb08c 100644 --- a/src/npc.py +++ b/src/npc.py @@ -1,3 +1,6 @@ +from typing import Optional + + class NPC: REQUIRED_PARAMETERS = 3 @@ -51,7 +54,7 @@ def __str__(self): return output - def equals(self, other): + def equals(self: "NPC", other: Optional["NPC"]) -> bool: if self == other: return True if other is None: From 65ef978a0e39e3c7800d83d383e7768c4cfff4df Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 19:46:35 -0400 Subject: [PATCH 091/113] Implement the rank command Implements a basic ranking system for NPCs in which a higher value places them higher in their listing. This command might require displaying output once it finishes since it could be hard to visualize how it would affect the current list. --- src/commands.py | 34 ++++++++++++++++++++++++++++++++++ src/encounter.py | 1 + src/npc.py | 6 ++++++ 3 files changed, 41 insertions(+) diff --git a/src/commands.py b/src/commands.py index d843b1f..42700db 100644 --- a/src/commands.py +++ b/src/commands.py @@ -707,6 +707,40 @@ def execute(self, args=[]) -> None: self.usage() +class rank(Command): + def __init__(self, encounter: NPCList): + super().__init__() + self.names = ['rank', 'initiative'] + self.encounter = encounter + self.description = "Orders NPCs by value." + self.details = dedent("""\ + NPCs order within the encounter will be determined by their rank. + NPCs with a higher value will appear higher in the list. + This command can also be called with the alias "initiative".\ + """).strip().replace('\n', ' ').replace('\r', '') + self.usageStr = "rank " + + def execute(self, args=[]) -> None: + if (len(self.encounter) < 1): + self.encounterEmpty() + return + + if len(args) == 2: + if not isValidInt(args[0], self.encounter) or not isInt(args[1]): + self.usage() + return + + rank = int(args[1]) + npc = self.encounter.data[int(args[0]) - 1] + if npc.currentHP > 0: + npc.currentRank = rank + npc.maxRank = rank + + self.encounter.data.sort(reverse = True) + else: + self.usage() + + if __name__ == "__main__": print("Something seems wrong, this file is not meant to be executed.") print("Did you mean to run encounter instead?") diff --git a/src/encounter.py b/src/encounter.py index 48a2867..ba56b3a 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -23,6 +23,7 @@ def initialize_commands() -> list[cmd.Command]: commands.append(cmd.name(encounter)) commands.append(cmd.mark(encounter)) commands.append(cmd.unmark(encounter)) + commands.append(cmd.rank(encounter)) commands.append(cmd.displayHelp(commands)) return commands diff --git a/src/npc.py b/src/npc.py index edbb08c..308e27c 100644 --- a/src/npc.py +++ b/src/npc.py @@ -51,9 +51,15 @@ def __str__(self): if self.currentHP <= 0: output += " [X]" + else: + if self.currentRank > 0: + output = "(" + str(self.currentRank) + ") " + output return output + def __lt__(self, other): + return self.currentRank < other.currentRank + def equals(self: "NPC", other: Optional["NPC"]) -> bool: if self == other: return True From 9c9ca3c1acf46cb3a5726eed11b8622eb2584a19 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sat, 18 Mar 2023 20:35:45 -0400 Subject: [PATCH 092/113] Simplify main program loop This change is made in preparation for sorting the encounter list after every command. This could be potentially wasteful, but would also ensure NPC ranking is always up to date. This new change excludes empty inputs from processing allowing for some trimmed down code branching. This code also provides a soft error message in the case the user provides some input that is visually empty, i.e. all whitespace or entirely empty, by changing the command prompt back to its longer form. --- src/encounter.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index ba56b3a..31dedfd 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -40,27 +40,23 @@ def main(): print("Type help or ? to get a list of availible commands.") while True: - usrRequest = input(prompt).split(" ") - prompt = "\ncmd: " + userInput = input(prompt).split(" ") + userInput = [token for token in userInput if not token.isspace() and not token == ''] - action = None + if not len(userInput) > 0: + prompt = "\nType a command: " + continue + else: + prompt = "\ncmd: " - if usrRequest != ['']: - action = usrRequest[0].lower() - - if action in ['quit', 'q', 'exit']: + userCommand = userInput.pop(0).lower() + if userCommand in ['quit', 'q', 'exit']: break - args = [] - - if (len(usrRequest) > 1): - for index in range(1, len(usrRequest)): - args.append(usrRequest[index]) - found = False for command in commands: - if action in command.names: - command.execute(args) + if userCommand in command.names: + command.execute(userInput) found = True break From 63001592f597406da05d3fdab59f45b9842116d2 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 6 Aug 2023 21:32:38 -0400 Subject: [PATCH 093/113] Correct relative imports Should allow github's test runners to run properly again. --- src/commands.py | 3 ++- src/encounter.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/commands.py b/src/commands.py index 42700db..2ea3d55 100644 --- a/src/commands.py +++ b/src/commands.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from textwrap import dedent -from npc import NPC, NPCList, findList + +from src.npc import NPC, NPCList, findList class Command(ABC): diff --git a/src/encounter.py b/src/encounter.py index 31dedfd..4336372 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -1,5 +1,5 @@ -import commands as cmd -from npc import NPCList +import src.commands as cmd +from src.npc import NPCList def initialize_commands() -> list[cmd.Command]: From b57f25b59e2e6fae18b4fc1a1cee054e735ad26c Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:03:48 -0400 Subject: [PATCH 094/113] Prefer double quotes Consistently use double quotes throughout code. --- src/commands.py | 68 ++++++++++++++++++++++++------------------------ src/encounter.py | 8 +++--- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/commands.py b/src/commands.py index 2ea3d55..65bd67d 100644 --- a/src/commands.py +++ b/src/commands.py @@ -6,7 +6,7 @@ class Command(ABC): def __init__(self): - self.names: list[str] = ['command', 'test'] + self.names: list[str] = ["command", "test"] self.description: str = "This command has no defined description yet." self.details: str | None = None self.usageStr: str = "This command has no defined usage yet." @@ -29,7 +29,7 @@ def execute(self, args = []) -> None: class load(Command): def __init__(self, bestiary): super().__init__() - self.names = ['load'] + self.names = ["load"] self.bestiary = bestiary self.description = "Replaces the loaded bestiary." self.details = dedent("""\ @@ -38,7 +38,7 @@ def __init__(self, bestiary): If the provided file cannot be loaded the current list will be kept. If the current list is empty and a new list cannot be found then some primitive entries will be generated.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "load " def execute(self, args = []): @@ -73,7 +73,7 @@ def execute(self, args = []): class displayHelp(Command): def __init__(self, commands: list[Command]): super().__init__() - self.names = ['help', '?'] + self.names = ["help", "?"] self.commands = commands self.description = "Prints a list of availible commands." self.usageStr = "help [command_name]" @@ -118,7 +118,7 @@ def execute(self, args = []): class displayMenu(Command): def __init__(self, referenceLists: list[NPCList]): super().__init__() - self.names = ['list', 'display', 'show'] + self.names = ["list", "display", "show"] self.referenceLists = referenceLists self.description = "Displays the selected list of NPCs." self.details = dedent("""\ @@ -126,7 +126,7 @@ def __init__(self, referenceLists: list[NPCList]): The selected list can be any valid alias for their respective list. Allowed aliases for "bestiary" are "book" and "b". Allowed aliases for "encounter" are "e", "combat", and "c".\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "list [all | bestiary | encounter]" def execute(self, args = []): @@ -178,14 +178,14 @@ def copyNPC(bestiary: NPCList, index: int, other: NPCList) -> None: class addNPC(Command): def __init__(self, referenceLists): super().__init__() - self.names = ['add'] + self.names = ["add"] self.referenceLists = referenceLists self.description = "Adds an NPC to the encounter." self.details = dedent("""\ Reference entries in the bestiary by number. Multiple NPCs (even multiple of the same type) can be added at the same time in a comma separated list without spaces.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "add " def execute(self, args = []): @@ -212,7 +212,7 @@ def execute(self, args = []): class clearNPCList(Command): def __init__(self, referenceLists): super().__init__() - self.names = ['clear'] + self.names = ["clear"] self.referenceLists = referenceLists self.description = "Removes all NPCs from a list." self.usageStr = "clear {all | bestiary | encounter}" @@ -240,12 +240,12 @@ def execute(self, args = []): class removeNPC(Command): def __init__(self, encounter): super().__init__() - self.names = ['remove'] + self.names = ["remove"] self.encounter = encounter self.description = "Removes selected NPC(s) from the encounter." self.details = dedent("""\ Can be used with the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "remove " def execute(self, args = []): @@ -281,14 +281,14 @@ def areAllDefeated(encounter: NPCList): class attack(Command): def __init__(self, encounter): super().__init__() - self.names = ['attack'] + self.names = ["attack"] self.encounter = encounter self.description = "Initiantiates D&D like combat with an NPC." self.details = dedent("""\ The attack command is interactive meaning if you leave out a required field you will be asked for the data instead of the command throwing an error state.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "attack [hit] [damage]" def execute(self, args = []): @@ -364,12 +364,12 @@ def execute(self, args = []): class damage(Command): def __init__(self, encounter): super().__init__() - self.names = ['damage'] + self.names = ["damage"] self.encounter = encounter self.description = "Directly subtracts from selected NPCs' health." self.details = dedent("""\ Can be used with the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "damage " def execute(self, args = []): @@ -421,13 +421,13 @@ def execute(self, args = []): class smite(Command): def __init__(self, encounter): super().__init__() - self.names = ['smite', 'kill'] + self.names = ["smite", "kill"] self.encounter = encounter self.description = "Immediately kills an NPC." self.details = dedent("""\ The smite command can be called using the alias "kill". Supports the all selector, i.e. "kill all" will smite all NPCs in the encounter.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "smite " def execute(self, args = []): @@ -466,12 +466,12 @@ def execute(self, args = []): class heal(Command): def __init__(self, encounter): super().__init__() - self.names = ['heal'] + self.names = ["heal"] self.encounter = encounter - self.description = "Directly adds to an NPC's health." + self.description = "Directly adds to selected NPCs' health." self.details = dedent("""\ Can be used with the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "heal " def __healNPC(self, npc: NPC, amount: int) -> int: @@ -516,14 +516,14 @@ def execute(self, args = []): class status(Command): def __init__(self, encounter): super().__init__() - self.names = ['status'] + self.names = ["status"] self.encounter = encounter self.description = "Displays an NPC's current stats." self.details = dedent("""\ Displays the current health of the selected NPC in the encounter. Additionally displays the contents of notes if any. Supports the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "status " def execute(self, args = []): @@ -554,7 +554,7 @@ def execute(self, args = []): class info(Command): def __init__(self, bestiary): super().__init__() - self.names = ['info', 'details'] + self.names = ["info", "details"] self.bestiary = bestiary self.description = "Displays detailed stats for a bestiary entry." self.usageStr = "info " @@ -576,13 +576,13 @@ def execute(self, args = []): class make(Command): def __init__(self, bestiary): super().__init__() - self.names = ['make'] + self.names = ["make"] self.bestiary = bestiary self.description = "Creates an NPC and adds it to the bestiary." self.details = dedent("""\ Entries added in this manner are temporary and will not persist across reloads of the program.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "make " def execute(self, args=[]) -> None: @@ -598,14 +598,14 @@ def execute(self, args=[]) -> None: class name(Command): def __init__(self, encounter): super().__init__() - self.names = ['name', 'nick'] + self.names = ["name", "nick"] self.encounter = encounter self.description = "Gives a specific name to an NPC in the encounter." self.details = dedent("""\ Nicknames work on a per NPC basis. Multiple NPCs may have the same nickname. The nickname does not replace the NPCs original name and will still be displayed alongside it.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "name " def execute(self, args=[]) -> None: @@ -628,15 +628,15 @@ def execute(self, args=[]) -> None: class mark(Command): def __init__(self, encounter): super().__init__() - self.names = ['mark', 'note'] + self.names = ["mark", "note"] self.encounter = encounter self.description = "Mark an NPC with a symbol and note." self.details = dedent("""\ - Can be used with the all selector. Will place an '*' next to the + Can be used with the all selector. Will place an "*" next to the NPC's name in any list display of NPCs. If this command is run on the same NPC again the new note will overwrite their old note. This can be used to delete a note entirely by replacing it with an empty note. - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "mark [note]" def execute(self, args=[]) -> None: @@ -674,12 +674,12 @@ def execute(self, args=[]) -> None: class unmark(Command): def __init__(self, encounter): super().__init__() - self.names = ['unmark'] + self.names = ["unmark"] self.encounter = encounter self.description = "Remove mark and symbol from an NPC." self.details = dedent("""\ Can be used with the all selector.\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "unmark " def execute(self, args=[]) -> None: @@ -711,14 +711,14 @@ def execute(self, args=[]) -> None: class rank(Command): def __init__(self, encounter: NPCList): super().__init__() - self.names = ['rank', 'initiative'] + self.names = ["rank", "initiative"] self.encounter = encounter self.description = "Orders NPCs by value." self.details = dedent("""\ NPCs order within the encounter will be determined by their rank. NPCs with a higher value will appear higher in the list. This command can also be called with the alias "initiative".\ - """).strip().replace('\n', ' ').replace('\r', '') + """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "rank " def execute(self, args=[]) -> None: diff --git a/src/encounter.py b/src/encounter.py index 4336372..aa8a449 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -3,8 +3,8 @@ def initialize_commands() -> list[cmd.Command]: - bestiary = NPCList(['bestiary', 'book', 'b']) - encounter = NPCList(['encounter', 'e', 'combat', 'c']) + bestiary = NPCList(["bestiary", "book", "b"]) + encounter = NPCList(["encounter", "e", "combat", "c"]) referenceLists = [bestiary, encounter] commands = [] @@ -41,7 +41,7 @@ def main(): while True: userInput = input(prompt).split(" ") - userInput = [token for token in userInput if not token.isspace() and not token == ''] + userInput = [token for token in userInput if not token.isspace() and not token == ""] if not len(userInput) > 0: prompt = "\nType a command: " @@ -50,7 +50,7 @@ def main(): prompt = "\ncmd: " userCommand = userInput.pop(0).lower() - if userCommand in ['quit', 'q', 'exit']: + if userCommand in ["quit", "q", "exit"]: break found = False From c78ee75ee46cd428d816407f04ecfebc5abe3316 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:14:32 -0400 Subject: [PATCH 095/113] Simplify argument validation Removes some unnecessary and confusing nesting based around validating command arguments. --- src/commands.py | 59 +++++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/src/commands.py b/src/commands.py index 65bd67d..1577e99 100644 --- a/src/commands.py +++ b/src/commands.py @@ -57,12 +57,12 @@ def execute(self, args = []): else: self.bestiary.data.clear() for line in bestiaryFile: - if not (line.startswith("#") or line.isspace()): + if not line.startswith("#") and not line.isspace(): parameters = line.rstrip("\n").split(",") if len(parameters) < NPC.REQUIRED_PARAMETERS: raise AttributeError("Missing parameters for line below. Expected " + str(NPC.REQUIRED_PARAMETERS) + " but got " - + str(len(parameters)) + "\n" + line + "") + + str(len(parameters)) + "\n" + line) npc = NPC(parameters[0], int(parameters[1]), int(parameters[2])) self.bestiary.data.append(npc) bestiaryFile.close() @@ -297,12 +297,16 @@ def execute(self, args = []): return lenArgs = len(args) - npc = None if lenArgs > 3 or lenArgs < 1: self.usage() return + for i in range(lenArgs): + if not isInt(args[i]): + self.usage() + return + if not isValidInt(args[0], self.encounter): self.OOBSelection(self.encounter) return @@ -313,20 +317,12 @@ def execute(self, args = []): return if lenArgs == 3: - if not isInt(args[1]) or not isInt(args[2]): - self.usage() - return - if int(args[1]) >= npc.ac: npc.currentHP = max(0, npc.currentHP - int(args[2])) print(npc.nick + " took " + args[2] + " damage.") else: print("Attack misses " + npc.nick + ".") elif lenArgs == 2: - if not isInt(args[1]): - self.usage() - return - if int(args[1]) >= npc.ac: damage = input("Roll for damage: ") if damage.isnumeric() is True: @@ -377,11 +373,8 @@ def execute(self, args = []): self.encounterEmpty() return - if len(args) == 2: - if not isInt(args[1]): - self.usage() - return - elif int(args[1]) < 1: + if len(args) == 2 and isInt(args[1]): + if int(args[1]) < 1: print("Amount must be more than zero.") return if args[0].lower() == "all": @@ -485,10 +478,7 @@ def execute(self, args = []): self.encounterEmpty() return - if len(args) == 2: - if not isInt(args[1]): - self.usage() - return + if len(args) == 2 and isInt(args[1]): if int(args[1]) < 1: print("Amount must be more than zero.") return @@ -560,15 +550,12 @@ def __init__(self, bestiary): self.usageStr = "info " def execute(self, args = []): - if len(args) == 1: - if isInt(args[0]): - if isValidInt(args[0], self.bestiary): - print("INFO:") - print(self.bestiary.data[int(args[0]) - 1].detailedInfo()) - else: - self.OOBSelection(self.bestiary) + if len(args) == 1 and isInt(args[0]): + if isValidInt(args[0], self.bestiary): + print("INFO:") + print(self.bestiary.data[int(args[0]) - 1].detailedInfo()) else: - self.usage() + self.OOBSelection(self.bestiary) else: self.usage() @@ -586,10 +573,7 @@ def __init__(self, bestiary): self.usageStr = "make " def execute(self, args=[]) -> None: - if len(args) == 3: - if args[0].isnumeric() or not isInt(args[1]) or not isInt(args[2]): - self.usage() - return + if len(args) == 3 and not args[0].isnumeric() and isInt(args[1]) and isInt(args[2]): self.bestiary.data.append(NPC(args[0], int(args[1]), int(args[2]))) else: self.usage() @@ -613,10 +597,7 @@ def execute(self, args=[]) -> None: self.encounterEmpty() return - if len(args) == 2: - if not args[0].isnumeric(): - self.usage() - return + if len(args) == 2 and args[0].isnumeric(): if isValidInt(args[0], self.encounter): self.encounter.data[int(args[0]) - 1].nick = args[1] else: @@ -726,11 +707,7 @@ def execute(self, args=[]) -> None: self.encounterEmpty() return - if len(args) == 2: - if not isValidInt(args[0], self.encounter) or not isInt(args[1]): - self.usage() - return - + if len(args) == 2 and isValidInt(args[0], self.encounter) and isInt(args[1]): rank = int(args[1]) npc = self.encounter.data[int(args[0]) - 1] if npc.currentHP > 0: From 3c1689fc5ae2130fdde6c231020f50255e5c1927 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:17:18 -0400 Subject: [PATCH 096/113] Update status command's description Reflects the commands ability to display status for multiple NPCs. --- src/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.py b/src/commands.py index 1577e99..c02b83a 100644 --- a/src/commands.py +++ b/src/commands.py @@ -508,7 +508,7 @@ def __init__(self, encounter): super().__init__() self.names = ["status"] self.encounter = encounter - self.description = "Displays an NPC's current stats." + self.description = "Displays selected NPCs' current stats." self.details = dedent("""\ Displays the current health of the selected NPC in the encounter. Additionally displays the contents of notes if any. From 75a939b03391c456619da7b008ae1d0a1e379392 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:17:47 -0400 Subject: [PATCH 097/113] Add aliases for nickname command --- src/commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands.py b/src/commands.py index c02b83a..e896d88 100644 --- a/src/commands.py +++ b/src/commands.py @@ -582,13 +582,14 @@ def execute(self, args=[]) -> None: class name(Command): def __init__(self, encounter): super().__init__() - self.names = ["name", "nick"] + self.names = ["nickname", "name", "nn"] self.encounter = encounter self.description = "Gives a specific name to an NPC in the encounter." self.details = dedent("""\ Nicknames work on a per NPC basis. Multiple NPCs may have the same nickname. The nickname does not replace the NPCs original - name and will still be displayed alongside it.\ + name and will still be displayed alongside it. + Can be called with the alias "name" or "nn".\ """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "name " From 69c50a4cc6c630942e737bcd1c4ec0a69cd14d14 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:19:10 -0400 Subject: [PATCH 098/113] Improve readability of main loop control flow Removing continue makes following the flow of control of the main loop more obvious. --- src/encounter.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/encounter.py b/src/encounter.py index aa8a449..7f66f98 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -41,28 +41,27 @@ def main(): while True: userInput = input(prompt).split(" ") - userInput = [token for token in userInput if not token.isspace() and not token == ""] + userInput = [token for token in userInput if not token.isspace() and token != ""] if not len(userInput) > 0: prompt = "\nType a command: " - continue else: prompt = "\ncmd: " - userCommand = userInput.pop(0).lower() - if userCommand in ["quit", "q", "exit"]: - break - - found = False - for command in commands: - if userCommand in command.names: - command.execute(userInput) - found = True + userCommand = userInput.pop(0).lower() + if userCommand in ["quit", "q", "exit"]: break - if not found: - print("Unrecognized command.") - print("Type help or ? to learn how to use availible commands.") + found = False + for command in commands: + if userCommand in command.names: + command.execute(userInput) + found = True + break + + if not found: + print("Unrecognized command.") + print("Type help or ? to learn how to use availible commands.") if __name__ == "__main__": From 8def0ee28baf694978924ed1ebcba30a43364996 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 30 Jul 2023 21:16:37 -0400 Subject: [PATCH 099/113] Allow NPC rank to be removed Use numbers 0 or below to reset NPC rank. Bounds all negative ranks to 0, so all negative values have the same ranking as 0. The distinction between ranked or not could help establish which NPCs are active in the encounter or not. --- pyproject.toml | 2 +- src/commands.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4139a4a..7fb353f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "encounter" -version = "4.19.1" +version = "4.20.0" requires-python = ">=3.11" [tool.setuptools] diff --git a/src/commands.py b/src/commands.py index e896d88..f60be8a 100644 --- a/src/commands.py +++ b/src/commands.py @@ -109,7 +109,7 @@ def execute(self, args = []): print("quit".ljust(spacing) + ": " + "Exits the program.") for command in self.commands: print(command.names[0].ljust(spacing) + ": " + command.description) - print("") + print() print("For more detailed information > Usage: " + self.usageStr) else: self.usage() @@ -695,10 +695,11 @@ def __init__(self, encounter: NPCList): super().__init__() self.names = ["rank", "initiative"] self.encounter = encounter - self.description = "Orders NPCs by value." + self.description = "Assigns NPCs a rank order." self.details = dedent("""\ NPCs order within the encounter will be determined by their rank. NPCs with a higher value will appear higher in the list. + Assigning an NPC a rank of 0 or below removes their ranking. This command can also be called with the alias "initiative".\ """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "rank " @@ -709,7 +710,7 @@ def execute(self, args=[]) -> None: return if len(args) == 2 and isValidInt(args[0], self.encounter) and isInt(args[1]): - rank = int(args[1]) + rank = max(int(args[1]), 0) npc = self.encounter.data[int(args[0]) - 1] if npc.currentHP > 0: npc.currentRank = rank From a2244f3b11406127b775c5114b83414c07a60416 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 6 Aug 2023 20:57:44 -0400 Subject: [PATCH 100/113] Improve NPC class str method readability This commit makes it more clear what the final output should look like and why. --- src/npc.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/npc.py b/src/npc.py index 308e27c..d532cbc 100644 --- a/src/npc.py +++ b/src/npc.py @@ -40,22 +40,12 @@ def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): self.maxRank = self.currentRank = 0 def __str__(self): - output = "" - if self.nick is not self.name: - output += self.nick + " (" + self.name + ")" - else: - output += self.name - - if self.marked: - output += "*" - - if self.currentHP <= 0: - output += " [X]" - else: - if self.currentRank > 0: - output = "(" + str(self.currentRank) + ") " + output + rank = f"({self.currentRank}) " if (self.currentRank > 0) else "" + name = (self.name if (self.nick == self.name) + else f"{self.nick} ({self.name})") + is_dead = " [X]" if (self.currentHP == 0) else "" - return output + return f"{rank}{name}{is_dead}" def __lt__(self, other): return self.currentRank < other.currentRank From 5ec1ed3e72eb637de407c3a5e2f218ceabe6f968 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 6 Aug 2023 21:10:35 -0400 Subject: [PATCH 101/113] Code styling on attack and smite commands Minor stylistic changes in preparation for changing rank command behavior. --- src/commands.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/commands.py b/src/commands.py index f60be8a..c24cbf4 100644 --- a/src/commands.py +++ b/src/commands.py @@ -350,11 +350,10 @@ def execute(self, args = []): else: print("Accuracy must be a number.") - if npc is not None: - if npc.currentHP <= 0: - print(npc.nick + " has been defeated.") - if areAllDefeated(self.encounter): - print("Party has defeated all enemies.") + if npc is not None and npc.currentHP <= 0: + print(npc.nick + " has been defeated.") + if areAllDefeated(self.encounter): + print("Party has defeated all enemies.") class damage(Command): @@ -431,8 +430,7 @@ def execute(self, args = []): if len(args) == 1: if args[0].lower() == "all": for npc in self.encounter.data: - if npc.currentHP > 0: - npc.currentHP = 0 + npc.currentHP = 0 else: selected = args[0].split(",") selected = list(set(selected)) # Remove duplicates from the selection From 1452c5acf8f9250b1cee9e25cd53892446d09241 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Sun, 6 Aug 2023 23:51:40 -0400 Subject: [PATCH 102/113] Perform error checking on bestiary load Uses the YAML file format instead of a home grown CSV format. This allows for use of the robust PyYAML parser for loading objects from a file. This commit also performs some basic error checking to keep the program from hard crashing when encountering some kind of syntactical error in the provided YAML file. --- README.md | 3 ++- bestiary.txt | 16 ---------------- bestiary.yaml | 33 +++++++++++++++++++++++++++++++++ pyproject.toml | 6 +++++- src/commands.py | 37 ++++++++++++++++++++++++++----------- src/encounter.py | 2 +- 6 files changed, 67 insertions(+), 30 deletions(-) delete mode 100644 bestiary.txt create mode 100644 bestiary.yaml diff --git a/README.md b/README.md index e019ee4..8a10fa5 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,9 @@ Run from the highest level directory (the one containing bestiary.txt) using the Enter `help` once the program has started to view a list of available commands and their functions. -* Create your own NPCs to add to your encounter. See the make command to create them dynamically or check the provided bestiary.txt file for the csv-like file format. +* Create your own NPCs to add to your encounter. See the `make` command to create them dynamically or check the provided bestiary.yaml file for the proper format. ## Dependencies * Python 3.11 or higher +* PyYAML diff --git a/bestiary.txt b/bestiary.txt deleted file mode 100644 index 66020b5..0000000 --- a/bestiary.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Example bestiary file for encounter -# Lines beginning with "#" are comments -# Blank lines are ignored -# Format for custom NPC types: -# Name, Max HP, AC -# Extra values, descriptions, and notes separated with a comma will be ignored - -# Nature -Wolf, 11, 13 -Fox, 3, 12, this note will be ignored -Spider, 1, 12, A tiny creepy crawley creature -# Fantasy -Skeleton, 13, 13 -Zombie, 16, 15 -# Human -Townsfolk, 9, 12 \ No newline at end of file diff --git a/bestiary.yaml b/bestiary.yaml new file mode 100644 index 0000000..3fbb804 --- /dev/null +++ b/bestiary.yaml @@ -0,0 +1,33 @@ +# Example bestiary file for an encounter + +# Creatures +Wolf: + hp: 11 + ac: 13 +Fox: + hp: 3 + ac: 12 + comment: this note will be ignored +Spider: + hp: 1 + ac: 12 + description: A tiny creepy crawley creature + +# Fantasy +Skeleton: + hp: 13 + ac: 13 +Zombie: + hp: 16 + ac: 15 +Dragon: + hp: 150 + ac: 18 + +# Human +Townsfolk: + hp: 9 + ac: 12 +Guard: + hp: 25 + ac: 13 diff --git a/pyproject.toml b/pyproject.toml index 7fb353f..7f9f996 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "encounter" -version = "4.20.0" +version = "4.20.1" +description = "Interactive command-line tool for managing tabletop combat encounters." requires-python = ">=3.11" +dependencies = [ + "pyyaml", +] [tool.setuptools] package-dir = {"" = "src"} diff --git a/src/commands.py b/src/commands.py index c24cbf4..c2ea33d 100644 --- a/src/commands.py +++ b/src/commands.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod from textwrap import dedent +import yaml + from src.npc import NPC, NPCList, findList @@ -46,7 +48,7 @@ def execute(self, args = []): if numArgs == 1: try: - bestiaryFile = open(args[0].strip()) + bestiary_text = open(args[0].strip()) except FileNotFoundError: print("Selected bestiary file could not be found.") if len(self.bestiary) == 0: @@ -56,16 +58,29 @@ def execute(self, args = []): self.bestiary.data.append(NPC("Enemy", 10, 13)) else: self.bestiary.data.clear() - for line in bestiaryFile: - if not line.startswith("#") and not line.isspace(): - parameters = line.rstrip("\n").split(",") - if len(parameters) < NPC.REQUIRED_PARAMETERS: - raise AttributeError("Missing parameters for line below. Expected " - + str(NPC.REQUIRED_PARAMETERS) + " but got " - + str(len(parameters)) + "\n" + line) - npc = NPC(parameters[0], int(parameters[1]), int(parameters[2])) - self.bestiary.data.append(npc) - bestiaryFile.close() + with bestiary_text: + try: + file = yaml.load(bestiary_text, Loader=yaml.BaseLoader) + except yaml.YAMLError: + print("Something is wrong the syntax of your bestiary file.") + print("Try validating the YAML file?") + exit() + else: + for npc, attributes in file.items(): + name = npc + try: + hp = int(attributes["hp"]) + ac = int(attributes["ac"]) + npc = NPC(name, hp, ac) + except KeyError as key: + print(f"NPC \"{name}\" is missing the {key} attribute!") + except TypeError: + print(f"Formatting of NPC \"{name}\" is incorrect somehow!") + except ValueError as attr_err: + print(f"The NPC \"{name}\" has an invalid attribute!") + print(attr_err) + else: + self.bestiary.data.append(npc) else: self.usage() diff --git a/src/encounter.py b/src/encounter.py index 7f66f98..68e97bf 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -33,7 +33,7 @@ def main(): for command in commands: if "load" in command.names: - command.execute(["bestiary.txt"]) + command.execute(["bestiary.yaml"]) break prompt = "\nType a command: " From 3f42638701bcb940130cc2e53c52443db4549181 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 00:06:54 -0400 Subject: [PATCH 103/113] Implement add all This commit implements the all selector for the add command. This commit also provides better error messages in the case the user provides a non integer for a selector index. --- src/commands.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/commands.py b/src/commands.py index c2ea33d..63cfcbb 100644 --- a/src/commands.py +++ b/src/commands.py @@ -199,7 +199,8 @@ def __init__(self, referenceLists): self.details = dedent("""\ Reference entries in the bestiary by number. Multiple NPCs (even multiple of the same type) can be added at the same time - in a comma separated list without spaces.\ + in a comma separated list without spaces. + Can be used with the all selector.\ """).strip().replace("\n", " ").replace("\r", "") self.usageStr = "add " @@ -212,14 +213,20 @@ def execute(self, args = []): raise TypeError("Encounter list must be an NPCList.") if len(args) == 1: - if not isValidInt(args[0], bestiary): + if args[0].lower() == "all": + for index, _ in enumerate(bestiary.data): + copyNPC(bestiary, index, encounter) + elif not isInt(args[0]): + self.usage() + return + elif not isValidInt(args[0], bestiary): self.OOBSelection(bestiary) return + else: + selected = args[0].split(",") - selected = args[0].split(",") - - for index in selected: - copyNPC(bestiary, int(index), encounter) + for index in selected: + copyNPC(bestiary, int(index), encounter) else: self.usage() From dcda8868e2f6b363eebf311737aba1d4dc66e52f Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 10:45:56 -0400 Subject: [PATCH 104/113] Remove unused constant No longer needed after modifying load behavior. --- src/npc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/npc.py b/src/npc.py index d532cbc..fa92012 100644 --- a/src/npc.py +++ b/src/npc.py @@ -2,8 +2,6 @@ class NPC: - REQUIRED_PARAMETERS = 3 - def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): # Type assertions if type(name) != str: From b513739f017571397ac02c00207677b40a953e41 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 10:47:03 -0400 Subject: [PATCH 105/113] Output number of loaded NPCs Gives an indication of load status success automatically. --- src/commands.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands.py b/src/commands.py index 63cfcbb..51a2d86 100644 --- a/src/commands.py +++ b/src/commands.py @@ -58,6 +58,7 @@ def execute(self, args = []): self.bestiary.data.append(NPC("Enemy", 10, 13)) else: self.bestiary.data.clear() + num_npc_loaded = 0 with bestiary_text: try: file = yaml.load(bestiary_text, Loader=yaml.BaseLoader) @@ -81,6 +82,8 @@ def execute(self, args = []): print(attr_err) else: self.bestiary.data.append(npc) + num_npc_loaded += 1 + print(f"Successfully loaded {num_npc_loaded}/{len(file.items())} NPCs") else: self.usage() From 0a225ffc7ea19c100ec3b4c760a5bb57f5b45098 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:15:05 -0400 Subject: [PATCH 106/113] Make command error methods static This method did not need to differ per instance and did not rely on any class attributes. --- src/commands.py | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/commands.py b/src/commands.py index 51a2d86..f34e82b 100644 --- a/src/commands.py +++ b/src/commands.py @@ -16,10 +16,12 @@ def __init__(self): def usage(self) -> None: print("Usage: " + self.usageStr) - def encounterEmpty(self) -> None: + @staticmethod + def encounterEmpty() -> None: print("The encounter is empty. Add some NPCs to it and try again.") - def OOBSelection(self, referenceList: NPCList) -> None: + @staticmethod + def OOBSelection(referenceList: NPCList) -> None: print("Your selection contains values out of range for the " + referenceList.name) print("Adjust your selection and try again.") @@ -223,7 +225,7 @@ def execute(self, args = []): self.usage() return elif not isValidInt(args[0], bestiary): - self.OOBSelection(bestiary) + Command.OOBSelection(bestiary) return else: selected = args[0].split(",") @@ -275,7 +277,7 @@ def __init__(self, encounter): def execute(self, args = []): if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) == 1: @@ -283,7 +285,7 @@ def execute(self, args = []): self.encounter.data.clear() else: if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -318,7 +320,7 @@ def __init__(self, encounter): def execute(self, args = []): if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return lenArgs = len(args) @@ -333,7 +335,7 @@ def execute(self, args = []): return if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) return npc = self.encounter.data[int(args[0]) - 1] @@ -394,7 +396,7 @@ def __init__(self, encounter): def execute(self, args = []): if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) == 2 and isInt(args[1]): @@ -411,7 +413,7 @@ def execute(self, args = []): print("Party has defeated all enemies.") else: if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -449,7 +451,7 @@ def __init__(self, encounter): def execute(self, args = []): if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) == 1: @@ -462,7 +464,7 @@ def execute(self, args = []): for index in selected: if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) return for index in selected: @@ -498,7 +500,7 @@ def __healNPC(self, npc: NPC, amount: int) -> int: def execute(self, args = []): if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) == 2 and isInt(args[1]): @@ -512,7 +514,7 @@ def execute(self, args = []): print(output) else: if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -541,7 +543,7 @@ def __init__(self, encounter): def execute(self, args = []): if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) == 1: @@ -559,7 +561,7 @@ def execute(self, args = []): print(npc.combatStatus()) else: - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) else: self.usage() @@ -578,7 +580,7 @@ def execute(self, args = []): print("INFO:") print(self.bestiary.data[int(args[0]) - 1].detailedInfo()) else: - self.OOBSelection(self.bestiary) + Command.OOBSelection(self.bestiary) else: self.usage() @@ -618,14 +620,14 @@ def __init__(self, encounter): def execute(self, args=[]) -> None: if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) == 2 and args[0].isnumeric(): if isValidInt(args[0], self.encounter): self.encounter.data[int(args[0]) - 1].nick = args[1] else: - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) else: self.usage() @@ -646,7 +648,7 @@ def __init__(self, encounter): def execute(self, args=[]) -> None: if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) >= 1: @@ -659,7 +661,7 @@ def execute(self, args=[]) -> None: npc.note = "" else: if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -689,7 +691,7 @@ def __init__(self, encounter): def execute(self, args=[]) -> None: if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) == 1: @@ -699,7 +701,7 @@ def execute(self, args=[]) -> None: npc.note = "" else: if not isValidInt(args[0], self.encounter): - self.OOBSelection(self.encounter) + Command.OOBSelection(self.encounter) return selected = args[0].split(",") @@ -729,7 +731,7 @@ def __init__(self, encounter: NPCList): def execute(self, args=[]) -> None: if (len(self.encounter) < 1): - self.encounterEmpty() + Command.encounterEmpty() return if len(args) == 2 and isValidInt(args[0], self.encounter) and isInt(args[1]): From 905239848cb94599b40c0dd047d8cce08b07a969 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:16:27 -0400 Subject: [PATCH 107/113] Display heal info only for hurt NPCs --- src/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands.py b/src/commands.py index f34e82b..ba22fb7 100644 --- a/src/commands.py +++ b/src/commands.py @@ -510,8 +510,8 @@ def execute(self, args = []): if args[0].lower() == "all": for npc in self.encounter.data: healedAmt = self.__healNPC(npc, int(args[1])) - output = "{} was healed {} points.".format(npc.nick, healedAmt) - print(output) + if healedAmt > 0: + print(f"{npc.nick} was healed {healedAmt} points.") else: if not isValidInt(args[0], self.encounter): Command.OOBSelection(self.encounter) From e52a8efbdb880731afc80a0fbcef9b6750d283f2 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:37:19 -0400 Subject: [PATCH 108/113] Display NPC marked status This behavior regressed after rewrite of NPC.__str__ --- src/npc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/npc.py b/src/npc.py index fa92012..5d54069 100644 --- a/src/npc.py +++ b/src/npc.py @@ -41,9 +41,10 @@ def __str__(self): rank = f"({self.currentRank}) " if (self.currentRank > 0) else "" name = (self.name if (self.nick == self.name) else f"{self.nick} ({self.name})") + mark = "*" if self.marked else "" is_dead = " [X]" if (self.currentHP == 0) else "" - return f"{rank}{name}{is_dead}" + return f"{rank}{name}{mark}{is_dead}" def __lt__(self, other): return self.currentRank < other.currentRank From 9bc78c02031502e54a9ab462a89532b04f2a8bdf Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:21:34 -0400 Subject: [PATCH 109/113] Make NPC display and status unique The NPC.__str__ method used for displaying NPCs in menus is somewhat simplified by hiding NPC base type if the NPC has a nickname. The status command now reveals the NPC base type and note details. --- src/npc.py | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/npc.py b/src/npc.py index 5d54069..95fddbc 100644 --- a/src/npc.py +++ b/src/npc.py @@ -29,18 +29,14 @@ def __init__(self, name: str, maxHP: int, ac: int, nick: str | None = None): self.marked = False self.note = "" self.name = name - if nick is None: - self.nick = name - else: - self.nick = nick + self.nick = name if (nick is None) else nick self.maxHP = self.currentHP = maxHP self.ac = int(ac) self.maxRank = self.currentRank = 0 def __str__(self): rank = f"({self.currentRank}) " if (self.currentRank > 0) else "" - name = (self.name if (self.nick == self.name) - else f"{self.nick} ({self.name})") + name = self.name if (self.nick == self.name) else self.nick mark = "*" if self.marked else "" is_dead = " [X]" if (self.currentHP == 0) else "" @@ -73,29 +69,20 @@ def equals(self: "NPC", other: Optional["NPC"]) -> bool: return True def combatStatus(self) -> str: - status = "" - if self.currentHP > 0: - if self.name is not self.nick: - status += self.nick + " (" + self.name + ")" - else: - status += self.name - status += " [" + str(self.currentHP) + "/" + str(self.maxHP) + "]" - else: - if self.name is not self.nick: - status += self.nick + " (" + self.name + ")" - else: - status += self.name - status += " [Dead]" + name = (self.name if (self.nick == self.name) + else f"{self.nick} ({self.name})") + health = " [Dead]" if (self.currentHP == 0) else f" [{self.currentHP}/{self.maxHP}]" if self.marked: - status += "\n Note:\n" + note = "\n> Note: " if not self.note.isspace() and len(self.note) > 0: - status += " " - status += self.note + note += self.note else: - status += "EMPTY" + note += "EMPTY" + else: + note = "" - return status + return f"{name}{health}{note}" def detailedInfo(self) -> str: info = "" From ef7f93af50d99002820b0d5ea66f2a39c8a01575 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:32:23 -0400 Subject: [PATCH 110/113] Correct off by one error in copyNPC method --- src/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands.py b/src/commands.py index ba22fb7..3512663 100644 --- a/src/commands.py +++ b/src/commands.py @@ -190,7 +190,7 @@ def isValidInt(selector: str, referencList: NPCList) -> bool: def copyNPC(bestiary: NPCList, index: int, other: NPCList) -> None: - npc = bestiary.data[index - 1] + npc = bestiary.data[index] copy = NPC(npc.name, npc.maxHP, npc.ac) other.data.append(copy) @@ -231,7 +231,7 @@ def execute(self, args = []): selected = args[0].split(",") for index in selected: - copyNPC(bestiary, int(index), encounter) + copyNPC(bestiary, int(index) - 1, encounter) else: self.usage() From 12308e027d0c466b5b26950b4c0ae7711835be14 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:12:00 -0400 Subject: [PATCH 111/113] Move rank sorting responsibility to main Sorts list after every operation meaning ordering can be maintained if other commands cause side effects that change the overall NPC ordering. --- src/commands.py | 1 - src/encounter.py | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/commands.py b/src/commands.py index 3512663..873b5c7 100644 --- a/src/commands.py +++ b/src/commands.py @@ -741,7 +741,6 @@ def execute(self, args=[]) -> None: npc.currentRank = rank npc.maxRank = rank - self.encounter.data.sort(reverse = True) else: self.usage() diff --git a/src/encounter.py b/src/encounter.py index 68e97bf..95e9e5a 100644 --- a/src/encounter.py +++ b/src/encounter.py @@ -2,9 +2,8 @@ from src.npc import NPCList -def initialize_commands() -> list[cmd.Command]: +def initialize_commands(encounter: NPCList) -> list[cmd.Command]: bestiary = NPCList(["bestiary", "book", "b"]) - encounter = NPCList(["encounter", "e", "combat", "c"]) referenceLists = [bestiary, encounter] commands = [] @@ -29,7 +28,8 @@ def initialize_commands() -> list[cmd.Command]: def main(): - commands = initialize_commands() + encounter = NPCList(["encounter", "e", "combat", "c"]) + commands = initialize_commands(encounter) for command in commands: if "load" in command.names: @@ -40,7 +40,9 @@ def main(): print("Type help or ? to get a list of availible commands.") while True: - userInput = input(prompt).split(" ") + encounter.data.sort(reverse = True) + + userInput = input(prompt).strip().split(" ") userInput = [token for token in userInput if not token.isspace() and token != ""] if not len(userInput) > 0: From 626bc2528829e7bd0da2d8fa2ab35a89b34e96a5 Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:17:13 -0400 Subject: [PATCH 112/113] Reset rank on death Effective rank is set to 0 on death. Rank can be edited after death. Effective rank will be restored on revive. --- src/commands.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/commands.py b/src/commands.py index 873b5c7..4087ac7 100644 --- a/src/commands.py +++ b/src/commands.py @@ -378,6 +378,7 @@ def execute(self, args = []): print("Accuracy must be a number.") if npc is not None and npc.currentHP <= 0: + npc.currentRank = 0 print(npc.nick + " has been defeated.") if areAllDefeated(self.encounter): print("Party has defeated all enemies.") @@ -408,6 +409,7 @@ def execute(self, args = []): if npc.currentHP > 0: npc.currentHP = max(0, npc.currentHP - int(args[1])) if npc.currentHP <= 0: + npc.currentRank = 0 print(npc.nick + " has been defeated.") if areAllDefeated(self.encounter): print("Party has defeated all enemies.") @@ -429,6 +431,7 @@ def execute(self, args = []): npc.currentHP = max(0, npc.currentHP - int(args[1])) if npc.currentHP <= 0: + npc.currentRank = 0 print(npc.nick + " has been defeated.") if areAllDefeated(self.encounter): print("Party has defeated all enemies.") @@ -458,6 +461,7 @@ def execute(self, args = []): if args[0].lower() == "all": for npc in self.encounter.data: npc.currentHP = 0 + npc.currentRank = 0 else: selected = args[0].split(",") selected = list(set(selected)) # Remove duplicates from the selection @@ -474,6 +478,7 @@ def execute(self, args = []): return else: npc.currentHP = 0 + npc.currentRank = 0 if areAllDefeated(self.encounter): print("Party has defeated all enemies.") @@ -509,6 +514,7 @@ def execute(self, args = []): return if args[0].lower() == "all": for npc in self.encounter.data: + npc.currentRank = npc.maxRank healedAmt = self.__healNPC(npc, int(args[1])) if healedAmt > 0: print(f"{npc.nick} was healed {healedAmt} points.") @@ -522,6 +528,7 @@ def execute(self, args = []): for index in selected: npc = self.encounter.data[int(index) - 1] + npc.currentRank = npc.maxRank healedAmt = self.__healNPC(npc, int(args[1])) print(npc.nick + " was healed " + str(healedAmt) + " points.") else: @@ -739,8 +746,9 @@ def execute(self, args=[]) -> None: npc = self.encounter.data[int(args[0]) - 1] if npc.currentHP > 0: npc.currentRank = rank - npc.maxRank = rank - + npc.maxRank = rank + else: + npc.maxRank = rank else: self.usage() From 7f7bd3a51d93b1f6143bdccac75a783e205afa7e Mon Sep 17 00:00:00 2001 From: yochien <43257524+Yochien@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:35:33 -0400 Subject: [PATCH 113/113] Install dependencies in test runner --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c591dda..b4f5743 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,5 +20,7 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: python -m pip install pyyaml - name: Test with Python unittest run: python -m unittest