diff --git a/Halma/halma.py b/Halma/halma.py new file mode 100644 index 0000000..839330e --- /dev/null +++ b/Halma/halma.py @@ -0,0 +1,332 @@ +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() \ No newline at end of file