Create the Halma game, Part 0? Not sure if this is really complete.

This commit is contained in:
Charles Descamps 2025-03-23 16:21:27 -07:00
parent c1da0ee15c
commit 5f87072b1b

332
Halma/halma.py Normal file
View file

@ -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("<Button-1>", 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()