Commit 3975c688 authored by Taddeüs Kroes's avatar Taddeüs Kroes

Tweak strategy parameters, add delay on each move to avoid inaccurate state...

Tweak strategy parameters, add delay on each move to avoid inaccurate state parsing, refactor some code
parent ed7e949a
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import time import time
from collections import deque
from itertools import count from itertools import count
from strategy import State, moves_to_keys from Xlib import error
from interaction import get_exapunks_window, focus_window, \ from strategy import State
screenshot_board, press_keys, listen_keys from interaction import get_exapunks_window, focus_window, screenshot_board, \
press_keys, listen_keys, KEY_DELAY
MAX_SPEED_ROWS = 3
def save_screenshot(win): def save_screenshot(win):
...@@ -19,46 +24,63 @@ def save_screenshot(win): ...@@ -19,46 +24,63 @@ def save_screenshot(win):
if __name__ == '__main__': if __name__ == '__main__':
win = get_exapunks_window() try:
focus_window(win) win = get_exapunks_window()
focus_window(win)
listen_keys({'s': lambda: save_screenshot(win)})
listen_keys({'s': lambda: save_screenshot(win)})
prev_score = None
solutions = deque([], maxlen=3)
while True:
board = screenshot_board(win) while True:
try:
try: board = screenshot_board(win)
state = State.detect(board)
except (TypeError, AssertionError): state = State.detect(board)
print('error during parsing, wait for a bit') print('\033c', end='')
time.sleep(.1) print('parsed:')
continue state.print()
print()
print('\033c', end='')
print('parsed:') start = time.time()
state.print() solution = state.solve()
print() end = time.time()
print('thought for', round((end - start) * 1000, 1), 'milliseconds')
start = time.time() except (TypeError, AssertionError):
moves = state.solve() print('\rerror during parsing, wait for a bit...', end='')
end = time.time() time.sleep(0.05)
print('thought for %.4f seconds' % (end - start)) continue
except error.BadMatch:
if moves: print('\rEXAPUNKS window lost, wait for a bit...', end='')
print('moves:', moves_to_keys(moves)) time.sleep(0.5)
points, newstate = state.simulate(moves) continue
score = newstate.score(points, moves, state)
print(' score:', score) if len(solutions) == 3 and solution.loops(solutions.popleft()):
print('prev score:', prev_score) print('\rloop detected, wait for a bit...', end='')
print() time.sleep(0.03)
prev_score = score elif solution.moves:
print('moves:', solution.keys())
print('target after moves:') print(' score:', solution.score)
newstate.print() if solutions:
print() print('prev score:', solutions[-1].score)
print()
press_keys(win, moves_to_keys(moves))
else: print('target after moves:')
print('no moves') solution.newstate.print()
press_keys(win, solution.keys())
keys_delay = len(solution.moves) * 2 * KEY_DELAY
moves_delay = max(0, solution.delay() - keys_delay)
print('wait for', moves_delay, 'ms')
time.sleep(moves_delay / 1000)
elif state.nrows() - 2 <= MAX_SPEED_ROWS:
print('no moves, speed up')
press_keys(win, 'l')
time.sleep(0.03)
else:
print('no moves')
solutions.append(solution)
except KeyboardInterrupt:
print('interrupted, quitting')
...@@ -10,7 +10,7 @@ BOARD_X = 367 ...@@ -10,7 +10,7 @@ BOARD_X = 367
BOARD_Y = 129 BOARD_Y = 129
BOARD_WIDTH = 420 BOARD_WIDTH = 420
BOARD_HEIGHT = 638 BOARD_HEIGHT = 638
KEY_DELAY = 0.015 KEY_DELAY = 14 # milliseconds
disp = display.Display() disp = display.Display()
...@@ -60,11 +60,11 @@ def press_keys(window, keys): ...@@ -60,11 +60,11 @@ def press_keys(window, keys):
ext.xtest.fake_input(disp, X.KeyPress, keycode) ext.xtest.fake_input(disp, X.KeyPress, keycode)
disp.sync() disp.sync()
time.sleep(KEY_DELAY) time.sleep(KEY_DELAY / 1000)
ext.xtest.fake_input(disp, X.KeyRelease, keycode) ext.xtest.fake_input(disp, X.KeyRelease, keycode)
disp.sync() disp.sync()
time.sleep(KEY_DELAY) time.sleep(KEY_DELAY / 1000)
def listen_keys(handlers): def listen_keys(handlers):
......
...@@ -8,19 +8,27 @@ from detection import COLUMNS, NOBLOCK, detect_blocks, detect_exa, \ ...@@ -8,19 +8,27 @@ from detection import COLUMNS, NOBLOCK, detect_blocks, detect_exa, \
GRAB, DROP, SWAP, LEFT, RIGHT, SPEED = range(6) GRAB, DROP, SWAP, LEFT, RIGHT, SPEED = range(6)
MOVE_DELAYS = (
# in milliseconds
50, # GRAB
50, # DROP
50, # SWAP
30, # LEFT
30, # RIGHT
30, # SPEED
)
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))
MIN_BASIC_GROUP_SIZE = 4 MIN_BASIC_GROUP_SIZE = 4
MIN_BOMB_GROUP_SIZE = 2 MIN_BOMB_GROUP_SIZE = 2
FIND_GROUPS_DEPTH = 3 POINTS_DEPTH = 3
FRAG_DEPTH = 3 FRAG_DEPTH = 5
DEFRAG_PRIO = 3 DEFRAG_PRIO = 4
COLSIZE_PRIO = 5 COLSIZE_PRIO = 5
COLSIZE_PANIC = 7 COLSIZE_PANIC = 7
COLSIZE_MAX = 8 COLSIZE_MAX = 9
BOMB_POINTS = 1 BOMB_POINTS = 1
MIN_ROWS = 2 MIN_ROWS = 2
MAX_SPEED_ROWS = 3
class State: class State:
...@@ -29,7 +37,7 @@ class State: ...@@ -29,7 +37,7 @@ class State:
self.exa = exa self.exa = exa
self.held = held self.held = held
def grabbing_of_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
...@@ -56,53 +64,52 @@ class State: ...@@ -56,53 +64,52 @@ class State:
def copy(self): def copy(self):
return State(list(self.blocks), self.exa, self.held) return State(list(self.blocks), self.exa, self.held)
def colsizes(self): def causes_panic(self):
for col in range(COLUMNS): return self.max_colsize() >= COLSIZE_PANIC
yield self.nrows() - self.colskip(col)
def colsize_panic(self): def max_colsize(self):
return int(max(self.colsizes()) >= COLSIZE_PANIC) return self.nrows() - self.empty_rows()
def empty_column_score(self): def empty_rows(self):
skip = 0
for i, block in enumerate(self.blocks): for i, block in enumerate(self.blocks):
if block != NOBLOCK: if block != NOBLOCK:
skip = i // COLUMNS return i // COLUMNS
break return 0
nrows = self.nrows() def holes(self):
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(skip, nrows): for row in range(start_row, total_rows):
if self.blocks[row * COLUMNS + col] != NOBLOCK: if self.blocks[row * COLUMNS + col] != NOBLOCK:
break break
score += row - skip + 1 score += row - start_row + 1
return score return score
def score(self, points, moves, prev): def score(self, points, moves, prev):
prev_colsize = max(prev.colsizes()) prev_colsize = prev.nrows() - 2
points = self.score_points()
#if prev_colsize >= COLSIZE_PANIC:
if prev_colsize >= COLSIZE_PANIC: # holes = self.holes()
colsize = self.empty_column_score() # frag = self.fragmentation()
#frag = self.fragmentation() # return holes, moves_delay(moves), -points, frag
return colsize, len(moves), -points #, frag if prev_colsize >= COLSIZE_PRIO:
holes = self.holes()
frag = self.fragmentation()
return -points, holes, frag, moves_delay(moves)
elif prev_colsize >= DEFRAG_PRIO: elif prev_colsize >= DEFRAG_PRIO:
colsize = self.empty_column_score() holes = self.holes()
frag = self.fragmentation() frag = self.fragmentation()
panic = self.colsize_panic() panic = int(self.causes_panic())
return -points, panic, frag, colsize, len(moves) return -points, panic, frag, holes, moves_delay(moves)
elif prev_colsize >= COLSIZE_PRIO:
colsize = self.empty_column_score()
return -points, colsize, len(moves)
else: else:
return -points, len(moves) return -points, moves_delay(moves)
def score_moves(self): def solutions(self):
for moves in self.gen_moves(): for moves in self.gen_moves():
try: try:
points, newstate = self.simulate(moves) yield Solution(self, moves)
yield newstate.score(points, moves, self), moves
except AssertionError: except AssertionError:
pass pass
...@@ -134,7 +141,7 @@ class State: ...@@ -134,7 +141,7 @@ class State:
def simulate(self, moves): def simulate(self, moves):
s = self.copy() s = self.copy()
points = 0 #points = 0
# avoid swapping/grabbing currently exploding items # avoid swapping/grabbing currently exploding items
#unmoveable = s.find_unmovable_blocks() #unmoveable = s.find_unmovable_blocks()
...@@ -161,7 +168,7 @@ class State: ...@@ -161,7 +168,7 @@ 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.remove_blocks()
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
...@@ -170,14 +177,15 @@ class State: ...@@ -170,14 +177,15 @@ class State:
#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] s.blocks[i], s.blocks[j] = s.blocks[j], s.blocks[i]
#points += s.score_points() #points += s.remove_blocks()
if moves and max(self.colsizes()) < COLSIZE_MAX: if moves and self.max_colsize() < COLSIZE_MAX:
assert max(s.colsizes()) <= COLSIZE_MAX assert s.max_colsize() <= COLSIZE_MAX
points = s.remove_blocks()
return points, s return points, s
def find_groups(self, depth=FIND_GROUPS_DEPTH, minsize=2): def find_groups(self, depth=POINTS_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)
...@@ -196,20 +204,15 @@ class State: ...@@ -196,20 +204,15 @@ class State:
yield block, group yield block, group
def neighbors(self, i): def neighbors(self, i):
def gen_indices(): row, col = divmod(i, COLUMNS)
row, col = divmod(i, COLUMNS) if col > 0 and self.blocks[i - 1] != NOBLOCK:
if col > 0: yield i - 1
yield i - 1 if col < COLUMNS - 1 and self.blocks[i + 1] != NOBLOCK:
if col < COLUMNS - 1: yield i + 1
yield i + 1 if row > 0 and self.blocks[i - COLUMNS] != NOBLOCK:
if row > 0: yield i - COLUMNS
yield i - COLUMNS if row < self.nrows() - 1 and self.blocks[i + COLUMNS] != NOBLOCK:
if row < self.nrows() - 1: yield i + COLUMNS
yield i + COLUMNS
for j in gen_indices():
if self.blocks[j] != NOBLOCK:
yield j
def fragmentation(self, depth=FRAG_DEPTH): def fragmentation(self, depth=FRAG_DEPTH):
""" """
...@@ -220,8 +223,8 @@ class State: ...@@ -220,8 +223,8 @@ class State:
yi, xi = divmod(i, COLUMNS) yi, xi = divmod(i, COLUMNS)
yj, xj = divmod(j, COLUMNS) yj, xj = divmod(j, COLUMNS)
# for blocks in the same group, only count vertical distance so that # for blocks in the same group, only count vertical distance so
# groups are spread out horizontally # that groups are spread out horizontally
if groups[i] == groups[j]: if groups[i] == groups[j]:
return abs(yj - yi) return abs(yj - yi)
...@@ -241,33 +244,43 @@ class State: ...@@ -241,33 +244,43 @@ class State:
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 score_points(self, multiplier=1): def remove_blocks(self):
#remove = [] removed = 0
points = 0
for block, group in self.find_groups(): for block, group in self.find_groups():
if is_basic(block) and len(group) >= MIN_BASIC_GROUP_SIZE: if is_basic(block) and len(group) >= MIN_BASIC_GROUP_SIZE:
#remove.extend(group) removed += len(group)
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 += BOMB_POINTS removed += BOMB_POINTS
#remove.extend(group)
#for i, other in enumerate(self.blocks): return removed
# if other == bomb_to_basic(block):
# remove.append(i) #def remove_blocks(self):
# remove = []
#remove.sort()
#prev = None # for block, group in self.find_groups():
#for i in remove: # if is_basic(block) and len(group) >= MIN_BASIC_GROUP_SIZE:
# if i != prev: # remove.extend(group)
# while self.blocks[i] != NOBLOCK: # elif is_bomb(block) and len(group) >= MIN_BOMB_GROUP_SIZE:
# self.blocks[i] = self.blocks[i - COLUMNS] # remove.extend(group)
# i -= COLUMNS # remove.extend(i for i, other in enumerate(self.blocks)
# prev = i # if other == bomb_to_basic(block))
#if points: # remove.sort()
# points += self.score_points(min(2, multiplier * 2)) # removed = 0
return points # prev = None
# for i in remove:
# if i != prev:
# while self.blocks[i] != NOBLOCK:
# self.blocks[i] = self.blocks[i - COLUMNS]
# i -= COLUMNS
# removed += 1
# prev = i
# if removed:
# removed += self.remove_blocks()
# return removed
def has_explosion(self): def has_explosion(self):
return any(is_bomb(block) and return any(is_bomb(block) and
...@@ -281,38 +294,35 @@ class State: ...@@ -281,38 +294,35 @@ class State:
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()
for src in range(COLUMNS): for src in range(COLUMNS):
mov1 = make_move(src - self.exa) mov1 = make_move(src - self.exa)
yield mov1 + (SWAP,) if mov1 or not ignore_exa_column:
yield mov1 + (GRAB, SWAP, DROP) yield mov1 + (SWAP,)
yield mov1 + (SWAP, GRAB, SWAP, DROP) yield mov1 + (GRAB, SWAP, DROP)
yield mov1 + (SWAP, GRAB, SWAP, DROP)
for dst in range(COLUMNS):
if dst != src: for dst in range(COLUMNS):
mov2 = make_move(dst - src) if dst != src:
for get in GET: mov2 = make_move(dst - src)
for put in PUT: for get in GET:
yield mov1 + get + mov2 + put for put in PUT:
yield mov1 + get + mov2 + put
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 (DROP,) return Solution(self, (DROP,))
if self.nrows() < MIN_ROWS: if self.nrows() < MIN_ROWS:
return () return Solution(self, ())
if self.grabbing_of_dropping():
return ()
if self.has_explosion(): if self.has_explosion():
return () return Solution(self, ())
score, moves = min(self.score_moves()) return min(self.solutions())
if not moves and max(self.colsizes()) <= MAX_SPEED_ROWS:
return (SPEED,)
return moves
def print(self): def print(self):
print_board(self.blocks, self.exa, self.held) print_board(self.blocks, self.exa, self.held)
...@@ -326,10 +336,43 @@ class State: ...@@ -326,10 +336,43 @@ class State:
def nrows(self): def nrows(self):
return len(self.blocks) // COLUMNS return len(self.blocks) // COLUMNS
def has_same_exa(self, state):
return self.exa == state.exa and self.held == state.held
class Solution:
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):
return moves_delay(self.moves)
def keys(self):
return moves_to_keys(self.moves)
def move_to_key(move):
return 'jjkadl'[move]
def moves_to_keys(moves): def moves_to_keys(moves):
return ''.join('jjkadl'[move] for move in moves) return ''.join(move_to_key(move) for move in moves)
def moves_delay(moves):
return sum(MOVE_DELAYS[m] for m in moves)
if __name__ == '__main__': if __name__ == '__main__':
...@@ -343,20 +386,16 @@ if __name__ == '__main__': ...@@ -343,20 +386,16 @@ 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() solution = state.solve()
end = time.time() end = time.time()
print('moves:', moves_to_keys(moves)) print('best moves:', solution.keys())
print('elapsed:', end - start) print('elapsed:', round((end - start) * 1000, 1), 'ms')
print() print()
print('target after moves:') print('target after moves:')
points, newstate = state.simulate(moves) solution.newstate.print()
newstate.print()
print() print()
for score, moves in sorted(state.score_moves()): for solution in sorted(state.solutions()):
print('move %18s:' % moves_to_keys(moves), 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