-
Notifications
You must be signed in to change notification settings - Fork 0
/
arena_hero_chooser.py
274 lines (224 loc) · 10.7 KB
/
arena_hero_chooser.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# Arena: The Contest Hero Chooser is a random PvP team generator for the board game Arena: The Contest
# Copyright (C) 2021 Donato Quartuccia
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# The creator of this program is not affiliated with Dragori Games or Arena: The Contest (https://arenathecontest.com/)
# All trademarks and copyrights are the property of their respective owners.
#
# Modified: 2021-06-13
# Description: A script that will randomly generate two PvP teams for Arena: The Contest.
import random
from os import path
class PathNotSetError(Exception):
"""
Raised when Team._hero_file_path is not set
"""
pass
class Team:
"""
A team belonging to a player. May have 3 or 4 heroes.
Teams are compared based on the value of their rolls and share a pool of possible heroes.
"""
# Class variables:
_hero_file_path = None # holds path to txt file containing hero & class data
_possible_heroes = None # initialized when first class instance is created
_team_size = 3 # should only be changed by the set_team_size class method
_allow_special_class = True # should only be changed by the disallow_special class method
@classmethod
def get_team_size(cls) -> int:
return cls._team_size
@classmethod
def set_hero_file(cls, path_to_file):
"""
Sets the path to the txt file containing properly-formatted (see readme) hero & class data to filename.
:raises FileNotFoundError: if path to filename doesn't exist
:param str path_to_file: path to file containing properly-formatted heroes dictionary
"""
if not path.isfile(path_to_file):
raise FileNotFoundError
else:
cls._hero_file_path = path_to_file
@classmethod
def load_heroes(cls):
"""
Initializes the cls._possible_heroes dictionary with class_name -> list[heroes] pairs.
Preconditions:
- cls.set_hero_file must have been called to set the file to load heroes from
- file must be properly formatted (see readme)
:raises PathNotSetError: if cls._hero_file_path was not set
:raises FileNotFoundError: if path to filename doesn't exist
"""
if not cls._hero_file_path:
raise PathNotSetError("cls._hero_file_path must be set before creating a Team")
elif not path.isfile(cls._hero_file_path):
raise FileNotFoundError
else:
all_heroes = {}
with open(cls._hero_file_path, 'r') as hero_file:
for line in hero_file:
heroes = line.split(", ")
hero_name = heroes[0]
hero_class = heroes[1].strip('\n')
# match the hero with its class
try:
# add the hero to its associated class list (if it's not the first hero of that class)
all_heroes[hero_class].append(hero_name)
except KeyError:
# create a list to store heroes of that class if this is the first hero
all_heroes[hero_class] = [hero_name]
cls._possible_heroes = all_heroes
@classmethod
def disallow_special(cls):
"""
Prevents any hero from the special class (e.g. The Faceless Emperor) from being chosen as a team member,
and removes the special class from _possible_heroes if it is present.
Sets cls._allow_special_class to False.
"""
if cls._allow_special_class:
cls._allow_special_class = False # prevent special class from being added to possible_heroes
if cls._possible_heroes and "Special" in cls._possible_heroes:
del(cls._possible_heroes["Special"]) # remove special class from possible_heroes if it was already added
@classmethod
def set_team_size(cls, team_size):
"""
Sets class variable team_size to 3 or 4.
:param int team_size: Number of heroes to place on each team. Must be 3 or 4.
:raises ValueError: if team_size is not 3 or 4
"""
if cls._team_size == 3 or cls._team_size == 4:
cls._team_size = team_size
else:
raise ValueError("team_size must be 3 or 4")
def __init__(self, team_name):
"""
Creates a Team with the specified team_name.
If this is the first time a Team has been created, initializes the heroes dictionary (cls._possible_heroes).
Preconditions:
- cls.set_hero_file must have been called to set the file to load heroes from
- *(optional)* cls.set_team_size must have been called to set the team size to 4 (default is 3)
- *(optional)* cls.disallow_special must have been called to exclude the special class (included by default)
:param str team_name: a name for the Team
"""
# if this is the first time any Team has been created, initialize the heroes database
if self._possible_heroes is None:
self.load_heroes()
# remove the special class from consideration if disallow_special() was previously called and
# the special class is somehow still among the possible_heroes to be chosen
if not self._allow_special_class and "Special" in self._possible_heroes:
del(self._possible_heroes["Special"])
self._name = team_name
self._roll = random.randint(1, 20) # roll a d20
self._possible_classes = list(self._possible_heroes.keys())
self._heroes = [] # len(self._heroes) should never exceed cls.team_size
def get_name(self) -> str:
return self._name
def get_roll(self) -> int:
return self._roll
def print_roll(self):
a_or_an = "an" if self._roll in {8, 11, 18} else 'a'
print(f"{self._name} rolled {a_or_an} {self._roll}")
def reroll(self):
"""
Sets self._roll to a random integer between 1 and 20, mimicking a d20 dice roll
"""
self._roll = random.randint(1, 20)
def choose_hero(self):
"""
Adds a randomly-chosen hero to the Team in compliance with the standard PvP rules of Arena: the Contest.
Heroes are chosen from cls._possible_heroes, are equally weighted, may only be chosen if no other hero of
that class is on the Team, and may not be chosen if present on an opponent's Team.
Removes the chosen hero from cls._possible_heroes and the chosen class from self._possible_classes.
Adds the chosen hero to self._heroes.
"""
# weight random choice of class by num of heroes in each class, prevent choosing from an empty list
keys_to_choose_from = [key for key in self._possible_classes for _ in self._possible_heroes[key]]
# choose a hero and add it to self.heroes
key = random.choice(keys_to_choose_from)
hero = random.choice(self._possible_heroes[key])
self._heroes.append(hero)
# prevent this team from choosing another hero of the same class
self._possible_classes.remove(key)
# prevent any team from choosing the same hero
self._possible_heroes[key].remove(hero)
def __lt__(self, other):
return self._roll < other._roll if isinstance(other, Team) else NotImplemented
def __gt__(self, other):
return self._roll > other._roll if isinstance(other, Team) else NotImplemented
def __eq__(self, other):
return self._roll == other._roll if isinstance(other, Team) else NotImplemented
def __str__(self):
return self._name + "\'s team:\n" + '\n'.join(self._heroes)
def __repr__(self):
return f"Team(_possible_heroes={self._possible_heroes}, _team_size={self._team_size}, " \
f"_allow_special_class={self._allow_special_class}, _name={self._name}, _roll = {self._roll} " \
f"_possible_classes={self._possible_classes}, _heroes={self._heroes})"
if __name__ == '__main__':
print("Welcome to the Arena! We're going to generate a random team for you.")
print("")
Team.set_hero_file("./res/heroes.txt")
# ask user to pick a team of size 3 or 4
while True:
num_heroes = input("Would you like to play with 3 or 4 heroes per team? ")
if num_heroes == "3" or num_heroes == "4":
Team.set_team_size(int(num_heroes))
break
else:
print("Please enter only \"3\" or \"4\".")
print("")
# ask user to choose whether to include special character(s)
while True:
use_special = input("Would you like to play with the special class (i.e. The Faceless Emperor)? ").lower()
if use_special == "y" or use_special == "yes":
break
elif use_special == "n" or use_special == "no":
Team.disallow_special()
break
else:
print("Please enter (y)es or (n)o.")
print("")
# ask user to choose unique team names
team_1_name = input("Please enter a name for the first team captain: ")
team_2_name = input("Please enter a name for the second team captain: ")
while team_1_name == team_2_name:
print("Please choose a different name for each team.")
print("")
team_1_name = input("Please enter a name for the first team captain: ")
team_2_name = input("Please enter a name for the second team captain: ")
# initialize teams
team_1 = Team(team_1_name)
team_2 = Team(team_2_name)
# determine who goes first, while tied, keep re-rolling
while team_1 == team_2:
print("")
team_1.print_roll()
team_2.print_roll()
print("Oops. It's a tie! Rolling again...")
team_1.reroll()
team_2.reroll()
# output team order
print("")
team_1.print_roll()
team_2.print_roll()
print("")
print(max(team_1, team_2).get_name() + "\'s team picks first.")
# fill each team with heroes, alternating between them
for i in range(Team.get_team_size()):
max(team_1, team_2).choose_hero()
min(team_1, team_2).choose_hero()
# output teams
print("")
print(team_1)
print("")
print(team_2)