Codementor Events

Klotski Adventure — Part 1

Published Sep 07, 2020Last updated Sep 09, 2020
Klotski Adventure — Part 1

I was casually watching Computerphile sudoku solver episode
And at the end of the video, Prof. Thorsten Altenkirch drops this puzzle Klotski.
Image for post
this simple looking puzzle(goal is to get the red block to bottom of the board) is very tricky to solve.
He asked to solve this using backtracking.
I started, tried to put a good structure(RIP OOPs 😜) so that I can extend this to solve MoveIt game
Let me define piece,since every piece is rectangular, i can use srow (starting row),erow(ending row) etc.
put_in_grid to place it a boards grid and get_boards to get all different boards that can be achieved after moving the piece.

class Piece:
    def __init__(self, x, y, id, type):
        self.srow = x
        self.scol = y
        self.erow = x + type % 2
        self.ecol = y + type // 2
        self.type = type
        self.id = id

    def put_in_grid(self, grid, replace=None):
        for row in range(self.srow, self.erow + 1):
            for col in range(self.scol, self.ecol + 1):
                grid[row][col] = self.id if replace is None else replace

    def get_boards(self, move_board):
        grid = move_board.grid
        piece_map = move_board.piece_map
        new_board = []
        if self.scol - 1 >= 0 and all(
                [grid[row][col] in [-1, self.id] for row in range(self.srow, self.erow + 1) for col in
                 range(self.scol - 1, self.ecol + 1 - 1)]):
            t_board = Board(pieces=piece_map.values())
            t_board.remove_piece(self)
            t_board.add_piece(Piece(self.srow, self.scol - 1, self.id, self.type))
            new_board.append(t_board)
        if self.ecol + 1 < 4 and all(
                [grid[row][col] in [-1, self.id] for row in range(self.srow, self.erow + 1) for col in
                 range(self.scol + 1, self.ecol + 1 + 1)]):
            t_board = Board(pieces=piece_map.values())
            t_board.remove_piece(self)
            t_board.add_piece(Piece(self.srow, self.scol + 1, self.id, self.type))
            new_board.append(t_board)
        if self.srow - 1 >= 0 and all(
                [grid[row][col] in [-1, self.id] for row in range(self.srow - 1, self.erow + 1 - 1) for col in
                 range(self.scol, self.ecol + 1)]):
            t_board = Board(pieces=piece_map.values())
            t_board.remove_piece(self)
            t_board.add_piece(Piece(self.srow - 1, self.scol, self.id, self.type))
            new_board.append(t_board)
        if self.erow + 1 < 5 and all(
                [grid[row][col] in [-1, self.id] for row in range(self.srow + 1, self.erow + 1 + 1) for col in
                 range(self.scol, self.ecol + 1)]):
            t_board = Board(pieces=piece_map.values())
            t_board.remove_piece(self)
            t_board.add_piece(Piece(self.srow + 1, self.scol, self.id, self.type))
            new_board.append(t_board)
        return new_board

Let me define Board with grid to place pieces,piece_map to access pieces
add_piece/remove_piece explanatory
all_boards to get all boards that can be achieved from current board.
eff_key string representation of board,using it as key/identifier to a board

class Board:
    def __init__(self, pieces=None):
        self.grid = [[-1 for col in range(4)] for row in range(5)]
        self.piece_map = {}
        for piece in pieces:
            self.piece_map[piece.id] = piece
            self.add_piece(piece)

    def add_piece(self, piece):
        piece.put_in_grid(self.grid)
        self.piece_map[piece.id] = piece

    def remove_piece(self, piece):
        piece.put_in_grid(self.grid, -1)
        del self.piece_map[piece.id]

    def print_grid(self):
        for row in range(5):
            for col in range(4):
                print(self.grid[row][col], end=' ')
            print('')
        print('______', end='\n\n')

    def all_boards(self,vis):
        next_boards = []
        for piece in self.piece_map.values():
            boards = piece.get_boards(self)
            next_boards.extend(boards)
        return next_boards

    def solved(self):
        return self.grid[4][1] == 0 and self.grid[4][2] == 0

    def eff_key(self):
        grid = self.grid
        return " ".join([str(grid[i][j]) for i in range(5) for j in range(4)])

Done a DFS/backtracking with board as state to traverse and identifying boards with eff_key method.

b = Board(pieces=[Piece(0, 0, 1, 1), Piece(0, 1, 0, 3), Piece(0, 3, 2, 1), Piece(2, 0, 3, 1), Piece(2, 1, 4, 2),
                      Piece(2, 3, 5, 1), Piece(4, 0, 8, 0), Piece(3, 1, 6, 0), Piece(3, 2, 7, 0), Piece(4, 3, 9, 0)])

start_time = time.time()
vis = set()


def dfs(cur_board, sol, vis):
    if cur_board.solved():
        return 1
    vis.add(cur_board.eff_key())
    next_boards = cur_board.all_boards(vis)
    for board in next_boards:
        if board.eff_key() not in vis:
            sol.append(board)
            if (dfs(board, sol, vis)):
                return 1
            sol.pop()
    return 0


solution = []

if dfs(b, solution, vis):
    for board in solution:
        board.print_grid()
    print("found in ", len(solution), " steps")
else:
    print(len(vis))
    
print("done in ", time.time() - start_time)

first solution found in 950 steps
done in 0.52 sec NOT BAD.
Can be done better.

#### Previous eff_key(string Identifier) for board

def eff_key(self):
    grid = self.grid
    return " ".join([str(grid[i][j]) for i in range(5) for j in range(4)])

#### New eff_key
eff_block = {1: "1", 2: "1", 3: "1", 5: "1", 4: "2", 6: "0", 7: "0", 8: "0", 9: "0", 0: "3", -1: "4"}

def eff_key(self):
    grid = self.grid
    return " ".join([str(eff_block[grid[i][j]]) for i in range(5) for j in range(4)])

with Enhanced eff_key method for board.
I treat all the similar block in swapped positions to be same board, this saves some unnecessary recursion.
first solution found in 398 steps
done in 0.25 sec, better
But wiki for this puzzle says the shortest solution for above layout is 81 steps.
Lets see if I can get to that solution , optimize even more and extend the code to solve freaking AIFactory’s Moveit in next adventure.

Discover and read more posts from Rahul Iragavarapu
get started