python code tweaked from gemini
import random
import math
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from collections import deque
from tqdm import tqdm
custom_board_1 = [
[True, False, True, False, True, False, True, True],
[False, True, False, True, False, True, False, True],
[True, False, True, False, True, False, True, False],
[False, True, False, True, False, True, False, True],
[True, False, True, False, True, False, True, False],
[True, True, False, True, False, True, False, True],
[False, False, True, False, True, False, True, False],
[True, True, False, True, False, True, False, True]
]
fixed_1_mask = [[False]*8,
[False]*8,
[False]*8,
[False]*8,
[False]*8,
[True, True, False, False, False, False, False, False],
[True, True, True, False, False, False, False, False],
[True, True, True, True, False, False, False, False]
]
custom_board_2 = [
[True, True, False, True, False, True, False, True],
[True, False, True, False, True, False, True, False],
[False, True, False, True, False, True, False, True],
[True, False, True, False, True, False, True, False],
[True, True, False, True, False, True, False, True],
[False, True, True, False, True, False, True, False],
[True, False, False, True, False, True, False, True],
[False, True, True, False, True, False, True, True]
]
fixed_1 = [r for r in zip(np.nonzero(fixed_1_mask)[0],np.nonzero(fixed_1_mask)[1])]
custom_board_3 = [
[True, True, True, False, True, False, True, True],
[True, True, False, True, False, True, False, True],
[True, False, True, False, True, False, True, False],
[False, True, False, True, False, True, False, True],
[True, False, True, False, True, False, True, False],
[True, True, False, True, False, True, False, True],
[False, False, True, False, True, False, True, True],
[True, True, False, True, False, True, True, True]
]
custom_board_4 = [
[True, True, True, False, True, True, True, True],
[True, True, False, True, False, True, True, True],
[True, False, True, False, True, False, True, True],
[False, True, False, True, False, True, False, True],
[True, False, True, False, True, False, True, False],
[True, True, False, True, False, True, False, True],
[False, False, True, False, True, False, True, True],
[True, True, False, True, False, True, True, True]
]
class GoBoardPuzzleSA:
def __init__(self, board_size=8, target_groups=9, target_sizes=range(1, 10), target_total_stones=45):#,fixed=[]):
self.board_size = board_size
self.target_groups = target_groups
self.target_sizes = set(target_sizes)
self.target_total_stones = target_total_stones
#self.fixed = fixed
def _get_black_groups(self, board):
visited = np.zeros_like(board, dtype=bool)
groups = []
for r in range(self.board_size):
for c in range(self.board_size):
if board[r, c] and not visited[r, c]:
group = []
q = deque([(r, c)])
visited[r, c] = True
group.append((r, c))
while q:
row, col = q.popleft()
for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
nr, nc = row + dr, col + dc
if 0 <= nr < self.board_size and 0 <= nc < self.board_size and \
board[nr, nc] and not visited[nr, nc]:
visited[nr, nc] = True
group.append((nr, nc))
q.append((nr, nc))
groups.append(tuple(sorted(group)))
return tuple(sorted(groups))
def _calculate_energy(self, board):
groups = self._get_black_groups(board)
num_groups = len(groups)
group_sizes = set(len(g) for g in groups)
total_stones = np.sum(board)
energy = 0
# Penalty for not having the target number of groups
energy += abs(num_groups - self.target_groups) * 30
# Penalty for not having the target group sizes
energy += len(self.target_sizes.symmetric_difference(group_sizes)) * np.prod(list(self.target_sizes.symmetric_difference(group_sizes)))
# Penalty for not having the target total number of stones
energy += abs(total_stones - self.target_total_stones) * 10
# Bonus for having the exact target configuration (0 energy is ideal)
if num_groups == self.target_groups and group_sizes == self.target_sizes and total_stones == self.target_total_stones:
return 0
return energy
def _get_neighbor(self, board):
new_board = np.copy(board)
empty_spaces = [(r, c) for r in range(self.board_size) for c in range(self.board_size) if not new_board[r, c]]
black_stones = [(r, c) for r in range(self.board_size) for c in range(self.board_size) if new_board[r, c]]
if not empty_spaces and not black_stones:
return new_board
operation = random.choice(['add', 'remove'])
if operation == 'add' and empty_spaces:
r, c = random.choice(empty_spaces)
new_board[r, c] = True #^ ((r,c) in self.fixed)
elif operation == 'remove' and black_stones:
r, c = random.choice(black_stones)
new_board[r, c] = False #^ ((r,c) in self.fixed)
return new_board
def solve(self, temp_start=100, temp_end=0.1, cooling_rate=0.99, max_iterations=10000, initial_board=None):
if initial_board is None:
current_board = np.zeros((self.board_size, self.board_size), dtype=bool)
elif initial_board == 'checkerboard':
current_board = np.array([[bool((r + c) % 2) for c in range(self.board_size)] for r in range(self.board_size)])
else:
if np.array(initial_board).shape == (self.board_size, self.board_size) and np.array(initial_board).dtype == bool:
current_board = np.array(initial_board)
else:
print("Warning: Invalid custom initial board provided. Starting with an empty board.")
current_board = np.zeros((self.board_size, self.board_size), dtype=bool)
current_energy = self._calculate_energy(current_board)
best_board = np.copy(current_board)
best_energy = current_energy
temperature = temp_start
for i in range(max_iterations):
if temperature <= temp_end:
break
next_board = self._get_neighbor(current_board)
next_energy = self._calculate_energy(next_board)
delta_energy = next_energy - current_energy
acceptance_probability = 1.0
if delta_energy > 0:
acceptance_probability = math.exp(-delta_energy / temperature)
if random.random() < acceptance_probability:
current_board = next_board
current_energy = next_energy
if current_energy < best_energy:
best_energy = current_energy
best_board = np.copy(current_board)
# print(f"Iteration {i}, Temperature: {temperature:.2f}, Best Energy: {best_energy}")
if best_energy == 0:
print("Perfect solution found!")
return best_board, best_energy
temperature *= cooling_rate
# print(f"Run finished. Best Energy: {best_energy}")
return best_board, best_energy
def plot_board(self, board, ax=None, title="Go Board"):
if ax is None:
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_xlim(-0.5, self.board_size - 0.5)
ax.set_ylim(-0.5, self.board_size - 0.5)
ax.set_xticks(range(self.board_size))
ax.set_yticks(range(self.board_size))
ax.grid(True, color='black', linewidth=1)
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.invert_yaxis()
ax.set_aspect('equal')
ax.set_title(title)
groups = self._get_black_groups(board)
num_groups = len(groups)
colors = plt.cm.viridis(np.linspace(0, 1, num_groups))
for i, group in enumerate(groups):
color = colors[i]
for r, c in group:
circle = patches.Circle((c, r), radius=0.45, color=color, edgecolor='black')
ax.add_patch(circle)
ax.text(c, r, str(i + 1), ha='center', va='center', color='white', fontsize=8)
return ax
puzzle_sa = GoBoardPuzzleSA()
num_retries = 500
best_solution = None
best_energy_overall = float('inf')
total_energy = 0
for retry in tqdm(range(num_retries)):
# print(f"\n--- Retry {retry + 1} ---")
solution_board, final_energy = puzzle_sa.solve(max_iterations=10000, initial_board=custom_board_4)
# solution_board, final_energy = puzzle_sa.solve(max_iterations=10000, initial_board=[[True]*8]*8)
# solution_board, final_energy = puzzle_sa.solve(max_iterations=10000, initial_board='checkerboard')
# solution_board, final_energy = puzzle_sa.solve(max_iterations=10000)
total_energy += final_energy
if final_energy < best_energy_overall:
best_energy_overall = final_energy
best_solution = solution_board
print(f"Run finished. Best Energy: {best_energy_overall}")
if final_energy == 0:
print("\nPerfect solution found, breaking out of the loop.")
break
if best_solution is not None:
print("\n--- Best Result Across All Retries ---")
print(f"Best Energy: {best_energy_overall}")
fig, ax = plt.subplots(figsize=(8, 8))
puzzle_sa.plot_board(best_solution, ax=ax, title=f"Best SA Result (Energy: {best_energy_overall})")
plt.show()
final_groups = puzzle_sa._get_black_groups(best_solution)
final_group_sizes = sorted(len(g) for g in final_groups)
final_total_stones = np.sum(best_solution)
print("Final Number of Groups:", len(final_groups))
print("Final Group Sizes:", final_group_sizes)
print("Final Total Stones:", final_total_stones)
print("Average Energy:", total_energy/num_retries)
else:
print("Simulated Annealing failed to find any solution across multiple retries.")
I was doing tweaking here and there which I’ve left commented out in some cases. I was also tweaking the energy penalties for the different aspects, like how much it’s punished for not having exactly 9 groups, or exactly the sizes 1-9, or not having 45 stones.
It seems quite fast though, but doesn’t get there either, though it looks cool enough