Created
December 6, 2021 18:29
-
-
Save axgkl/3102fb6d270c2093ec851635181fcf6c to your computer and use it in GitHub Desktop.
unblockme
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
~/repos/blog/src master ⇣1⇡1 +3 !22 ?18 ❯ cat unblockme.py | |
#!/usr/bin/env python | |
""" | |
# A Solver for the Android game [unblockme][1] | |
## Config | |
Supply the board's initial state like this: | |
- Every piece (block) gets a unique number, starting from 1, counting up | |
- The red piece must have nr 1 | |
- Empty cells get 0 | |
### Exmple: | |
Board 605 would be: | |
```python | |
board_605 = [ | |
[2 , 3 , 4 , 4 , 4 , 0] , | |
[2 , 3 , 0 , 5 , 6 , 0] , | |
[2 , 1 , 1 , 5 , 6 , 0] , | |
[7 , 7 , 8 , 0 , 6 , 0] , | |
[0 , 0 , 8 , 9 , 9 , 0] , | |
[0 , 10 , 10 , 11 , 11 , 0] , | |
] | |
``` | |
## Solving Method: Brute Force | |
- States are indexed by topleft position of all pieces, e.g. this is one state: | |
'122:230:311:403:533:614:731:802:944:1052:1154:' | |
(with piece nr 4 at row0, col3 - piece nr 10 at row 5, col 2) | |
- Loop: | |
a) Committing *first* possible move, which produces new state | |
b) If NO new state can be produced, we undo the last move | |
We do not forget state indizes when undoing, so b) will lead to comitting the | |
originally *second* possible move at a) (since the first one won't produce new state) | |
If there is also no next possible move, we take back the last but one move (...) | |
- After any committed move we check if all right of a piece 1 is 0 -> solved then. | |
!!! important | |
That method will NOT find the *optimal* solution - but will guaranteed find *a* | |
solution - if there is one. | |
[1]: https://play.google.com/store/apps/details?id=com.kiragames.unblockmefree | |
""" | |
import sys | |
from dataclasses import dataclass # for nice print outs of pieces | |
from time import time | |
# fmt:off | |
# ------------------------------------------------------------------------------- config | |
# piece nr 1 must be the one to free (the red one): | |
board_simple = [ | |
[4, 0, 2, 0, 3, 3], | |
[4, 0, 2, 0, 0, 5], | |
[1, 1, 0, 0, 0, 5], | |
[0, 0, 0, 0, 0, 5], | |
[0, 0, 0, 0, 0, 0], | |
[6, 6, 6, 0, 0, 0], | |
] | |
board_605 = [ | |
[2 , 3 , 4 , 4 , 4 , 0] , | |
[2 , 3 , 0 , 5 , 6 , 0] , | |
[2 , 1 , 1 , 5 , 6 , 0] , | |
[7 , 7 , 8 , 0 , 6 , 0] , | |
[0 , 0 , 8 , 9 , 9 , 0] , | |
[0 , 10 , 10 , 11 , 11 , 0] , | |
] | |
# different indexing: | |
board_6052 = [ | |
[11 , 3 , 4 , 4 , 4 , 0] , | |
[11 , 3 , 0 , 5 , 6 , 0] , | |
[11 , 1 , 1 , 5 , 6 , 0] , | |
[7 , 7 , 8 , 0 , 6 , 0] , | |
[0 , 0 , 8 , 9 , 9 , 0] , | |
[0 , 10 , 10 , 2 , 2 , 0] , | |
] | |
board_638 = [ | |
[0 , 3 , 4 , 5 , 5 , 5] , | |
[2 , 3 , 4 , 6 , 0 , 0] , | |
[2 , 1 , 1 , 6 , 0 , 0] , | |
[2 , 7 , 7 , 8 , 8 , 9] , | |
[0 , 0 , 10 , 0 , 0 , 9] , | |
[11 , 11 , 10 , 0 , 0 , 9] , | |
] | |
board_impossible = [ | |
[0 , 3 , 4 , 5 , 5 , 5] , | |
[2 , 3 , 4 , 6 , 0 , 0] , | |
[2 , 1 , 1 , 6 , 12,12] , | |
[2 , 7 , 7 , 8 , 8 , 9] , | |
[0 , 0 , 10 , 0 , 0 , 9] , | |
[11 , 11 , 10 , 0 , 0 , 9] , | |
] | |
# fmt:on | |
board = board_638 | |
# --------------------------------------------------------------------------- end config | |
_ = {0: 244, 7: 59, 9: 58, -1: 56} | |
bgs = {k: '\x1b[48;5;%sm%%s' % _.get(k, k) for k in range(-1, 20)} | |
t0 = time() | |
def print_board(b=None): | |
b = b or board | |
print() | |
for r in b: | |
for c in r: | |
bg = bgs.get(c, bgs[2]) | |
if moves and c == abs(moves[-1]): | |
bg = bgs[-1] | |
if c == 0: | |
c = ' ' | |
elif c > 9: # a, b, c, ...: | |
c = chr(87 + c) | |
# c = fg % c if fg else c | |
print(bg % c, end='') | |
print('\x1b[0m') | |
print() | |
pb = print_board # debug alias | |
horiz, vert = 'h', 'v' | |
def board_by_row_and_col(r, c): | |
"""handling out of bounds w/o exceptions""" | |
if r < 0 or c < 0: | |
return -1 | |
try: | |
return board[r][c] | |
except: | |
return -1 | |
# (Pdb) pp pieces | |
# {1: Piece(nr=1, row=2, col=0, len=2, dir='h'), | |
# 2: Piece(nr=2, row=0, col=2, len=2, dir='v'), | |
# (...) | |
# 6: Piece(nr=6, row=5, col=0, len=3, dir='h')} | |
pieces = {} | |
moves = [] | |
states = set() | |
undo_mode = [False] | |
@dataclass | |
class Piece: | |
nr: int = 0 | |
row: int = 0 | |
col: int = 0 | |
len: int = 0 | |
dir: int = vert | |
def setup(self): | |
"""Determine vertical or horizontal and set the length""" | |
r, c, n = self.row, self.col, self.nr | |
self.dir = vert if (r < len(board) - 1 and board[r + 1][c] == n) else horiz | |
self.set_len() | |
return self | |
def set_len(self): | |
"""How long is this piece""" | |
r, c, n = self.row, self.col, self.nr | |
l = 0 | |
if self.dir == horiz: | |
while board_by_row_and_col(r, c + l) == n: | |
l += 1 | |
self.len = l | |
elif self.dir == vert: | |
while board_by_row_and_col(r + l, c) == n: | |
l += 1 | |
self.len = l | |
def go_left_if_new_state(self): | |
"""left or up""" | |
if self.dir == vert: | |
return self.move(-1, 0) | |
else: | |
return self.move(0, -1) | |
def go_right_if_new_state(self): | |
"""right or down""" | |
if self.dir == vert: | |
return self.move(1, 0) | |
else: | |
return self.move(0, 1) | |
def move(self, row_offs, col_offs): | |
# CAN we move? | |
or_, oc = self.row, self.col | |
nr, nc = self.row + row_offs, self.col + col_offs | |
rl, cl = 0, 0 | |
if row_offs == 1: | |
rl = self.len - 1 | |
elif col_offs == 1: | |
cl = self.len - 1 | |
if board_by_row_and_col(nr + rl, nc + cl) != 0: | |
return | |
# Try do the move - then check for having new state: | |
self.row, self.col = nr, nc | |
if not undo_mode[0]: | |
if not have_new_state(): | |
self.row, self.col = or_, oc | |
return | |
# are we right/down (-> 1) or left/up (-> -1): | |
is_right_or_down = row_offs + col_offs | |
# register the move (our nr with minus if it's left/up, else +): | |
moves.append(self.nr * is_right_or_down) | |
l = ': ' if len(moves) < 10 else ': ...' | |
print('Move', len(moves), l, ', '.join([str(i) for i in moves[-10:]])) | |
# set the board accordingly: | |
if is_right_or_down == 1: | |
# move right or down: | |
board[or_][oc] = 0 | |
board[nr + rl][nc + cl] = self.nr | |
else: | |
# move left or up: | |
board[self.row][self.col] = self.nr | |
if self.dir == horiz: | |
board[self.row][self.col + self.len] = 0 | |
else: | |
board[self.row + self.len][self.col] = 0 | |
print_board() | |
return True | |
def register_pieces(): | |
def top_left_piece_position(p): | |
for r, row in zip(board, range(len(board))): | |
for c, col in zip(r, range(len(r))): | |
if c == p: | |
return row, col | |
p = 0 | |
while True: | |
p += 1 | |
tl = top_left_piece_position(p) | |
if not tl: | |
break | |
pieces[p] = Piece(nr=p, row=tl[0], col=tl[1]).setup() | |
def calc_state(): | |
"""get a unique id per board(=pieces) state""" | |
s = '' | |
for nr in range(1, len(pieces) + 1): | |
p = pieces[nr] | |
row, col = p.row, p.col | |
s += f'{nr}{row}{col}:' | |
# if nr == 5 and row == 2: breakpoint() # FIXME BREAKPOINT | |
return s | |
def have_new_state(): | |
s = calc_state() | |
if not s in states: | |
states.add(s) | |
return True | |
def check_solved(): | |
"""Is all right of piece nr 1 a zero?""" | |
p = pieces[1] | |
row, col, ln = p.row, p.col, p.len | |
for c in board[row][col + ln :]: | |
if c != 0: | |
return False | |
lmoves = len(moves) | |
lstates = len(states) | |
sec = round(time() - t0, 2) | |
print(f'Solved in {lmoves} Moves (producing {lstates} States). Took {sec}sec.') | |
print_board() | |
sys.exit(0) | |
def next_move(): | |
for nr in range(1, len(pieces) + 1): | |
p = pieces[nr] | |
# print(p) | |
if p.go_left_if_new_state() or p.go_right_if_new_state(): | |
return True | |
def take_last_move_back(): | |
if not moves: | |
print('No solution') | |
sys.exit(1) | |
lm = moves[-1] | |
print('Taking back: ', lm) | |
if len(moves) > 1 and lm == -moves[-2]: | |
# loop over the last piece -> have to take back more: | |
return | |
undo_mode[0] = True | |
if lm < 0: | |
# was a go left -> force go right: | |
pieces[-lm].go_right_if_new_state() | |
else: | |
pieces[lm].go_left_if_new_state() | |
undo_mode[0] = False | |
return True | |
def main(): | |
print('Start State:') | |
print_board() | |
register_pieces() | |
have_new_state() # indexes the initial state | |
while True: | |
check_solved() | |
if not next_move(): | |
# when this is False we have x, -x as the last 2 moves (indicating taking | |
# back the last move did not lead to new states) -> we then remove those 2, | |
# so we take back the last but one move - until we have new state again: | |
while not take_last_move_back(): | |
moves.pop() | |
moves.pop() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment