Skip to content

Instantly share code, notes, and snippets.

@phoenixthrush
Last active August 13, 2025 01:59
Show Gist options
  • Select an option

  • Save phoenixthrush/5334b402664280f171f04f193f6053dd to your computer and use it in GitHub Desktop.

Select an option

Save phoenixthrush/5334b402664280f171f04f193f6053dd to your computer and use it in GitHub Desktop.
Nine Men's Morris in Python #Python #NineMenMorris
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