-
Notifications
You must be signed in to change notification settings - Fork 0
/
battlestar_generator.py
158 lines (141 loc) · 4.77 KB
/
battlestar_generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import pickle
import os.path
import numpy as np
from dataclasses import dataclass
from sbdrawer import StarBattleDrawer
## backtracker to generate star configs
@dataclass
class ProblemSpecs:
size: int
n_stars: int
candidates: list
successors: list
boards: list
def compute_candidates(size, n_stars):
def recurse(candidates, cols=[]):
if len(cols) == n_stars:
candidates.append(cols)
return
for c in range(cols[-1] + 2 if cols else 0, size - 2 * (n_stars - len(cols) - 1)):
recurse(candidates, cols + [c])
candidates = []; recurse(candidates)
return candidates
def compute_successors(candidates):
successors = [[] for _ in range(len(candidates))]
def compare_candidates(candidates, i, j):
for c1 in candidates[i]:
for c2 in candidates[j]:
if abs(c2 - c1) <= 1:
return False
return True
for i in range(len(candidates)):
for j in range(len(candidates)):
if compare_candidates(candidates,i, j):
successors[i].append(j)
return successors
def backtrack(specs, board, col_counts, successors, row):
if row == specs.size:
specs.boards.append(tuple(board))
return
for s in successors:
cols = specs.candidates[s]
valid = True
for col in cols:
if col_counts[col] == specs.n_stars:
valid = False
break
if valid:
board[row] = cols
for col in cols:
col_counts[col] += 1
backtrack(specs, board, col_counts, specs.successors[s], row + 1)
board[row] = None
for col in cols:
col_counts[col] -= 1
def generate_star_configs(S, N):
candidates = compute_candidates(S, N)
successors = compute_successors(candidates)
specs = ProblemSpecs(
size=S,
n_stars=N,
candidates=candidates,
successors=successors,
boards=[]
)
backtrack(specs, [None] * S, [0] * S, range(len(candidates)), 0)
return specs.boards
def matrixify_star_configs(S, star_configs):
mat = np.zeros((len(star_configs), S, S), dtype=int)
for i, star_config in enumerate(star_configs):
for r in range(S):
for c in star_config[r]:
mat[i, r, c] = 1
return mat
## puzzle generator
def generate_puzzle(S, N, temperature, star_configs):
while True:
# init board
board = np.zeros((S, S), dtype=int)
# sample solution
idx = np.random.randint(star_configs.shape[0])
solution = star_configs[idx]
mask = 1 - solution
# copy database
remaining_sols = np.delete(star_configs, idx, axis=0)
# place blocks
while remaining_sols.shape[0] > 0:
# compute star frequencies
star_freqs = remaining_sols.mean(0) * mask
# generate distribution (weighted boltzmann thing)
logits = np.exp(temperature * (star_freqs - star_freqs.max())) * star_freqs
p = logits / logits.sum()
# sample block
block = np.random.choice(S * S, p=p.flatten())
board[block // S, block % S] = 1
# prune solutions
remaining_sols = remaining_sols[(remaining_sols * board).sum(2).sum(1) == 0]
# extra constraints (filter boards, etc.)
if not ((board.sum(0) == S - N).any() or (board.sum(1) == S - N).any()):
break
return board, solution
if __name__ == '__main__':
import sys
# generator settings
S = int(sys.argv[1])
N = int(sys.argv[2])
temperature = int(sys.argv[3])
n_puzzles = int(sys.argv[4])
# star config database
if os.path.exists(f'star_configs_{S}_{N}.p'):
print('> loading star configs...')
with open(f'star_configs_{S}_{N}.p', 'rb') as file:
star_configs = pickle.load(file)
else:
print('> generating star configs...')
star_configs = generate_star_configs(S, N)
with open(f'star_configs_{S}_{N}.p', 'wb') as file:
pickle.dump(star_configs, file)
# unpack star configs
print('> processing star configs...')
star_configs = matrixify_star_configs(S, star_configs)
# puzzle drawer
drawer = StarBattleDrawer(
cell_w=33,
border_w=4,
out_bg_color=[0, 0, 0, 255],
out_cell_color=[0, 0, 0, 255]
)
# generate
print('> generating...')
for i in range(1, n_puzzles + 1):
board, solution = generate_puzzle(S, N, temperature, star_configs)
# output
print('board:')
print('\n'.join(''.join('▓▓' if board[r, c] == 1 else '░░' for c in range(S)) for r in range(S)))
print('solution:')
print('\n'.join(' '.join('*' if solution[r, c] == 1 else '.' for c in range(S)) for r in range(S)))
drawer.draw(
out_file=f'battlestar_{S}_{N}_{temperature}_{i}.png',
regions_str='\n'.join([''.join(['.' if x == 1 else 'a' for x in board[r]]) for r in range(S)])
)
print(f'> saved battlestar_{S}_{N}_{temperature}_{i}.png')