-
-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy paththe_prestige.py
2870 lines (2441 loc) Β· 137 KB
/
the_prestige.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
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues, datetime, gametext, real_players, archetypes, sys, traceback
import database as db
import onomancer as ono
from league_storage import league_exists, season_save, season_restart, get_mods, get_team_mods, set_mods
from the_draft import Draft
from flask import Flask
from uuid import uuid4
from typing import Optional
from discord import app_commands
import weather
data_dir = "data"
config_filename = os.path.join(data_dir, "config.json")
app = main_controller.app
class newClient(discord.Client):
def __init__(self, *, intents: discord.Intents):
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
await self.tree.sync()
client = newClient(intents = discord.Intents.default())
class Command:
def isauthorized(self, user):
return True
async def execute(self, int, command, flags):
return
async def reply(self, interaction, message, embed=None, ephemeral=False):
await interaction.response.send_message(message, embed=embed, ephemeral=ephemeral)
def process_flags(flag_list):
flags = []
if "-" in flag_list:
check = flag_list.split("-")[1:]
for flag in [_ for _ in check if _ != ""]:
try:
flags.append((flag.split(" ")[0][0].lower(), flag.split(" ",1)[1].strip()))
except IndexError:
flags.append((flag.split(" ")[0][0].lower(), None))
return flags
class DraftError(Exception):
pass
class SlowDraftError(DraftError):
pass
class CommandError(Exception):
pass
class IntroduceCommand(Command):
name = "introduce"
template = ""
description = ""
def isauthorized(self, user):
return user.id in config()["owners"]
async def execute(self, msg, command, flags):
text = """Our avatar was graciously provided to us, with permission, by @HetreaSky on Twitter.
"""
await msg.channel.send(text)
class CountActiveGamesCommand(Command):
name = "countactivegames"
template = ""
description = ""
def isauthorized(self, user):
return user.id in config()["owners"]
async def execute(self, msg, command, flags):
await msg.channel.send(f"There's {len(gamesarray)} active games right now, boss.")
class RomanCommand(Command):
name = "roman"
template = "m;roman [number]"
description = "Converts any natural number less than 4,000,000 into roman numerals. This one is just for fun."
async def execute(self, msg, command, flags):
try:
await msg.channel.send(roman.roman_convert(command))
except ValueError:
raise CommandError(f"\"{command}\" isn't an integer in Arabic numerals.")
class ShowPlayerCommand(Command):
name = "showplayer"
template = "showplayer [name]"
description = "Displays any name's stars in a nice discord embed, there's a limit of 70 characters. That should be *plenty*. Note: if you want to lookup a lot of different players you can do it on onomancer here instead of spamming this command a bunch and clogging up discord: <https://onomancer.sibr.dev/reflect>"
@client.tree.command()
@app_commands.rename(command="name")
async def showplayer(interaction: discord.Interaction, command: str):
"""Show a single player's stats."""
player_name = json.loads(ono.get_stats(command))
await interaction.response.send_message(embed=build_star_embed(player_name))
class StartGameCommand(Command):
name = "startgame"
template = "m;startgame [away] [home] [innings]"
description ="""Starts a game with premade teams made using saveteam, use this command at the top of a list followed by each of these in a new line (shift+enter in discord, or copy+paste from notepad):
- the away team's name.
- the home team's name.
- and finally, optionally, the number of innings, which must be greater than 2 and less than 201. if not included it will default to 9.
- this command has fuzzy search so you don't need to type the full name of the team as long as you give enough to identify the team you're looking for."""
@client.tree.command()
@app_commands.rename(wthr="weather")
@app_commands.choices(wthr=weather.weather_choices())
async def startgame(interaction, away: str, home: str, innings: Optional[int]=9, wthr: Optional[app_commands.Choice[str]] = None, flags: Optional[str] = ""):
"""Start a game with the given teams and, optionally, weather choice and custom inning number."""
league = None
day = None
voice = None
if config()["game_freeze"]:
raise CommandError("Patch incoming. We're not allowing new games right now.")
flags = process_flags(flags)
for flag in flags:
if flag[0] == "l":
league = flag[1]
elif flag[0] == "d":
try:
day = int(flag[1])
except:
raise CommandError("Make sure you put an integer after the -d flag.")
elif flag[0] == "v" or flag[0] == "a":
if flag[1] in gametext.all_voices():
voice = gametext.all_voices()[flag[1]]
else:
raise CommandError("We can't find that broadcaster.")
else:
raise CommandError("One or more of those flags wasn't right. Try and fix that for us and we'll see about sorting you out.")
try:
team_name1 = away
team1 = get_team_fuzzy_search(team_name1)
team_name2 = home
team2 = get_team_fuzzy_search(team_name2)
except IndexError:
raise CommandError("We need at least three lines: startgame, away team, and home team are required. Optionally, the number of innings can go at the end, if you want a change of pace.")
except IndexError:
pass
except ValueError:
raise CommandError("That number of innings isn't even an integer, chief. We can't do fractional innings, nor do we want to.")
if innings is not None and innings < 2 and interaction.user.id not in config()["owners"]:
raise CommandError("Anything less than 2 innings isn't even an outing. Try again.")
elif innings is not None and innings > 200 and interaction.user.id not in config()["owners"]:
raise CommandError("Y'all can behave, so we've upped the limit on game length to 200 innings.")
if team1 is not None and team2 is not None:
game = games.game(team1.finalize(), team2.finalize(), length=innings)
if day is not None:
game.teams['away'].set_pitcher(rotation_slot = day)
game.teams['home'].set_pitcher(rotation_slot = day)
if voice is not None:
game.voice = voice()
channel = interaction.channel
if wthr is not None:
game.weather = weather.all_weathers()[wthr.value](game)
game_task = asyncio.create_task(watch_game(channel, game, user=interaction.user, league=league, interaction=interaction))
await game_task
else:
raise CommandError("We can't find one or both of those teams. Check your staging, chief.")
class StartRandomGameCommand(Command):
name = "randomgame"
template = "randomgame"
description = "Starts a 9-inning game between 2 entirely random teams. Embrace chaos!"
@client.tree.command()
async def randomgame(interaction):
"""Start a game between two random teams."""
if config()["game_freeze"]:
raise CommandError("Patch incoming. We're not allowing new games right now.")
channel = interaction.channel
await interaction.response.send_message("Rolling the bones... This might take a while.")
teamslist = games.get_all_teams()
game = games.game(random.choice(teamslist).finalize(), random.choice(teamslist).finalize())
game_task = asyncio.create_task(watch_game(channel, game, user="the winds of chaos"))
await game_task
class SaveTeamCommand(Command):
name = "saveteam"
template = """m;saveteam [bot ping]
[name]
[slogan]
[lineup]
[rotation]
"""
description = """Saves a team to the database allowing it to be used for games. Send this command at the top of a list, with entries separated by new lines (shift+enter in discord, or copy+paste from notepad).
- the first line of the list is your team's name.
- the second line is the team's icon and slogan, generally this is an emoji followed by a space, followed by a short slogan.
- the third line must be blank.
- the next lines are your batters' names in the order you want them to appear in your lineup, lineups can contain any number of batters between 1 and 12.
- there must be another blank line between your batters and your pitchers.
- the final lines are the names of the pitchers in your rotation, rotations can contain any number of pitchers between 1 and 8.
If you did it correctly, you'll get a team embed with a prompt to confirm. hit the π and your team will be saved!"""
async def execute(self, msg, command, flags):
if db.get_team(command.split('\n',1)[1].split("\n")[0]) == None:
await msg.channel.send(f"Fetching players...")
team = team_from_message(command)
save_task = asyncio.create_task(save_team_confirm(msg, team))
await save_task
else:
name = command.split('\n',1)[1].split('\n')[0]
raise CommandError(f"{name} already exists. Try a new name, maybe?")
@client.tree.command()
async def newteam(interaction):
"""Get new team instructions. Sent privately, don't worry!"""
await interaction.response.send_message(SaveTeamCommand.template + SaveTeamCommand.description , ephemeral=True)
class AssignArchetypeCommand(Command):
name = "archetype"
template = "m;archetype [team name]\n[player name]\n[archetype name]"
description = """Assigns an archetype to a player on your team. This can be changed at any time! For a description of a specific archetype, or a list of all archetypes, use m;archetypehelp."""
@client.tree.command()
@app_commands.choices(archetype=archetypes.archetype_choices())
async def setarchetype(interaction, team: str, player: str, archetype: app_commands.Choice[str]):
"""Assigns an archetype to a player on an owned team. Reversible."""
try:
team = get_team_fuzzy_search(team)
player_name = player
archetype_name = archetype.value
except IndexError:
raise CommandError("You didn't give us enough info, boss. Check the help text.")
if team is None:
raise CommandError("We can't find that team.")
team, ownerid = games.get_team_and_owner(team.name)
if ownerid != interaction.user.id and interaction.user.id not in config()["owners"]:
raise CommandError("That team ain't yours, and we're not about to help you cheat.")
player = team.find_player(player_name)[0]
if player is None:
raise CommandError("That player isn't on your team, boss.")
archetype = archetypes.search_archetypes(archetype_name)
if archetype is None:
raise CommandError("We can't find that archetype, chief. Try m;archetypehelp.")
try:
team.archetypes[player.name] = archetype
except AttributeError:
team.archetypes = {player.name : archetype}
games.update_team(team)
await interaction.response.send_message("Player specialization is a beautiful thing, ain't it? Here's hoping they like it.")
class ArchetypeHelpCommand(Command):
name = "archetypehelp"
template = "archetypehelp [archetype name]"
description = "Describes a given archetype."
@client.tree.command()
@app_commands.choices(archetype=archetypes.archetype_choices())
async def archetypehelp(interaction, archetype: app_commands.Choice[str]):
"""Describes a specific archetype."""
arch = archetypes.search_archetypes(archetype.value)
if arch is None:
raise CommandError("We don't know what that archetype is. If you're trying to break new ground, here isn't the time *or* the place.")
await interaction.response.send_message(f"""{arch.display_name}
Short name: {arch.name}
{arch.description}""")
class ViewArchetypesCommand(Command):
name = "teamarchetypes"
template = "teamarchetypes [team name]"
description = "Lists the current archetypes on the given team."
@client.tree.command()
@app_commands.rename(team_name="team")
async def teamarchetypes(interaction, team_name: str):
"""Lists the current archetypes on a team."""
team = get_team_fuzzy_search(team_name)
if team is None:
raise CommandError("We can't find that team, boss.")
elif team.archetypes == {}:
raise CommandError("That team doesn't have any specializations set.")
embed_string = ""
for player_name, archetype in team.archetypes.items():
embed_string += f"**{player_name}**:\n{archetype.display_name}\n\n"
embed = discord.Embed(color=discord.Color.dark_green(), title=f"{team.name} Archetypes")
embed.add_field(name="-",value=embed_string)
await interaction.reaction.send_message(embed=embed)
class ImportCommand(Command):
name = "import"
template = "m;import [onomancer collection URL]"
description = "Imports an onomancer collection as a new team. You can use the new onomancer simsim setting to ensure compatibility. Similarly to saveteam, you'll get a team embed with a prompt to confirm, hit the π and your team will be saved! Only functions in bot DMs."
async def execute(self, msg, command, flags):
team_raw = ono.get_collection(command.strip())
if not team_raw == None:
team_json = json.loads(team_raw)
if db.get_team(team_json["fullName"]) == None:
team = team_from_collection(team_json)
await asyncio.create_task(save_team_confirm(msg, team))
else:
raise CommandError(f"{team_json['fullName']} already exists. Try a new name, maybe?")
else:
raise CommandError("Something went pear-shaped while we were looking for that collection. You certain it's a valid onomancer URL?")
class ShowTeamCommand(Command):
name = "showteam"
template = "m;showteam [name]"
description = "Shows the lineup, rotation, and slogan of any saved team in a discord embed with primary stat star ratings for all of the players. This command has fuzzy search so you don't need to type the full name of the team as long as you give enough to identify the team you're looking for."
@client.tree.command()
@app_commands.rename(team_name="team")
async def showteam(interaction, team_name: str):
"""Display a team's roster and relevant ratings."""
team = get_team_fuzzy_search(team_name)
if team is not None:
await interaction.response.send_message(embed=build_team_embed(team))
return
raise CommandError("Can't find that team, boss. Typo?")
class ShowAllTeamsCommand(Command):
name = "showallteams"
template = "m;showallteams"
description = "Shows a paginated list of all teams available for games which can be scrolled through."
async def execute(self, msg, command, flags):
list_task = asyncio.create_task(team_pages(msg, games.get_all_teams()))
await list_task
class SearchTeamsCommand(Command):
name = "searchteams"
template = "m;searchteams [searchterm]"
description = "Shows a paginated list of all teams whose names contain the given search term."
async def execute(self, msg, command, flags):
search_term = command.strip()
if len(search_term) > 30:
raise CommandError("Team names can't even be that long, chief. Try something shorter.")
list_task = asyncio.create_task(team_pages(msg, games.search_team(search_term), search_term=search_term))
await list_task
class CreditCommand(Command):
name = "credit"
template = "m;credit"
description = "Shows artist credit for matteo's avatar."
async def execute(self, msg, command, flags):
await msg.channel.send("Our avatar was graciously provided to us, with permission, by @HetreaSky on Twitter.")
@client.tree.command()
async def credit(interaction):
"""Show artist credit for the bot's avatar."""
await interaction.response.send_message("Our avatar was graciously provided to us, with permission, by @HetreaSky on Twitter.")
class SwapPlayerCommand(Command):
name = "swapsection"
template = """m;swapsection
[team name]
[player name]"""
description = "Swaps a player from your lineup to the end of your rotation or your rotation to the end of your lineup. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command, flags):
try:
team_name = command.split("\n")[1].strip()
player_name = command.split("\n")[2].strip()
team, owner_id = games.get_team_and_owner(team_name)
if team is None:
raise CommandError("Can't find that team, boss. Typo?")
elif owner_id != interaction.user.id and interaction.user.id not in config()["owners"]:
raise CommandError("You're not authorized to mess with this team. Sorry, boss.")
elif not team.swap_player(player_name):
raise CommandError("Either we can't find that player, you've got no space on the other side, or they're your last member of that side of the roster. Can't field an empty lineup, and we *do* have rules, chief.")
else:
await msg.channel.send(embed=build_team_embed(team))
games.update_team(team)
await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
except IndexError:
raise CommandError("Three lines, remember? Command, then team, then name.")
@client.tree.command()
@app_commands.rename(team_name="team", player_name="player")
async def swapplayer(interaction, team_name: str, player_name: str):
"""Swaps a player on an owned team between lineup and rotation."""
team, owner_id = games.get_team_and_owner(team_name)
if team is None:
raise CommandError("Can't find that team, boss. Typo?")
elif owner_id != interaction.user.id and interaction.user.id not in config()["owners"]:
raise CommandError("You're not authorized to mess with this team. Sorry, boss.")
elif not team.swap_player(player_name):
raise CommandError("Either we can't find that player, you've got no space on the other side, or they're your last member of that side of the roster. Can't field an empty lineup, and we *do* have rules, chief.")
else:
await interaction.channel.send(embed=build_team_embed(team))
games.update_team(team)
await interaction.response.send_message("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
class MovePlayerCommand(Command):
name = "moveplayer"
template = """m;moveplayer
[team name]
[player name]
[new lineup/rotation position number] (indexed with 1 being the top)"""
description = "Moves a player within your lineup or rotation. If you want to instead move a player from your rotation to your lineup or vice versa, use m;swapsection instead. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command, flags):
try:
team_name = command.split("\n")[1].strip()
player_name = command.split("\n")[2].strip()
except IndexError:
raise CommandError("Four lines, remember? Command, then team, then name, and finally, new spot on the lineup or rotation.")
@client.tree.command()
@app_commands.rename(team_name="team", player_name="player", is_pitcher="type", new_pos="newposition")
@app_commands.choices(is_pitcher=[app_commands.Choice(name="pitcher", value=1), app_commands.Choice(name="batter", value=0)])
async def moveplayer(interaction, team_name: str, player_name: str, is_pitcher: app_commands.Choice[int], new_pos: int):
"""Moves a player to a different position in your lineup or rotation."""
team, owner_id = games.get_team_and_owner(team_name)
if new_pos < 0:
raise CommandError("Hey, quit being cheeky. We're just trying to help. New position has to be a natural number, boss.")
elif owner_id != interaction.user.id and interaction.user.id not in config()["owners"]:
raise CommandError("You're not authorized to mess with this team. Sorry, boss.")
elif team is None:
raise CommandError("We can't find that team, boss. Typo?")
else:
if team.find_player(player_name)[2] is None or len(team.find_player(player_name)[2]) < new_pos:
raise CommandError("You either gave us a number that was bigger than your current roster, or we couldn't find the player on the team. Try again.")
if is_pitcher.value == 0:
roster = team.lineup
else:
roster = team.rotation
if (roster is not None and team.slide_player_spec(player_name, new_pos, roster)) or (roster is None and team.slide_player(player_name, new_pos)):
await interaction.channel.send(embed=build_team_embed(team))
games.update_team(team)
await interaction.response.send_message("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
else:
raise CommandError("You either gave us a number that was bigger than your current roster, or we couldn't find the player on the team. Try again.")
class AddPlayerCommand(Command):
name = "addplayer"
template = """m;addplayer pitcher (or m;addplayer batter)
[team name]
[player name]"""
description = "Adds a new player to the end of your team, either in the lineup or the rotation depending on which version you use. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command, flags):
try:
team_name = command.split("\n")[1].strip()
player_name = command.split("\n")[2].strip()
except IndexError:
raise CommandError("Three lines, remember? Command, then team, then name.")
@client.tree.command()
@app_commands.choices(is_pitcher=[app_commands.Choice(name="pitcher", value=1), app_commands.Choice(name="batter", value=0)])
@app_commands.rename(team_name="team", player_name="player", is_pitcher="type")
async def addplayer(interaction, team_name: str, player_name: str, is_pitcher: app_commands.Choice[int]):
"""Adds a new player to your team."""
if len(player_name) > 70:
raise CommandError("70 characters per player, boss. Quit being sneaky.")
team, owner_id = games.get_team_and_owner(team_name)
if team is None:
raise CommandError("We can't find that team, boss. Typo?")
if owner_id != interaction.user.id and interaction.user.id not in config()["owners"]:
raise CommandError("You're not authorized to mess with this team. Sorry, boss.")
new_player = games.player(ono.get_stats(player_name))
if is_pitcher.value == 0:
if not team.add_lineup(new_player)[0]:
raise CommandError("Too many batters πΆ")
else:
if not team.add_pitcher(new_player):
raise CommandError("8 pitchers is quite enough, we think.")
await interaction.channel.send(embed=build_team_embed(team))
games.update_team(team)
await interaction.response.send_message("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
class RemovePlayerCommand(Command):
name = "removeplayer"
template = """m;removeplayer
[team name]
[player name]"""
description = "Removes a player from your team. If there are multiple copies of the same player on a team this will only delete the first one. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command, flags):
try:
team_name = command.split("\n")[1].strip()
player_name = command.split("\n")[2].strip()
except IndexError:
raise CommandError("Three lines, remember? Command, then team, then name.")
@client.tree.command()
@app_commands.rename(team_name="team", player_name="player")
async def removeplayer(interaction, team_name: str, player_name: str):
"""Removes a player from your team."""
team, owner_id = games.get_team_and_owner(team_name)
if owner_id != interaction.user.id and interaction.user.id not in config()["owners"]:
raise CommandError("You're not authorized to mess with this team. Sorry, boss.")
elif team is None:
raise CommandError("Can't find that team, boss. Typo?")
if not team.delete_player(player_name):
raise CommandError("We've got bad news: that player isn't on your team. The good news is that... that player isn't on your team?")
else:
await interaction.channel.send(embed=build_team_embed(team))
games.update_team(team)
await interaction.response.send_message("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
class ReplacePlayerCommand(Command):
name = "replaceplayer"
template = """m;replaceplayer
[team name]
[player name to **remove**]
[player name to **add**]"""
description = "Replaces a player on your team. If there are multiple copies of the same player on a team this will only replace the first one. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command, flags):
try:
team_name = command.split("\n")[1].strip()
remove_name = command.split("\n")[2].strip()
add_name = command.split("\n")[3].strip()
if len(add_name) > 70:
raise CommandError("70 characters per player, boss. Quit being sneaky.")
team, owner_id = games.get_team_and_owner(team_name)
if owner_id != msg.author.id and msg.author.id not in config()["owners"]:
raise CommandError("You're not authorized to mess with this team. Sorry, boss.")
old_player, old_pos, old_list = team.find_player(remove_name)
new_player = games.player(ono.get_stats(add_name))
if old_player is None:
raise CommandError("We've got bad news: that player isn't on your team. The good news is that... that player isn't on your team?")
else:
if old_list == team.lineup:
team.delete_player(remove_name)
team.add_lineup(new_player)
team.slide_player(add_name, old_pos+1)
else:
team.delete_player(remove_name)
team.add_pitcher(new_player)
team.slide_player(add_name, old_pos+1)
await msg.channel.send(embed=build_team_embed(team))
games.update_team(team)
await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
except IndexError:
raise CommandError("Four lines, remember? Command, then team, then the two names.")
class HelpCommand(Command):
name = "help"
template = "m;help [command]"
description = "Shows the instructions from the readme for a given command. If no command is provided, we will instead provide a list of all of the commands that instructions can be provided for."
async def execute(self, msg, command, flags):
query = command.strip()
if query == "":
text = "Here's everything we know how to do; use `m;help [command]` for more info:"
for comm in commands:
if comm.isauthorized(msg.author):
text += f"\n - {comm.name}"
else:
try:
comm = next(c for c in commands if c.name == query and c.isauthorized(msg.author))
text = f"`{comm.template}`\n{comm.description}"
except:
text = "Can't find that command, boss; try checking the list with `m;help`."
await msg.channel.send(text)
class DeleteTeamCommand(Command):
name = "deleteteam"
template = "m;deleteteam [name]"
description = "Allows you to delete the team with the provided name. You'll get an embed with a confirmation to prevent accidental deletions. Hit the π and your team will be deleted.. Requires team ownership. If you are the owner and the bot is telling you it's not yours, contact xvi and xie can assist."
@client.tree.command()
@app_commands.rename(team_name="team")
async def deleteteam(interaction, team_name: str):
"""Deletes a team. Requires confirmation."""
team, owner_id = games.get_team_and_owner(team_name)
if owner_id != interaction.user.id and interaction.user.id not in config()["owners"]: #returns if person is not owner and not bot mod
raise CommandError("That team ain't yours, chief. If you think that's not right, bug xvi about deleting it for you.")
elif team is not None:
delete_task = asyncio.create_task(team_delete_confirm(interaction.channel, team, interaction.user))
await delete_task
class AssignOwnerCommand(Command):
name = "assignowner"
template = "m;assignowner [mention] [team]"
description = "assigns a discord user as the owner for a team."
def isauthorized(self, user):
return user.id in config()["owners"]
async def execute(self, msg, command, flags):
if self.isauthorized(msg.author):
new_owner = msg.mentions[0]
team_name = command.strip().split("> ",1)[1]
if db.assign_owner(team_name, new_owner.id):
await msg.channel.send(f"{team_name} is now owned by {new_owner.display_name}. Don't break it.")
else:
raise CommandError("We couldn't find that team. Typo?")
class StartTournamentCommand(Command):
name = "starttournament"
template = """m;starttournament [bot ping]
[tournament name]
[list of teams, each on a new line]
"""
description = "Starts a randomly seeded tournament with the provided teams, automatically adding byes as necessary. All series have a 5 minute break between games and by default there is a 10 minute break between rounds. The current tournament format is:\nBest of 5 until the finals, which are Best of 7."
async def execute(self, msg, command, flags):
round_delay = 10
series_length = 5
finals_series_length = 7
rand_seed = True
pre_seeded = False
list_of_team_names = command.split("\n")[2:]
if config()["game_freeze"]:
raise CommandError("Patch incoming. We're not allowing new games right now.")
for flag in flags:
if flag[0] == "r": #rounddelay
try:
round_delay = int(flag[1])
except ValueError:
raise CommandError("The delay between rounds should be a whole number.")
if round_delay < 1 or round_delay > 120:
raise CommandError("The delay between rounds has to bebetween 1 and 120 minutes.")
elif flag[0] == "b": #bestof
try:
series_length = int(flag[1])
if series_length % 2 == 0 or series_length < 0:
raise ValueError
except ValueError:
raise CommandError("Series length has to be an odd positive integer.")
if msg.author.id not in config()["owners"] and series_length > 21:
raise CommandError("That's too long, boss. We have to run patches *some* time.")
if len(list_of_team_names) == 2:
raise CommandError("--bestof is only for non-finals matches! You probably want --finalsbestof, boss. -f works too, if you want to pay respects.")
elif flag[0] == "f": #pay respects (finalsbestof)
try:
finals_series_length = int(flag[1])
if finals_series_length % 2 == 0 or finals_series_length < 0:
raise ValueError
except ValueError:
raise CommandError("Finals series length has to be an odd positive integer.")
if msg.author.id not in config()["owners"] and finals_series_length > 21:
raise CommandError("That's too long, boss. We have to run patches *some* time.")
elif flag[0] == "s": #seeding
if flag[1] == "stars":
rand_seed = False
elif flag[1] == "given":
rand_seed = False
pre_seeded = True
elif flag[1] == "random":
pass
else:
raise CommandError("Valid seeding types are: 'random' (default), 'stars', and 'given'.")
else:
raise CommandError("One or more of those flags wasn't right. Try and fix that for us and we'll see about sorting you out.")
tourney_name = command.split("\n")[1]
team_dic = {}
for name in list_of_team_names:
team = get_team_fuzzy_search(name.strip())
if team == None:
raise CommandError(f"We couldn't find {name}. Try again?")
add = True
for extant_team in team_dic.keys():
if extant_team.name == team.name:
add = False
if add:
team_dic[team] = {"wins": 0}
channel = msg.channel
if len(team_dic) < 2:
await msg.channel.send("One team does not a tournament make.")
return
tourney = leagues.tournament(tourney_name, team_dic, series_length = series_length, finals_series_length = finals_series_length, secs_between_rounds = round_delay * 60)
tourney.build_bracket(random_sort = rand_seed)
await start_tournament_round(channel, tourney)
@client.tree.command()
async def starttournament(interaction):
"""Get tournament instructions. Sent privately, don't worry!"""
await interaction.response.send_message(StartTournamentCommand.template + StartTournamentCommand.description , ephemeral=True)
class DraftPlayerCommand(Command):
name = "draft"
template = "m;draft [playername]"
description = "On your turn during a draft, use this command to pick your player."
async def execute(self, msg, command, flags):
"""
This is a no-op definition. `StartDraftCommand` handles the orchestration directly,
this is just here to provide a help entry and so the command dispatcher recognizes it
as valid.
"""
pass
class DraftFlagsCommand(Command):
name = "draftflags"
template = "m;draftflags"
description = "Shows all currently accepted flags for the startdraft command."
async def execute(self, msg, command, flags):
text = """Currently accepted flags:
--draftsize or -d: Sets the size of each draft pool.
--refresh or -r: Sets the size at which the pool completely refreshes.
--teamsize or -t: How big each team should be, including pitchers.
--pitchercount or -p: How many pitchers each team should have.
--wait or -w: Sets the timeout, in seconds, to wait for draftees to pick a player.
--chaos or -c: The percentage of onomancer names in the pool. Higher numbers mean less real names, but faster pool generation. Accepts any number between 0 and 1.
"""
await msg.channel.send(text)
class StartDraftCommand(Command):
name = "startdraft"
template = "m;startdraft\n[mention]\n[teamname]\n[slogan]"
description = """Starts a draft with an arbitrary number of participants. Send this command at the top of the list with each mention, teamname, and slogan on their own lines (shift+enter in discord).
- The draft will proceed in the order that participants were entered.
- 20 players will be available for draft at a time, and the pool will refresh automatically when it becomes small.
- Each participant will be asked to draft 12 hitters then finally one pitcher.
- The draft will start only once every participant has given a π to begin.
- use the command `d`, `draft`, or `m;draft` on your turn to draft someone
"""
async def execute(self, msg, command, flags):
teamsize = 13
draftsize = 20
minsize = 4
pitchers = 3
ono_ratio = 0.5
timeout = 120
for flag in flags:
try:
if flag[0] == "t":
teamsize = int(flag[1])
elif flag[0] == "d":
draftsize = int(flag[1])
elif flag[0] == "r": #refreshsize, default 4
minsize = int(flag[1])
elif flag[0] == "p":
pitchers = int(flag[1])
elif flag[0] == "c":
ono_ratio = float(flag[1])
if ono_ratio > 1 or ono_ratio < 0:
raise CommandError("The Chaos value needs to be between 0 and 1, chief. Probability has rules.")
elif flag[0] == "w": #wait
timeout = int(flag[1])
else:
raise CommandError(f"We don't recognize that {flag[0]} flag.")
except ValueError:
raise CommandError(f"Your {flag[0]} flag isn't a valid nummber, boss.")
if teamsize-pitchers > 20 or pitchers > 8:
raise CommandError("You can't fit that many players on a team, chief. Slow your roll.")
if teamsize < 3 or pitchers < 1 or draftsize < 5 or minsize < 2:
raise CommandError("One of those numbers is too low. Draft size has to be at least 5, the rest should be obvious.")
if draftsize > 40:
raise CommandError("40 players is the max. We're not too confident about pushing for more.")
await msg.channel.send("Got it, boss. Give me a sec to find all the paperwork.")
try:
draft = Draft.make_draft(teamsize, draftsize, minsize, pitchers, ono_ratio)
except ConnectionError:
await msg.channel.send("Baseball Reference isn't playing nice. Could you try again for us in a minute or so?")
mentions = {f'<@!{m.id}>' for m in msg.mentions}
content = msg.content.split('\n')[1:] # drop command out of message
if not content or len(content) % 3:
await msg.channel.send('Invalid list')
raise ValueError('Invalid length')
for i in range(0, len(content), 3):
handle_token = content[i].strip()
for mention in mentions:
if mention in handle_token:
handle = mention
break
else:
await msg.channel.send(f"I don't recognize {handle_token}.")
return
team_name = content[i + 1].strip()
if games.get_team(team_name):
await msg.channel.send(f'Sorry {handle}, {team_name} already exists')
return
slogan = content[i + 2].strip()
draft.add_participant(handle, team_name, slogan)
success = await self.wait_start(msg.channel, mentions)
if not success:
return
draft.start_draft()
footer = f"The draft class of {random.randint(2007, 2075)}"
while draft.round <= draft.DRAFT_ROUNDS:
message_prefix = f'Round {draft.round}/{draft.DRAFT_ROUNDS}:'
if draft.round == draft.DRAFT_ROUNDS:
body = random.choice([
f"Now just choose a pitcher and we can finish off this paperwork for you, {draft.active_drafter}",
f"Pick a pitcher, {draft.active_drafter}, and we can all go home happy. 'Cept your players. They'll have to play baseball.",
f"Almost done, {draft.active_drafter}. Pick your pitcher.",
])
message = f"βΎοΈ {message_prefix} {body}"
elif draft.round <= draft.DRAFT_ROUNDS - draft.pitchers:
body = random.choice([
f"Choose a batter, {draft.active_drafter}.",
f"{draft.active_drafter}, your turn. Pick one.",
f"Pick one to fill your next lineup slot, {draft.active_drafter}.",
f"Alright, {draft.active_drafter}, choose a batter.",
])
message = f"π {message_prefix} {body}"
else:
body = random.choice([
f"Warning: Pitcher Zone. Enter if you dare, {draft.active_drafter}.",
f"Time to pitch a picker, {draft.active_drafter}.\nWait, that doesn't sound right.",
f"Choose a yeeter, {draft.active_drafter}.\nDid we use that word right?",
f"Choose a pitcher, {draft.active_drafter}."])
message = f"βΎοΈ {message_prefix} {body}"
await msg.channel.send(
message,
embed=build_draft_embed(draft.get_draftees(), footer=footer),
)
try:
draft_message = await self.wait_draft(msg.channel, draft, timeout)
draft.draft_player(f'<@!{draft_message.author.id}>', draft_message.content.split(' ', 1)[1])
except SlowDraftError:
player = random.choice(draft.get_draftees())
await msg.channel.send(f"I'm not waiting forever. You get {player}. Next.")
draft.draft_player(draft.active_drafter, player)
except ValueError as e:
await msg.channel.send(str(e))
except IndexError:
await msg.channel.send("Quit the funny business.")
for handle, team in draft.get_teams():
await msg.channel.send(
random.choice([
f"Done and dusted, {handle}. Here's your squad.",
f"Behold the {team.name}, {handle}. Flawless, we think.",
f"Oh, huh. Interesting stat distribution. Good luck, {handle}.",
]),
embed=build_team_embed(team),
)
try:
draft.finish_draft()
except Exception as e:
await msg.channel.send(str(e))
async def wait_start(self, channel, mentions):
start_msg = await channel.send("Sound off, folks. π if you're good to go " + " ".join(mentions))
await start_msg.add_reaction("π")
await start_msg.add_reaction("π")
def react_check(react, user):
return f'<@!{user.id}>' in mentions and react.message == start_msg
while True:
try:
react, _ = await client.wait_for('reaction_add', timeout=60.0, check=react_check)
if react.emoji == "π":
await channel.send("We dragged out the photocopier for this! Fine, putting it back.")
return False
if react.emoji == "π":
reactors = set()
async for user in react.users():
reactors.add(f'<@!{user.id}>')
if reactors.intersection(mentions) == mentions:
return True
except asyncio.TimeoutError:
await channel.send("Y'all aren't ready.")
return False
return False
async def wait_draft(self, channel, draft, timeout):
def check(m):
if m.channel != channel:
return False
if m.content.startswith('d ') or m.content.startswith('draft '):
return True
for prefix in config()['prefix']:
if m.content.startswith(prefix + 'draft '):
return True
return False
try:
draft_message = await client.wait_for('message', timeout=timeout, check=check)
except asyncio.TimeoutError:
raise SlowDraftError('Too slow, boss.')
return draft_message
class StartLeagueCommand(Command):
name = "startleague"
template = "m;startleague [league name]\n[games per hour]"
description = """Optional flags for the first line: `--queue X` or `-q X` to play X number of series before stopping; `--autopostseason` will automatically start postseason at the end of the season.
Starts games from a league with a given name, provided that league has been saved on the website and has been claimed using claimleague. The games per hour sets how often the games will start (e.g. GPH 2 will start games at X:00 and X:30). By default it will play the entire season followed by the postseason and then stop but this can be customized using the flags.
Not every team will play every series, due to how the scheduling algorithm is coded but it will all even out by the end."""
@client.tree.command()
@app_commands.rename(league_name="leaguename", autoplay="queue", postseason_mode="postseasonmode")
@app_commands.describe(gph="Games per hour to play.")
@app_commands.choices(postseason_mode=[app_commands.Choice(name="pause", value=0), app_commands.Choice(name="auto", value=1), app_commands.Choice(name="skip", value=2)])
async def startleague(interaction, league_name: str, gph: int, autoplay: Optional[int]=None, postseason_mode: Optional[app_commands.Choice[int]]=0):
"""Starts up a league previously formed on the site."""
autopost = False
nopost = False
if config()["game_freeze"]:
raise CommandError("Patch incoming. We're not allowing new games right now.")
if autoplay is not None:
if autoplay <= 0:
raise CommandError("Sorry boss, the queue flag needs a natural number. Any whole number over 0 will do just fine.")
else:
autoplay = -1
if postseason_mode == 0:
postseason_mode = app_commands.Choice(name="pause", value=0)
if postseason_mode is None or postseason_mode.value == 0: #noautopostseason
await interaction.response.send_message("We'll pause the games before postseason starts, when we get there.")
elif postseason_mode.value == 1: #autopostseason
await interaction.response.send_message("We'll automatically start postseason for you, when we get there.")
autopost = True
elif postseason_mode.value == 2: #skippostseason
await interaction.response.send_message("We'll **skip postseason** for you! Make sure you wanted to do this.")
autopost = True
nopost = True
if gph < 1 or gph > 12:
raise CommandError("Chief, we need a games per hour number between 1 and 12. We think that's reasonable.")
if league_exists(league_name):
league = leagues.load_league_file(league_name)
if autoplay == -1 and not autopost:
autoplay = int(list(league.schedule.keys())[-1]) - league.day_to_series_num(league.day) + 1
if nopost:
league.postseason = False
if league.historic:
raise CommandError("That league is done and dusted, chief. Sorry.")
for active_league in active_leagues:
if active_league.name == league.name:
raise CommandError("That league is already running, boss. Patience is a virtue, you know.")
if (league.owner is not None and interaction.user.id in league.owner) or interaction.user.id in config()["owners"] or league.owner is None:
league.autoplay = autoplay
league.games_per_hour = gph
if str(league.day_to_series_num(league.day)) not in league.schedule.keys():
await league_postseason(interaction.channel, league)
elif league.day % league.series_length == 1:
await start_league_day(interaction.channel, league)
else:
await start_league_day(interaction.channel, league, partial = True)
else:
raise CommandError("You don't have permission to manage that league.")
else:
raise CommandError("Couldn't find that league, boss. Did you save it on the website?")
class LeagueSetPlayerModifiersCommand(Command):