-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbb_tournament.py
513 lines (456 loc) · 19.2 KB
/
bb_tournament.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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
#! python3
"""This module implements classes and methods for manipulating Blood Bowl 2
team names and league schedules in order to facilitate command line operation
and a Discord Bot API in the future."""
import argparse
import yaml
################################################################################
class Team:
"""Class encapsulates team data as well as multiple methods for
constructing the object from different sources."""
def __init__(self, name, race, coach, dtag):
self.name = name
self.race = race
self.coach = coach
self.dtag = dtag
@classmethod
def from_dict(cls, team_info):
"""Returns a class object filled with data from a dictionary (the
format the YAML file will return)."""
return cls(
team_info["name"], team_info["race"], team_info["coach"], team_info["dtag"]
)
@classmethod
def from_str(cls, team_str):
"""Returns a class object filled with data from a string (the
format when creating a team from the command line)."""
team_list = team_str.split(",")
if team_list[2].lstrip() == "AI":
return cls(
team_list[0].lstrip(),
team_list[1].lstrip(),
team_list[2].lstrip(),
"None",
)
return cls(
team_list[0].lstrip(),
team_list[1].lstrip(),
team_list[2].lstrip(),
team_list[3].lstrip(),
)
@property
def yaml(self):
"""Returns a dictionary object to be used to create the data structure
that is built up into the final overall YAML structure."""
return {
"name": self.name,
"race": self.race,
"coach": self.coach,
"dtag": self.dtag,
}
class League(list):
"""A customized list class encapsulating specific method for constructing
the class out of a dictionary and Team objects. May have additional
custom methods later."""
def __init__(self, teams_dict=None):
# Expect to receive nothing and initialize an empty list, or a list
# of dictionary objects that must be parsed and then we fill our
# list with Team objects.
super().__init__()
teams_dict = teams_dict or []
for team_dict in teams_dict:
self.append(Team.from_dict(team_dict))
@property
def yaml(self):
"""Returns a list object to be used to create the data structure
that is built up into the final overall YAML structure."""
return [team.yaml for team in self]
################################################################################
class Game:
"""Class that encapsulates the data pertaining to a single game in a
tournament structure."""
def __init__(self, game_data):
"""Initialization of Game object. Keeping the Team object associated
with each position as well as the index. The index is used for
recreating the YAML object."""
# Initial state variables from the instance data
self.home_index = game_data["home"]
self.away_index = game_data["away"]
self.result = {}
self.result["home"] = game_data["result"]["home"]
self.result["away"] = game_data["result"]["away"]
# Secondary variables
self.home = None
self.away = None
self.played = False
if self.result["home"] != -1 and self.result["away"] != -1:
self.played = True
def add_team_data(self, league):
"""Method contains a Team object for the purposes of reporting and
matching an index number to a Team name. Index of 9999 indicates
a bye game and should always be in the away position."""
self.home = league[self.home_index]
if self.away_index == 9999:
self.away = Team("Bye", "", "", "")
else:
self.away = league[self.away_index]
def add_result(self, result_list):
"""Gets a list of scores [home, away] and sets the instances
values accordingly."""
self.result["home"] = result_list[0]
self.result["away"] = result_list[1]
@property
def yaml(self):
"""Returns a dictionary object to be used to create the data structure
that is built up into the final overall YAML structure."""
return {"home": self.home_index, "away": self.away_index, "result": self.result}
def __str__(self):
if self.played:
return f"Home: {self.home.name:25} Away: {self.away.name:25} Result: {self.result['home']}-{self.result['away']}"
return f"Home: {self.home.name:25} Away: {self.away.name:25}"
class Week(list):
"""Class that encapsulates the data pertaining to a single week in a
tournament structure."""
def __init__(self, game_list=None):
"""Receives a list of dictionaries."""
super().__init__()
self.current = False
game_list = game_list or []
for game in game_list:
self.append(Game(game))
def add_games(self, game_list):
"""Gets a manufactured list in the same format as the one retrieved
from the YAML file and adds games to this week object."""
for game in game_list:
self.append(Game(game))
def add_result(self, result_list):
"""Gets a list of values for a game result. [Game num, home score,
away score] and calls the game's method to set those values."""
self[result_list[0]].add_result(result_list[1:])
@property
def yaml(self):
"""Returns a list object to be used to create the data structure
that is built up into the final overall YAML structure."""
return [game.yaml for game in self]
def __str__(self):
if self.current:
lines = [" Current " + "-" * 63]
else:
lines = ["-" * 72]
for game_idx, game in enumerate(self):
lines.append(f"Game: {game_idx} | {game}")
return "\n".join(lines)
class Schedule(list):
"""Class that encapsulates the data holding every week (and then every
subsequent game) in a tournament structure."""
def __init__(self, schedule_dict=None):
# Again we expect to get either nothing, or a dictionary with
# keys labeling the weeks.
super().__init__()
schedule_dict = schedule_dict or {}
for week in schedule_dict:
self.append(Week(schedule_dict[week]))
def add_week(self):
"""Adds a blank week object to the schedule."""
self.append(Week())
def add_games(self, game_list, week_num=-1):
"""Adds a list of games in the correct format to the last week in
the schedule. (or other week specified)"""
self[week_num].add_games(game_list)
def add_result(self, result_list, week_num=-1):
"""Receives a list representing the result of a game, as well as the
week to apply the result to and call's the appropriate week object's
method to set the game result."""
self[week_num].add_result(result_list)
@property
def yaml(self):
"""Returns a dictionary object to be used to create the data structure
that is built up into the final overall YAML structure."""
# Special note here so that I don't forget the bug and why it's not
# 100% fixed. Most of the YAML file is lists which does come back in
# order in YAML 1.1 (and that's what I'm using with pyyaml). However
# for that "YAML readability" aspect, I used a dictionary for each week
# and then each week is labeled. Dictionaries in 1.1 and PyYAML do
# NOT come back ordered. They are printed in alphabetical order of
# keyword. So once week numbers went to 2 digits it was printed
# week_0, week_1, week_10. However there's something in the way it
# parses through the structure once the week is added in the middle
# that breaks everything.
#
# The workaround is to make the game numbers padded 3 digits for a
# maximum schedule of 1000 games (which is definitely more than we'd
# ever do. However the parsing weakness is still in here. I'm not
# sure if the bug is when the 2 digit week structure is read, or when
# games are added, or when it's written.
#
# Final solution would be to rework the YAML data structure to either
# full on YAML, or use ruamel.yaml and YAML 1.2, or go to SQLite or
# something like that.
return {f"week_{idx:03}": week.yaml for (idx, week) in enumerate(self)}
@property
def full_report(self):
"""Returns a string containing a full schedule report (every
week)"""
lines = [f"Number of weeks in the schedule: {len(self)}"]
for week_idx, week in enumerate(self):
lines.append(f"-- Week: {week_idx+1} --{week}")
return "\n".join(lines)
def week_report(self, week_num):
"""Returns a string containing a single week report"""
lines = []
for week_idx, week in enumerate(self):
if week_idx == week_num:
lines.append(f"-- Week: {week_idx+1} --{week}")
return "\n".join(lines)
def __str__(self):
lines = [f"Number of weeks in the schedule: {len(self)}"]
for week_idx, week in enumerate(self):
lines.append(f"-- Week: {week_idx} --{week}")
return "\n".join(lines)
################################################################################
class TourneyFile:
"""Class encapsulates interactions with the YAML file. No one outside
of this class ought to be exposed to THE BLOB."""
def __init__(self, filename):
self.filename = filename
self.league = League()
self.schedule = Schedule()
self.current_week = 0
def read(self):
"""Reading the YAML file and parsing the results. Have to check
to make sure fields are populated before creating the data structures.
"""
with open(self.filename, "r") as f:
blob = yaml.safe_load(f)
# Checking the population of the blob against these keys. The
# list initializer does not like None as an input.
if blob["teams"]:
self.league = League(blob["teams"])
if blob["current_week"]:
self.current_week = blob["current_week"]
if blob["schedule"]:
self.schedule = Schedule(blob["schedule"])
for week_idx, week in enumerate(self.schedule):
if week_idx == self.current_week:
week.current = True
for game in week:
game.add_team_data(self.league)
return self.league, self.schedule, self.current_week
def write(self, blob):
"""Encapsulated YAML writing method."""
with open(self.filename, "w") as f:
yaml.dump(blob, f)
def create(self):
"""Encapsulated YAML initial file state method."""
# blob = {"current_week": 0, "teams": None, "schedule": None}
self.write(self.make_blob)
def add_team(self, team_str):
"""Encapsulated team addition method."""
self.read()
self.league.append(Team.from_str(team_str))
self.write(self.make_blob)
def del_team(self, team_name):
"""Encapsulated team deletion method."""
self.read()
print(f"self.league is {self.league}")
for idx, team in enumerate(self.league):
if team.name == team_name:
del self.league[idx]
self.write(self.make_blob)
break
else:
print(f"Team {team_name} not found!")
def add_week(self):
"""Adds a blank week to the schedule."""
self.read()
self.schedule.add_week()
self.write(self.make_blob)
def add_games(self, game_list):
"""Receives a list of integers in strings. Proceeds to create games
out of this list and add it to the last week in the schedule."""
self.read()
# Need to create a translation from the list of strings of numbers
# to the format the Game object wants.
game_list = list(map(int, game_list))
newlist = []
for idx in range(0, len(game_list), 2):
if idx != len(game_list) - 1:
newlist.append(
{
"home": game_list[idx],
"away": game_list[idx + 1],
"result": {"home": -1, "away": -1},
}
)
else:
newlist.append(
{
"home": game_list[idx],
"away": 9999,
"result": {"home": -1, "away": -1},
}
)
self.schedule.add_games(newlist)
self.write(self.make_blob)
def add_result(self, result_list):
"""Receives a list of integers in strings. Calls the schedule
object to record the result of the game."""
self.read()
result_list = list(map(int, result_list))
self.schedule.add_result(result_list, self.current_week)
self.write(self.make_blob)
def incr_week(self):
"""Method to increment the current week."""
self.read()
if self.current_week < len(self.schedule) - 1:
self.current_week += 1
self.write(self.make_blob)
def decr_week(self):
"""Method to increment the current week."""
self.read()
if self.current_week > 0:
self.current_week -= 1
self.write(self.make_blob)
@property
def make_blob(self):
"""Method that constructs the YAML data block (the "blob") from our
internal data state."""
teams_result = None
if self.league is not None:
teams_result = self.league.yaml
schedule_result = None
if self.schedule is not None:
schedule_result = self.schedule.yaml
return {
"current_week": self.current_week,
"teams": teams_result,
"schedule": schedule_result,
}
def report_teams_long(self):
"""Method to print a report for the team data structures retrieved from the
YAML file."""
self.read()
print(f"Number of teams: {len(self.league)}")
for idx, team in enumerate(self.league):
print(f"-- Team #{idx} ---------------------------")
print(f"Team Name: {team.name}")
print(f"Team Race: {team.race}")
print(f"Coach Name: {team.coach}")
print(f"Coach Discord Tag: {team.dtag}")
def report_teams_short(self):
"""Produces a condensed team name only list of the current teams"""
self.read()
lines = []
for idx, team in enumerate(self.league):
lines.append(f"{idx:2}: Name: {team.name:30} Coach: {team.coach:15} Tag: {team.dtag:10}")
text = "\n".join(lines)
return text
def report_full_schedule(self):
"""Method to print a report of the schedule data retrieved from the YAML
file."""
self.read()
return self.schedule.full_report
def report_current_week(self):
"""Method to print a report of the current week of schedule data
in the YAML file."""
self.read()
return self.schedule.week_report(self.current_week)
################################################################################
def main():
"""Main command line entry point."""
parser = argparse.ArgumentParser(
prog="bb_tournament",
description="Program to manipulate a Blood Bowl 2 tournament data file.",
)
parser.add_argument("filename", help="The tournament data file (YAML format).")
parser.add_argument(
"--create",
action="store_true",
help="Creates a base tournament file (YAML format).",
)
parser.add_argument(
"--add_team",
help='''Adds a team to the tournament. Format should be a comma
separated list inside quotes in the following order: "Team Name, Race,
Coach Name, Coach Discoprd Tag". If the coach is an AI team, use "AI"
for the Coach Name field, and leave off the Discord Tag field.
Example #1 --add_team "Super Joes, Khemri, John Doe, JDoe#9999"
Example #2 --add_team "Doofus Name, Human, AI"''',
)
parser.add_argument(
"--del_team",
help="""Removes a team from the list. Team should be specified by
team name. If there are spaces in the name, please surround the name
with quotes.""",
)
parser.add_argument(
"--add_week",
action="store_true",
help="""Adds a week to the current schedule.""",
)
parser.add_argument(
"--incr_week",
action="store_true",
help="""Increments the current schedule week.""",
)
parser.add_argument(
"--decr_week",
action="store_true",
help="""Decrements the current schedule week.""",
)
parser.add_argument(
"--add_games",
nargs="+",
help="""Adds any number of games to the week. Should be followed by a
list of numbers corresponding to team indexes (as seen by --report
teams). The numbers are interpreted as pairs, home team first,
followed by the away team. If an odd number of teams are added, the
last team in the list is given a 'bye' game for the week. Example #1
--add_game 0 1 2 3 4 5 will produce 3 games in the week with Team 0
playing at home against Team 1 playing away, and so on with pairings,
2 vs 3, and 4 vs 5. Example #2 --add_game 3 2 1 4 5 will produce three
games, 3 vs 2, 1 vs 4, and Team 5 gets a bye.""",
)
parser.add_argument(
"--result",
nargs=3,
help="""Adds a result to a game in the current week. Requires three
numbers following. The first value is the number of the game as
listed in the schedule. The second value is the home team score, and
the third and final value is the away team score. Example: --result
3 1 0 adds a 1-0 result to game 3.""",
)
parser.add_argument(
"--report",
choices=["longteams", "shortteams", "schedule", "full", "current"],
help="Produces the selected report for the tournament.",
)
args = parser.parse_args()
tfile = TourneyFile(args.filename)
if args.create:
tfile.create()
if args.add_team:
tfile.add_team(args.add_team)
if args.del_team:
tfile.del_team(args.del_team)
if args.add_week:
tfile.add_week()
if args.incr_week:
tfile.incr_week()
if args.decr_week:
tfile.decr_week()
if args.add_games:
tfile.add_games(args.add_games)
if args.result:
tfile.add_result(args.result)
if args.report == "longteams" or args.report == "full":
tfile.report_teams_long()
if args.report == "shortteams":
print(tfile.report_teams_short())
if args.report == "schedule" or args.report == "full":
print(tfile.report_full_schedule())
if args.report == "current":
print(tfile.report_current_week())
################################################################################
if __name__ == "__main__":
main()