tilingPuzzles 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tilingpuzzles/__init__.py +6 -0
- tilingpuzzles/benchmark/README.md +10 -0
- tilingpuzzles/benchmark/__init__.py +0 -0
- tilingpuzzles/benchmark/data/timingResulsts.csv +193 -0
- tilingpuzzles/benchmark/git_state.py +22 -0
- tilingpuzzles/benchmark/run_benchmark.py +105 -0
- tilingpuzzles/examples/README.md +3 -0
- tilingpuzzles/examples/__init__.py +0 -0
- tilingpuzzles/examples/rectangularPentomino.py +40 -0
- tilingpuzzles/examples/scaledStones.py +163 -0
- tilingpuzzles/examples/tests/__init__.py +0 -0
- tilingpuzzles/examples/tests/test_rectangularPentomino.py +24 -0
- tilingpuzzles/examples/tests/test_scaledStones.py +8 -0
- tilingpuzzles/games/__init__.py +2 -0
- tilingpuzzles/games/game.py +20 -0
- tilingpuzzles/games/generic.py +7 -0
- tilingpuzzles/games/komino.py +147 -0
- tilingpuzzles/games/realisations.py +78 -0
- tilingpuzzles/games/stone.py +536 -0
- tilingpuzzles/games/stone_core.py +48 -0
- tilingpuzzles/games/tests/__init__.py +0 -0
- tilingpuzzles/games/tests/test_game.py +7 -0
- tilingpuzzles/games/tests/test_komino.py +30 -0
- tilingpuzzles/games/tests/test_realisations.py +28 -0
- tilingpuzzles/games/tests/test_stone.py +172 -0
- tilingpuzzles/games/tests/test_tile.py +19 -0
- tilingpuzzles/games/tile.py +39 -0
- tilingpuzzles/logUtils/__init__.py +0 -0
- tilingpuzzles/logUtils/callGraph.py +47 -0
- tilingpuzzles/logger.py +39 -0
- tilingpuzzles/solvers/__init__.py +0 -0
- tilingpuzzles/solvers/hights.py +3 -0
- tilingpuzzles/solvers/kominoSolver.py +191 -0
- tilingpuzzles/solvers/tests/test_komino_solver.py +30 -0
- tilingpuzzles/visualize/__init__.py +0 -0
- tilingpuzzles/visualize/visualize.py +61 -0
- tilingpuzzles-0.2.0.dist-info/METADATA +44 -0
- tilingpuzzles-0.2.0.dist-info/RECORD +40 -0
- tilingpuzzles-0.2.0.dist-info/WHEEL +4 -0
- tilingpuzzles-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,536 @@
|
|
1
|
+
|
2
|
+
from __future__ import annotations
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
from tilingpuzzles.visualize.visualize import Visualize
|
6
|
+
from tilingpuzzles.logUtils.callGraph import GTracker
|
7
|
+
import queue
|
8
|
+
|
9
|
+
from . import stone,tile
|
10
|
+
|
11
|
+
from logging import info
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
class Stone_Symetries(list):
|
16
|
+
|
17
|
+
def __init__(self,symertries=None):
|
18
|
+
super().__init__()
|
19
|
+
|
20
|
+
if symertries is None:
|
21
|
+
symertries=[
|
22
|
+
|
23
|
+
Stone_Symetries.rotate,
|
24
|
+
Stone_Symetries.flip
|
25
|
+
]
|
26
|
+
self+=symertries
|
27
|
+
|
28
|
+
|
29
|
+
def __call__(self,stone:Stone):
|
30
|
+
res=set()
|
31
|
+
checkStack=[stone.shift_positive()]
|
32
|
+
while checkStack:
|
33
|
+
front=checkStack.pop()
|
34
|
+
if front in res:
|
35
|
+
continue
|
36
|
+
res.add(front)
|
37
|
+
for sym in self:
|
38
|
+
new_st:Stone=sym(front)
|
39
|
+
checkStack.append(new_st.shift_positive())
|
40
|
+
return res
|
41
|
+
|
42
|
+
pass
|
43
|
+
|
44
|
+
|
45
|
+
def rotate(stone:Stone) -> Stone:
|
46
|
+
new_tiles=[ (ty,-tx) for (tx,ty) in stone]
|
47
|
+
return Stone(new_tiles)
|
48
|
+
|
49
|
+
def flip(stone:Stone) -> Stone:
|
50
|
+
new_tiles=[(tx,-ty) for (tx,ty) in stone]
|
51
|
+
return Stone(new_tiles)
|
52
|
+
|
53
|
+
pass
|
54
|
+
|
55
|
+
class Stone_config():
|
56
|
+
K_STONE_MAX_LAYER_SIZE=1000
|
57
|
+
# Maximum number of stones in a layer until a assertion error is trown
|
58
|
+
SYMS=Stone_Symetries()
|
59
|
+
MaxCacheStoneSize:int=0
|
60
|
+
|
61
|
+
class Stone(frozenset):
|
62
|
+
"""
|
63
|
+
Frozen set of tiles
|
64
|
+
All sets of tiles are considered as a stone
|
65
|
+
"""
|
66
|
+
# change to frozenset to make it hashable
|
67
|
+
|
68
|
+
_NormalizeCache={}
|
69
|
+
|
70
|
+
def __new__(cls,*tiles):
|
71
|
+
tiles=map(tile.Tile,*tiles)
|
72
|
+
return super(Stone,cls).__new__(cls,tiles)
|
73
|
+
|
74
|
+
def __init__(self,*tiles):
|
75
|
+
super().__init__()
|
76
|
+
pass
|
77
|
+
|
78
|
+
|
79
|
+
def normalize(self) -> Stone:
|
80
|
+
#TODO Optimize, use bounding first, avoid generating all symetries at all cost
|
81
|
+
# Use dictionary to cache
|
82
|
+
|
83
|
+
#WARNING slow
|
84
|
+
if len(self)<=Stone_config.MaxCacheStoneSize:
|
85
|
+
positive=self.shift_positive()
|
86
|
+
if positive in Stone._NormalizeCache:
|
87
|
+
return Stone._NormalizeCache[positive]
|
88
|
+
#BUG not done jet
|
89
|
+
syms=self.get_symetries()
|
90
|
+
syms=list(syms)
|
91
|
+
syms.sort(key=hash)
|
92
|
+
res=syms[0]
|
93
|
+
if len(self)<Stone_config.MaxCacheStoneSize:
|
94
|
+
Stone._NormalizeCache.update({sym:res for sym in syms})
|
95
|
+
assert syms
|
96
|
+
return res
|
97
|
+
|
98
|
+
|
99
|
+
#@GTracker.track_calls
|
100
|
+
def shift_positive(self):
|
101
|
+
# TODO use self.shift
|
102
|
+
sx=[t[0] for t in self]
|
103
|
+
sx=np.array(sx)
|
104
|
+
sy=[t[1] for t in self]
|
105
|
+
sy=np.array(sy)
|
106
|
+
|
107
|
+
sy-=min(sy)
|
108
|
+
sx-=min(sx)
|
109
|
+
|
110
|
+
return Stone(zip(sx,sy))
|
111
|
+
|
112
|
+
def shift(self,dx,dy) -> Stone :
|
113
|
+
# TODO ditch numpy
|
114
|
+
"""
|
115
|
+
Creates a new stone that is shiftet dx and dy
|
116
|
+
"""
|
117
|
+
sx=[t[0] for t in self]
|
118
|
+
sx=np.array(sx)
|
119
|
+
sy=[t[1] for t in self]
|
120
|
+
sy=np.array(sy)
|
121
|
+
sx+=dx
|
122
|
+
sy+=dy
|
123
|
+
return Stone(zip(sx,sy))
|
124
|
+
|
125
|
+
@property
|
126
|
+
def bounding_Box(self):
|
127
|
+
"""
|
128
|
+
upper bounds not included
|
129
|
+
(xmin,ymin),(xmax,ymax)
|
130
|
+
"""
|
131
|
+
assert self
|
132
|
+
for t in self:
|
133
|
+
min_vals=list(t)
|
134
|
+
max_vals=list(t)
|
135
|
+
break
|
136
|
+
|
137
|
+
for tile in self:
|
138
|
+
for i,cord in enumerate(tile):
|
139
|
+
min_vals[i]=min(min_vals[i],cord)
|
140
|
+
max_vals[i]=max(max_vals[i],cord)
|
141
|
+
|
142
|
+
return tuple(min_vals),tuple(max_vals)
|
143
|
+
|
144
|
+
|
145
|
+
def to_mask(self,n=None ,m=None):
|
146
|
+
sx=[t[0] for t in self]
|
147
|
+
sy=[t[1] for t in self]
|
148
|
+
|
149
|
+
if n==None:
|
150
|
+
n=max(sx)+1
|
151
|
+
if m== None:
|
152
|
+
m=max(sy)+1
|
153
|
+
mask=np.zeros((n,m))
|
154
|
+
mask[sx,sy]=1
|
155
|
+
return mask
|
156
|
+
|
157
|
+
|
158
|
+
def display(self):
|
159
|
+
Visualize.draw_stone(st=self)
|
160
|
+
# make it default for ipynb
|
161
|
+
def _ipython_display_(self):
|
162
|
+
self.display()
|
163
|
+
|
164
|
+
|
165
|
+
def splitConnected(self) -> tuple[Stone,Stone]:
|
166
|
+
# Flood fill
|
167
|
+
#TODO Stone -> isConected
|
168
|
+
|
169
|
+
toCheck:list[tile.Tile]=[self.get_any_tile()]
|
170
|
+
found:set[tile.Tile]=set()
|
171
|
+
|
172
|
+
while toCheck:
|
173
|
+
front=toCheck.pop()
|
174
|
+
if front in found:
|
175
|
+
continue
|
176
|
+
found.add(front)
|
177
|
+
toCheck+=list(front.get_neighbores() & self)
|
178
|
+
|
179
|
+
connected=Stone(found)
|
180
|
+
reminder=Stone(self - connected)
|
181
|
+
|
182
|
+
return connected, reminder
|
183
|
+
|
184
|
+
pass
|
185
|
+
|
186
|
+
def isConected(self) -> bool:
|
187
|
+
if not self:
|
188
|
+
return True
|
189
|
+
_,reminder=self.splitConnected()
|
190
|
+
return not reminder
|
191
|
+
|
192
|
+
def ConectedComponents(self) -> set[Stone]:
|
193
|
+
reminder=self
|
194
|
+
res=set()
|
195
|
+
while reminder:
|
196
|
+
split,reminder=reminder.splitConnected()
|
197
|
+
res.add(split)
|
198
|
+
return res
|
199
|
+
|
200
|
+
|
201
|
+
|
202
|
+
#@GTracker.track_calls
|
203
|
+
def outer_bound(self,allow_diag=False) -> Stone:
|
204
|
+
res=set()
|
205
|
+
for t in self:
|
206
|
+
res.update(stone.Stone.get_neighbores(t,allow_diag=allow_diag))
|
207
|
+
|
208
|
+
res -=self
|
209
|
+
return Stone(res)
|
210
|
+
|
211
|
+
#@GTracker.track_calls
|
212
|
+
def inner_bound(self) -> Stone:
|
213
|
+
|
214
|
+
res=set()
|
215
|
+
|
216
|
+
for t in self:
|
217
|
+
t:tile.Tile
|
218
|
+
if not t.get_neighbores() <= self:
|
219
|
+
res.add(t)
|
220
|
+
|
221
|
+
return Stone(res)
|
222
|
+
|
223
|
+
|
224
|
+
#@GTracker.track_calls
|
225
|
+
def get_k_stone_on_tile(self,t,k=5) -> set[Stone]:
|
226
|
+
res = self._k_stone_subtree(Stone({t}),Stone(tuple()),Stone(tuple()),k=k)
|
227
|
+
return set(res)
|
228
|
+
|
229
|
+
|
230
|
+
|
231
|
+
def _layer_wise_expansion(self,t,k=5):
|
232
|
+
|
233
|
+
# coplexity replaced by _k_stone_subtree
|
234
|
+
# Solution recursive Decision Tree
|
235
|
+
# - in Substone, not in Substone
|
236
|
+
|
237
|
+
res=set()
|
238
|
+
if not t in self:
|
239
|
+
return res
|
240
|
+
|
241
|
+
cur_layer={Stone((t,))}
|
242
|
+
|
243
|
+
for i in range(1,k):
|
244
|
+
next_layer=set()
|
245
|
+
for cur in cur_layer:
|
246
|
+
cur:Stone
|
247
|
+
for boundTile in cur.outer_bound():
|
248
|
+
if boundTile in self:
|
249
|
+
next_layer.add(Stone(cur | {boundTile} ))
|
250
|
+
|
251
|
+
assert len(next_layer)<=Stone_config.K_STONE_MAX_LAYER_SIZE,'COMPLEXITY !!! To many possible stones'
|
252
|
+
cur_layer=next_layer
|
253
|
+
|
254
|
+
|
255
|
+
return cur_layer
|
256
|
+
|
257
|
+
def _k_stone_subtree(self,inSubtree:Stone,notInSutree:Stone,bound:Stone,k=5):
|
258
|
+
if len(inSubtree)==k:
|
259
|
+
return [inSubtree]
|
260
|
+
if not bound:
|
261
|
+
bound=inSubtree.outer_bound()
|
262
|
+
bound&=self
|
263
|
+
bound-=notInSutree
|
264
|
+
bound=Stone(bound)
|
265
|
+
if not bound:
|
266
|
+
return []
|
267
|
+
|
268
|
+
expPoint=bound.get_any_tile()
|
269
|
+
res=[]
|
270
|
+
new_bound=Stone(bound-{expPoint})
|
271
|
+
|
272
|
+
# All Stones that contain the expansion Point
|
273
|
+
res+=self._k_stone_subtree(
|
274
|
+
Stone(inSubtree |{expPoint} ),
|
275
|
+
notInSutree,
|
276
|
+
new_bound,
|
277
|
+
k
|
278
|
+
)
|
279
|
+
# All Stones that dont contain the expansion Point
|
280
|
+
res += self._k_stone_subtree(
|
281
|
+
inSubtree,
|
282
|
+
Stone(notInSutree | {expPoint}),
|
283
|
+
new_bound,
|
284
|
+
k
|
285
|
+
)
|
286
|
+
|
287
|
+
|
288
|
+
return res
|
289
|
+
|
290
|
+
|
291
|
+
|
292
|
+
|
293
|
+
|
294
|
+
def get_symetries(self):
|
295
|
+
return Stone_config.SYMS(self)
|
296
|
+
|
297
|
+
def get_any_tile(self):
|
298
|
+
for tile in self:
|
299
|
+
return tile
|
300
|
+
|
301
|
+
def getMinTile(self):
|
302
|
+
assert self
|
303
|
+
minTile= self.get_any_tile()
|
304
|
+
|
305
|
+
for t in self:
|
306
|
+
if t<minTile:
|
307
|
+
minTile= t
|
308
|
+
return minTile
|
309
|
+
|
310
|
+
def from_string(s:str) -> Stone:
|
311
|
+
lines=s.splitlines()
|
312
|
+
res=set()
|
313
|
+
|
314
|
+
for i, l in enumerate(lines):
|
315
|
+
for j,c in enumerate(l):
|
316
|
+
if c.strip():
|
317
|
+
res.add((i,j))
|
318
|
+
return Stone(res)
|
319
|
+
|
320
|
+
|
321
|
+
def good_cut_point(self,epsi=0.1,perc=0.1,offset=1)-> tuple[int,int]:
|
322
|
+
#TODO
|
323
|
+
#if len(self)<15:
|
324
|
+
# return self.getMinTile()
|
325
|
+
|
326
|
+
outerBound=self.outer_bound(allow_diag=True)
|
327
|
+
delta=lambda a,b: int(abs(a-b)*perc/2+offset)
|
328
|
+
split=lambda a,b: (a+delta(a,b),b-delta(a,b))
|
329
|
+
|
330
|
+
((xmin,ymin),(xmax,ymax))=outerBound.bounding_Box
|
331
|
+
x_self_bound,y_self_bound=self.bounding_Box
|
332
|
+
|
333
|
+
cases=(
|
334
|
+
(split(xmin,xmax),(ymin,ymax)),
|
335
|
+
((xmin,xmax),split(ymin,ymax))
|
336
|
+
)
|
337
|
+
points_and_costs=[]
|
338
|
+
for c in cases:
|
339
|
+
(x1,x2),(y1,y2)=c
|
340
|
+
#info(f" bounds = {c =} ")
|
341
|
+
if not (x1<= x2 and y1 <= y2):
|
342
|
+
continue
|
343
|
+
substone=outerBound.clip_to_bounds(x1,x2,y1,y2)
|
344
|
+
#substone.display()
|
345
|
+
components =list(substone.ConectedComponents())
|
346
|
+
assert components
|
347
|
+
max_edge=[ (comp._max_edge_value() ,comp) for comp in components ]
|
348
|
+
min_edge=[ (comp._min_edge_value() ,comp) for comp in components ]
|
349
|
+
|
350
|
+
_,starts=max(max_edge)
|
351
|
+
_,ends=min(min_edge)
|
352
|
+
starts-=ends
|
353
|
+
ends -= starts
|
354
|
+
if not starts:
|
355
|
+
#starts=stone.Stone(starts)
|
356
|
+
info(f"{substone = }")
|
357
|
+
substone.display()
|
358
|
+
info(f"{self = }")
|
359
|
+
self.display()
|
360
|
+
assert starts
|
361
|
+
assert ends
|
362
|
+
|
363
|
+
|
364
|
+
#starts.display()
|
365
|
+
#ends.display()
|
366
|
+
pac=self._nearest_endpoint_from_inner_AStar(
|
367
|
+
starts=starts,
|
368
|
+
ends=ends,
|
369
|
+
epsi_cost=epsi
|
370
|
+
)
|
371
|
+
cost,t =pac
|
372
|
+
#happends somehow
|
373
|
+
assert t in self
|
374
|
+
points_and_costs.append(pac)
|
375
|
+
#info(f"{points_and_costs = }")
|
376
|
+
if not points_and_costs:
|
377
|
+
return self.getMinTile()
|
378
|
+
elif len(points_and_costs)==1:
|
379
|
+
cost,t= points_and_costs[0]
|
380
|
+
return t
|
381
|
+
else:
|
382
|
+
cost,t=min(*points_and_costs)
|
383
|
+
return t
|
384
|
+
|
385
|
+
|
386
|
+
|
387
|
+
|
388
|
+
|
389
|
+
def _nearest_endpoint_from_inner_AStar(
|
390
|
+
self,
|
391
|
+
starts:stone.Stone,
|
392
|
+
ends:stone.Stone,
|
393
|
+
epsi_cost=0.3,
|
394
|
+
phase_change_cost=0.75,
|
395
|
+
dist_center_of_mass_cost=1
|
396
|
+
) -> tuple[float,tuple]:
|
397
|
+
"""
|
398
|
+
returns
|
399
|
+
- end_tile
|
400
|
+
- optimal tile in ends
|
401
|
+
- cost
|
402
|
+
- cost for making a cut from starts to ends
|
403
|
+
epsi_cost: cost for moving outside the tile >0
|
404
|
+
phase_change_cost: cost for going from outside to inside or inside to outside
|
405
|
+
"""
|
406
|
+
assert starts
|
407
|
+
assert ends
|
408
|
+
|
409
|
+
xCenter=sum([x for x,y in self])
|
410
|
+
yCenter=sum([y for x,y in self])
|
411
|
+
xCenter/=len(self)
|
412
|
+
yCenter/=len(self)
|
413
|
+
(xmin,ymin),(xmax,ymax)=self.bounding_Box
|
414
|
+
diameter=xmax-xmin+ymax-ymin
|
415
|
+
|
416
|
+
|
417
|
+
|
418
|
+
|
419
|
+
pq=queue.PriorityQueue()
|
420
|
+
|
421
|
+
for start in starts:
|
422
|
+
pq.put((0,0,start,start))
|
423
|
+
visited=set()
|
424
|
+
min_dist= lambda x,y: epsi_cost*min([ abs(x -x_) +abs(y-y_) for x_,y_ in ends ])
|
425
|
+
distance_com_cost=lambda x,y: (abs(x -xCenter)+abs(y-yCenter))*dist_center_of_mass_cost/diameter
|
426
|
+
min_steps=lambda min_dist,t: min(min_dist,int(abs(t[0]-xCenter))+int(abs(t[1]-yCenter)))
|
427
|
+
heuristic_distance_com_cost=lambda steps: steps*(steps+1)/2*dist_center_of_mass_cost/diameter
|
428
|
+
while(pq.not_empty):
|
429
|
+
front=pq.get()
|
430
|
+
|
431
|
+
_,front_cost,front_item,prev=front # _ = heuristic
|
432
|
+
if front_item in ends and tile.Tile(prev) in self:
|
433
|
+
#TODO remove Tile this is why this Tile/tuple has to be split
|
434
|
+
x,y=prev
|
435
|
+
return front_cost,(x,y)
|
436
|
+
if front_item in ends:
|
437
|
+
continue
|
438
|
+
|
439
|
+
if front_item in visited:
|
440
|
+
continue
|
441
|
+
#info(f"{front}")
|
442
|
+
visited.add(front_item)
|
443
|
+
|
444
|
+
neighbores=stone.Stone.get_neighbores(front_item)
|
445
|
+
for neigbore in neighbores:
|
446
|
+
if neigbore in visited:
|
447
|
+
continue
|
448
|
+
front_in_stone=front_item in self
|
449
|
+
next_in_stone=neigbore in self
|
450
|
+
|
451
|
+
#TODO add distance cost, reduces number of stones visited
|
452
|
+
|
453
|
+
next_cost=front_cost + distance_com_cost(*neigbore)
|
454
|
+
match (front_in_stone,next_in_stone):
|
455
|
+
case (True,True):
|
456
|
+
next_cost+=1
|
457
|
+
case (False,False):
|
458
|
+
next_cost+=epsi_cost
|
459
|
+
case (True,False):
|
460
|
+
next_cost+=phase_change_cost+epsi_cost
|
461
|
+
case (False,True):
|
462
|
+
next_cost+=1
|
463
|
+
case _:
|
464
|
+
assert False, "Unreachable"
|
465
|
+
dst=min_dist(*neigbore)
|
466
|
+
steps=min_steps(dst,neigbore)
|
467
|
+
next_heuristic=next_cost+dst+ heuristic_distance_com_cost(dst)
|
468
|
+
#Todo min cost for distance cost
|
469
|
+
assert next_heuristic>=0
|
470
|
+
pq.put((next_heuristic,next_cost,neigbore,front_item))
|
471
|
+
|
472
|
+
assert False ,"Unreachable"
|
473
|
+
|
474
|
+
|
475
|
+
|
476
|
+
def clip_to_bounds(self,xmin,xmax,ymin,ymax)-> Stone:
|
477
|
+
"""
|
478
|
+
returns a stone cliped to bounds
|
479
|
+
"""
|
480
|
+
res =set()
|
481
|
+
#TODO
|
482
|
+
|
483
|
+
for t in self:
|
484
|
+
t:tile.Tile
|
485
|
+
x,y=t
|
486
|
+
inBounds=xmin <= x and x <= xmax
|
487
|
+
inBounds &= ymin <= y and y <= ymax
|
488
|
+
if inBounds:
|
489
|
+
res.add(t)
|
490
|
+
return Stone(res)
|
491
|
+
|
492
|
+
|
493
|
+
|
494
|
+
def get_neighbores(tile:tuple,lowerB=None,upperB=None,allow_diag=False) -> stone.Stone:
|
495
|
+
res=[]
|
496
|
+
x,y=tile
|
497
|
+
if not allow_diag:
|
498
|
+
delta_neig=(
|
499
|
+
(1,0),(-1,0),(0,1),(0,-1)
|
500
|
+
)
|
501
|
+
else:
|
502
|
+
delta_neig=(
|
503
|
+
(1,0),(-1,0),(0,1),(0,-1),
|
504
|
+
(1,1),(-1,-1),(1,-1),(-1,1)
|
505
|
+
)
|
506
|
+
|
507
|
+
for dx,dy in delta_neig:
|
508
|
+
res.append((x+dx,y+dy))
|
509
|
+
|
510
|
+
return stone.Stone(res)
|
511
|
+
|
512
|
+
def _min_edge_value(self):
|
513
|
+
x,y =self.get_any_tile()
|
514
|
+
res=x+y
|
515
|
+
|
516
|
+
for x,y in self:
|
517
|
+
res=min(res,x+y)
|
518
|
+
return res
|
519
|
+
|
520
|
+
def _max_edge_value(self):
|
521
|
+
x,y =self.get_any_tile()
|
522
|
+
res=x+y
|
523
|
+
|
524
|
+
for x,y in self:
|
525
|
+
res=max(res,x+y)
|
526
|
+
return res
|
527
|
+
|
528
|
+
|
529
|
+
|
530
|
+
|
531
|
+
|
532
|
+
|
533
|
+
|
534
|
+
|
535
|
+
|
536
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
# New core data structure
|
4
|
+
# Wrapp Frozenset
|
5
|
+
|
6
|
+
|
7
|
+
class Core():
|
8
|
+
|
9
|
+
def __init__(self,tiles):
|
10
|
+
|
11
|
+
self.tiles=frozenset(tiles)
|
12
|
+
pass
|
13
|
+
|
14
|
+
def __hash__(self):
|
15
|
+
return hash(self.tiles)
|
16
|
+
pass
|
17
|
+
|
18
|
+
def __eq__(self, other):
|
19
|
+
return self.tiles ==other.tiles
|
20
|
+
pass
|
21
|
+
|
22
|
+
def __add__(self,other):
|
23
|
+
pass
|
24
|
+
|
25
|
+
def __or__(self, value):
|
26
|
+
pass
|
27
|
+
|
28
|
+
def __and__(self,other):
|
29
|
+
pass
|
30
|
+
|
31
|
+
def __le__(self,other):
|
32
|
+
pass
|
33
|
+
|
34
|
+
def __ge__(self,other):
|
35
|
+
pass
|
36
|
+
|
37
|
+
def __sub__(self,other):
|
38
|
+
pass
|
39
|
+
|
40
|
+
def __repr__(self):
|
41
|
+
return f"Stone({self.tiles})"
|
42
|
+
pass
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
|
File without changes
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
from tilingpuzzles.games.komino import Komino
|
3
|
+
|
4
|
+
import logging
|
5
|
+
|
6
|
+
def test_komino():
|
7
|
+
Komino(TilesToFill={(1,0),(0,1)})
|
8
|
+
Komino.generate(M=10)
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def test_generate():
|
14
|
+
|
15
|
+
for i in range(2,4):
|
16
|
+
for j in range(2,4):
|
17
|
+
game,used =Komino.generate(i,j)
|
18
|
+
assert len(game.T)==i*j
|
19
|
+
|
20
|
+
|
21
|
+
def test_mask():
|
22
|
+
|
23
|
+
komi,used=Komino.generate(5,5)
|
24
|
+
|
25
|
+
assert komi.to_mask().sum().sum() == len(komi.T), "tiles should be preserved"
|
26
|
+
|
27
|
+
|
28
|
+
def test_unique_stones():
|
29
|
+
k =Komino(())
|
30
|
+
assert len(k.unique_stones())==12
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
from tilingpuzzles.games.komino import Komino
|
3
|
+
from tilingpuzzles.games.realisations import Realisations
|
4
|
+
from tilingpuzzles.games.stone import Stone
|
5
|
+
|
6
|
+
from logging import info
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
def test_realisations():
|
11
|
+
|
12
|
+
M=5
|
13
|
+
k=5
|
14
|
+
komino,used=Komino.generate(M,k)
|
15
|
+
|
16
|
+
# Single tile stone
|
17
|
+
s=Stone(((1,1),))
|
18
|
+
|
19
|
+
r=Realisations(komino.T)
|
20
|
+
r.add_stone(s)
|
21
|
+
|
22
|
+
# should equal the number of tiles
|
23
|
+
info(f"{komino.T = }")
|
24
|
+
info(f"{r.indexToReal.values() = }")
|
25
|
+
assert len(set(r.indexToReal.values()))== len(komino.T)
|
26
|
+
assert len(r.indexToReal) == len(komino.T)
|
27
|
+
|
28
|
+
pass
|