Project Overview
Tic Tac Toe on a 16×16 LED Matrix
Embark on an Exciting Journey with Tic Tac Toe on a 16×16 LED Matrix Display and experience the timeless game of Tic Tac Toe like never before with our innovative project that combines classic gameplay with cutting-edge technology. Utilizing the BrainPad Pulse microcomputer and a vibrant 16×16 LED matrix, this project promises hours of immersive entertainment for players of all ages.
How It Works
The project serves as a platform to enhance Python proficiency, showcasing its seamless integration with LEDs. Upon initiation, the LED matrix initializes the gaming arena, with a dynamic square pointer navigating automatically by coloring square borders. Pressing button A on the BrainPad Pulse selects the square indicated by the pointer, marking it with an ‘X’. The AI Bot strategically places its ‘O’, engaging players until the bot wins or a draw is achieved, note: you won’t win against the AI bot.
Hardware Requirements
To bring this project to life, you’ll need the following hardware components:
- BrainPad Pulse Microcomputer: The heart of the project, providing computational power and control capabilities.
- 16×16 LED Matrix (WS2812B Digital Flexible LED): A dazzling display that serves as the game board, offering vibrant visuals and dynamic animations.
- BrainTronics Kit or 3 Alligator Clips to Dupont Wire: Essential accessories for easily and reliably connecting the microcomputer to the LED matrix.
Software Requirements
Install DUELink Python Library: Begin by installing the DUELink Python library, a versatile tool that facilitates seamless control of the microcomputer’s GPIO pins and NeoPixels. Execute the command pip install DUELink
in your terminal to install the library and its dependencies effortlessly.
pip install DUELink
Code Overview
Let’s break down the Python code and provide a comprehensive explanation for each method by comments:
import time
# Define colors using hexadecimal values
redColor = 0xff0000
greenColor = 0x00ff00
blueColor = 0x0000ff
# Define the Game class
class Game:
def __init__(self, bb, neoPin, neoCount):
# Initialize game parameters
self.bb = bb
self.neoPin = neoPin
self.neoCount = neoCount
self.board = [[]] # Initialize an empty board
self.player_moves = {"x": [], "o": []} # Store moves made by each player
# Define square configurations for the tic-tac-toe grid
self.SQUARES = {
1: [(177, 205, 211, 235, 238, 210, 204, 180), (178, 179, 206, 209, 237, 236, 212, 203)],
2: [(110, 114, 140, 148, 145, 141, 115, 107), (109, 108, 113, 142, 146, 147, 139, 116)],
3: [(17, 45, 51, 75, 78, 50, 44, 20), (18, 19, 46, 49, 77, 76, 52, 43)],
4: [(182, 200, 216, 230, 233, 215, 199, 185), (183, 184, 201, 214, 232, 231, 217, 198)],
5: [(105, 119, 135, 153, 150, 136, 120, 102), (104, 103, 118, 137, 151, 152, 134, 121)],
6: [(22, 40, 56, 70, 73, 55, 39, 25), (23, 24, 41, 54, 72, 71, 57, 38)],
7: [(187, 195, 221, 225, 228, 220, 194, 190), (188, 189, 196, 219, 227, 226, 222, 193)],
8: [(100, 124, 130, 158, 155, 131, 125, 97), (99, 98, 123, 132, 156, 157, 129, 126)],
9: [(27, 35, 61, 65, 68, 60, 34, 30), (28, 29, 36, 59, 67, 66, 62, 33)]
}
# Define square borders for highlighting
self.SQUARE_BORDERS = {
1: [170, 171, 172, 173, 174, 175, 176, 240, 241, 242, 243, 244, 245, 176, 207, 208, 239, 181, 202, 213,
234],
2: [80, 81, 82, 83, 84, 85, 170, 171, 172, 173, 174, 175, 111, 112, 143, 144, 106, 117, 138, 149],
3: [10, 11, 12, 13, 14, 15, 80, 81, 82, 83, 84, 85, 16, 47, 48, 79, 21, 42, 53, 74],
4: [165, 166, 167, 168, 169, 170, 245, 246, 247, 248, 249, 250, 181, 202, 213, 234, 186, 197, 218, 229],
5: [85, 86, 87, 88, 89, 90, 165, 166, 167, 168, 169, 170, 106, 117, 138, 149, 101, 122, 133, 154],
6: [5, 6, 7, 8, 9, 10, 85, 86, 87, 88, 89, 90, 21, 42, 53, 74, 26, 37, 58, 69],
7: [160, 161, 162, 163, 164, 165, 250, 251, 252, 253, 254, 255, 186, 197, 218, 229, 191, 192, 223, 224],
8: [90, 91, 92, 93, 94, 95, 160, 161, 162, 163, 164, 165, 101, 122, 133, 154, 96, 127, 128, 159],
9: [0, 1, 2, 3, 4, 5, 90, 91, 92, 93, 94, 95, 26, 37, 58, 69, 31, 32, 63, 64]
}
# Method to check if a player has won
def check_win(self, player):
# Define winning combinations
winning_combinations = [
[1, 2, 3], [4, 5, 6], [7, 8, 9], # Rows
[1, 4, 7], [2, 5, 8], [3, 6, 9], # Columns
[1, 5, 9], [3, 5, 7] # Diagonals
]
# Check if any winning combination is present in player's moves
for combination in winning_combinations:
if all(square in self.player_moves[player] for square in combination):
return True, combination # Return True if winning combination found
return False, None # Otherwise, return False
# Method to convert cell number to row and column
def cell_number_to_row_col(self, cell_number):
row = (cell_number - 1) // 3
col = (cell_number - 1) % 3
return row, col
# Method to highlight square borders
def set_square_borders(self, square_num, color):
borders = self.SQUARE_BORDERS.get(square_num)
if borders:
for val in borders:
self.bb.Neo.SetColor(val, color)
# Method to set square item (X or O) and color
def set_square(self, square_num, item, color):
square = self.SQUARES.get(square_num)
if square:
for index, val in enumerate(square[0 if item == 'x' else 1]):
self.bb.Neo.SetColor(val, color)
# Method to draw external borders of the grid
def draw_external_borders(self, color):
for z in range(0, 16):
self.bb.Neo.SetColor(z, color)
self.bb.Neo.SetColor(z + 240, color)
# Method to draw horizontal rows of the grid
def draw_rows(self, color):
for z in range(0, 16, 5):
for x in range(0, 256, 32):
self.bb.Neo.SetColor(x + z, color)
self.bb.Neo.SetColor(x + 31 - z, color)
# Method to draw vertical columns of the grid
def draw_columns(self, color):
for z in range(80, 96):
self.bb.Neo.SetColor(z, color)
for z in range(160, 176):
self.bb.Neo.SetColor(z, color)
# Method to highlight winning lights
def winning_lights(self, item, color, combination):
# Highlight the winning combination
item = item
color = color
toggle_time = 0.05
toggle_count = 20
self.draw_external_borders(blueColor)
self.draw_rows(blueColor)
self.draw_columns(blueColor)
self.bb.Neo.Show(self.neoPin, self.neoCount)
# Toggle the colors for visual effect
for i in range(toggle_count):
self.set_square(combination[0], item, 0x000000)
self.set_square(combination[1], item, 0x000000)
self.set_square(combination[2], item, 0x000000)
self.bb.Neo.Show(self.neoPin, self.neoCount)
time.sleep(toggle_time)
self.set_square(combination[0], item, color)
self.set_square(combination[1], item, color)
self.set_square(combination[2], item, color)
self.bb.Neo.Show(self.neoPin, self.neoCount)
time.sleep(toggle_time)
# Method to color square borders
def coloring_square_borders(self, color, square_number):
self.draw_external_borders(blueColor)
self.draw_rows(blueColor)
self.draw_columns(blueColor)
self.bb.Neo.Show(self.neoPin, self.neoCount)
for i in range(1, 10):
if square_number == str(i):
self.set_square_borders(i, color)
self.bb.Neo.Show(self.neoPin, self.neoCount)
break
# Method to print the current state of the board in console and on microcomputer LCD screen
def print_board(self):
self.bb.Display.Clear(0)
x = 10
y = 10
print("_______________________________")
for i in range(3):
row = [str(i * 3 + j + 1) if cell == ' ' else cell for j, cell in enumerate(self.board[i])]
print(" | ".join(row))
self.bb.Display.DrawTextScale(" | ".join(row), 1, x, y, 2, 1)
if i < 2:
print("-" * 9)
self.bb.Display.DrawTextScale("-" * 9, 1, x, y + 10, 2, 1)
y += 20
self.bb.Display.Show()
# Method to check if the game is over
def is_game_over(self):
# Check rows, columns, and diagonals for a win or a tie
for row in self.board:
if row.count(row[0]) == 3 and row[0] != ' ':
return True
for col in range(3):
check = []
for row in self.board:
check.append(row[col])
if check.count(check[0]) == 3 and check[0] != ' ':
return True
if self.board[0][0] == self.board[1][1] == self.board[2][2] != ' ':
return True
if self.board[0][2] == self.board[1][1] == self.board[2][0] != ' ':
return True
# Check for tie
for row in self.board:
for val in row:
if val == ' ':
return False
return True
# Method to evaluate the current state of the game
def evaluate(self):
# Check for winning conditions
for row in self.board:
if row.count('O') == 3:
return 1 # Player O wins
if row.count('X') == 3:
return -1 # Player X wins
for col in range(3):
check = []
for row in self.board:
check.append(row[col])
if check.count('O') == 3:
return 1 # Player O wins
if check.count('X') == 3:
return -1 # Player X wins
if self.board[0][0] == self.board[1][1] == self.board[2][2] == 'O':
return 1 # Player O wins
if self.board[0][0] == self.board[1][1] == self.board[2][2] == 'X':
return -1 # Player X wins
if self.board[0][2] == self.board[1][1] == self.board[2][0] == 'O':
return 1 # Player O wins
if self.board[0][2] == self.board[1][1] == self.board[2][0] == 'X':
return -1 # Player O wins
return 0 # It's a tie
# Method to implement the minimax algorithm for AI move selection
def minimax(self, depth, is_maximizing, alpha, beta):
if self.is_game_over():
return self.evaluate()
if is_maximizing:
max_eval = -float('inf')
for i in range(3):
for j in range(3):
if self.board[i][j] == ' ':
self.board[i][j] = 'O'
evaluation = self.minimax(depth + 1, False, alpha, beta)
self.board[i][j] = ' '
max_eval = max(max_eval, evaluation)
alpha = max(alpha, evaluation)
if beta <= alpha:
break
return max_eval
else:
min_eval = float('inf')
for i in range(3):
for j in range(3):
if self.board[i][j] == ' ':
self.board[i][j] = 'X'
evaluation = self.minimax(depth + 1, True, alpha, beta)
self.board[i][j] = ' '
min_eval = min(min_eval, evaluation)
beta = min(beta, evaluation)
if beta <= alpha:
break
return min_eval
# Method for AI to make a move using the minimax algorithm
def bot_move(self):
best_eval = -float('inf')
best_move = (-1, -1)
for i in range(3):
for j in range(3):
if self.board[i][j] == ' ':
self.board[i][j] = 'O'
evaluation = self.minimax(0, False, -float('inf'), float('inf'))
self.board[i][j] = ' '
if evaluation > best_eval:
best_eval = evaluation
best_move = (i, j)
return best_move
# Method to run the game
def do_game(self):
# Initialize the game state
self.board = [[' ' for _ in range(3)] for _ in range(3)]
self.bb.Display.Clear(0)
self.draw_external_borders(blueColor)
self.draw_rows(blueColor)
self.draw_columns(blueColor)
self.bb.Neo.Show(self.neoPin, self.neoCount)
self.print_board()
arena = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] # Available squares
moves_made = 0 # Track number of moves
self.bb.Button.Enable('a', True) # Enable button 'a' for user input
while len(arena) > 0: # Continue until all squares are filled
while True:
index = 0
while True:
self.coloring_square_borders(redColor, arena[index]) # Highlight square
if self.bb.Button.JustPressed('a'): # Check for button press
square = arena[index - 1] # Get selected square
row, col = self.cell_number_to_row_col(int(square))
self.board[row][col] = 'X' # Set player's move
break
index = (index + 1) % len(arena) # Move to next square
time.sleep(0.5) # Delay for visual effect
if square in arena: # Check if selected square is available
moves_made += 1
for i in range(1, 10):
if square == str(i):
self.set_square(i, "x", redColor) # Mark player's move on the board
self.player_moves["x"].append(i) # Record player's move
break
break
else:
print("try another square") # Prompt user to try another square
continue
self.bb.Neo.Show(self.neoPin, self.neoCount) # Update NeoPixel display
arena.remove(square) # Remove selected square from available squares
self.print_board() # Print the updated board
time.sleep(1) # Delay for visual effect
x_has_won, x_winning_combination = self.check_win("x") # Check if player X has won
y_has_won, y_winning_combination = self.check_win("o") # Check if player O has won
if x_has_won: # If player X has won
print("Player X wins with combination:", x_winning_combination) # Print winning message
self.bb.System.Println("Player X wins with")
self.bb.System.Println(f"combination:{x_winning_combination}") # Print winning combination
self.winning_lights("x", redColor, x_winning_combination) # Highlight winning combination
self.bb.Neo.Clear() # Clear NeoPixel display
break
if y_has_won: # If player O has won
print("Player Y wins with combination:", y_winning_combination) # Print winning message
self.bb.System.Println("Player Y wins with")
self.bb.System.Println(f"combination:{y_winning_combination}") # Print winning combination
self.winning_lights("o", greenColor, y_winning_combination) # Highlight winning combination
self.bb.Neo.Clear() # Clear NeoPixel display
break
if moves_made == 9: # If all squares are filled (tie)
print("It's a tie!") # Print tie message
self.bb.System.Println("It's a tie!") # Print tie message
self.bb.Neo.Clear() # Clear NeoPixel display
break
if len(arena) > 0: # If there are available squares
bot_row, bot_col = self.bot_move() # Get AI move
self.board[bot_row][bot_col] = 'O' # Set AI move on the board
square = str(bot_row * 3 + bot_col + 1) # Get square number
time.sleep(0.5) # Delay for visual effect
if len(arena) > 0: # If there are available squares
moves_made += 1
for i in range(1, 10):
if square == str(i):
self.set_square(i, "o", greenColor) # Mark AI's move on the board
self.player_moves["o"].append(i) # Record AI's move
break
else:
print("try another square") # Prompt user to try another square
self.bb.Neo.Show(self.neoPin, self.neoCount) # Update NeoPixel display
x_has_won, x_winning_combination = self.check_win("x") # Check if player X has won
y_has_won, y_winning_combination = self.check_win("o") # Check if player O has won
self.print_board() # Print the updated board
if x_has_won: # If player X has won
print("Player X wins with combination:", x_winning_combination) # Print winning message
self.bb.System.Println("Player X wins with")
self.bb.System.Println(f"combination:{x_winning_combination}") # Print winning combination
self.winning_lights("x", redColor, x_winning_combination) # Highlight winning combination
self.bb.Neo.Clear() # Clear NeoPixel display
break
if y_has_won: # If player O has won
print("Player Y wins with combination:", y_winning_combination) # Print winning message
self.bb.System.Println("Player Y wins with")
self.bb.System.Println(f"combination:{y_winning_combination}") # Print winning combination
self.winning_lights("o", greenColor, y_winning_combination) # Highlight winning combination
self.bb.Neo.Clear() # Clear NeoPixel display
break
if moves_made == 9: # If all squares are filled (tie)
print("It's a tie!") # Print tie message
self.bb.System.Println("It's a tie!") # Print tie message
self.bb.Neo.Clear() # Clear NeoPixel display
break
if len(arena) != 0: # If there are available squares
arena.remove(square) # Remove selected square from available squares
self.bb.Neo.Show(self.neoPin, self.neoCount) # Update NeoPixel display
Customization:
- Algorithm Selection:
Explore a variety of the world of artificial intelligence by experimenting with different smart search algorithms beyond the MiniMax approach. Consider implementing algorithms such as A*, Depth-First Search (DFS), Breadth-First Search (BFS), and more to tailor the gameplay experience to your preferences and challenges.
- Sound Effects Integration:
Enhance the gaming experience further by incorporating sound effects into the project. Add special sounds for player and bot moves, victory celebrations, and game-ending scenarios you can take a look at the sounds with BrainPad microcomputers.
- Multiplayer Support:
Extend the project’s capabilities by introducing multiplayer functionality. Modify the code to accommodate two players competing against each other on the same LED matrix by adding custom buttons for each player from the BrainTronics kit.
- Explore Different Programming Languages with BrainPad Edge:
Try rewriting the project code in alternative languages such as C# or JavaScript using the BrainPad Edge or BrainPad Rave. Explore the various coding options here and observe how the project behaves in different languages.