forked from Taylor2ya/Stoneman
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Bot.py
1996 lines (1707 loc) · 78.7 KB
/
Bot.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
from discord.ext import commands, tasks
import random
import asyncio
import os
import json
import traceback
from dotenv import load_dotenv
from collections import defaultdict, deque
from datetime import datetime, timedelta
import logging
from functools import wraps
# Load environment variables from .env file
load_dotenv()
# Existing command cooldown and spam control
user_cooldowns = defaultdict(dict)
team_sabotaged = defaultdict(lambda: False)
# Get paths and IDs from environment variables
GIF_PATHS = os.getenv('GIF_PATHS').split(',')
GAME_CHANNEL_ID = int(os.getenv('GAME_CHANNEL_ID'))
CHANNEL_ID = int(os.getenv('CHANNEL_ID'))
DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN')
CATEGORY_ID = int(os.getenv('CATEGORY_ID'))
CAPTAIN_COMMANDS_CHANNEL_ID = int(os.getenv('CAPTAIN_COMMANDS_CHANNEL_ID'))
TAUNT_INTERVAL_SECONDS = 14400 # 4 hours
NUMBER_OF_TEAMS = 8
logging.basicConfig(level=logging.DEBUG)
intents = discord.Intents.default()
intents.message_content = True
intents.members = True # Enable the members intent
# Print GIF paths for debugging
print("GIF paths loaded from .env:")
for path in GIF_PATHS:
print(path, "exists:", os.path.isfile(path))
# Initialize bot with command prefix and intents
bot = commands.Bot(command_prefix='!', intents=intents)
team_sabotages = defaultdict(int) # Track the number of sabotages per team
# Variables to track teams and game state
number_of_teams = 0
teams_set = False
game_started = False # Track if the game has started
# Initialize bot with command prefix and intents, set case_insensitive to True
bot = commands.Bot(command_prefix='!', intents=intents, case_insensitive=True)
# Data storage (consider using a database for persistence)
team_data = {}
captains = {}
team_positions = defaultdict(lambda: 1)
completed_tiles = defaultdict(set)
tile_completions = defaultdict(list)
player_completions = defaultdict(int)
team_members = defaultdict(set)
hard_stop_tiles = [1, 5, 15, 23, 32, 40, 46, 52, 59, 63, 64, 65, 66, 67, 68, 69]
team_has_rolled = defaultdict(lambda: False)
bonus_tile_completions = defaultdict(dict)
team_advantages = defaultdict(lambda: None)
bonus_choices = defaultdict(dict)
rolls_count = defaultdict(int)
tile_completion_times = defaultdict(list)
tile_start_times = {}
teams_needing_bonus_choice = defaultdict(lambda: None)
tiles_revealed = set()
team_gp_bonus = defaultdict(int)
# List of hard stop messages
hard_stop_messages = [
"You go to move {roll} tiles but a magical force stops you at tile {tile}. Complete this tile to roll again.",
"You move {roll} tiles but soup's guide did not prepare you for tile {tile}. Complete this tile to proceed.",
"Your progress is halted at tile {tile}. Complete this tile before continuing your journey.",
"You attempt to move {roll} tiles but must stop at tile {tile} and finish its task to move on.",
"You are stopped at tile {tile}. You have a funny feeling you must complete this tile before continuing.",
]
# Dictionary for team roles
team_roles = {
'team1': 1192366243324366891, # Replace with actual role ID for Team 1
'team2': 1192366338186940527, # Replace with actual role ID for Team 2
'team3': 1192366365965815848, # Replace with actual role ID for Team 3
'team4': 1192366394155745291, # Replace with actual role ID for Team 4
'team5': 1251364935053738074, # Replace with actual role ID for Team 5
'team6': 1251720714050998273, # Replace with actual role ID for Team 6
'team7': 1251720752235937802, # Replace with actual role ID for Team 7
'team8': 1251720795152060497 # Replace with actual role ID for Team 8
}
team_symbols = {
'team1': '1️⃣',
'team2': '2️⃣',
'team3': '3️⃣',
'team4': '4️⃣',
'team5': '5️⃣',
'team6': '6️⃣',
'team7': '7️⃣',
'team8': '8️⃣'
}
# Tasks for Monkey's Paw
monkey_paw_tasks = {
1: " .",
2: " .",
3: " .",
4: " .",
5: " .",
6: " .",
7: " .",
8: " .",
9: " .",
10: " .",
11: " .",
12: " .",
13: " .",
14: " .",
15: " .",
16: " .",
17: " .",
18: " .",
19: " .",
20: " ."
}
# Dictionary for tile tasks
tile_tasks = {
1: 'task',
2: 'task',
3: 'task',
4: 'task',
5: 'task',
6: 'task',
7: 'task',
8: 'task',
9: 'task',
10: 'task',
11: 'task',
12: 'task',
13: 'task',
14: 'task',
15: 'task',
16: 'task',
17: 'task',
18: 'task',
19: 'task',
20: 'task',
21: 'task',
22: 'task',
23: 'task',
24: 'task',
25: 'task',
26: 'task',
27: 'task',
28: 'task',
29: 'task',
30: 'task',
31: 'task',
32: 'task',
33: 'task',
34: 'task',
35: 'task',
36: 'task',
37: 'task',
38: 'task',
39: 'task',
40: 'task',
41: 'task',
42: 'task',
43: 'task',
44: 'task',
45: 'task',
46: 'task',
47: 'task',
48: 'task',
49: 'task',
50: 'task',
51: 'task',
52: 'task',
53: 'task',
54: 'task',
55: 'task',
56: 'task',
57: 'task',
58: 'task',
59: 'task',
60: 'task',
61: 'task',
62: 'task',
63: 'task',
64: 'task',
65: 'task',
66: 'task',
67: 'task',
68: 'task',
69: 'task',
}
# Tasks for bonus tiles
bonus_tasks = {
"1": "Obtain a Pet",
"2": "Obtain a Jar",
"3": "Obtain a Cox or Tob Kit",
"4": "Obtain a Cox or Tob Dust"
}
# Default message for undefined tasks
default_task_message = "No task defined for this tile yet."
# Predefined taunts
taunts = [
"Hey {team}, are you taking a nap? Complete your tile already!",
"The game's waiting on you, {team}! Finish up and let's move!",
"Are you still with us, {team}? Your tile is calling!",
"Come on, {team}, it's not rocket science! Complete your tile!",
"Earth to {team}! Complete your tile!",
"Tick-tock, {team}! Time's wasting, get it done!",
"Don't keep us in suspense, {team}! Complete your tile!",
"Have you forgotten your tile, {team}? Get your act together!",
"Move it, {team}! We're all waiting, hurry up!",
"What's the hold-up, {team}? The game won't play itself, get moving!",
"Speed it up, {team}! We haven't got all day!",
"Get your head in the game, {team}! Complete your tile!",
"Did you fall asleep, {team}? Your tile's not gonna complete itself!",
"Quit stalling, {team}! The game awaits, hurry up!",
"Let's go, {team}! Time to complete that tile!",
# More taunts...
]
# Predefined responses for trying to roll before finishing the tile
incomplete_tile_responses = [
"Nice try, {team}, but you need to complete your tile before rolling! Get with it!",
"Patience, {team}! Finish your task before rolling the dice. Seriously.",
"You can't roll yet, {team}. Complete the tile first! Don't get ahead of yourself.",
"Hold up, {team}! You haven't finished your task yet. What's the rush?",
"Not so fast, {team}! Complete your tile before rolling! We're all waiting!",
"No shortcuts, {team}! Finish your tile task first. We see you trying!",
"Did you forget something, {team}? Complete your tile first! It's not that hard.",
"Whoa there, {team}! You need to finish your tile before rolling. Keep up!",
"Complete your task, {team}, then you can roll! Don't jump the gun.",
"Oops, {team}! Finish the tile before rolling the dice. What's the holdup?",
# More responses...
]
# New responses for no tiles completed
no_tiles_completed_responses = [
"We have a slacker here. Get back to work and help your team!",
"Zero tiles completed? Seriously? Get your act together!",
"Hey, what are you waiting for? Finish a tile already!",
"Not a single tile? Stop messing around and do something!",
"Do you even know how to play? Get to work!",
"Are you kidding me? Zero tiles? Quit slacking off!",
"What's the holdup? Get off your butt and complete a tile!",
"No tiles done? Pathetic. Get to work!",
"This isn't a vacation! Start completing tiles!",
"Wow, zero tiles? Move and help your team!",
"Not even one tile? Get it together and do your part!",
"Stop being useless and complete a tile!",
"What's wrong? Can't complete a single tile? Get to it!",
"No tiles completed? Embarrassing. Get to work!",
"Zero tiles? Quit being a dead weight and help your team!",
# More responses...
]
# Responses for landing on a hard stop tile
hard_stop_responses = [
"You cannot move past tile {stop_tile} until you complete its task.",
"You have a funny feeling that you must complete tile {stop_tile} before continuing.",
"Roses are red, your hit-splats are blue, finish tile {stop_tile} and then you can continue.",
"You feel drawn to stop on tile {stop_tile}. Finish its task before rolling again.",
"You cannot proceed beyond tile {stop_tile} until its task is done.",
"A mystical force compels you to finish tile {stop_tile} before moving on.",
"Tile {stop_tile} is your current quest. Complete it before advancing.",
"The path ahead is blocked by tile {stop_tile}'s challenge. Overcome it to proceed.",
"Your journey halts at tile {stop_tile}. Complete its task to continue your adventure.",
"Tile {stop_tile} holds you captive. Conquer its challenge to roll again."
]
# Track user messages for spam control
user_messages = defaultdict(deque)
user_timeouts = {}
SPAM_LIMIT = 5 # Number of messages
SPAM_TIME = 10 # Time window in seconds
TIMEOUT_DURATION = 300 # Timeout duration in seconds (5 minutes)
async def handle_spam(ctx):
user_id = ctx.author.id
now = datetime.utcnow()
if user_id in user_cooldowns:
if "spam" in user_cooldowns[user_id]:
user_cooldowns[user_id]["spam"].append(now)
user_cooldowns[user_id]["spam"] = [t for t in user_cooldowns[user_id]["spam"] if now - t < timedelta(seconds=10)]
if len(user_cooldowns[user_id]["spam"]) > 5:
user_cooldowns[user_id]["global"] = now + timedelta(minutes=5)
return True
else:
user_cooldowns[user_id]["spam"] = [now]
else:
user_cooldowns[user_id]["spam"] = [now]
return False
async def check_timeout(ctx):
user_id = ctx.author.id
if user_id in user_cooldowns:
if "global" in user_cooldowns[user_id]:
timeout = user_cooldowns[user_id]["global"]
if datetime.utcnow() < timeout:
return True
return False
async def sync_team_members():
"""Synchronize team members based on existing roles in the guild."""
await bot.wait_until_ready()
guild = bot.get_guild(GAME_CHANNEL_ID)
if not guild:
logging.error(f"Guild with ID {GAME_CHANNEL_ID} not found.")
return
for team, role_id in team_roles.items():
role = discord.utils.get(guild.roles, id=role_id)
if role:
team_members[team] = set(member.id for member in role.members)
else:
logging.warning(f"Role with ID {role_id} not found in guild.")
logging.debug("Team members synchronized based on existing roles.")
def is_captain(ctx):
"""Check if the user has the Team Captain role."""
captain_role = discord.utils.get(ctx.guild.roles, name="Team Captain")
return captain_role in ctx.author.roles
def resolve_team_identifier(identifier):
"""Resolve the team identifier to the actual team key used in the data structures."""
identifier = identifier.lower().strip()
if identifier in team_data:
return identifier
for team, name in team_data.items():
if name.lower().strip() == identifier:
return team
return None
def in_game_channel(ctx):
"""Check if the command was issued in the game channel."""
return ctx.channel.id == CHANNEL_ID
def in_designated_category(ctx):
"""Check if the command was issued in the designated category."""
return ctx.channel.category_id == CATEGORY_ID
def in_captain_commands_channel(ctx):
"""Check if the command was issued in the captain command channel."""
return ctx.channel.id == CAPTAIN_COMMANDS_CHANNEL_ID
def save_state():
logging.debug("Entered save_state function")
state = {
"team_data": team_data,
"captains": captains,
"team_positions": {k: v for k, v in team_positions.items()},
"completed_tiles": {k: list(v) for k, v in completed_tiles.items()},
"tile_completions": {k: v for k, v in tile_completions.items()},
"player_completions": player_completions,
"team_members": {k: list(v) for k, v in team_members.items()},
"team_has_rolled": {k: v for k, v in team_has_rolled.items()},
"bonus_tile_completions": {k: {str(kk): vv for kk, vv in v.items()} for k, v in bonus_tile_completions.items()},
"team_advantages": {k: v for k, v in team_advantages.items()},
"bonus_choices": {k: {str(kk): vv for kk, vv in v.items()} for k, v in bonus_choices.items()},
"rolls_count": {k: v for k, v in rolls_count.items()},
"tile_completion_times": {k: [(tile, time.total_seconds()) for tile, time in v] for k, v in tile_completion_times.items()},
"teams_set": teams_set,
"game_started": game_started,
"tile_start_times": {k: v.isoformat() for k, v in tile_start_times.items()},
"teams_needing_bonus_choice": {k: v for k, v in teams_needing_bonus_choice.items()},
"team_gp_bonus": dict(team_gp_bonus),
"team_sabotaged": dict(team_sabotaged),
"tiles_revealed": list(tiles_revealed)
}
logging.debug(f"State to be saved: {state}")
logging.debug(f"Bonus Tile Completions to be saved: {bonus_tile_completions}")
with open("state.json", "w") as f:
json.dump(state, f)
logging.debug("Exited save_state function")
def load_state():
global team_data, captains, team_positions, completed_tiles, tile_completions, player_completions, team_members
global team_has_rolled, bonus_tile_completions, team_advantages, bonus_choices, rolls_count, tile_completion_times
global teams_set, number_of_teams, game_started, tile_start_times, teams_needing_bonus_choice, team_gp_bonus, team_sabotaged
global tiles_revealed
try:
with open("state.json", "r") as f:
state = json.load(f)
team_data = state.get("team_data", {})
captains = state.get("captains", {})
team_positions = defaultdict(lambda: 1, state.get("team_positions", {}))
completed_tiles = defaultdict(set, {k: set(v) for k, v in state.get("completed_tiles", {}).items()})
tile_completions = defaultdict(list, {k: v for k, v in state.get("tile_completions", {}).items()})
player_completions = state.get("player_completions", {})
team_members = defaultdict(set, {k: set(v) for k, v in state.get("team_members", {}).items()})
team_has_rolled = defaultdict(lambda: False, state.get("team_has_rolled", {}))
bonus_tile_completions = defaultdict(dict, {k: v for k, v in state.get("bonus_tile_completions", {}).items()})
team_advantages = defaultdict(lambda: None, state.get("team_advantages", {}))
bonus_choices = defaultdict(dict, {k: v for k, v in state.get("bonus_choices", {}).items()})
rolls_count = defaultdict(int, state.get("rolls_count", {}))
tile_completion_times = defaultdict(list, {k: [(tile, timedelta(seconds=time)) for tile, time in v] for k, v in state.get("tile_completion_times", {}).items()})
tile_start_times = {k: datetime.fromisoformat(v) for k, v in state.get("tile_start_times", {}).items()}
teams_set = state.get("teams_set", False)
game_started = state.get("game_started", False)
teams_needing_bonus_choice = defaultdict(lambda: None, state.get("teams_needing_bonus_choice", {}))
team_gp_bonus = defaultdict(int, state.get("team_gp_bonus", {}))
team_sabotaged = defaultdict(lambda: False, state.get("team_sabotaged", {}))
number_of_teams = len(team_data)
tiles_revealed = set(state.get("tiles_revealed"))
logging.debug("Loaded state from state.json.")
logging.debug(f"Loaded Bonus Tile Completions: {bonus_tile_completions}")
except FileNotFoundError:
logging.debug("No saved state found, starting fresh.")
except Exception as e:
logging.error(f"Error loading state: {e}")
def infer_revealed_tiles():
revealed_tiles = set().union(team_positions.values())
for _, tiles in tile_completions:
revealed_tiles.union(tiles)
return revealed_tiles
def generate_streak_msg(unrevealed_streak_tiles):
response = ""
start, end = unrevealed_streak_tiles[0], unrevealed_streak_tiles[-1]
if end - start > 0:
response += f"Tiles {start}-{end}: ❓unknown tasks❓\n"
else:
response += f"Tile {start}: ❓unknown task❓\n"
unrevealed_streak_tiles.clear()
return response
def generate_revealed_tiles_msg(istaylor=False):
global tiles_revealed
# if there is no data, infer the currently revealed tiles from database
if tiles_revealed == set():
tiles_revealed = infer_revealed_tiles()
response = "**Currently revealed tiles\n\n"
_tile = 1
unrevealed_streak_tiles = []
for tile, task in tile_tasks.items():
# gather symbols of teams at tile
teams = ""
for team, position in team_positions:
if position == tile:
teams += team_symbols[team]
if istaylor or tile in tiles_revealed:
# generate str chunk for streak of unrevealed tiles if not empty
if unrevealed_streak_tiles:
response += generate_streak_msg(unrevealed_streak_tiles)
if tile in hard_stop_tiles:
response += f"{teams}🚧Tile {tile}: {task}\n"
else:
response += f"{teams}Tile {tile}: {task}\n"
else:
if tile in hard_stop_tiles:
# generate str chunk for streak of unrevealed tiles if not empty
if unrevealed_streak_tiles:
response += generate_streak_msg(unrevealed_streak_tiles)
response += f"{teams}🚧Tile {tile}: {task}\n"
else:
unrevealed_streak_tiles.append(tile)
return response
async def initialize_team_members():
"""Initialize team members based on existing roles in the guild."""
await bot.wait_until_ready()
guild = bot.get_guild(GAME_CHANNEL_ID)
if not guild:
logging.error(f"Guild with ID {GAME_CHANNEL_ID} not found.")
return
for team, role_id in team_roles.items():
role = discord.utils.get(guild.roles, id=role_id)
if role:
for member in role.members:
team_members[team].add(member.id)
logging.debug("Team members initialized based on existing roles.")
@bot.event
async def on_ready():
"""Start the taunt loop when the bot is ready."""
await initialize_team_members()
if teams_set and game_started:
taunt_teams.start()
logging.debug(f"Logged in as {bot.user}")
logging.debug(f"Loaded Bonus Tile Completions on startup: {bonus_tile_completions}")
logging.debug(f"Loaded Team Positions on startup: {team_positions}")
logging.debug(f"Loaded Team Data on startup: {team_data}")
async def prompt_confirmation(ctx, number):
def check(m):
return m.author == ctx.author and m.channel == ctx.channel and m.content.lower() in ['y', 'n']
await ctx.send(f"Confirm {number} of teams, Y or N.")
try:
msg = await bot.wait_for('message', check=check, timeout=60.0)
return msg.content.lower() == 'y'
except asyncio.TimeoutError:
await ctx.send("Confirmation timed out.")
return False
async def prompt_bonus_choice(ctx):
def check(m):
return m.author == ctx.author and m.channel == ctx.channel and m.content.lower() in ['1', '2', '3']
bonus_message = (
"**Choose your bonus option:**\n"
"1 - Monkey's Paw 🐾\n"
"2 - GP 💰\n"
"3 - Sabotage 😈"
)
await ctx.send(bonus_message)
try:
msg = await bot.wait_for('message', check=check, timeout=60.0)
return msg.content.lower()
except asyncio.TimeoutError:
return None
def command_cooldown(func):
@wraps(func)
async def wrapper(ctx, *args, **kwargs):
user_id = ctx.author.id
if await check_timeout(ctx):
await ctx.send(f"You are currently in a timeout, {ctx.author.mention}.")
return
if await handle_spam(ctx):
await ctx.send(f"To prevent spam, you are on a 5 minute timeout, {ctx.author.mention}.")
return
await func(ctx, *args, **kwargs)
return wrapper
# Specific cooldown for !pester command
def specific_cooldown(rate, per):
def decorator(func):
@wraps(func)
async def wrapper(ctx, *args, **kwargs):
user_id = ctx.author.id
if user_id in user_cooldowns:
if "pester" in user_cooldowns[user_id]:
last_used = user_cooldowns[user_id]["pester"]
now = datetime.utcnow()
if now < last_used + timedelta(seconds=per):
remaining_time = (last_used + timedelta(seconds=per) - now).total_seconds()
await ctx.send(f"You're using !pester too frequently. Try again in {remaining_time:.0f} seconds.")
return
user_cooldowns[user_id]["pester"] = datetime.utcnow()
return await func(ctx, *args, **kwargs)
return wrapper
return decorator
def category_check():
def predicate(ctx):
return in_designated_category(ctx)
return commands.check(predicate)
def captain_command_check():
def predicate(ctx):
return in_captain_commands_channel(ctx)
return commands.check(predicate)
@bot.command()
@commands.has_permissions(administrator=True)
@category_check()
@command_cooldown
async def set_teams(ctx, number: int):
"""Set the number of teams for the game, if the user is an administrator."""
global number_of_teams, teams_set
if teams_set:
await ctx.send("Teams have already been set and cannot be changed.")
return
if not ctx.author.guild_permissions.administrator:
await ctx.send("Only administrators can set the number of teams.")
return
if number < 1 or number > 8:
await ctx.send("Please choose a number of teams between 1 and 8.")
return
if await prompt_confirmation(ctx, number):
number_of_teams = number
teams_set = True
for i in range(1, number_of_teams + 1):
team_name = f"team{i}"
team_data[team_name] = ""
team_positions[team_name] = 1
team_has_rolled[team_name] = False
team_advantages[team_name] = None
tile_completion_times[team_name] = []
await ctx.send(f"The game will be played with {number_of_teams} teams.")
save_state()
else:
await ctx.send("Number of teams setting canceled.")
@bot.command()
@commands.has_permissions(administrator=True)
@category_check()
@command_cooldown
async def start(ctx):
"""Start the game, if the user is an administrator."""
global game_started
if not teams_set:
await ctx.send("Teams have not been set yet. Please use !set_teams to set the number of teams.")
return
if game_started:
await ctx.send("The game has already started.")
return
if not ctx.author.guild_permissions.administrator:
await ctx.send("Only administrators can start the game.")
return
game_started = True
for team in list(team_data.keys())[:number_of_teams]:
team_positions[team] = 1 # Ensure all teams are at tile 1
team_has_rolled[team] = False # Reset the roll status for all teams
# Initialize tile_completion_times for each team
tile_completion_times[team] = []
# Start the timer for the first tile
tile_start_times[team] = datetime.now()
await ctx.send("The game has started! All teams are on tile 1.")
# Notify each team about their starting tile and task
for team in list(team_data.keys())[:number_of_teams]:
role_id = team_roles.get(team)
if role_id:
role = ctx.guild.get_role(role_id)
if role:
task = tile_tasks.get(1, default_task_message)
await ctx.send(f"{role.mention} You are beginning on tile 1. Your task is: {task}. Good luck!")
save_state()
@bot.command()
@commands.check(is_captain)
@captain_command_check()
@command_cooldown
async def set_name(ctx, team: str, *team_name):
"""Set the team name, if the user is a captain."""
if not teams_set:
await ctx.send("Teams have not been set yet. Please use !set_teams to set the number of teams.")
return
team = resolve_team_identifier(team)
if team is None:
await ctx.send(f"Invalid team. Please choose from {', '.join(team_data.keys())}.")
return
team_name = " ".join(team_name) # Join the team_name tuple into a single string with spaces
team_data[team] = team_name
await ctx.send(f"Team {team} is now called {team_name}.")
save_state()
@bot.command()
@commands.has_permissions(administrator=True)
@captain_command_check()
@command_cooldown
async def set_captain(ctx, member: discord.Member, team: str):
"""Assign a captain, if the user is an administrator."""
if not teams_set:
await ctx.send("Teams have not been set yet. Please use !set_teams to set the number of teams.")
return
team = resolve_team_identifier(team)
if team is None:
await ctx.send(f"The team {team} does not exist.")
return
captains[member.id] = team
captain_role = discord.utils.get(ctx.guild.roles, name="Team Captain")
if captain_role:
await member.add_roles(captain_role)
await ctx.send(f"{member.display_name} has been assigned as captain of {team} and given the Team Captain role.")
save_state()
@bot.command()
@commands.check(is_captain)
@captain_command_check()
@command_cooldown
async def assign_members(ctx, team: str, *members: discord.Member):
"""Assign members to a team."""
if not teams_set:
await ctx.send("Teams have not been set yet. Please use !set_teams to set the number of teams.")
return
team = resolve_team_identifier(team)
if team is None:
await ctx.send(f"Invalid team. Please choose from {', '.join(team_data.keys())}.")
return
team_role = discord.utils.get(ctx.guild.roles, id=team_roles.get(team))
if not team_role:
await ctx.send(f"Role for {team} not found. Please ensure the role exists.")
return
for member in members:
try:
team_members[team].add(member.id)
await member.add_roles(team_role)
await ctx.send(f"{member.display_name} has been assigned to {team} and given the role {team_role.name}.")
except discord.Forbidden:
await ctx.send(
f"Failed to assign role to {member.display_name}. Check the bot's role hierarchy and permissions.")
except Exception as e:
logging.error(f"Unexpected error: {e}")
save_state()
dice_emojis = {
1: "1️⃣",
2: "2️⃣",
3: "3️⃣",
4: "4️⃣",
5: "5️⃣",
6: "6️⃣"
}
async def handle_roll(team):
roll = random.randint(1, 6)
roll_message = f"You rolled a {dice_emojis[roll]}"
# Apply advantage or sabotage if present
if team_advantages[team] == "advantage":
roll += 1
roll_message += f". With your advantage, you move {roll} tiles."
elif team_advantages[team] == "sabotage":
roll -= 2
roll_message += f". But you are subject to sabotage, so your roll is reduced by 2 and only move {roll} tiles."
team_advantages[team] = None # Reset sabotage after it's been applied
# Apply multiple sabotages
if team_sabotages[team] > 0:
roll -= 2 * team_sabotages[team]
roll_message += f" Due to {team_sabotages[team]} sabotages, you move {roll} tiles."
team_sabotages[team] = 0 # Reset sabotage counter after applying
return roll, roll_message
import random
async def update_team_position(team, roll):
current_position = team_positions[team]
new_position = current_position + roll
hard_stop_message = ""
# Ensure the new position is within the bounds of the game board
if new_position < 1:
new_position = 1
elif new_position > 69:
new_position = 69
if roll > 0:
for stop_tile in hard_stop_tiles:
if current_position < stop_tile <= new_position:
new_position = stop_tile
hard_stop_message = random.choice(hard_stop_messages).format(roll=roll, tile=new_position)
break
else:
for stop_tile in reversed(hard_stop_tiles):
if new_position <= stop_tile < current_position and stop_tile not in completed_tiles[team]:
new_position = stop_tile
hard_stop_message = random.choice(hard_stop_messages).format(roll=roll, tile=new_position)
break
team_positions[team] = new_position
global tiles_revealed
tiles_revealed.add(new_position)
return new_position, hard_stop_message
@bot.command()
@commands.check(is_captain)
@captain_command_check()
@command_cooldown
async def roll(ctx, *, team: str):
"""Roll the dice to move the team forward, if the user is a captain or administrator."""
if not teams_set:
await ctx.send("Teams have not been set yet. Please use !set_teams to set the number of teams.")
return
if not game_started:
await ctx.send("The game has not started yet. Please use !start to start the game.")
return
team = resolve_team_identifier(team)
if team is None:
await ctx.send(f"The team {team} does not exist.")
return
if team_data[team] == "":
await ctx.send(f"Team {team} must set a name using !set_name before rolling.")
return
try:
current_position = team_positions[team]
logging.debug(f"Team {team} current position: {current_position}")
if current_position not in completed_tiles[team]:
response = random.choice(incomplete_tile_responses).format(team=team_data[team])
await ctx.send(response)
return
roll, roll_message = await handle_roll(team)
new_position, hard_stop_message = await update_team_position(team, roll)
# Send a random GIF from the list
random_gif = random.choice(GIF_PATHS)
if random_gif and os.path.isfile(random_gif):
await ctx.send(file=discord.File(random_gif))
else:
await ctx.send("Error: Could not find the GIF file.")
if new_position == current_position:
# Team did not move, they must complete the current tile again
await ctx.send(f"{roll_message} Due to sabotage, Team {team_data[team]} did not move and must re-complete tile {current_position}.")
if current_position in completed_tiles[team]:
completed_tiles[team].remove(current_position) # Reset the task completion status
team_has_rolled[team] = False
return
else:
team_positions[team] = new_position
team_has_rolled[team] = True # Mark the team as having rolled
move_message = f" You moved to tile {new_position}."
await ctx.send(f"🫳 🎲\n\n{roll_message}{move_message}")
if new_position == 69:
task = tile_tasks.get(new_position, default_task_message)
await ctx.send("You've landed on tile 69, nice! Good luck on the last task - may the odds be ever in your favor.")
else:
task = tile_tasks.get(new_position, default_task_message)
await ctx.send(f"Your new task is to complete tile {new_position}: {task}")
if hard_stop_message:
await ctx.send(hard_stop_message)
rolls_count[team] += 1
# Start the timer for the new tile
tile_start_times[team] = datetime.now()
save_state() # Save state after a successful roll
# Reset sabotage counter and flag after roll
team_sabotages[team] = 0
team_sabotaged[team] = False
except KeyError as e:
logging.error(f"KeyError in roll command for team {team}: {e}")
await ctx.send(f"An error occurred while processing the command: Missing key {e}")
except Exception as e:
logging.error(f"Error in roll command: {e}")
await ctx.send(f"An error occurred while processing the command: {str(e)}")
@bot.command()
@commands.check(is_captain)
@captain_command_check()
@command_cooldown
async def complete(ctx, *, input: str):
"""Mark a tile task as complete, if the user is a captain or administrator."""
if not teams_set:
await ctx.send("Teams have not been set yet. Please use !set_teams to set the number of teams.")
return
if not game_started:
await ctx.send("The game has not started yet. Please use !start to start the game.")
return
await sync_team_members() # Sync team members before processing the command
try:
team, tile_str, member_str = input.rsplit(' ', 2)
team = resolve_team_identifier(team)
if team is None:
await ctx.send(f"The team {team} does not exist.")
return
try:
tile = int(tile_str)
except ValueError:
await ctx.send(
f"Invalid tile number '{tile_str}'. Please provide a valid number for the tile number. For example: !complete <team name> <tile_number> <@member>")
return
member = await commands.MemberConverter().convert(ctx, member_str)
if member.id not in team_members[team]:
await ctx.send(f"{member.display_name} is not a member of team {team_data[team]}.")
return
current_position = team_positions.get(team)
if current_position is None:
await ctx.send(f"Team {team_data.get(team, 'Unknown')} does not have a valid position.")
return
if tile != current_position:
await ctx.send(f"Team {team_data[team]} is not on tile {tile}. Current position is {current_position}.")
return
if tile in completed_tiles[team]:
await ctx.send(f"Tile {tile} has already been marked as complete for team {team_data[team]}.")
return
completed_tiles[team].add(tile)
tile_completions[team].append((tile, member.id))
player_completions[member.id] = player_completions.get(member.id, 0) + 1
await ctx.send(f"Tile {tile} marked as complete for team {team_data[team]} by {member.display_name}.")
# Record the completion time
completion_time = datetime.now()
if team in tile_start_times:
start_time = tile_start_times.pop(team)
tile_completion_times[team].append((tile, completion_time - start_time))
# Handle completion of tile 69
if tile == 69:
await ctx.send(f"Tile 69 marked as complete for team {team_data[team]} by {member.display_name}.")
await ctx.send(
f"**🎉 CUMGRADULATIONS {team_data[team]}! You've completed the final tile and won the game! 🎉**")
gif_path = os.getenv('GIF11_PATH') # Ensure this environment variable is set with the correct path to gif11
if gif_path and os.path.isfile(gif_path):
await ctx.send(file=discord.File(gif_path))
else:
await ctx.send("Error: Could not find the GIF file for the congratulatory message.")
save_state() # Save state after marking a tile as complete
except Exception as e:
logging.error(f"Error in complete command: {e}")
await ctx.send(f"An error occurred while processing the command: {str(e)}")
@bot.command()
@category_check()
@command_cooldown
async def board(ctx, *, input:str):
"""Secret command to display all tiles on the board."""
taylor2ya_id = 472000479476580362
input = input.strip()
try:
if ctx.author.id == taylor2ya_id:
if input == "spoiler":
response = generate_revealed_tiles_msg(istaylor=True)
elif input == "":
response = generate_revealed_tiles_msg(istaylor=False)
else:
# keyphrase is to prevent accidentally spoilering board
response = "incorrect keyphrase \"{input}\" provided, correct keyphrase is \"spoiler\""
await ctx.send(response)
else:
response = generate_revealed_tiles_msg(istaylor=False)
await ctx.send(response)
except Exception as e:
logging.debug(traceback.format_exc())
@bot.command()
@commands.check(is_captain)
@captain_command_check()
@command_cooldown
async def complete_bonus(ctx, *, input: str):
"""Mark a bonus tile task as complete, if the user is a captain."""
if not teams_set:
await ctx.send("Teams have not been set yet. Please use !set_teams to set the number of teams.")
return
if not game_started: