Create the Halma game, Part 0? Not sure if this is really complete.
This commit is contained in:
parent
c1da0ee15c
commit
5f87072b1b
1 changed files with 332 additions and 0 deletions
332
Halma/halma.py
Normal file
332
Halma/halma.py
Normal 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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue