import tkinter as tk import time import string import sys # Define constants # Player color constants PLAYER_1_COLOR = "red" PLAYER_2_COLOR = "green" # Board color constants HIGHLIGHT_COLOR = "yellow" VALID_MOVE_COLOR = "lightblue" BOARD_LIGHT_COLOR = "white" BOARD_DARK_COLOR = "lightgray" # UI Constants FONT = "System" class MoveHandler: def __init__(self, board_size): self.board_size = board_size def is_valid_move(self, board, start_position, end_position): # Checks to see if the move that is done is valid, returns true or false # Save the start and end positions start_x, start_y = start_position end_x, end_y = end_position # Ensure the move is within bounds if not (0 <= end_x < self.board_size and 0 <= end_y < self.board_size): return False # Ensure the destination is empty if board[end_y][end_x] != 0: return False # Calculate the move distance, using sorta distance formula delta_x, delta_y = abs(end_x - start_x), abs(end_y - start_y) # Step moves to check if (delta_x, delta_y) in [(1, 0), (0, 1), (1, 1)]: return True # Jump moves to check if (delta_x, delta_y) == (2, 0) or (delta_x, delta_y) == (0, 2) or (delta_x, delta_y) == (2, 2): # Find the middle points mid_x, mid_y = (start_x + end_x) // 2, (start_y + end_y) // 2 if board[mid_y][mid_x] != 0 and board[mid_y][mid_x] != board[start_y][start_x]: return True return False def generate_moves(self, board, position): # Function to generate all the valid moves for a piece given board and position # Use the selected piece's position, saved as two variables piece_x, piece_y = position # Create an empty move list valid_moves = [] # Generate all available moves within it's potential range for delta_x in range(-2, 3): for delta_y in range(-2, 3): if delta_x == 0 and delta_y == 0: continue new_x, new_y = piece_x + delta_x, piece_y + delta_y if self.is_valid_move(board, (piece_x, piece_y), (new_x, new_y)): # If the move is valid, then add to the move list valid_moves.append((new_x, new_y)) return valid_moves class Halma: def __init__(self, window, board_size=8, seconds_limit=10, human_player=PLAYER_1_COLOR, board_file=None): # Initialize the Halma game # tkinter Window passed in through here self.window = window self.board_size = board_size # Define the size of each square self.cell_size = 50 # Define the timeout for the player self.seconds_limit = seconds_limit self.human_player_color = 1 if human_player.lower() == PLAYER_1_COLOR else 2 # Define the board size (square) self.board = [[0] * self.board_size for _ in range(self.board_size)] # Pick which player goes first (typically the human player) self.current_player = 1 # Use the MoveHandler, pass it through to the Halma game since it uses the move functions self.move_handler = MoveHandler(self.board_size) # Start a timer for the game self.start_time = time.time() # Save the last move in these variables self.last_move_start_position = None self.last_move_end_position = None # Save the valid move set here self.valid_moves = [] # Save the number of moves and jumps to display when won self.player_1_moves = 0 self.player_1_jumps = 0 self.player_2_moves = 0 self.player_2_jumps = 0 # Start the initial board self.setup_board() # Show the UI self.create_ui() def setup_board(self): # Determine the number of rows/columns for the goal area based on the board size if self.board_size == 8: # 10 pieces per player goal_size = 4 elif self.board_size == 10: # 15 pieces per player goal_size = 5 elif self.board_size == 16: # 19 pieces per player goal_size = 6 else: raise ValueError("Unsupported board size. Choose from 8, 10, or 16.") # Place Player 1's pieces in the top-left corner for row in range(goal_size): for col in range(goal_size): if col + row < goal_size: self.board[row][col] = 1 # Place Player 2's pieces in the bottom-right corner for row in range(goal_size): for col in range(goal_size): if col + row < goal_size: self.board[self.board_size - 1 - row][self.board_size - 1 - col] = 2 def create_ui(self): # This function creates and updates the UI # Decide the width and height of the window canvas_width = self.board_size * self.cell_size + 50 canvas_height = self.board_size * self.cell_size + 50 # Create the canvas self.canvas = tk.Canvas(self.window, width=canvas_width, height=canvas_height) self.canvas.pack() # Add labels for the sides human_label = f"Human ({PLAYER_1_COLOR.capitalize()})" if self.human_player_color == 1 else f"Human ({PLAYER_2_COLOR.capitalize()})" computer_label = f"Computer ({PLAYER_2_COLOR.capitalize()})" if self.human_player_color == 1 else f"Computer ({PLAYER_1_COLOR.capitalize()})" tk.Label(self.window, text=human_label, font=(FONT, 14)).pack(side=tk.LEFT, padx=20) tk.Label(self.window, text=computer_label, font=(FONT, 14)).pack(side=tk.RIGHT, padx=20) self.status_label = tk.Label(self.window, text=f"Player 1's turn ({PLAYER_1_COLOR.capitalize()})", font=(FONT, 14)) self.status_label.pack() # Bind the left mouse button to the click function self.canvas.bind("", self.on_click) # Draw the inital board self.draw_board() def draw_board(self): # This function is called whenever the board needs to (re)drawn. # Remove the board from the screen self.canvas.delete("all") # Draw the coordinate labels, centered over their respective rows/columns for index in range(self.board_size): # Horizontal labels (letters) x_position = index * self.cell_size + 50 self.canvas.create_text( x_position + self.cell_size // 2, 25, text=string.ascii_uppercase[index], font=(FONT, 12) ) # Vertical labels (numbers) y_position = index * self.cell_size + 50 self.canvas.create_text( 25, y_position + self.cell_size // 2, text=str(index + 1), font=(FONT, 12) ) # Draw the board grid and pieces for row in range(self.board_size): for col in range(self.board_size): x1, y1 = col * self.cell_size + 50, row * self.cell_size + 50 x2, y2 = x1 + self.cell_size, y1 + self.cell_size cell_color = BOARD_LIGHT_COLOR if (col + row) % 2 == 0 else BOARD_DARK_COLOR # Highlight the last move start and end positions on the board if self.last_move_start_position == (col, row) or self.last_move_end_position == (col, row): cell_color = HIGHLIGHT_COLOR # Highlight valid moves on the board if (col, row) in self.valid_moves: cell_color = VALID_MOVE_COLOR self.canvas.create_rectangle(x1, y1, x2, y2, fill=cell_color) # Draw the pieces, center them in their respective square if self.board[row][col] == 1: self.canvas.create_oval(x1 + 5, y1 + 5, x2 - 5, y2 - 5, fill=PLAYER_1_COLOR) elif self.board[row][col] == 2: self.canvas.create_oval(x1 + 5, y1 + 5, x2 - 5, y2 - 5, fill=PLAYER_2_COLOR) def on_click(self, event): # Bound function to the left mouse button, controls the game # Calculate where the click happened clicked_column = (event.x - 50) // self.cell_size clicked_row = (event.y - 50) // self.cell_size # Ensure the click is within the board boundaries if not (0 <= clicked_column < self.board_size and 0 <= clicked_row < self.board_size): return # Check if the clicked piece belongs to the current player if not hasattr(self, "selected_piece") or self.selected_piece is None: if self.board[clicked_row][clicked_column] == self.current_player: self.selected_piece = (clicked_column, clicked_row) self.valid_moves = self.move_handler.generate_moves(self.board, (clicked_column, clicked_row)) self.highlight_piece(clicked_column, clicked_row) # Redraw the board to show valid moves self.draw_board() else: # Attempt to move the selected piece start_position = self.selected_piece end_position = (clicked_column, clicked_row) if end_position in self.valid_moves and self.move_handler.is_valid_move(self.board, start_position, end_position): # Handle the move result = self.make_move(start_position, end_position) if result == "continue": # Allow the player to continue jumping self.status_label.config(text="Jump Possible.") return elif result: # Move completed, check the winner self.check_winner() # Switch to the other player self.current_player = 3 - self.current_player current_color = PLAYER_1_COLOR.capitalize() if self.current_player == 1 else PLAYER_2_COLOR.capitalize() self.status_label.config(text=f"Player {self.current_player}'s turn ({current_color})") else: print(f"Invalid Move. The move you attempted is invalid. Try again.") # Reset the selected piece and valid moves after attempting a move self.selected_piece = None self.valid_moves = [] self.draw_board() def highlight_piece(self, column, row): x1, y1 = column * self.cell_size + 50, row * self.cell_size + 50 x2, y2 = x1 + self.cell_size, y1 + self.cell_size self.canvas.create_rectangle(x1, y1, x2, y2, outline=HIGHLIGHT_COLOR, width=3) def make_move(self, start_position, end_position): # Function that actually moves the piece # Define the start and end positions that the piece wants to move start_x, start_y = start_position end_x, end_y = end_position # Calculate the move distance delta_x, delta_y = abs(end_x - start_x), abs(end_y - start_y) # Check if the move is a jump (distance of 2 in any direction) if delta_x == 2 or delta_y == 2: # Calculate the coordinates of the jumped piece mid_x, mid_y = (start_x + end_x) // 2, (start_y + end_y) // 2 # Ensure the jumped piece belongs to the opponent if self.board[mid_y][mid_x] != 0 and self.board[mid_y][mid_x] != self.current_player: # Remove the jumped piece self.board[mid_y][mid_x] = 0 # Update jump counter if self.current_player == 1: self.player_1_jumps += 1 else: self.player_2_jumps += 1 else: # Invalid jump (trying to jump over an empty square or own piece) print(f"Invalid Move. You cannot jump over an empty square or your own piece.") # Indicate that the move was invalid return False elif delta_x > 1 or delta_y > 1: # Invalid move (too far without jumping) print(f"Invalid Move. You can only jump over a piece or move one step.") # Indicate that the move was invalid return False else: # Update move counter for step moves if self.current_player == 1: self.player_1_moves += 1 else: self.player_2_moves += 1 # Move the piece to the new position self.board[start_y][start_x] = 0 self.board[end_y][end_x] = self.current_player # Store the last moves start and end positions self.last_move_start_position = start_position self.last_move_end_position = end_position # Check if additional jumps are possible only if the move was a jump if delta_x == 2 or delta_y == 2: self.valid_moves = self.move_handler.generate_moves(self.board, end_position) if any(abs(next_x - end_x) == 2 or abs(next_y - end_y) == 2 for next_x, next_y in self.valid_moves): # Allow the player to continue jumping self.selected_piece = end_position self.highlight_piece(end_x, end_y) self.draw_board() # Indicate that the move can continue return "continue" # Indicate that the move was valid and completed return True def check_winner(self): # Define the goal areas for both players player_1_goal = [(self.board_size - 1 - y, self.board_size - 1 - x) for y in range(3) for x in range(3) if x + y < 3] player_2_goal = [(y, x) for y in range(3) for x in range(3) if x + y < 3] # Get the positions of all pieces for both players player_1_pieces = [(y, x) for y in range(self.board_size) for x in range(self.board_size) if self.board[y][x] == 1] player_2_pieces = [(y, x) for y in range(self.board_size) for x in range(self.board_size) if self.board[y][x] == 2] # Check if Player 1 has won player_1_won = all(piece in player_1_goal for piece in player_1_pieces) and len(player_1_pieces) == len(player_1_goal) # Check if Player 2 has won player_2_won = all(piece in player_2_goal for piece in player_2_pieces) and len(player_2_pieces) == len(player_2_goal) # Check if a player has no pieces left if not player_1_pieces: player_2_won = True elif not player_2_pieces: player_1_won = True # If either player has won, display a message and end the game if player_1_won or player_2_won: winner = 1 if player_1_won else 2 elapsed_time = time.time() - self.start_time print(f"Game Over!") print(f"\tPlayer {winner} wins! Time: {elapsed_time:.2f} seconds") print(f"Game Stats:\n\tPlayer 1 Moves: {self.player_1_moves}\n\tPlayer 2 Moves: {self.player_2_moves}\n") print(f"\tPlayer 1 Jumps: {self.player_1_jumps}\n\tPlayer 2 Jumps: {self.player_2_jumps}") self.window.quit() if __name__ == "__main__": # Default values for the game board_size = 8 seconds_limit = 10 human_player = "Red" board_file = None # Parse the command line arguments if len(sys.argv) == 5: board_size = int(sys.argv[1]) seconds_limit = int(sys.argv[2]) human_player = sys.argv[3] board_file = sys.argv[4] elif len(sys.argv) == 4: board_size = int(sys.argv[1]) seconds_limit = int(sys.argv[2]) human_player = sys.argv[3] elif len(sys.argv) == 3: board_size = int(sys.argv[1]) seconds_limit = int(sys.argv[2]) elif len(sys.argv) == 2: board_size = int(sys.argv[1]) # Start the game halma_window = tk.Tk() halma_window.title("Halma") game = Halma(halma_window, board_size=board_size, seconds_limit=seconds_limit, human_player=human_player, board_file=board_file) halma_window.mainloop()