initiative-tracker 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lukas Wagenvoort
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: initiative-tracker
3
+ Version: 0.1.0
4
+ Summary: A simple cli based initiative tracker for dnd or other ttrpgs
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Author: Quaeren
8
+ Author-email: ldwagenvoort@hotmail.com
9
+ Requires-Python: >=3.14
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Dist: questionary (>=2.1.1,<3.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Initiative Tracker for TTRPGS
16
+ A simple command line interface for running a initiative tracker in the terminal. You can create an encounter, add combatants, roll initiative, track health and save encounters for later use.
17
+
18
+ ## Features
19
+ - **Combatant Mangement** Add or remove players, enemies or allies in an encounter
20
+ - **Automatic Turn Tracking** After letting the program roll the initiative it autmatically tracks whoose turn it is
21
+ - **Health Management** Add or remove health via the damage and heal funcitons
22
+ - **Save and Load** Save and load encounters between sessions for quick and easy playthroughs
23
+
24
+ ## How to Use
25
+ ### Main menu
26
+ When the application starts, you can choose to:
27
+ - **Start new encounter:** Create an empty encounter.
28
+ - **Load** encounter:** Load the saved encounter.
29
+ - **Exit:** Close the application.
30
+
31
+ ### Managing an encounter
32
+ Once an encounter has been created or loaded, the encounter menu provides the following options:
33
+ - **Add combatant:** Enter a name, maximum hit points, initiative modifier, and combatant type.
34
+ - **Remove combatant:** Select combatants to remove from the encounter.
35
+ - **View turn order:** Sort combatants by initiative and display their current health and initiative.
36
+ - **Roll initiative:** Roll initiative for every combatant.
37
+ - **Start encounter:** Open the turn-order menu and begin managing combat.
38
+ - **Save encounter:** Save the encounter's current state to the database.
39
+ - **Exit:** Return to the main menu.
40
+
41
+ During an encounter, the currently active combatant is marked with an arrow. You can advance to the next turn, reset the round counter, or select a combatant to perform a health-related action.
42
+
43
+ ### Actions on a combatant
44
+ Once you select a combatant you are presented with the option to input a command, the following commands are allowed:
45
+ - **damage** after inputting enter a value and the corresponding health will be removed
46
+ - **heal** after inputting enter a value and the corresponding health will be healed
47
+ - **set** after inputting manually set and initiative value for the combatant
48
+ - **exit** after inputting press enter again and exit this menu
49
+
50
+ ## License
51
+ MIT
@@ -0,0 +1,37 @@
1
+ # Initiative Tracker for TTRPGS
2
+ A simple command line interface for running a initiative tracker in the terminal. You can create an encounter, add combatants, roll initiative, track health and save encounters for later use.
3
+
4
+ ## Features
5
+ - **Combatant Mangement** Add or remove players, enemies or allies in an encounter
6
+ - **Automatic Turn Tracking** After letting the program roll the initiative it autmatically tracks whoose turn it is
7
+ - **Health Management** Add or remove health via the damage and heal funcitons
8
+ - **Save and Load** Save and load encounters between sessions for quick and easy playthroughs
9
+
10
+ ## How to Use
11
+ ### Main menu
12
+ When the application starts, you can choose to:
13
+ - **Start new encounter:** Create an empty encounter.
14
+ - **Load** encounter:** Load the saved encounter.
15
+ - **Exit:** Close the application.
16
+
17
+ ### Managing an encounter
18
+ Once an encounter has been created or loaded, the encounter menu provides the following options:
19
+ - **Add combatant:** Enter a name, maximum hit points, initiative modifier, and combatant type.
20
+ - **Remove combatant:** Select combatants to remove from the encounter.
21
+ - **View turn order:** Sort combatants by initiative and display their current health and initiative.
22
+ - **Roll initiative:** Roll initiative for every combatant.
23
+ - **Start encounter:** Open the turn-order menu and begin managing combat.
24
+ - **Save encounter:** Save the encounter's current state to the database.
25
+ - **Exit:** Return to the main menu.
26
+
27
+ During an encounter, the currently active combatant is marked with an arrow. You can advance to the next turn, reset the round counter, or select a combatant to perform a health-related action.
28
+
29
+ ### Actions on a combatant
30
+ Once you select a combatant you are presented with the option to input a command, the following commands are allowed:
31
+ - **damage** after inputting enter a value and the corresponding health will be removed
32
+ - **heal** after inputting enter a value and the corresponding health will be healed
33
+ - **set** after inputting manually set and initiative value for the combatant
34
+ - **exit** after inputting press enter again and exit this menu
35
+
36
+ ## License
37
+ MIT
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "initiative-tracker"
3
+ version = "0.1.0"
4
+ description = "A simple cli based initiative tracker for dnd or other ttrpgs"
5
+ authors = [
6
+ {name = "Quaeren",email = "ldwagenvoort@hotmail.com"}
7
+ ]
8
+ license = "MIT"
9
+ readme = "README.md"
10
+ requires-python = ">=3.14"
11
+ dependencies = [
12
+ "questionary (>=2.1.1,<3.0.0)"
13
+ ]
14
+
15
+
16
+ [build-system]
17
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
18
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,182 @@
1
+ import questionary
2
+ from questionary import Separator
3
+ from .tracker import Database, Encounter, Combatant
4
+ import subprocess
5
+ import os
6
+
7
+
8
+ def clear_screen():
9
+ subprocess.run(
10
+ ["cmd", "/c", "cls"] if os.name == "nt" else ["clear"], check=False)
11
+
12
+ def sub_menu_questions():
13
+ clear_screen()
14
+
15
+ return questionary.select(
16
+ "Please choose an action to do:",
17
+ choices=[
18
+ "Add combatant",
19
+ "Remove combatant",
20
+ "View turn order",
21
+ "Roll initiative",
22
+ "Start encounter",
23
+ "Save encounter",
24
+ "Exit"
25
+ ], qmark="", instruction=None, use_shortcuts=True).ask() or "do nothing"
26
+
27
+ def remove_combatant_cli(encounter):
28
+ clear_screen()
29
+
30
+ choices = []
31
+ for i, combatant in enumerate(encounter.combatant_list):
32
+ name = f"{combatant}"
33
+
34
+ choices.append(questionary.Choice(
35
+ title=name,
36
+ value=combatant))
37
+ choices.append("Exit")
38
+
39
+ choice = questionary.checkbox("Please select combatants to be removed:", choices=choices, qmark="", instruction=None, use_shortcuts=True).ask() or []
40
+
41
+ if "Exit" in choice:
42
+ pass
43
+
44
+ for item in choice:
45
+ encounter.remove_combatant(item)
46
+ print("Succesfully removed all selected combatants combatants")
47
+
48
+ def choice_menu(choice, encounter):
49
+ clear_screen()
50
+
51
+ match choice:
52
+ case "Add combatant":
53
+ name = questionary.text("Name: ", qmark="", instruction=None).ask()
54
+ health = int(questionary.text("Health: ", qmark="", instruction=None).ask())
55
+ init_mod = int(questionary.text("Initiative modifier: ", qmark="", instruction=None).ask())
56
+ type = questionary.select(
57
+ "Select combatant tupe: ",
58
+ choices=["player", "enemy", "ally"], qmark="", instruction=None, use_shortcuts=True).ask()
59
+
60
+ encounter.add_combatant(Combatant(name, health, init_mod, type))
61
+ print(f"Combatant: {name} added succesfully")
62
+ return None
63
+ case "Remove combatant":
64
+ remove_combatant_cli(encounter)
65
+ return None
66
+ case "View turn order":
67
+ clear_screen()
68
+ encounter.sort_list()
69
+ encounter.print_turn_order()
70
+ questionary.press_any_key_to_continue().ask()
71
+ return None
72
+ case "Roll initiative":
73
+ encounter.roll_all_initiative()
74
+ return None
75
+ case "Start encounter":
76
+ return "Start"
77
+ case "Save encounter":
78
+ return "Save"
79
+ case "Exit":
80
+ return "Exit"
81
+
82
+ def active_encounter(encounter):
83
+ while True:
84
+ clear_screen()
85
+ choices = []
86
+
87
+ for i, combatant in enumerate(encounter.combatant_list):
88
+ name = f"▶ {combatant}" if i == encounter.active_combatant_index else f" {combatant}"
89
+
90
+ choices.append(questionary.Choice(
91
+ title=name,
92
+ value=combatant
93
+ ))
94
+ choices.append(Separator())
95
+ choices.append(" Reset round number")
96
+ choices.append(" End Turn")
97
+ choices.append(" Exit")
98
+
99
+ print(f"Round Number: {encounter.round_number}")
100
+ choice = questionary.select("Turn Order:", choices=choices, qmark="", instruction=None, use_shortcuts=True).ask() or "do nothing"
101
+
102
+ if choice == " End Turn":
103
+ encounter.advance_turn()
104
+ continue
105
+ elif choice == " Exit":
106
+ break
107
+ elif choice == " Reset round number":
108
+ encounter.reset_round_number()
109
+ continue
110
+ else:
111
+ action = input(choice).strip()
112
+ value = input("Value: ").strip()
113
+ if action.lower() == "damage":
114
+ encounter.combatant_list[encounter.combatant_list.index(choice)].damage(int(value))
115
+ elif action.lower() == "heal":
116
+ encounter.combatant_list[encounter.combatant_list.index(choice)].heal(int(value))
117
+ elif action.lower() == "set":
118
+ encounter.combatant_list[encounter.combatant_list.index(choice)].set_init(int(value))
119
+ elif action.lower() == "exit":
120
+ continue
121
+
122
+
123
+
124
+
125
+ def main():
126
+ database = Database()
127
+ database.create_tables()
128
+ clear_screen()
129
+
130
+ while True:
131
+ print("===========================================================================================")
132
+ print(""" ▄▄▄ ▗▄ ▗▖ ▄▄▄ ▗▄▄▄▖ ▄▄▄ ▄ ▗▄▄▄▖ ▄▄▄ ▗▖ ▗▖▗▄▄▄▖ ▗▄▄▄▖▗▄▄▖ ▄ ▄▄ ▗▖ ▄▖▗▄▄▄▖▗▄▄▖
133
+ ▀█▀ ▐█ ▐▌ ▀█▀ ▝▀█▀▘ ▀█▀ ▐█▌ ▝▀█▀▘ ▀█▀ ▝█ █▘▐▛▀▀▘ ▝▀█▀▘▐▛▀▜▌ ▐█▌ █▀▀▌▐▌▐▛ ▐▛▀▀▘▐▛▀▜▌
134
+ █ ▐▛▌▐▌ █ █ █ ▐█▌ █ █ █ █ ▐▌ █ ▐▌ ▐▌ ▐█▌ ▐▛ ▐▙█ ▐▌ ▐▌ ▐▌
135
+ █ ▐▌█▐▌ █ █ █ █ █ █ █ █ █ ▐███ █ ▐███ █ █ ▐▌ ▐██ ▐███ ▐███
136
+ █ ▐▌▐▟▌ █ █ █ ███ █ █ ▐█▌ ▐▌ █ ▐▌▝█▖ ███ ▐▙ ▐▌▐▙ ▐▌ ▐▌▝█▖
137
+ ▄█▄ ▐▌ █▌ ▄█▄ █ ▄█▄ ▗█ █▖ █ ▄█▄ ▐█▌ ▐▙▄▄▖ █ ▐▌ ▐▌▗█ █▖ █▄▄▌▐▌ █▖▐▙▄▄▖▐▌ ▐▌
138
+ ▀▀▀ ▝▘ ▀▘ ▀▀▀ ▀ ▀▀▀ ▝▘ ▝▘ ▀ ▀▀▀ ▝▀▘ ▝▀▀▀▘ ▀ ▝▘ ▝▀▝▘ ▝▘ ▀▀ ▝▘ ▝▘▝▀▀▀▘▝▘ ▝▀ """)
139
+ print("===========================================================================================")
140
+ action = (questionary.select("Welcome to tracker, please select from the options below:",
141
+ choices=[
142
+ "Start new encounter",
143
+ "Load encounter",
144
+ "Exit"], qmark="", instruction=None, use_shortcuts=True).ask() or "do nothing")
145
+
146
+ match action:
147
+ case "Start new encounter":
148
+ encounter = Encounter()
149
+
150
+ while True:
151
+ choice = sub_menu_questions()
152
+ check = choice_menu(choice, encounter)
153
+ if check == None:
154
+ continue
155
+ elif check == "Exit":
156
+ break
157
+ elif check == "Save":
158
+ database.save_encounter(encounter)
159
+ elif check == "Start":
160
+ active_encounter(encounter)
161
+ case "Load encounter":
162
+ encounter = database.load_encounter()
163
+
164
+ while True:
165
+ choice = sub_menu_questions()
166
+ check = choice_menu(choice, encounter)
167
+ if check == None:
168
+ continue
169
+ elif check == "Exit":
170
+ break
171
+ elif check == "Save":
172
+ database.save_encounter(encounter)
173
+ elif check == "Start":
174
+ active_encounter(encounter)
175
+ case "Exit":
176
+ clear_screen()
177
+ break
178
+ case _:
179
+ print("No option chosen")
180
+
181
+ if __name__ == "__main__":
182
+ main()
@@ -0,0 +1,231 @@
1
+ import random
2
+ import sqlite3
3
+ from pathlib import Path
4
+
5
+ class Combatant:
6
+ def __init__(self, name, hp, init_mod, type):
7
+ self.name = name.strip()
8
+ self.max_hp = hp
9
+ self.hp = hp
10
+ self.init_mod = init_mod # initiative modifer
11
+ self.init = 0 # set default initiative to zero
12
+ self.type = type
13
+
14
+ def damage(self, value: int) -> None:
15
+ if value <= 0:
16
+ raise ValueError("Only positive numbers above zero accepted")
17
+ else:
18
+ if (self.hp - value) < 0:
19
+ self.hp = 0
20
+ else:
21
+ self.hp -= value
22
+ #print(f"{value} damage done to {self.name}. Current HP: {self.hp}")
23
+
24
+ def heal(self, value:int) -> None:
25
+ if value <= 0:
26
+ raise ValueError("Only positive number above zero accepted")
27
+ else:
28
+ if (self.hp + value) > self.max_hp:
29
+ self.hp = self.max_hp
30
+ else:
31
+ self.hp += value
32
+ #print(f"{value} damage healed for {self.name}. Current HP: {self.hp}")
33
+
34
+ def set_init(self, value:int) -> None:
35
+ if value > 0 and value < 26:
36
+ self.init = value
37
+ else:
38
+ raise ValueError("Value has to be between 1 and 25")
39
+
40
+ def set_init_mod(self, value:int) -> None:
41
+ if value >= -5 and value <= 5:
42
+ self.init_mod = value
43
+ else:
44
+ raise ValueError("Value has to be between -5 and 5")
45
+
46
+ def roll_init(self) -> int:
47
+ init = random.randint(1,20) + self.init_mod
48
+ if init <= 0:
49
+ init = 1
50
+ self.init = init
51
+ return init
52
+
53
+ def change_name(self, name):
54
+ if not isinstance(name, str):
55
+ raise TypeError("Name must be a string")
56
+
57
+ if not name.strip():
58
+ raise ValueError("Name cannnot be empty")
59
+
60
+ self.name = name.strip()
61
+
62
+ def __str__(self):
63
+ return f"{self.name} ({self.hp}/{self.max_hp}) HP, ({self.init})"
64
+
65
+ def __lt__(self, other):
66
+ return self.init > other.init
67
+
68
+ class Encounter:
69
+ def __init__(self):
70
+ self.combatant_list = []
71
+ self.active_combatant_index = 0
72
+ self.round_number = 0
73
+
74
+
75
+ def add_combatant(self, combatant):
76
+ self.combatant_list.append(combatant)
77
+
78
+ def remove_combatant(self, removee):
79
+ for i, combatant in enumerate(self.combatant_list):
80
+ if combatant == removee:
81
+ if self.combatant_list.index(removee) == self.active_combatant_index:
82
+ self.active_combatant_index += 1
83
+ if self.active_combatant_index > (len(self.combatant_list) - 1):
84
+ self.active_combatant_index = 0
85
+ self.round_number += 1
86
+ del self.combatant_list[i]
87
+
88
+ def sort_list(self):
89
+ self.combatant_list.sort()
90
+
91
+ def print_turn_order(self):
92
+ for mem in self.combatant_list:
93
+ print(mem)
94
+
95
+ def roll_all_initiative(self):
96
+ for mem in self.combatant_list:
97
+ mem.roll_init()
98
+
99
+ def advance_turn(self):
100
+ self.active_combatant_index += 1
101
+
102
+ if self.active_combatant_index >= len(self.combatant_list):
103
+ self.active_combatant_index = 0
104
+ self.round_number += 1
105
+
106
+ while self.combatant_list[self.active_combatant_index].hp == 0:
107
+ self.active_combatant_index += 1
108
+
109
+ if self.active_combatant_index >= len(self.combatant_list):
110
+ self.active_combatant_index = 0
111
+ self.round_number += 1
112
+
113
+ def reset_round_number(self):
114
+ self.round_number = 0
115
+
116
+ class Database:
117
+ def __init__(self, db_name="tracker.db"):
118
+ self.db_name = db_name
119
+
120
+ def check(self):
121
+ return Path(self.db_name).exists()
122
+
123
+ def connect(self):
124
+ conn = sqlite3.connect(self.db_name)
125
+ conn.execute("PRAGMA foreign_keys = ON")
126
+ return conn
127
+
128
+ def create_tables(self):
129
+ with self.connect() as conn:
130
+ conn.execute("""
131
+ CREATE TABLE IF NOT EXISTS encounters (
132
+ id INTEGER PRIMARY KEY,
133
+ round_number INTEGER NOT NULL DEFAULT 0,
134
+ active_index INTEGER
135
+ )
136
+ """)
137
+
138
+ conn.execute("""
139
+ CREATE TABLE IF NOT EXISTS combatants (
140
+ id INTEGER PRIMARY KEY,
141
+ encounter_id INTEGER NOT NULL,
142
+ name TEXT NOT NULL,
143
+ max_hp INTEGER NOT NULL,
144
+ hp INTEGER NOT NULL,
145
+ init_mod INTEGER NOT NULL,
146
+ init INTEGER NOT NULL,
147
+ type TEXT NOT NULL,
148
+
149
+ FOREIGN KEY (encounter_id)
150
+ REFERENCES encounters(id)
151
+ ON DELETE CASCADE
152
+ )
153
+ """)
154
+
155
+ conn.execute("""
156
+ INSERT OR IGNORE INTO encounters (id, round_number)
157
+ VALUES (1, 0)
158
+ """)
159
+
160
+ def save_encounter(self, encounter):
161
+ with self.connect() as conn:
162
+ conn.execute("PRAGMA foreign_keys = ON")
163
+
164
+ conn.execute("""
165
+ UPDATE encounters
166
+ SET round_number = ?, active_index = ?
167
+ WHERE id = 1
168
+ """, (encounter.round_number, encounter.active_combatant_index))
169
+
170
+ conn.execute("""
171
+ DELETE FROM combatants
172
+ WHERE encounter_id = 1
173
+ """)
174
+
175
+ for combatant in encounter.combatant_list:
176
+ conn.execute("""
177
+ INSERT INTO combatants (
178
+ encounter_id,
179
+ name,
180
+ max_hp,
181
+ hp,
182
+ init_mod,
183
+ init,
184
+ type
185
+ )
186
+ VALUES (1, ?, ?, ?, ?, ?, ?)
187
+ """, (combatant.name, combatant.max_hp, combatant.hp, combatant.init_mod, combatant.init, combatant.type))
188
+
189
+ def load_encounter(self):
190
+ with self.connect() as conn:
191
+ conn.row_factory = sqlite3.Row
192
+
193
+ encounter_row = conn.execute("""
194
+ SELECT round_number, active_index
195
+ FROM encounters
196
+ WHERE id = 1
197
+ """).fetchone()
198
+
199
+ if encounter_row is None:
200
+ return None
201
+
202
+ encounter = Encounter()
203
+ encounter.round_number = encounter_row["round_number"]
204
+
205
+ rows = conn.execute("""
206
+ SELECT name, max_hp, hp, init_mod, init, type
207
+ FROM combatants
208
+ WHERE encounter_id = 1
209
+ ORDER BY id
210
+ """).fetchall()
211
+
212
+ for row in rows:
213
+ combatant = Combatant(
214
+ row["name"],
215
+ row["max_hp"],
216
+ row["init_mod"],
217
+ row["type"]
218
+ )
219
+
220
+ combatant.hp = row["hp"]
221
+ combatant.init = row["init"]
222
+
223
+ encounter.add_combatant(combatant)
224
+
225
+ active_index = encounter_row["active_index"]
226
+
227
+ if active_index is not None:
228
+ if 0 <= active_index < len(encounter.combatant_list):
229
+ encounter.active_combatant_index = active_index
230
+
231
+ return encounter