1- from typing import Sequence , TypedDict
21import os
32import random
3+ from typing import Sequence , TypedDict
4+
45from dotenv import load_dotenv
56
67import discord
1415
1516# Player identifiers
1617PLAYER_NONE = 0 # Empty cell
17- PLAYER_X = 1 # X player
18- PLAYER_O = 2 # O player
18+ PLAYER_X = 1 # X player
19+ PLAYER_O = 2 # O player
1920
2021# Display symbols for each player
2122X_EMOJI = "❌"
3637# TYPE DEFINITIONS
3738# ==============================================================================
3839
39- type Board = list [list [int ]] # 3x3 grid of player identifiers
40+ Board = list [list [int ]] # 3x3 grid of player identifiers
41+
4042
4143class GameState (TypedDict ):
4244 """Represents the complete state of a Tic Tac Toe game.
@@ -47,12 +49,14 @@ class GameState(TypedDict):
4749 - Database with automatic cleanup of old games
4850 - Any persistent storage with expiration support
4951 """
50- board : Board # Current board state
51- current_turn : int # Which player's turn (1 or 2)
52- player_x_id : int # Discord user ID of player X
53- player_o_id : int # Discord user ID of player O
54- game_over : bool # Whether the game has ended
55- winner : int | None # Winner (1, 2, or None for tie)
52+
53+ board : Board # Current board state
54+ current_turn : int # Which player's turn (1 or 2)
55+ player_x_id : int # Discord user ID of player X
56+ player_o_id : int # Discord user ID of player O
57+ game_over : bool # Whether the game has ended
58+ winner : int | None # Winner (1, 2, or None for tie)
59+
5660
5761# ==============================================================================
5862# GAME STATE STORAGE
@@ -63,6 +67,7 @@ class GameState(TypedDict):
6367# with automatic expiration (e.g., Redis SETEX with 3600 seconds TTL)
6468GAME_STATES : dict [int , GameState ] = {} # Keyed by message ID
6569
70+
6671def create_initial_game_state (player_x_id : int , player_o_id : int ) -> GameState :
6772 """Create a new game state with empty board and random first player.
6873
@@ -85,6 +90,7 @@ def create_initial_game_state(player_x_id: int, player_o_id: int) -> GameState:
8590 winner = None ,
8691 )
8792
93+
8894def get_player_for_user (game_state : GameState , user_id : int ) -> int | None :
8995 """Get which player (X or O) a user is controlling.
9096
@@ -101,10 +107,12 @@ def get_player_for_user(game_state: GameState, user_id: int) -> int | None:
101107 return PLAYER_O
102108 return None
103109
110+
104111# ==============================================================================
105112# CUSTOM ID HELPERS
106113# ==============================================================================
107114
115+
108116def create_button_custom_id (row : int , col : int ) -> str :
109117 """Create a custom ID for a Tic Tac Toe button.
110118
@@ -116,6 +124,7 @@ def create_button_custom_id(row: int, col: int) -> str:
116124 """
117125 return f"{ CUSTOM_ID_PREFIX } :{ row } :{ col } "
118126
127+
119128def parse_button_custom_id (custom_id : str ) -> tuple [int , int ]:
120129 """Parse a button's custom ID to extract coordinates.
121130
@@ -125,16 +134,13 @@ def parse_button_custom_id(custom_id: str) -> tuple[int, int]:
125134 parts = custom_id .split (":" )
126135 return int (parts [1 ]), int (parts [2 ])
127136
137+
128138# ==============================================================================
129139# BUTTON CREATION
130140# ==============================================================================
131141
132- def create_cell_button (
133- cell_value : int ,
134- row : int ,
135- col : int ,
136- disabled : bool = False
137- ) -> components .Button :
142+
143+ def create_cell_button (cell_value : int , row : int , col : int , disabled : bool = False ) -> components .Button :
138144 """Create a button representing a single Tic Tac Toe cell.
139145
140146 Args:
@@ -151,25 +157,21 @@ def create_cell_button(
151157 match cell_value :
152158 case 0 : # Empty cell - clickable
153159 return components .Button (
154- style = discord .ButtonStyle .primary ,
155- label = EMPTY_CELL ,
156- custom_id = custom_id ,
157- disabled = disabled
160+ style = discord .ButtonStyle .primary , label = EMPTY_CELL , custom_id = custom_id , disabled = disabled
158161 )
159162 case 1 | 2 : # Occupied cell - always disabled
160163 return components .Button (
161- style = discord .ButtonStyle .primary ,
162- emoji = PLAYER_SYMBOLS [cell_value ],
163- custom_id = custom_id ,
164- disabled = True
164+ style = discord .ButtonStyle .primary , emoji = PLAYER_SYMBOLS [cell_value ], custom_id = custom_id , disabled = True
165165 )
166166 case _:
167167 raise ValueError (f"Invalid cell value: { cell_value } " )
168168
169+
169170# ==============================================================================
170171# BOARD STATE MANAGEMENT
171172# ==============================================================================
172173
174+
173175def check_winner (board : Board ) -> int | None :
174176 """Check if there's a winner on the board.
175177
@@ -198,6 +200,7 @@ def check_winner(board: Board) -> int | None:
198200
199201 return None
200202
203+
201204def is_board_full (board : Board ) -> bool :
202205 """Check if the board is completely filled (tie game).
203206
@@ -213,14 +216,13 @@ def is_board_full(board: Board) -> bool:
213216 return False
214217 return True
215218
219+
216220# ==============================================================================
217221# UI COMPONENT BUILDERS
218222# ==============================================================================
219223
220- def create_game_buttons (
221- board : Board ,
222- disable_all : bool = False
223- ) -> list [components .ActionRow ]:
224+
225+ def create_game_buttons (board : Board , disable_all : bool = False ) -> list [components .ActionRow ]:
224226 """Create the 3x3 grid of buttons for the Tic Tac Toe game.
225227
226228 Args:
@@ -234,22 +236,15 @@ def create_game_buttons(
234236
235237 for row_idx , row in enumerate (board ):
236238 buttons = [
237- create_cell_button (
238- cell_value = cell_value ,
239- row = row_idx ,
240- col = col_idx ,
241- disabled = disable_all
242- )
239+ create_cell_button (cell_value = cell_value , row = row_idx , col = col_idx , disabled = disable_all )
243240 for col_idx , cell_value in enumerate (row )
244241 ]
245242 action_rows .append (components .ActionRow (* buttons ))
246243
247244 return action_rows
248245
249- def create_game_container (
250- game_buttons : list [components .ActionRow ],
251- game_state : GameState
252- ) -> components .Container :
246+
247+ def create_game_container (game_buttons : list [components .ActionRow ], game_state : GameState ) -> components .Container :
253248 """Create the container for an active game.
254249
255250 Args:
@@ -273,10 +268,8 @@ def create_game_container(
273268 * game_buttons ,
274269 )
275270
276- def create_game_over_container (
277- game_buttons : list [components .ActionRow ],
278- game_state : GameState
279- ) -> components .Container :
271+
272+ def create_game_over_container (game_buttons : list [components .ActionRow ], game_state : GameState ) -> components .Container :
280273 """Create the container for a finished game.
281274
282275 Args:
@@ -302,6 +295,7 @@ def create_game_over_container(
302295 * game_buttons ,
303296 )
304297
298+
305299# ==============================================================================
306300# BOT SETUP
307301# ==============================================================================
@@ -312,10 +306,9 @@ def create_game_over_container(
312306# EVENT HANDLERS
313307# ==============================================================================
314308
309+
315310@bot .component_listener (lambda custom_id : custom_id .startswith (CUSTOM_ID_PREFIX ))
316- async def handle_tic_tac_toe_move (
317- interaction : discord .ComponentInteraction [components .PartialButton ]
318- ):
311+ async def handle_tic_tac_toe_move (interaction : discord .ComponentInteraction [components .PartialButton ]):
319312 """Handle a player clicking a Tic Tac Toe cell.
320313
321314 This function:
@@ -335,29 +328,21 @@ async def handle_tic_tac_toe_move(
335328 # Retrieve game state from storage
336329 if message_id not in GAME_STATES :
337330 await interaction .respond (
338- "❌ Game state not found! This game may have expired or the bot was restarted." ,
339- ephemeral = True
331+ "❌ Game state not found! This game may have expired or the bot was restarted." , ephemeral = True
340332 )
341333 return
342334
343335 game_state = GAME_STATES [message_id ]
344336
345-
346337 # Validate the user is in this game
347338 user_player = get_player_for_user (game_state , interaction .user .id )
348339 if user_player is None :
349- await interaction .respond (
350- "❌ You're not a player in this game!" ,
351- ephemeral = True
352- )
340+ await interaction .respond ("❌ You're not a player in this game!" , ephemeral = True )
353341 return
354342
355343 # Validate it's this user's turn
356344 if user_player != game_state ["current_turn" ]:
357- await interaction .respond (
358- "❌ It's not your turn!" ,
359- ephemeral = True
360- )
345+ await interaction .respond ("❌ It's not your turn!" , ephemeral = True )
361346 return
362347
363348 # Parse the clicked cell coordinates
@@ -380,31 +365,27 @@ async def handle_tic_tac_toe_move(
380365 game_state ["current_turn" ] = PLAYER_O if game_state ["current_turn" ] == PLAYER_X else PLAYER_X
381366
382367 # Create updated button grid
383- updated_buttons = create_game_buttons (
384- board = game_state ["board" ],
385- disable_all = game_over
386- )
368+ updated_buttons = create_game_buttons (board = game_state ["board" ], disable_all = game_over )
387369
388370 # Update the message with new game state
389371 if game_over :
390372 await interaction .edit (
391373 components = [create_game_over_container (updated_buttons , game_state )],
392374 )
393- del GAME_STATES [message_id ] # The message can't be interacted with anymore because all buttons are disabled
375+ del GAME_STATES [message_id ] # The message can't be interacted with anymore because all buttons are disabled
394376 else :
395377 await interaction .edit (
396378 components = [create_game_container (updated_buttons , game_state )],
397379 )
398380
381+
399382# ==============================================================================
400383# SLASH COMMANDS
401384# ==============================================================================
402385
386+
403387@bot .slash_command ()
404- async def tic_tac_toe (
405- ctx : discord .ApplicationContext ,
406- opponent : discord .User
407- ):
388+ async def tic_tac_toe (ctx : discord .ApplicationContext , opponent : discord .User ):
408389 """Start a new Tic Tac Toe game against another user.
409390
410391 Args:
@@ -456,13 +437,16 @@ async def tic_tac_toe(
456437 f"🎮 Game started! { first_player_symbol } (<@{ first_player_id } >) goes first!" ,
457438 )
458439
440+
459441# ==============================================================================
460442# BOT STARTUP
461443# ==============================================================================
462444
445+
463446# Optional: Add a cleanup task for old games if not using Redis TTL
464447@bot .event
465448async def on_ready ():
466449 print (f"Bot ready! Logged in as { bot .user } " )
467450
468- bot .run (os .getenv ("TOKEN_2" ))
451+
452+ bot .run (os .getenv ("TOKEN_2" ))
0 commit comments