|
@@ -1,19 +1,21 @@
|
|
|
import time
|
|
import time
|
|
|
-from collections import deque
|
|
|
|
|
-from itertools import islice
|
|
|
|
|
-from parser import COLUMNS, NOBLOCK, detect_blocks, detect_exa, \
|
|
|
|
|
- detect_held, print_board, is_basic, is_bomb, bomb_to_basic
|
|
|
|
|
|
|
+from itertools import combinations, islice
|
|
|
|
|
+from parse import COLUMNS, NOBLOCK, detect_blocks, detect_exa, \
|
|
|
|
|
+ detect_held, print_board, is_basic, is_bomb, bomb_to_basic
|
|
|
|
|
|
|
|
|
|
|
|
|
-GRAB, DROP, SWAP, LEFT, RIGHT = range(5)
|
|
|
|
|
|
|
+GRAB, DROP, SWAP, LEFT, RIGHT, SPEED = range(6)
|
|
|
GET = ((GRAB,), (SWAP, GRAB), (GRAB, SWAP, DROP, SWAP, GRAB))
|
|
GET = ((GRAB,), (SWAP, GRAB), (GRAB, SWAP, DROP, SWAP, GRAB))
|
|
|
PUT = ((DROP,), (DROP, SWAP), (DROP, SWAP, GRAB, SWAP, DROP))
|
|
PUT = ((DROP,), (DROP, SWAP), (DROP, SWAP, GRAB, SWAP, DROP))
|
|
|
-#REVERSE = [DROP, GRAB, SWAP, RIGHT, LEFT]
|
|
|
|
|
MIN_BASIC_GROUP_SIZE = 4
|
|
MIN_BASIC_GROUP_SIZE = 4
|
|
|
MIN_BOMB_GROUP_SIZE = 2
|
|
MIN_BOMB_GROUP_SIZE = 2
|
|
|
-FIND_GROUPS_DEPTH = 20
|
|
|
|
|
-FRAG_DEPTH = 20
|
|
|
|
|
|
|
+FIND_GROUPS_DEPTH = 4
|
|
|
|
|
+FRAG_DEPTH = 4
|
|
|
COLSIZE_PRIO = 5
|
|
COLSIZE_PRIO = 5
|
|
|
|
|
+COLSIZE_PRIO_HIGH = 7
|
|
|
|
|
+COLSIZE_MAX = 8
|
|
|
|
|
+BOMB_POINTS = 2
|
|
|
|
|
+MIN_ROWS = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
class State:
|
|
class State:
|
|
@@ -23,10 +25,12 @@ class State:
|
|
|
self.exa = exa
|
|
self.exa = exa
|
|
|
self.held = held
|
|
self.held = held
|
|
|
|
|
|
|
|
- def has_holes(self):
|
|
|
|
|
- return any(self.blocks[i] == NOBLOCK
|
|
|
|
|
- for col in self.iter_columns()
|
|
|
|
|
- for i in col)
|
|
|
|
|
|
|
+ def grabbing_of_dropping(self):
|
|
|
|
|
+ skip = self.colskip(self.exa)
|
|
|
|
|
+ i = (skip + 1) * COLUMNS + self.exa
|
|
|
|
|
+ return i < len(self.blocks) and self.blocks[i] == NOBLOCK
|
|
|
|
|
+ #return any(len(col) > 1 and col[1] == NOBLOCK
|
|
|
|
|
+ # for col in map(tuple, self.iter_columns()))
|
|
|
|
|
|
|
|
def iter_columns(self):
|
|
def iter_columns(self):
|
|
|
nrows = self.nrows()
|
|
nrows = self.nrows()
|
|
@@ -54,45 +58,60 @@ class State:
|
|
|
for col in range(COLUMNS):
|
|
for col in range(COLUMNS):
|
|
|
yield self.nrows() - self.colskip(col)
|
|
yield self.nrows() - self.colskip(col)
|
|
|
|
|
|
|
|
|
|
+ #def highest_column(self):
|
|
|
|
|
+ # for i, block in enumerate(self.blocks):
|
|
|
|
|
+ # if block != NOBLOCK:
|
|
|
|
|
+ # return self.nrows() - i // COLUMNS
|
|
|
|
|
|
|
|
- def hole_score(self):
|
|
|
|
|
|
|
+ def empty_column_score(self):
|
|
|
|
|
+ skip = 0
|
|
|
|
|
+ for i, block in enumerate(self.blocks):
|
|
|
|
|
+ if block != NOBLOCK:
|
|
|
|
|
+ skip = i // COLUMNS
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ nrows = self.nrows()
|
|
|
score = 0
|
|
score = 0
|
|
|
for col in range(COLUMNS):
|
|
for col in range(COLUMNS):
|
|
|
- for row in range(self.nrows()):
|
|
|
|
|
- i = row * COLUMNS + col
|
|
|
|
|
- if self.blocks[i] != NOBLOCK:
|
|
|
|
|
|
|
+ for row in range(skip, nrows):
|
|
|
|
|
+ if self.blocks[row * COLUMNS + col] != NOBLOCK:
|
|
|
break
|
|
break
|
|
|
- score += row + 1
|
|
|
|
|
|
|
+ score += row - skip + 1
|
|
|
return score
|
|
return score
|
|
|
|
|
|
|
|
def score(self, points, moves, prev):
|
|
def score(self, points, moves, prev):
|
|
|
- frag = self.fragmentation()
|
|
|
|
|
- colsizes = list(self.colsizes())
|
|
|
|
|
|
|
+ #colsizes = list(self.colsizes())
|
|
|
#mincol = min(colsizes)
|
|
#mincol = min(colsizes)
|
|
|
- maxcol = max(colsizes)
|
|
|
|
|
- colsize_score = maxcol, colsizes.count(maxcol) #, -mincol
|
|
|
|
|
|
|
+ #maxcol = max(colsizes)
|
|
|
|
|
+ #colsize_score = maxcol, colsizes.count(maxcol) #, -mincol
|
|
|
#colsize_score = tuple(sorted(colsizes, reverse=True))
|
|
#colsize_score = tuple(sorted(colsizes, reverse=True))
|
|
|
#if prev.nrows() >= 6:
|
|
#if prev.nrows() >= 6:
|
|
|
# return colsize_score, -points, frag, len(moves)
|
|
# return colsize_score, -points, frag, len(moves)
|
|
|
|
|
|
|
|
- colsize_score = maxcol, self.hole_score()
|
|
|
|
|
|
|
+ #colsize_score = maxcol, self.empty_column_score()
|
|
|
|
|
|
|
|
- prev_colsize = max(prev.colsizes())
|
|
|
|
|
|
|
+ frag = self.fragmentation()
|
|
|
|
|
+ colsize_score = self.empty_column_score()
|
|
|
|
|
+ #return -points, frag + colsize_score, len(moves)
|
|
|
|
|
|
|
|
- if prev_colsize >= COLSIZE_PRIO:
|
|
|
|
|
- return colsize_score, frag, -points, len(moves)
|
|
|
|
|
|
|
+ frag += colsize_score
|
|
|
|
|
+ prev_colsize = max(prev.colsizes())
|
|
|
|
|
+ if prev_colsize >= COLSIZE_PRIO_HIGH:
|
|
|
|
|
+ return colsize_score, len(moves), -points, frag
|
|
|
|
|
+ elif prev_colsize >= COLSIZE_PRIO:
|
|
|
|
|
+ return -points, colsize_score, frag, len(moves)
|
|
|
else:
|
|
else:
|
|
|
return -points, frag, colsize_score, len(moves)
|
|
return -points, frag, colsize_score, len(moves)
|
|
|
|
|
|
|
|
def score_moves(self):
|
|
def score_moves(self):
|
|
|
# clear exploding blocks before computing colsize
|
|
# clear exploding blocks before computing colsize
|
|
|
- prev = self.copy()
|
|
|
|
|
- prev.score_points()
|
|
|
|
|
|
|
+ #prev = self.copy()
|
|
|
|
|
+ #prev.score_points()
|
|
|
|
|
|
|
|
for moves in self.gen_moves():
|
|
for moves in self.gen_moves():
|
|
|
try:
|
|
try:
|
|
|
points, newstate = self.simulate(moves)
|
|
points, newstate = self.simulate(moves)
|
|
|
- yield newstate.score(points, moves, prev), moves
|
|
|
|
|
|
|
+ yield newstate.score(points, moves, self), moves
|
|
|
except AssertionError:
|
|
except AssertionError:
|
|
|
pass
|
|
pass
|
|
|
|
|
|
|
@@ -126,14 +145,11 @@ class State:
|
|
|
s = self.copy()
|
|
s = self.copy()
|
|
|
points = 0
|
|
points = 0
|
|
|
|
|
|
|
|
- # clear the current board before planning the next move
|
|
|
|
|
- #s.score_points()
|
|
|
|
|
-
|
|
|
|
|
- if not moves:
|
|
|
|
|
- return s.score_points(), s
|
|
|
|
|
|
|
+ #if not moves:
|
|
|
|
|
+ # return s.score_points(), s
|
|
|
|
|
|
|
|
# avoid swapping/grabbing currently exploding items
|
|
# avoid swapping/grabbing currently exploding items
|
|
|
- unmoveable = s.find_unmovable_blocks()
|
|
|
|
|
|
|
+ #unmoveable = s.find_unmovable_blocks()
|
|
|
|
|
|
|
|
for move in moves:
|
|
for move in moves:
|
|
|
if move == LEFT:
|
|
if move == LEFT:
|
|
@@ -147,7 +163,7 @@ class State:
|
|
|
row = s.colskip(s.exa)
|
|
row = s.colskip(s.exa)
|
|
|
assert row < s.nrows()
|
|
assert row < s.nrows()
|
|
|
i = row * COLUMNS + s.exa
|
|
i = row * COLUMNS + s.exa
|
|
|
- assert i not in unmoveable
|
|
|
|
|
|
|
+ #assert i not in unmoveable
|
|
|
s.held = s.blocks[i]
|
|
s.held = s.blocks[i]
|
|
|
s.blocks[i] = NOBLOCK
|
|
s.blocks[i] = NOBLOCK
|
|
|
elif move == DROP:
|
|
elif move == DROP:
|
|
@@ -157,20 +173,24 @@ class State:
|
|
|
i = row * COLUMNS + s.exa
|
|
i = row * COLUMNS + s.exa
|
|
|
s.blocks[i - COLUMNS] = s.held
|
|
s.blocks[i - COLUMNS] = s.held
|
|
|
s.held = NOBLOCK
|
|
s.held = NOBLOCK
|
|
|
- points += s.score_points()
|
|
|
|
|
|
|
+ #points += s.score_points()
|
|
|
elif move == SWAP:
|
|
elif move == SWAP:
|
|
|
row = s.colskip(s.exa)
|
|
row = s.colskip(s.exa)
|
|
|
assert row < s.nrows() - 2
|
|
assert row < s.nrows() - 2
|
|
|
i = row * COLUMNS + s.exa
|
|
i = row * COLUMNS + s.exa
|
|
|
j = i + COLUMNS
|
|
j = i + COLUMNS
|
|
|
- assert i not in unmoveable
|
|
|
|
|
- assert j not in unmoveable
|
|
|
|
|
|
|
+ #assert i not in unmoveable
|
|
|
|
|
+ #assert j not in unmoveable
|
|
|
s.blocks[i], s.blocks[j] = s.blocks[j], s.blocks[i]
|
|
s.blocks[i], s.blocks[j] = s.blocks[j], s.blocks[i]
|
|
|
- points += s.score_points()
|
|
|
|
|
|
|
+ #points += s.score_points()
|
|
|
|
|
+
|
|
|
|
|
+ if moves and max(self.colsizes()) < COLSIZE_MAX:
|
|
|
|
|
+ assert max(s.colsizes()) <= COLSIZE_MAX
|
|
|
|
|
|
|
|
|
|
+ points += s.score_points()
|
|
|
return points, s
|
|
return points, s
|
|
|
|
|
|
|
|
- def find_groups(self, depth=FIND_GROUPS_DEPTH):
|
|
|
|
|
|
|
+ def find_groups(self, depth=FIND_GROUPS_DEPTH, minsize=2):
|
|
|
def follow_group(i, block, group):
|
|
def follow_group(i, block, group):
|
|
|
if self.blocks[i] == block and i not in visited:
|
|
if self.blocks[i] == block and i not in visited:
|
|
|
group.append(i)
|
|
group.append(i)
|
|
@@ -185,9 +205,7 @@ class State:
|
|
|
block = self.blocks[i]
|
|
block = self.blocks[i]
|
|
|
group = []
|
|
group = []
|
|
|
follow_group(i, block, group)
|
|
follow_group(i, block, group)
|
|
|
- if len(group) > 1:
|
|
|
|
|
- #for j in group:
|
|
|
|
|
- # visited.add(j)
|
|
|
|
|
|
|
+ if len(group) >= minsize:
|
|
|
yield block, group
|
|
yield block, group
|
|
|
|
|
|
|
|
def neighbors(self, i):
|
|
def neighbors(self, i):
|
|
@@ -208,31 +226,30 @@ class State:
|
|
|
|
|
|
|
|
def fragmentation(self, depth=FRAG_DEPTH):
|
|
def fragmentation(self, depth=FRAG_DEPTH):
|
|
|
"""
|
|
"""
|
|
|
- Sum the minimum distance from every block in the first 3 layers to the
|
|
|
|
|
- closest block of the same color.
|
|
|
|
|
|
|
+ Minimize the sum of dist(i,j) for all blocks i,j of the same color.
|
|
|
|
|
+ Prioritize horitontal distance to avoid column stacking.
|
|
|
"""
|
|
"""
|
|
|
- def find_closest(i):
|
|
|
|
|
- block = self.blocks[i]
|
|
|
|
|
- work = deque([(i, -1)])
|
|
|
|
|
- visited = {i}
|
|
|
|
|
-
|
|
|
|
|
- while work:
|
|
|
|
|
- i, dist = work.popleft()
|
|
|
|
|
-
|
|
|
|
|
- if dist >= 0 and self.blocks[i] == block:
|
|
|
|
|
- return dist
|
|
|
|
|
-
|
|
|
|
|
- for j in self.neighbors(i):
|
|
|
|
|
- if j not in visited:
|
|
|
|
|
- visited.add(j)
|
|
|
|
|
- work.append((j, dist + 1))
|
|
|
|
|
-
|
|
|
|
|
- # only one of this type -> don't count as fragmented
|
|
|
|
|
- return 0
|
|
|
|
|
-
|
|
|
|
|
- return sum(find_closest(i)
|
|
|
|
|
- for col in self.iter_columns()
|
|
|
|
|
- for i in islice(col, depth))
|
|
|
|
|
|
|
+ def dist(i, j):
|
|
|
|
|
+ yi, xi = divmod(i, COLUMNS)
|
|
|
|
|
+ yj, xj = divmod(j, COLUMNS)
|
|
|
|
|
+ if groups[i] == groups[j]:
|
|
|
|
|
+ return abs(yj - yi)
|
|
|
|
|
+ #return abs(xj - xi) * 2 + abs(yj - yi) - 1
|
|
|
|
|
+ return abs(xj - xi) + abs(yj - yi) * 2 - 1
|
|
|
|
|
+
|
|
|
|
|
+ colors = {}
|
|
|
|
|
+ groups = {}
|
|
|
|
|
+ groupsizes = {}
|
|
|
|
|
+
|
|
|
|
|
+ for groupid, (block, group) in enumerate(self.find_groups(depth, 1)):
|
|
|
|
|
+ colors.setdefault(block, []).extend(group)
|
|
|
|
|
+ for i in group:
|
|
|
|
|
+ groups[i] = groupid
|
|
|
|
|
+ groupsizes[i] = len(group)
|
|
|
|
|
+
|
|
|
|
|
+ return sum(dist(i, j) # * (1 + 2 * is_bomb(block))
|
|
|
|
|
+ for block, color in colors.items()
|
|
|
|
|
+ for i, j in combinations(color, 2))
|
|
|
|
|
|
|
|
def score_points(self, multiplier=1):
|
|
def score_points(self, multiplier=1):
|
|
|
remove = []
|
|
remove = []
|
|
@@ -243,7 +260,7 @@ class State:
|
|
|
remove.extend(group)
|
|
remove.extend(group)
|
|
|
points += len(group) * multiplier
|
|
points += len(group) * multiplier
|
|
|
elif is_bomb(block) and len(group) >= MIN_BOMB_GROUP_SIZE:
|
|
elif is_bomb(block) and len(group) >= MIN_BOMB_GROUP_SIZE:
|
|
|
- #points += 10
|
|
|
|
|
|
|
+ points += BOMB_POINTS
|
|
|
remove.extend(group)
|
|
remove.extend(group)
|
|
|
for i, other in enumerate(self.blocks):
|
|
for i, other in enumerate(self.blocks):
|
|
|
if other == bomb_to_basic(block):
|
|
if other == bomb_to_basic(block):
|
|
@@ -262,24 +279,28 @@ class State:
|
|
|
points += self.score_points(min(2, multiplier * 2))
|
|
points += self.score_points(min(2, multiplier * 2))
|
|
|
return points
|
|
return points
|
|
|
|
|
|
|
|
|
|
+ def has_explosion(self):
|
|
|
|
|
+ return any(is_bomb(block) and
|
|
|
|
|
+ any(self.blocks[j] == block for j in self.neighbors(i))
|
|
|
|
|
+ for i, block in enumerate(self.blocks))
|
|
|
|
|
+
|
|
|
def gen_moves(self):
|
|
def gen_moves(self):
|
|
|
yield ()
|
|
yield ()
|
|
|
|
|
|
|
|
- for src in range(COLUMNS):
|
|
|
|
|
- diff = src - self.exa
|
|
|
|
|
|
|
+ def make_move(diff):
|
|
|
direction = RIGHT if diff > 0 else LEFT
|
|
direction = RIGHT if diff > 0 else LEFT
|
|
|
- mov1 = abs(diff) * (direction,)
|
|
|
|
|
|
|
+ return abs(diff) * (direction,)
|
|
|
|
|
|
|
|
|
|
+ for src in range(COLUMNS):
|
|
|
|
|
+ mov1 = make_move(src - self.exa)
|
|
|
yield mov1 + (SWAP,)
|
|
yield mov1 + (SWAP,)
|
|
|
yield mov1 + (GRAB, SWAP, DROP)
|
|
yield mov1 + (GRAB, SWAP, DROP)
|
|
|
|
|
+ yield mov1 + (SWAP, GRAB, SWAP, DROP)
|
|
|
|
|
|
|
|
for dst in range(COLUMNS):
|
|
for dst in range(COLUMNS):
|
|
|
- diff = dst - src
|
|
|
|
|
- direction = RIGHT if diff > 0 else LEFT
|
|
|
|
|
- mov2 = abs(diff) * (direction,)
|
|
|
|
|
-
|
|
|
|
|
- for i, get in enumerate(GET):
|
|
|
|
|
- if i > 1 or diff != 0:
|
|
|
|
|
|
|
+ if dst != src:
|
|
|
|
|
+ mov2 = make_move(dst - src)
|
|
|
|
|
+ for get in GET:
|
|
|
for put in PUT:
|
|
for put in PUT:
|
|
|
yield mov1 + get + mov2 + put
|
|
yield mov1 + get + mov2 + put
|
|
|
|
|
|
|
@@ -287,7 +308,18 @@ class State:
|
|
|
if self.held != NOBLOCK:
|
|
if self.held != NOBLOCK:
|
|
|
return (DROP,)
|
|
return (DROP,)
|
|
|
|
|
|
|
|
|
|
+ if self.nrows() < MIN_ROWS:
|
|
|
|
|
+ return ()
|
|
|
|
|
+
|
|
|
|
|
+ if self.grabbing_of_dropping():
|
|
|
|
|
+ return ()
|
|
|
|
|
+
|
|
|
|
|
+ if self.has_explosion():
|
|
|
|
|
+ return ()
|
|
|
|
|
+
|
|
|
score, moves = min(self.score_moves())
|
|
score, moves = min(self.score_moves())
|
|
|
|
|
+ if not moves:
|
|
|
|
|
+ return (SPEED,)
|
|
|
return moves
|
|
return moves
|
|
|
|
|
|
|
|
def print(self):
|
|
def print(self):
|
|
@@ -299,7 +331,7 @@ class State:
|
|
|
|
|
|
|
|
|
|
|
|
|
def moves_to_keys(moves):
|
|
def moves_to_keys(moves):
|
|
|
- return ''.join('jjkad'[move] for move in moves)
|
|
|
|
|
|
|
+ return ''.join('jjkadl'[move] for move in moves)
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if __name__ == '__main__':
|
|
@@ -314,10 +346,13 @@ if __name__ == '__main__':
|
|
|
state.print()
|
|
state.print()
|
|
|
print()
|
|
print()
|
|
|
|
|
|
|
|
|
|
+ print('empty cols:', state.empty_column_score())
|
|
|
|
|
+ print()
|
|
|
|
|
+
|
|
|
start = time.time()
|
|
start = time.time()
|
|
|
moves = state.solve()
|
|
moves = state.solve()
|
|
|
- print('moves:', moves_to_keys(moves))
|
|
|
|
|
end = time.time()
|
|
end = time.time()
|
|
|
|
|
+ print('moves:', moves_to_keys(moves))
|
|
|
print('elapsed:', end - start)
|
|
print('elapsed:', end - start)
|
|
|
print()
|
|
print()
|
|
|
|
|
|
|
@@ -326,9 +361,9 @@ if __name__ == '__main__':
|
|
|
newstate.print()
|
|
newstate.print()
|
|
|
print()
|
|
print()
|
|
|
|
|
|
|
|
- #for score, moves in sorted(state.score_moves()):
|
|
|
|
|
- # print('move %18s:' % moves_to_keys(moves), score)
|
|
|
|
|
- # #print('moves:', moves_to_keys(moves), moves)
|
|
|
|
|
- # #print('score:', score)
|
|
|
|
|
|
|
+ for score, moves in sorted(state.score_moves()):
|
|
|
|
|
+ print('move %18s:' % moves_to_keys(moves), score)
|
|
|
|
|
+ #print('moves:', moves_to_keys(moves), moves)
|
|
|
|
|
+ #print('score:', score)
|
|
|
|
|
|
|
|
#print('\nmoves:', moves_to_keys(state.solve()))
|
|
#print('\nmoves:', moves_to_keys(state.solve()))
|