Commit 213ebb76 authored by Taddeüs Kroes's avatar Taddeüs Kroes

Optimize strategy implementation:

- Instead of computing a complete score for each state, compute+compare
  the first component for each one, then the second, etc.
- Set colskip (and nrows) once and update occasionally once instead of
  constantly recomputing it.
- Add some more actions to do on the destination column.
- Fix move loop detection.
- Do not wait on bomb explosion (since they no longer cause loops).
parent a4129c62
...@@ -4,9 +4,10 @@ import time ...@@ -4,9 +4,10 @@ import time
from collections import deque from collections import deque
from itertools import count from itertools import count
from Xlib import error from Xlib import error
from strategy import State from detection import NOBLOCK
from interaction import get_exapunks_window, focus_window, screenshot_board, \ from interaction import get_exapunks_window, focus_window, screenshot_board, \
press_keys, listen_keys, KEY_DELAY press_keys, listen_keys, KEY_DELAY
from strategy import State
MAX_SPEED_ROWS = 3 MAX_SPEED_ROWS = 3
...@@ -33,7 +34,7 @@ if __name__ == '__main__': ...@@ -33,7 +34,7 @@ if __name__ == '__main__':
listen_keys({'s': lambda: save_screenshot(win)}) listen_keys({'s': lambda: save_screenshot(win)})
solutions = deque([], maxlen=3) buf = deque([], maxlen=3)
def vprint(*args, **kwargs): def vprint(*args, **kwargs):
if verbose: if verbose:
...@@ -56,10 +57,10 @@ if __name__ == '__main__': ...@@ -56,10 +57,10 @@ if __name__ == '__main__':
vprint() vprint()
start = time.time() start = time.time()
solution = state.solve() newstate = state.solve()
end = time.time() end = time.time()
vprint('thought for', round((end - start) * 1000, 1), 'ms') vprint('thought for', round((end - start) * 1000, 1), 'ms')
except (TypeError, AssertionError): except (TypeError, AssertionError) as e:
vprint('\rerror during parsing, wait for a bit...', end='') vprint('\rerror during parsing, wait for a bit...', end='')
time.sleep(0.050) time.sleep(0.050)
continue continue
...@@ -68,33 +69,33 @@ if __name__ == '__main__': ...@@ -68,33 +69,33 @@ if __name__ == '__main__':
time.sleep(0.500) time.sleep(0.500)
continue continue
if len(solutions) == 3 and solution.loops(solutions.popleft()): if state.held == NOBLOCK and any(map(newstate.loops, buf)):
vprint('\rloop detected, wait for a bit...', end='') vprint('\rloop detected, wait for a bit...', end='')
time.sleep(0.03) time.sleep(0.03)
elif solution.moves: elif newstate.moves:
vprint('moves:', solution.keys()) vprint('moves:', newstate.keys())
vprint(' score:', solution.score) vprint(' score:', newstate.score)
if solutions: if buf:
vprint('prev score:', solutions[-1].score) vprint('prev score:', buf[-1].score)
vprint() vprint()
vprint('target after moves:') vprint('target after moves:')
vprint_state(solution.newstate) vprint_state(newstate)
press_keys(win, solution.keys()) press_keys(win, newstate.keys())
#keys_delay = len(solution.moves) * 2 * KEY_DELAY #keys_delay = len(newstate.moves) * 2 * KEY_DELAY
#moves_delay = max(0, solution.delay() - keys_delay) #moves_delay = max(0, newstate.delay() - keys_delay)
#vprint('wait for', moves_delay, 'ms') #vprint('wait for', moves_delay, 'ms')
#time.sleep(moves_delay / 1000) #time.sleep(moves_delay / 1000)
time.sleep(0.070) time.sleep(0.075)
elif state.nrows() - 2 <= MAX_SPEED_ROWS: elif state.nrows - 2 <= MAX_SPEED_ROWS:
vprint('no moves, speed up') vprint('no moves, speed up')
press_keys(win, 'l') press_keys(win, 'l')
time.sleep(0.030) time.sleep(0.030)
else: else:
vprint('no moves') vprint('no moves')
solutions.append(solution) buf.append(newstate)
except KeyboardInterrupt: except KeyboardInterrupt:
print('interrupted, quitting') print('interrupted, quitting')
import io import io
import time import time
from collections import deque
from contextlib import redirect_stdout from contextlib import redirect_stdout
from itertools import combinations, islice from itertools import combinations, islice
from detection import COLUMNS, NOBLOCK, detect_blocks, detect_exa, \ from detection import COLUMNS, NOBLOCK, detect_blocks, detect_exa, \
...@@ -22,31 +23,43 @@ PUT = ((DROP,), (DROP, SWAP), (DROP, SWAP, GRAB, SWAP, DROP)) ...@@ -22,31 +23,43 @@ PUT = ((DROP,), (DROP, SWAP), (DROP, SWAP, GRAB, SWAP, DROP))
MIN_BASIC_GROUP_SIZE = 4 MIN_BASIC_GROUP_SIZE = 4
MIN_BOMB_GROUP_SIZE = 2 MIN_BOMB_GROUP_SIZE = 2
POINTS_DEPTH = 3 POINTS_DEPTH = 3
FRAG_DEPTH = 5 FRAG_DEPTH = 4
DEFRAG_PRIO = 4 DEFRAG_PRIO = 4
COLSIZE_PRIO = 5 COLSIZE_PRIO = 5
COLSIZE_PANIC = 8 COLSIZE_PANIC = 8
COLSIZE_MAX = 9 COLSIZE_MAX = 9
BOMB_POINTS = 1 BOMB_POINTS = 5
MIN_ROWS = 2
class State: class State:
def __init__(self, blocks, exa, held): def __init__(self, blocks, exa, held, colskip=None):
self.blocks = blocks self.blocks = blocks
self.exa = exa self.exa = exa
self.held = held self.held = held
self.moves = ()
self.score = ()
self.nrows = len(self.blocks) // COLUMNS
if colskip is None:
colskip = []
for col in range(COLUMNS):
for row in range(self.nrows):
if self.blocks[row * COLUMNS + col] != NOBLOCK:
colskip.append(row)
break
else:
colskip.append(self.nrows)
self.colskip = colskip
def grabbing_or_dropping(self): def grabbing_or_dropping(self):
skip = self.colskip(self.exa) skip = self.colskip[self.exa]
i = (skip + 1) * COLUMNS + self.exa i = (skip + 1) * COLUMNS + self.exa
return i < len(self.blocks) and self.blocks[i] == NOBLOCK return i < len(self.blocks) and self.blocks[i] == NOBLOCK
def iter_columns(self): def iter_columns(self):
nrows = self.nrows()
def gen_col(col): def gen_col(col):
for row in range(nrows): for row in range(self.nrows):
i = row * COLUMNS + col i = row * COLUMNS + col
if self.blocks[i] != NOBLOCK: if self.blocks[i] != NOBLOCK:
yield i yield i
...@@ -62,13 +75,13 @@ class State: ...@@ -62,13 +75,13 @@ class State:
return cls(blocks, exa, held) return cls(blocks, exa, held)
def copy(self): def copy(self):
return State(list(self.blocks), self.exa, self.held) return State(list(self.blocks), self.exa, self.held, list(self.colskip))
def causes_panic(self): def causes_panic(self):
return self.max_colsize() >= COLSIZE_PANIC return self.max_colsize() >= COLSIZE_PANIC
def max_colsize(self): def max_colsize(self):
return self.nrows() - self.empty_rows() return self.nrows - self.empty_rows()
def empty_rows(self): def empty_rows(self):
for i, block in enumerate(self.blocks): for i, block in enumerate(self.blocks):
...@@ -78,17 +91,16 @@ class State: ...@@ -78,17 +91,16 @@ class State:
def holes(self): def holes(self):
start_row = self.empty_rows() start_row = self.empty_rows()
total_rows = self.nrows()
score = 0 score = 0
for col in range(COLUMNS): for col in range(COLUMNS):
for row in range(start_row, total_rows): for row in range(start_row, self.nrows):
if self.blocks[row * COLUMNS + col] != NOBLOCK: if self.blocks[row * COLUMNS + col] != NOBLOCK:
break break
score += row - start_row + 1 score += row - start_row + 1
return score return score
def score(self, points, moves, prev): def score(self, points, moves, prev):
prev_colsize = prev.nrows() - 2 prev_colsize = prev.nrows - 2
#delay = moves_delay(moves) #delay = moves_delay(moves)
delay = len(moves) delay = len(moves)
...@@ -100,7 +112,7 @@ class State: ...@@ -100,7 +112,7 @@ class State:
return -points, delay return -points, delay
holes = self.holes() holes = self.holes()
frag = self.fragmentation() frag = 0 if points else self.fragmentation()
# When rows start stacking up, start defragmenting colors to make # When rows start stacking up, start defragmenting colors to make
# opportunities for scoring points. # opportunities for scoring points.
...@@ -116,20 +128,6 @@ class State: ...@@ -116,20 +128,6 @@ class State:
# Column heights are getting out of hand, just move shit DOWN. # Column heights are getting out of hand, just move shit DOWN.
return holes, delay, -points, frag return holes, delay, -points, frag
def solutions(self):
for moves in self.gen_moves():
try:
yield Solution(self, moves)
except AssertionError:
pass
def colskip(self, col):
nrows = self.nrows()
for row in range(nrows):
if self.blocks[row * COLUMNS + col] != NOBLOCK:
return row
return nrows
def find_unmovable_blocks(self): def find_unmovable_blocks(self):
unmoveable = set() unmoveable = set()
bombed = set() bombed = set()
...@@ -149,13 +147,16 @@ class State: ...@@ -149,13 +147,16 @@ class State:
return unmoveable return unmoveable
def simulate(self, moves): def move(self, moves):
s = self.copy() s = self.copy() if moves else self
#points = 0 s.moves = moves
# avoid swapping/grabbing currently exploding items # avoid swapping/grabbing currently exploding items
#unmoveable = s.find_unmovable_blocks() #unmoveable = s.find_unmovable_blocks()
s.placed = set()
s.grabbed = {}
for move in moves: for move in moves:
if move == LEFT: if move == LEFT:
assert s.exa > 0 assert s.exa > 0
...@@ -165,35 +166,44 @@ class State: ...@@ -165,35 +166,44 @@ class State:
s.exa += 1 s.exa += 1
elif move == GRAB: elif move == GRAB:
assert s.held == NOBLOCK assert s.held == NOBLOCK
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
s.grabbed[i] = s.held
s.colskip[s.exa] += 1
elif move == DROP: elif move == DROP:
assert s.held != NOBLOCK assert s.held != NOBLOCK
row = s.colskip(s.exa) row = s.colskip[s.exa]
assert row > 0 assert row > 0
i = row * COLUMNS + s.exa i = (row - 1) * COLUMNS + s.exa
s.blocks[i - COLUMNS] = s.held s.blocks[i] = s.held
s.held = NOBLOCK s.held = NOBLOCK
#points += s.remove_blocks() s.placed.add(i)
s.colskip[s.exa] -= 1
elif move == SWAP: elif move == SWAP:
row = s.colskip(s.exa) row = s.colskip[s.exa]
assert row < s.nrows() - 2
i = row * COLUMNS + s.exa i = row * COLUMNS + s.exa
j = i + COLUMNS j = i + COLUMNS
assert j < len(s.blocks)
#assert i not in unmoveable #assert i not in unmoveable
#assert j not in unmoveable #assert j not in unmoveable
s.blocks[i], s.blocks[j] = s.blocks[j], s.blocks[i] bi = s.blocks[i]
#points += s.remove_blocks() bj = s.blocks[j]
if bi != bj:
s.blocks[i] = bj
s.blocks[j] = bi
s.grabbed[i] = bi
s.grabbed[j] = bj
s.placed.add(i)
s.placed.add(j)
if moves and self.max_colsize() < COLSIZE_MAX: if moves and self.max_colsize() < COLSIZE_MAX:
assert s.max_colsize() <= COLSIZE_MAX assert s.max_colsize() <= COLSIZE_MAX
points = s.remove_blocks() return s
return points, s
def find_groups(self, depth=POINTS_DEPTH, minsize=2): def find_groups(self, depth=POINTS_DEPTH, minsize=2):
def follow_group(i, block, group): def follow_group(i, block, group):
...@@ -221,7 +231,7 @@ class State: ...@@ -221,7 +231,7 @@ class State:
yield i + 1 yield i + 1
if row > 0 and self.blocks[i - COLUMNS] != NOBLOCK: if row > 0 and self.blocks[i - COLUMNS] != NOBLOCK:
yield i - COLUMNS yield i - COLUMNS
if row < self.nrows() - 1 and self.blocks[i + COLUMNS] != NOBLOCK: if row < self.nrows - 1 and self.blocks[i + COLUMNS] != NOBLOCK:
yield i + COLUMNS yield i + COLUMNS
def fragmentation(self, depth=FRAG_DEPTH): def fragmentation(self, depth=FRAG_DEPTH):
...@@ -250,47 +260,46 @@ class State: ...@@ -250,47 +260,46 @@ class State:
groups[i] = groupid groups[i] = groupid
groupsizes[i] = len(group) groupsizes[i] = len(group)
return sum(dist(i, j) # * (1 + 2 * is_bomb(block)) return sum(dist(i, j)
for block, color in colors.items() for block, color in colors.items()
for i, j in combinations(color, 2)) for i, j in combinations(color, 2))
def remove_blocks(self): def points(self):
removed = 0 def group_size(start):
work = [start]
for block, group in self.find_groups(): visited.add(start)
if is_basic(block) and len(group) >= MIN_BASIC_GROUP_SIZE: size = 0
removed += len(group) block = self.blocks[start]
elif is_bomb(block) and len(group) >= MIN_BOMB_GROUP_SIZE:
removed += BOMB_POINTS while work:
i = work.pop()
return removed
# avoid giving points to moving a block within the same group
#def remove_blocks(self): if self.grabbed.get(i, None) == block:
# remove = [] return 0
# for block, group in self.find_groups(): if self.blocks[i] == block:
# if is_basic(block) and len(group) >= MIN_BASIC_GROUP_SIZE: size += 1
# remove.extend(group) for nb in self.neighbors(i):
# elif is_bomb(block) and len(group) >= MIN_BOMB_GROUP_SIZE: if nb not in visited:
# remove.extend(group) visited.add(nb)
# remove.extend(i for i, other in enumerate(self.blocks) work.append(nb)
# if other == bomb_to_basic(block)) return size
points = 0
visited = set()
# remove.sort() for i in self.placed:
# removed = 0 if i not in visited:
# prev = None block = self.blocks[i]
# for i in remove: size = group_size(i)
# if i != prev:
# while self.blocks[i] != NOBLOCK:
# self.blocks[i] = self.blocks[i - COLUMNS]
# i -= COLUMNS
# removed += 1
# prev = i
# if removed: if is_basic(block) and size >= MIN_BASIC_GROUP_SIZE:
# removed += self.remove_blocks() points += size
elif is_bomb(block) and size >= MIN_BOMB_GROUP_SIZE:
points += BOMB_POINTS
# return removed return -points
def has_explosion(self): def has_explosion(self):
return any(is_bomb(block) and return any(is_bomb(block) and
...@@ -300,39 +309,78 @@ class State: ...@@ -300,39 +309,78 @@ class State:
def gen_moves(self): def gen_moves(self):
yield () yield ()
def make_move(diff): def shift_exa(diff):
direction = RIGHT if diff > 0 else LEFT direction = RIGHT if diff > 0 else LEFT
return abs(diff) * (direction,) return abs(diff) * (direction,)
ignore_exa_column = self.grabbing_or_dropping() ignore_exa_column = self.grabbing_or_dropping()
for src in range(COLUMNS): for src in range(COLUMNS):
mov1 = make_move(src - self.exa) mov1 = shift_exa(src - self.exa)
if mov1 or not ignore_exa_column: if mov1 or not ignore_exa_column:
yield mov1 + (SWAP,) yield mov1 + (SWAP,)
yield mov1 + (GRAB, SWAP, DROP) yield mov1 + (GRAB, SWAP, DROP)
yield mov1 + (SWAP, GRAB, SWAP, DROP) yield mov1 + (SWAP, GRAB, SWAP, DROP)
yield mov1 + (GRAB, SWAP, DROP, SWAP)
yield mov1 + (SWAP, GRAB, SWAP, DROP, SWAP)
for dst in range(COLUMNS): for dst in range(COLUMNS):
if dst != src: if dst != src:
mov2 = make_move(dst - src) mov2 = shift_exa(dst - src)
for get in GET: for get in GET:
for put in PUT: for put in PUT:
yield mov1 + get + mov2 + put yield mov1 + get + mov2 + put
def gen_valid_moves(self):
for moves in self.gen_moves():
try:
yield self.move(moves)
except AssertionError:
pass
def solve(self): def solve(self):
assert self.exa is not None assert self.exa is not None
if self.held != NOBLOCK: if self.held != NOBLOCK:
return Solution(self, (DROP,)) return self.move((DROP,))
valid = deque(self.gen_valid_moves())
if len(valid) == 0:
return self.move(())
best_score = ()
for key in self.score_keys():
if len(valid) == 1:
break
for state in valid:
state.score = key(state)
if self.nrows() < MIN_ROWS: best = min(state.score for state in valid)
return Solution(self, ()) best_score += (best,)
if self.has_explosion(): for i in range(len(valid)):
return Solution(self, ()) state = valid.popleft()
if state.score == best:
valid.append(state)
return min(self.solutions()) best = valid.popleft()
best.score = best_score
return best
def score_keys(self):
cls = self.__class__
colsize = self.nrows - 2
if colsize >= COLSIZE_PANIC:
return cls.holes, cls.nmoves, cls.points, cls.fragmentation
if colsize >= COLSIZE_PRIO:
return cls.causes_panic, cls.points, cls.holes, cls.fragmentation, cls.nmoves
return cls.points, cls.fragmentation, cls.holes, cls.nmoves
def print(self): def print(self):
print_board(self.blocks, self.exa, self.held) print_board(self.blocks, self.exa, self.held)
...@@ -343,28 +391,11 @@ class State: ...@@ -343,28 +391,11 @@ class State:
self.print() self.print()
return stream.getvalue() return stream.getvalue()
def nrows(self):
return len(self.blocks) // COLUMNS
def has_same_exa(self, state): def has_same_exa(self, state):
return self.exa == state.exa and self.held == state.held return self.exa == state.exa and self.held == state.held
def nmoves(self):
class Solution: return len(self.moves)
def __init__(self, state, moves):
self.state = state
self.moves = moves
points, self.newstate = state.simulate(moves)
self.score = self.newstate.score(points, moves, state)
def __lt__(self, other):
return self.score < other.score
def loops(self, prev_prev):
return self.moves and \
self.state.exa == prev_prev.state.exa and \
self.moves == prev_prev.moves and \
self.score == prev_prev.score
def delay(self): def delay(self):
return moves_delay(self.moves) return moves_delay(self.moves)
...@@ -372,6 +403,15 @@ class Solution: ...@@ -372,6 +403,15 @@ class Solution:
def keys(self): def keys(self):
return moves_to_keys(self.moves) return moves_to_keys(self.moves)
def __lt__(self, other):
return self.score < other.score
def loops(self, prev):
return self.moves and \
self.exa == prev.exa and \
self.moves == prev.moves and \
self.score == prev.score
def move_to_key(move): def move_to_key(move):
return 'jjkadl'[move] return 'jjkadl'[move]
...@@ -397,15 +437,16 @@ if __name__ == '__main__': ...@@ -397,15 +437,16 @@ if __name__ == '__main__':
print() print()
start = time.time() start = time.time()
solution = state.solve() newstate = state.solve()
end = time.time() end = time.time()
print('best moves:', solution.keys()) print('best move:', newstate.keys())
print('score:', newstate.score)
print('elapsed:', round((end - start) * 1000, 1), 'ms') print('elapsed:', round((end - start) * 1000, 1), 'ms')
print() print()
print('target after moves:') print('target after move:')
solution.newstate.print() newstate.print()
print() print()
for solution in sorted(state.solutions()): #for solution in sorted(state.solutions()):
print('move %18s:' % solution.keys(), solution.score) # print('move %18s:' % solution.keys(), solution.score)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment