Last active
August 13, 2025 01:59
-
-
Save phoenixthrush/5334b402664280f171f04f193f6053dd to your computer and use it in GitHub Desktop.
Nine Men's Morris in Python #Python #NineMenMorris
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import pygame | |
| import sys | |
| # Initialize pygame | |
| pygame.init() | |
| # Set up the display dimensions | |
| game_width, game_height = 600, 600 # Original game area size | |
| padding = 50 # Padding around the game area | |
| window_width = game_width + (padding * 2) | |
| window_height = game_height + (padding * 2) | |
| # Set up the display | |
| screen = pygame.display.set_mode((window_width, window_height)) | |
| pygame.display.set_caption("Das Mühlespiel") | |
| # Define player positions for Nine Men's Morris board | |
| player_positions = [ | |
| # Outer square | |
| (padding + 75, padding + 75), (padding + game_width//2, padding + 75), (padding + game_width-75, padding + 75), | |
| (padding + 75, padding + game_height//2), (padding + game_width-75, padding + game_height//2), | |
| (padding + 75, padding + game_height-75), (padding + game_width//2, padding + game_height-75), (padding + game_width-75, padding + game_height-75), | |
| # Middle square | |
| (padding + 175, padding + 175), (padding + game_width//2, padding + 175), (padding + game_width-175, padding + 175), | |
| (padding + 175, padding + game_height//2), (padding + game_width-175, padding + game_height//2), | |
| (padding + 175, padding + game_height-175), (padding + game_width//2, padding + game_height-175), (padding + game_width-175, padding + game_height-175), | |
| # Inner square | |
| (padding + 275, padding + 275), (padding + game_width//2, padding + 275), (padding + game_width-275, padding + 275), | |
| (padding + 275, padding + game_height//2), (padding + game_width-275, padding + game_height//2), | |
| (padding + 275, padding + game_height-275), (padding + game_width//2, padding + game_height-275), (padding + game_width-275, padding + game_height-275), | |
| ] | |
| # Colors for drawing | |
| RED = (255, 0, 0) | |
| BLUE = (0, 0, 255) | |
| GREEN = (0, 255, 0) | |
| WHITE = (255, 255, 255) | |
| BLACK = (0, 0, 0) | |
| GRAY = (128, 128, 128) | |
| # Game state | |
| current_player = "BLACK" # "BLACK" or "WHITE" | |
| placed_pieces = {} # Dictionary to store which positions have pieces and their color | |
| mill_message = "" # Message to display when a mill is formed | |
| mill_timer = 0 # Timer for displaying the mill message | |
| black_mills = 0 # Counter for BLACK player's mills | |
| white_mills = 0 # Counter for WHITE player's mills | |
| game_won = False # Flag to check if game is won | |
| winner = "" # Store the winner | |
| # Game phase tracking | |
| game_phase = "PLACEMENT" # "PLACEMENT", "MOVEMENT", "JUMPING" | |
| black_pieces_placed = 0 # How many pieces BLACK has placed | |
| white_pieces_placed = 0 # How many pieces WHITE has placed | |
| selected_piece = None # For movement phase - which piece is selected | |
| awaiting_removal = False # Flag when player needs to remove opponent's piece | |
| last_mill_formed = None # Track the last mill formed to prevent immediate removal | |
| # Captured pieces tracking | |
| black_captured_pieces = [] # List of captured BLACK pieces | |
| white_captured_pieces = [] # List of captured WHITE pieces | |
| # Adjacent positions for movement (which positions connect to which) | |
| adjacent_positions = { | |
| 0: [1, 3], 1: [0, 2, 9], 2: [1, 4], | |
| 3: [0, 5, 11], 4: [2, 7, 12], 5: [3, 6], | |
| 6: [5, 7, 14], 7: [4, 6], 8: [9, 11], | |
| 9: [1, 8, 10, 17], 10: [9, 12], 11: [3, 8, 13, 19], | |
| 12: [4, 10, 15, 20], 13: [11, 14], 14: [6, 13, 15, 22], | |
| 15: [12, 14], 16: [17, 19], 17: [9, 16, 18], | |
| 18: [17, 20], 19: [11, 16, 21], 20: [12, 18, 23], | |
| 21: [19, 22], 22: [14, 21, 23], 23: [20, 22] | |
| } | |
| def get_nearest_position(mouse_pos): | |
| """Find the nearest valid position to the mouse click""" | |
| min_distance = float('inf') | |
| nearest_pos = None | |
| nearest_index = None | |
| for i, pos in enumerate(player_positions): | |
| distance = ((mouse_pos[0] - pos[0])**2 + (mouse_pos[1] - pos[1])**2)**0.5 | |
| if distance < min_distance and distance < 30: # 30 pixel tolerance | |
| min_distance = distance | |
| nearest_pos = pos | |
| nearest_index = i | |
| return nearest_index, nearest_pos | |
| def check_mill(player_color): | |
| """Check if the player has formed a mill (3 in a row)""" | |
| # Define all possible mill combinations based on the actual board layout | |
| mill_combinations = [ | |
| # Outer square horizontal lines | |
| [0, 1, 2], # Top horizontal | |
| [5, 6, 7], # Bottom horizontal | |
| # Outer square vertical lines | |
| [0, 3, 5], # Left vertical | |
| [2, 4, 7], # Right vertical | |
| # Middle square horizontal lines | |
| [8, 9, 10], # Top horizontal | |
| [13, 14, 15], # Bottom horizontal | |
| # Middle square vertical lines | |
| [8, 11, 13], # Left vertical | |
| [10, 12, 15], # Right vertical | |
| # Inner square horizontal lines | |
| [16, 17, 18], # Top horizontal | |
| [21, 22, 23], # Bottom horizontal | |
| # Inner square vertical lines | |
| [16, 19, 21], # Left vertical | |
| [18, 20, 23], # Right vertical | |
| # Connecting lines (vertical) | |
| [1, 9, 17], # Top center vertical | |
| [6, 14, 22], # Bottom center vertical | |
| # Connecting lines (horizontal) | |
| [3, 11, 19], # Left center horizontal | |
| [4, 12, 20] # Right center horizontal | |
| ] | |
| # Check each mill combination | |
| for mill in mill_combinations: | |
| if all(pos in placed_pieces and placed_pieces[pos] == player_color for pos in mill): | |
| return True, mill | |
| return False, None | |
| def is_piece_in_mill(position, player_color): | |
| """Check if a specific piece is part of a mill""" | |
| mill_combinations = [ | |
| # Outer square horizontal lines | |
| [0, 1, 2], [5, 6, 7], | |
| # Outer square vertical lines | |
| [0, 3, 5], [2, 4, 7], | |
| # Middle square horizontal lines | |
| [8, 9, 10], [13, 14, 15], | |
| # Middle square vertical lines | |
| [8, 11, 13], [10, 12, 15], | |
| # Inner square horizontal lines | |
| [16, 17, 18], [21, 22, 23], | |
| # Inner square vertical lines | |
| [16, 19, 21], [18, 20, 23], | |
| # Connecting lines | |
| [1, 9, 17], [6, 14, 22], [3, 11, 19], [4, 12, 20] | |
| ] | |
| for mill in mill_combinations: | |
| if position in mill and all(pos in placed_pieces and placed_pieces[pos] == player_color for pos in mill): | |
| return True | |
| return False | |
| def get_removable_pieces(opponent_color): | |
| """Get list of opponent pieces that can be removed (not in mills unless no other choice)""" | |
| opponent_pieces = [pos for pos, color in placed_pieces.items() if color == opponent_color] | |
| removable = [pos for pos in opponent_pieces if not is_piece_in_mill(pos, opponent_color)] | |
| # If all pieces are in mills, any piece can be removed | |
| if not removable: | |
| removable = opponent_pieces | |
| return removable | |
| def count_pieces(player_color): | |
| """Count how many pieces a player has on the board""" | |
| return sum(1 for color in placed_pieces.values() if color == player_color) | |
| def can_move(player_color): | |
| """Check if player can make any legal moves""" | |
| player_pieces = [pos for pos, color in placed_pieces.items() if color == player_color] | |
| # In jumping phase (3 pieces), can always move to any free position | |
| if len(player_pieces) == 3: | |
| return len(placed_pieces) < 24 # There are free positions | |
| # In movement phase, check if any piece can move to adjacent position | |
| for piece_pos in player_pieces: | |
| for adjacent_pos in adjacent_positions.get(piece_pos, []): | |
| if adjacent_pos not in placed_pieces: | |
| return True | |
| return False | |
| def update_game_phase(): | |
| """Update the current game phase based on pieces placed and remaining""" | |
| global game_phase | |
| if black_pieces_placed < 9 or white_pieces_placed < 9: | |
| game_phase = "PLACEMENT" | |
| else: | |
| black_count = count_pieces("BLACK") | |
| white_count = count_pieces("WHITE") | |
| if black_count == 3 or white_count == 3: | |
| game_phase = "JUMPING" | |
| else: | |
| game_phase = "MOVEMENT" | |
| def draw_board(): | |
| """Draw the Nine Men's Morris board""" | |
| # Clear the screen with a light background | |
| screen.fill((240, 240, 240)) | |
| # Draw the three squares with padding offset | |
| # Outer square | |
| pygame.draw.rect(screen, BLACK, (padding + 75, padding + 75, game_width-150, game_height-150), 3) | |
| # Middle square | |
| pygame.draw.rect(screen, BLACK, (padding + 175, padding + 175, game_width-350, game_height-350), 3) | |
| # Inner square | |
| pygame.draw.rect(screen, BLACK, (padding + 275, padding + 275, game_width-550, game_height-550), 3) | |
| # Draw connecting lines | |
| # Top center line | |
| pygame.draw.line(screen, BLACK, (padding + game_width//2, padding + 75), (padding + game_width//2, padding + 275), 3) | |
| # Bottom center line | |
| pygame.draw.line(screen, BLACK, (padding + game_width//2, padding + game_height-275), (padding + game_width//2, padding + game_height-75), 3) | |
| # Left center line | |
| pygame.draw.line(screen, BLACK, (padding + 75, padding + game_height//2), (padding + 275, padding + game_height//2), 3) | |
| # Right center line | |
| pygame.draw.line(screen, BLACK, (padding + game_width-275, padding + game_height//2), (padding + game_width-75, padding + game_height//2), 3) | |
| def draw_piece_stacks(): | |
| """Draw stacks of pieces: unplaced pieces on left, captured pieces on right""" | |
| # Draw remaining pieces to be placed (left side) | |
| black_remaining = 9 - black_pieces_placed | |
| white_remaining = 9 - white_pieces_placed | |
| # Black pieces stack (bottom left) | |
| stack_x = 30 | |
| stack_y = window_height - 50 | |
| for i in range(black_remaining): | |
| y_offset = stack_y - (i * 5) # Stack them slightly overlapping | |
| pygame.draw.circle(screen, BLACK, (stack_x, y_offset), 12) | |
| pygame.draw.circle(screen, WHITE, (stack_x, y_offset), 12, 2) | |
| # White pieces stack (bottom left, next to black) | |
| stack_x = 70 | |
| for i in range(white_remaining): | |
| y_offset = stack_y - (i * 5) | |
| pygame.draw.circle(screen, WHITE, (stack_x, y_offset), 12) | |
| pygame.draw.circle(screen, BLACK, (stack_x, y_offset), 12, 2) | |
| # Draw captured pieces (right side) | |
| # Black captured pieces (bottom right) | |
| captured_x = window_width - 70 | |
| for i, piece in enumerate(white_captured_pieces): # White pieces captured by black | |
| y_offset = stack_y - (i * 5) | |
| pygame.draw.circle(screen, WHITE, (captured_x, y_offset), 12) | |
| pygame.draw.circle(screen, BLACK, (captured_x, y_offset), 12, 2) | |
| # White captured pieces (bottom right, next to black captured) | |
| captured_x = window_width - 30 | |
| for i, piece in enumerate(black_captured_pieces): # Black pieces captured by white | |
| y_offset = stack_y - (i * 5) | |
| pygame.draw.circle(screen, BLACK, (captured_x, y_offset), 12) | |
| pygame.draw.circle(screen, WHITE, (captured_x, y_offset), 12, 2) | |
| # Add labels | |
| font_small = pygame.font.Font(None, 20) | |
| if game_phase == "PLACEMENT": | |
| left_label = font_small.render("To Place", True, BLACK) | |
| screen.blit(left_label, (10, window_height - 20)) | |
| right_label = font_small.render("Captured", True, BLACK) | |
| screen.blit(right_label, (window_width - 80, window_height - 20)) | |
| # Main loop | |
| running = True | |
| clock = pygame.time.Clock() | |
| while running: | |
| for event in pygame.event.get(): | |
| if event.type == pygame.QUIT: | |
| running = False | |
| elif event.type == pygame.MOUSEBUTTONDOWN: | |
| mouse_pos = pygame.mouse.get_pos() | |
| position_index, position = get_nearest_position(mouse_pos) | |
| if game_won or position_index is None: | |
| continue | |
| # Handle piece removal after mill formation | |
| if awaiting_removal: | |
| opponent_color = "WHITE" if current_player == "BLACK" else "BLACK" | |
| removable_pieces = get_removable_pieces(opponent_color) | |
| # Check if the clicked position has an opponent piece and is removable | |
| if (position_index in placed_pieces and | |
| placed_pieces[position_index] == opponent_color and | |
| position_index in removable_pieces): | |
| # Add the removed piece to captured list | |
| if opponent_color == "BLACK": | |
| black_captured_pieces.append(position_index) | |
| else: | |
| white_captured_pieces.append(position_index) | |
| del placed_pieces[position_index] | |
| awaiting_removal = False | |
| mill_message = f"{opponent_color} piece removed!" | |
| mill_timer = 120 | |
| # Check win condition (only after placement phase is complete) | |
| if black_pieces_placed == 9 and white_pieces_placed == 9: | |
| opponent_count = count_pieces(opponent_color) | |
| if opponent_count < 3: | |
| game_won = True | |
| winner = current_player | |
| mill_message = f"{current_player} WINS!" | |
| mill_timer = 300 | |
| elif not can_move(opponent_color): | |
| game_won = True | |
| winner = current_player | |
| mill_message = f"{current_player} WINS! (Blockade)" | |
| mill_timer = 300 | |
| else: | |
| # Switch player only if game continues | |
| current_player = opponent_color | |
| update_game_phase() | |
| else: | |
| # During placement phase, just switch player | |
| current_player = opponent_color | |
| update_game_phase() | |
| else: | |
| # If clicked somewhere else during removal, do nothing (stay in removal mode) | |
| pass | |
| continue | |
| # PLACEMENT PHASE | |
| if game_phase == "PLACEMENT": | |
| if position_index not in placed_pieces: | |
| placed_pieces[position_index] = current_player | |
| if current_player == "BLACK": | |
| black_pieces_placed += 1 | |
| else: | |
| white_pieces_placed += 1 | |
| # Check for mill formation | |
| has_mill, mill_positions = check_mill(current_player) | |
| if has_mill: | |
| opponent_color = "WHITE" if current_player == "BLACK" else "BLACK" | |
| removable_pieces = get_removable_pieces(opponent_color) | |
| if removable_pieces: | |
| awaiting_removal = True | |
| mill_message = f"{current_player} Mühle! Remove opponent piece" | |
| mill_timer = 300 | |
| else: | |
| # Switch player if no pieces can be removed | |
| current_player = opponent_color | |
| else: | |
| # Switch player if no mill formed | |
| current_player = "WHITE" if current_player == "BLACK" else "BLACK" | |
| update_game_phase() | |
| # MOVEMENT PHASE | |
| elif game_phase == "MOVEMENT": | |
| if selected_piece is None: | |
| # Select a piece to move | |
| if position_index in placed_pieces and placed_pieces[position_index] == current_player: | |
| # Check if this piece can move | |
| can_move_piece = any(adj not in placed_pieces for adj in adjacent_positions.get(position_index, [])) | |
| if can_move_piece: | |
| selected_piece = position_index | |
| else: | |
| # Move the selected piece | |
| if (position_index not in placed_pieces and | |
| position_index in adjacent_positions.get(selected_piece, [])): | |
| # Move the piece | |
| placed_pieces[position_index] = current_player | |
| del placed_pieces[selected_piece] | |
| selected_piece = None | |
| # Check for mill formation | |
| has_mill, mill_positions = check_mill(current_player) | |
| if has_mill: | |
| opponent_color = "WHITE" if current_player == "BLACK" else "BLACK" | |
| removable_pieces = get_removable_pieces(opponent_color) | |
| if removable_pieces: | |
| awaiting_removal = True | |
| mill_message = f"{current_player} Mühle! Remove opponent piece" | |
| mill_timer = 300 | |
| else: | |
| current_player = opponent_color | |
| else: | |
| current_player = "WHITE" if current_player == "BLACK" else "BLACK" | |
| else: | |
| # Deselect if invalid move | |
| selected_piece = None | |
| update_game_phase() | |
| # JUMPING PHASE (when player has only 3 pieces) | |
| elif game_phase == "JUMPING": | |
| current_player_pieces = count_pieces(current_player) | |
| if current_player_pieces == 3: | |
| if selected_piece is None: | |
| # Select a piece to move | |
| if position_index in placed_pieces and placed_pieces[position_index] == current_player: | |
| selected_piece = position_index | |
| else: | |
| # Jump to any free position | |
| if position_index not in placed_pieces: | |
| # Move the piece | |
| placed_pieces[position_index] = current_player | |
| del placed_pieces[selected_piece] | |
| selected_piece = None | |
| # Check for mill formation | |
| has_mill, mill_positions = check_mill(current_player) | |
| if has_mill: | |
| opponent_color = "WHITE" if current_player == "BLACK" else "BLACK" | |
| removable_pieces = get_removable_pieces(opponent_color) | |
| if removable_pieces: | |
| awaiting_removal = True | |
| mill_message = f"{current_player} Mühle! Remove opponent piece" | |
| mill_timer = 300 | |
| else: | |
| current_player = opponent_color | |
| else: | |
| current_player = "WHITE" if current_player == "BLACK" else "BLACK" | |
| else: | |
| # Deselect if invalid move | |
| selected_piece = None | |
| else: | |
| # Handle normal movement for player with more than 3 pieces | |
| if selected_piece is None: | |
| if position_index in placed_pieces and placed_pieces[position_index] == current_player: | |
| can_move_piece = any(adj not in placed_pieces for adj in adjacent_positions.get(position_index, [])) | |
| if can_move_piece: | |
| selected_piece = position_index | |
| else: | |
| if (position_index not in placed_pieces and | |
| position_index in adjacent_positions.get(selected_piece, [])): | |
| placed_pieces[position_index] = current_player | |
| del placed_pieces[selected_piece] | |
| selected_piece = None | |
| has_mill, mill_positions = check_mill(current_player) | |
| if has_mill: | |
| opponent_color = "WHITE" if current_player == "BLACK" else "BLACK" | |
| removable_pieces = get_removable_pieces(opponent_color) | |
| if removable_pieces: | |
| awaiting_removal = True | |
| mill_message = f"{current_player} Mühle! Remove opponent piece" | |
| mill_timer = 300 | |
| else: | |
| current_player = opponent_color | |
| else: | |
| current_player = "WHITE" if current_player == "BLACK" else "BLACK" | |
| else: | |
| selected_piece = None | |
| update_game_phase() | |
| elif event.type == pygame.KEYDOWN: | |
| if event.key == pygame.K_r and game_won: | |
| # Reset the game | |
| current_player = "BLACK" | |
| placed_pieces = {} | |
| mill_message = "" | |
| mill_timer = 0 | |
| black_mills = 0 | |
| white_mills = 0 | |
| game_won = False | |
| winner = "" | |
| game_phase = "PLACEMENT" | |
| black_pieces_placed = 0 | |
| white_pieces_placed = 0 | |
| selected_piece = None | |
| awaiting_removal = False | |
| last_mill_formed = None | |
| black_captured_pieces = [] | |
| white_captured_pieces = [] | |
| # Draw the board | |
| draw_board() | |
| # Draw piece stacks | |
| draw_piece_stacks() | |
| # Draw coordinate markers for player positions | |
| for i, pos in enumerate(player_positions): | |
| # Draw a circle at each position | |
| if i in placed_pieces: | |
| # Draw placed piece | |
| piece_color = BLACK if placed_pieces[i] == "BLACK" else WHITE | |
| border_color = WHITE if placed_pieces[i] == "BLACK" else BLACK | |
| # Highlight selected piece | |
| if i == selected_piece: | |
| pygame.draw.circle(screen, RED, pos, 20, 3) | |
| pygame.draw.circle(screen, piece_color, pos, 15) | |
| pygame.draw.circle(screen, border_color, pos, 15, 2) | |
| # Highlight removable pieces | |
| if awaiting_removal: | |
| opponent_color = "WHITE" if current_player == "BLACK" else "BLACK" | |
| removable_pieces = get_removable_pieces(opponent_color) | |
| if i in removable_pieces: | |
| pygame.draw.circle(screen, RED, pos, 20, 2) | |
| else: | |
| # Show possible moves for selected piece | |
| if selected_piece is not None: | |
| if game_phase == "JUMPING" and count_pieces(current_player) == 3: | |
| # In jumping phase, can move to any free position | |
| pygame.draw.circle(screen, GREEN, pos, 8, 2) | |
| elif i in adjacent_positions.get(selected_piece, []): | |
| # In movement phase, show adjacent positions | |
| pygame.draw.circle(screen, GREEN, pos, 8, 2) | |
| # Position numbers are now hidden | |
| # Display current player and game status | |
| if not game_won: | |
| font_large = pygame.font.Font(None, 36) | |
| if awaiting_removal: | |
| player_text = font_large.render(f"{current_player}: Remove opponent piece", True, RED) | |
| else: | |
| phase_text = game_phase.title() | |
| player_text = font_large.render(f"{current_player} - {phase_text} Phase", True, BLACK) | |
| # Position the text at the top right of the screen | |
| text_rect = player_text.get_rect() | |
| text_rect.right = window_width - 15 | |
| text_rect.y = 10 | |
| # Draw background for text | |
| pygame.draw.rect(screen, WHITE, text_rect.inflate(20, 10)) | |
| pygame.draw.rect(screen, BLACK, text_rect.inflate(20, 10), 2) | |
| screen.blit(player_text, text_rect) | |
| # Display piece counts and phase info | |
| font_counter = pygame.font.Font(None, 24) | |
| # Piece counts on board | |
| black_on_board = count_pieces("BLACK") | |
| white_on_board = count_pieces("WHITE") | |
| if game_phase == "PLACEMENT": | |
| black_text = f"BLACK: {black_pieces_placed}/9 placed, {black_on_board} on board" | |
| white_text = f"WHITE: {white_pieces_placed}/9 placed, {white_on_board} on board" | |
| else: | |
| black_text = f"BLACK: {black_on_board} pieces on board" | |
| white_text = f"WHITE: {white_on_board} pieces on board" | |
| black_counter_text = font_counter.render(black_text, True, BLACK) | |
| white_counter_text = font_counter.render(white_text, True, BLACK) | |
| screen.blit(black_counter_text, (10, 10)) | |
| screen.blit(white_counter_text, (10, 35)) | |
| # Display mill message if active | |
| if mill_timer > 0: | |
| mill_timer -= 1 | |
| font_mill = pygame.font.Font(None, 48) | |
| mill_color = GREEN if game_won else RED | |
| mill_text = font_mill.render(mill_message, True, mill_color) | |
| mill_rect = mill_text.get_rect() | |
| mill_rect.centerx = window_width // 2 | |
| mill_rect.y = window_height // 2 - 50 | |
| # Draw background for mill message | |
| pygame.draw.rect(screen, WHITE, mill_rect.inflate(30, 15)) | |
| pygame.draw.rect(screen, mill_color, mill_rect.inflate(30, 15), 3) | |
| screen.blit(mill_text, mill_rect) | |
| # If game is won, always show restart instruction | |
| if game_won: | |
| restart_text = font_counter.render("Press R to restart", True, BLACK) | |
| restart_rect = restart_text.get_rect() | |
| restart_rect.centerx = window_width // 2 | |
| restart_rect.y = window_height // 2 + 20 | |
| # Draw background for restart text | |
| pygame.draw.rect(screen, WHITE, restart_rect.inflate(20, 10)) | |
| pygame.draw.rect(screen, BLACK, restart_rect.inflate(20, 10), 2) | |
| screen.blit(restart_text, restart_rect) | |
| # Update the display | |
| pygame.display.flip() | |
| clock.tick(60) | |
| # Quit pygame | |
| pygame.quit() | |
| sys.exit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment