Commit d0515a68 authored by Taddeüs Kroes's avatar Taddeüs Kroes

Dynamic programming: reuse changes made by common prefix moves

parent 409354ab
...@@ -56,10 +56,8 @@ if __name__ == '__main__': ...@@ -56,10 +56,8 @@ if __name__ == '__main__':
vprint_state(state) vprint_state(state)
vprint() vprint()
start = time.time() assert state.exa is not None
newstate = state.solve()
end = time.time()
vprint('thought for', round((end - start) * 1000, 1), 'ms')
except (TypeError, AssertionError): except (TypeError, AssertionError):
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)
...@@ -69,14 +67,23 @@ if __name__ == '__main__': ...@@ -69,14 +67,23 @@ if __name__ == '__main__':
time.sleep(0.500) time.sleep(0.500)
continue continue
try:
start = time.time()
newstate = state.solve()
end = time.time()
vprint('thought for', round((end - start) * 1000, 1), 'ms')
except AssertionError:
print('error board 99:')
state.print()
board.convert('RGB').save('screens/board99.png')
break
if state.held == NOBLOCK and any(map(newstate.loops, buf)): 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 newstate.moves: elif newstate.moves:
vprint('moves:', newstate.keys()) vprint('moves:', newstate.keys())
vprint(' score:', newstate.score) vprint('score:', newstate.score)
if buf:
vprint('prev score:', buf[-1].score)
vprint() vprint()
vprint('target after moves:') vprint('target after moves:')
...@@ -88,7 +95,7 @@ if __name__ == '__main__': ...@@ -88,7 +95,7 @@ if __name__ == '__main__':
#moves_delay = max(0, newstate.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.075) time.sleep(0.080)
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')
......
...@@ -2,6 +2,7 @@ import io ...@@ -2,6 +2,7 @@ import io
import time import time
from collections import deque from collections import deque
from contextlib import redirect_stdout from contextlib import redirect_stdout
from copy import copy
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, \
detect_held, print_board, is_basic, is_bomb detect_held, print_board, is_basic, is_bomb
...@@ -17,8 +18,6 @@ MOVE_DELAYS = ( ...@@ -17,8 +18,6 @@ MOVE_DELAYS = (
30, # RIGHT 30, # RIGHT
30, # SPEED 30, # SPEED
) )
GET = ((GRAB,), (SWAP, GRAB), (GRAB, SWAP, DROP, SWAP, GRAB))
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
...@@ -31,25 +30,39 @@ BOMB_POINTS = 5 ...@@ -31,25 +30,39 @@ BOMB_POINTS = 5
class State: class State:
def __init__(self, blocks, exa, held, colskip=None): def __init__(self, blocks, exa, held, colskip, busy, moves, placed, grabbed):
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 self.colskip = colskip
self.busy = busy
self.moves = moves
self.placed = placed
self.grabbed = grabbed
self.nrows = len(blocks) // COLUMNS
@classmethod
def detect(cls, board, pad=2):
blocks = [NOBLOCK] * (COLUMNS * pad) + list(detect_blocks(board))
exa = detect_exa(board)
held = detect_held(board, exa)
colskip = get_colskip(blocks)
busy = get_busy(blocks, colskip)
return cls(blocks, exa, held, colskip, busy, (), set(), {})
def copy(self, deep):
mcopy = copy if deep else lambda x: x
return self.__class__(mcopy(self.blocks),
self.exa,
self.held,
mcopy(self.colskip),
self.busy,
self.moves,
mcopy(self.placed),
mcopy(self.grabbed))
def colbusy(self, col):
return (self.busy >> col) & 1
def grabbing_or_dropping(self): def grabbing_or_dropping(self):
skip = self.colskip[self.exa] skip = self.colskip[self.exa]
...@@ -66,17 +79,6 @@ class State: ...@@ -66,17 +79,6 @@ class State:
for col in range(COLUMNS): for col in range(COLUMNS):
yield gen_col(col) yield gen_col(col)
@classmethod
def detect(cls, board, pad=2):
blocks = [NOBLOCK] * (COLUMNS * pad) + list(detect_blocks(board))
exa = detect_exa(board)
held = detect_held(board, exa)
return cls(blocks, exa, held)
def copy(self):
return self.__class__(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
...@@ -99,38 +101,48 @@ class State: ...@@ -99,38 +101,48 @@ class State:
score += row - start_row + 1 score += row - start_row + 1
return score return score
def move(self, moves): def colrows(self, col):
s = self.copy() if moves else self return self.nrows - self.colskip[col]
s.moves = moves
s.placed = set() def move(self, *moves):
s.grabbed = {} deep = any(move in (GRAB, DROP, SWAP) for move in moves)
s = self.copy(deep)
s.moves += moves
for move in moves: for move in moves:
if move == LEFT: if move == LEFT:
assert s.exa > 0 assert s.exa > 0
s.exa -= 1 s.exa -= 1
elif move == RIGHT: elif move == RIGHT:
assert s.exa < COLUMNS - 1 assert s.exa < COLUMNS - 1
s.exa += 1 s.exa += 1
elif move == GRAB: elif move == GRAB:
assert not s.colbusy(s.exa)
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
s.held = s.blocks[i] s.grabbed[i] = s.held = s.blocks[i]
s.blocks[i] = NOBLOCK s.blocks[i] = NOBLOCK
s.grabbed[i] = s.held s.grabbed[i] = s.held
s.colskip[s.exa] += 1 s.colskip[s.exa] += 1
elif move == DROP: elif move == DROP:
assert not s.colbusy(s.exa)
assert s.held != NOBLOCK assert s.held != NOBLOCK
row = s.colskip[s.exa] row = s.colskip[s.exa]
assert row > 0 assert row > 0
# XXX assert s.nrows - row < COLSIZE_MAX
i = (row - 1) * COLUMNS + s.exa i = (row - 1) * COLUMNS + s.exa
s.blocks[i] = s.held s.blocks[i] = s.held
s.held = NOBLOCK s.held = NOBLOCK
s.placed.add(i) s.placed.add(i)
s.colskip[s.exa] -= 1 s.colskip[s.exa] -= 1
elif move == SWAP: elif move == SWAP:
assert not s.colbusy(s.exa)
row = s.colskip[s.exa] row = s.colskip[s.exa]
i = row * COLUMNS + s.exa i = row * COLUMNS + s.exa
j = i + COLUMNS j = i + COLUMNS
...@@ -145,9 +157,6 @@ class State: ...@@ -145,9 +157,6 @@ class State:
s.placed.add(i) s.placed.add(i)
s.placed.add(j) s.placed.add(j)
if moves and self.max_colsize() < COLSIZE_MAX:
assert s.max_colsize() <= COLSIZE_MAX
return s return s
def find_groups(self, depth=POINTS_DEPTH, minsize=2): def find_groups(self, depth=POINTS_DEPTH, minsize=2):
...@@ -247,66 +256,110 @@ class State: ...@@ -247,66 +256,110 @@ class State:
return -points return -points
def gen_moves(self): def gen_moves(self):
yield () yield self
def shift_exa(diff): for src in self.gen_shift(not self.grabbing_or_dropping()):
direction = RIGHT if diff > 0 else LEFT yield from src.gen_stationary()
return abs(diff) * (direction,)
for get in src.gen_get():
ignore_exa_column = self.grabbing_or_dropping() for dst in get.gen_shift(False):
yield from dst.gen_put()
for src in range(COLUMNS):
mov1 = shift_exa(src - self.exa) def gen_shift(self, allow_noshift):
if mov1 or not ignore_exa_column: if allow_noshift:
yield mov1 + (SWAP,) yield self
yield mov1 + (GRAB, SWAP, DROP)
yield mov1 + (SWAP, GRAB, SWAP, DROP) left = self
yield mov1 + (GRAB, SWAP, DROP, SWAP) for i in range(self.exa):
yield mov1 + (SWAP, GRAB, SWAP, DROP, SWAP) left = left.move(LEFT)
yield left
for dst in range(COLUMNS):
if dst != src: right = self
mov2 = shift_exa(dst - src) for i in range(COLUMNS - self.exa - 1):
for get in GET: right = right.move(RIGHT)
for put in PUT: yield right
yield mov1 + get + mov2 + put
def gen_stationary(self):
def gen_valid_moves(self): # SWAP
for moves in self.gen_moves(): # GRAB, SWAP, DROP
try: # GRAB, SWAP, DROP, SWAP
yield self.move(moves) # SWAP, GRAB, SWAP, DROP
except AssertionError: # SWAP, GRAB, SWAP, DROP, SWAP
pass if not self.colbusy(self.exa):
avail = self.colrows(self.exa)
if avail >= 2:
swap = self.move(SWAP)
yield swap
if avail >= 3:
grab = self.move(GRAB, SWAP, DROP)
yield grab
yield grab.move(SWAP)
swap = swap.move(GRAB, SWAP, DROP)
yield swap
yield swap.move(SWAP)
def gen_get(self):
# GRAB
# SWAP, GRAB
# GRAB, SWAP, DROP, SWAP, GRAB
if not self.colbusy(self.exa):
avail = self.colrows(self.exa)
if avail >= 1:
grab = self.move(GRAB)
yield grab
if avail >= 2:
yield self.move(SWAP, GRAB)
if avail >= 3:
yield grab.move(SWAP, DROP, SWAP, GRAB)
def gen_put(self):
# DROP
# DROP, SWAP
# DROP, SWAP, GRAB, SWAP, DROP
if not self.colbusy(self.exa):
avail = self.colrows(self.exa)
drop = self.move(DROP)
yield drop
if avail >= 1:
swap = drop.move(SWAP)
yield swap
if avail >= 2:
yield swap.move(GRAB, SWAP, DROP)
def force(self, *moves):
state = self.move(*moves)
state.score = ()
return state
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 self.move((DROP,)) return self.force(DROP)
valid = deque(self.gen_valid_moves()) pool = deque(self.gen_moves())
if len(valid) == 0: if len(pool) == 0:
return self.move(()) return self.force()
best_score = () best_score = ()
for key in self.score_keys(): for key in self.score_keys():
if len(valid) == 1: if len(pool) == 1:
break break
for state in valid: for state in pool:
state.score = key(state) state.score = key(state)
best = min(state.score for state in valid) best = min(state.score for state in pool)
best_score += (best,) best_score += (best,)
for i in range(len(valid)): for i in range(len(pool)):
state = valid.popleft() state = pool.popleft()
if state.score == best: if state.score == best:
valid.append(state) pool.append(state)
best = valid.popleft() best = pool.popleft()
best.score = best_score best.score = best_score
return best return best
...@@ -344,9 +397,6 @@ class State: ...@@ -344,9 +397,6 @@ class State:
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): def loops(self, prev):
return self.moves and \ return self.moves and \
self.exa == prev.exa and \ self.exa == prev.exa and \
...@@ -354,6 +404,25 @@ class State: ...@@ -354,6 +404,25 @@ class State:
self.score == prev.score self.score == prev.score
def get_colskip(blocks):
def colskip(col):
for row, block in enumerate(blocks[col::COLUMNS]):
if block != NOBLOCK:
return row
return len(blocks) // COLUMNS
return list(map(colskip, range(COLUMNS)))
def get_busy(blocks, colskip):
mask = 0
for col, skip in enumerate(colskip):
start = (skip + 1) * COLUMNS + col
colbusy = NOBLOCK in blocks[start::COLUMNS]
mask |= colbusy << col
return mask
def move_to_key(move): def move_to_key(move):
return 'jjkadl'[move] return 'jjkadl'[move]
...@@ -387,3 +456,8 @@ if __name__ == '__main__': ...@@ -387,3 +456,8 @@ if __name__ == '__main__':
print('target after move:') print('target after move:')
newstate.print() newstate.print()
#print()
#print('generated moves:')
#for state in state.gen_moves():
# print(state.keys())
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