Last active
February 9, 2017 04:43
-
-
Save srli/658bc71c24dd50b6066ff8920b0dd432 to your computer and use it in GitHub Desktop.
Better Knights Tour w/ visualizer
This file contains hidden or 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
#!/usr/bin/env python | |
""" | |
This code generates the path required for a knight's tour | |
around a chessboard with user-specified dimensions | |
Written by Sophie Li, 2017 | |
http://blog.justsophie.com/algorithm-for-knights-tour-in-python/ | |
""" | |
import sys | |
import time | |
try: | |
import pygame | |
from tour_visualizer import Model, View | |
GUI_ON = True | |
except ImportError: | |
GUI_ON = False | |
class PathFound(RuntimeError): | |
pass | |
class KnightsTour: | |
def __init__(self, size, rules): | |
""" | |
size = size of the chessboard | |
rules = rules the tour must follow. Type is a dictionary where the key is | |
the move number (int) and the value is the location of the square. | |
i.e: | |
rules = {1: (0,0), 5: (4,5)} etc.. | |
""" | |
self.w = size[0] | |
self.h = size[1] | |
self.initial_pos = (0, 0) | |
self.rules = rules | |
self.reserved_positions = [] | |
self.closed_tour = False | |
self.closed_positions = [] | |
self.board = [] | |
self.generate_board() | |
def generate_board(self): | |
""" | |
Creates a nested list to represent the game board | |
""" | |
for i in range(self.h): | |
self.board.append([0]*self.w) | |
for k in rules.keys(): | |
self.reserved_positions.append(rules[k]) | |
def print_board(self): | |
print " " | |
print "------" | |
for elem in self.board: | |
print elem | |
# print [x for (x,y) in elem] | |
print "------" | |
print " " | |
def generate_legal_moves(self, cur_pos): | |
""" | |
Generates a list of legal moves for the knight to take next | |
""" | |
possible_pos = [] | |
move_offsets = [(1, 2), (1, -2), (-1, 2), (-1, -2), | |
(2, 1), (2, -1), (-2, 1), (-2, -1)] | |
for move in move_offsets: | |
new_x = cur_pos[0] + move[0] | |
new_y = cur_pos[1] + move[1] | |
if (new_x >= self.h): | |
continue | |
elif (new_x < 0): | |
continue | |
elif (new_y >= self.w): | |
continue | |
elif (new_y < 0): | |
continue | |
else: | |
possible_pos.append((new_x, new_y)) | |
return possible_pos | |
def sort_lonely_neighbors(self, to_visit): | |
""" | |
It is more efficient to visit the lonely neighbors first, | |
since these are at the edges of the chessboard and cannot | |
be reached easily if done later in the traversal | |
""" | |
neighbor_list = self.generate_legal_moves(to_visit) | |
empty_neighbours = [] | |
for neighbor in neighbor_list: | |
np_value = self.board[neighbor[0]][neighbor[1]] | |
if (np_value == 0) and (neighbor not in self.reserved_positions): | |
empty_neighbours.append(neighbor) | |
scores = [] | |
for empty in empty_neighbours: | |
score = [empty, 0] | |
moves = self.generate_legal_moves(empty) | |
for m in moves: | |
if self.board[m[0]][m[1]] == 0: | |
score[1] += 1 | |
scores.append(score) | |
scores_sort = sorted(scores, key = lambda s: s[1]) | |
sorted_neighbours = [s[0] for s in scores_sort] | |
return sorted_neighbours | |
def tour(self, n, path, to_visit): | |
""" | |
Recursive definition of knights tour. Inputs are as follows: | |
n = current depth of search tree | |
path = current path taken | |
to_visit = node to visit | |
""" | |
self.board[to_visit[0]][to_visit[1]] = n | |
path.append(to_visit) #append the newest vertex to the current point | |
print "Visiting: ", to_visit | |
if n == self.w * self.h: #if every grid is filled | |
if self.closed_tour: | |
if path[-1] in self.closed_positions: | |
self.path = path | |
raise PathFound | |
else: | |
print "Not a tour" | |
self.board[to_visit[0]][to_visit[1]] = 0 | |
try: | |
path.pop() | |
except IndexError: | |
raise Exception("No successful path was found") | |
else: | |
self.path = path | |
raise PathFound | |
else: | |
rule_location = self.rules.get(n+1, None) | |
if rule_location is None: | |
sorted_neighbours = self.sort_lonely_neighbors(to_visit) | |
for neighbor in sorted_neighbours: | |
self.tour(n+1, path, neighbor) | |
else: | |
if rule_location in self.generate_legal_moves(to_visit): | |
print "Obeying rule: ", rule_location | |
self.tour(n+1, path, rule_location) | |
#If we exit this loop, all neighbours failed so we reset | |
self.board[to_visit[0]][to_visit[1]] = 0 | |
try: | |
path.pop() | |
print "Going back to: ", path[-1] | |
except IndexError: | |
raise Exception("No successful path was found") | |
def find_path(self, n, path, start): | |
try: | |
if self.closed_tour: | |
self.closed_positions = self.generate_legal_moves(self.initial_pos) | |
self.tour(n, path, start) | |
except PathFound: | |
if GUI_ON: | |
pygame.init() | |
size = (60*self.w, 60*self.h) | |
model = Model(self.w, self.h, path) | |
view = View(model, size) | |
view.animate_path() | |
return self.path | |
if __name__ == '__main__': | |
# rules = {2:(2,1), 3:(3,3)} #example possible ruleset | |
# rules = {2:(2,1), 3:(4,4)} #example impossible ruleset | |
rules = {} | |
#Define the size of grid. We are currently solving for an 8x8 grid | |
kt = KnightsTour(size=(8, 8), rules=rules) | |
# kt.closed_tour = True #uncomment if you want a closed tour | |
kt.initial_pos = (0,0) | |
kt.find_path(1, [], kt.initial_pos) | |
kt.print_board() |
This file contains hidden or 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
import pygame | |
from pygame.locals import QUIT, KEYDOWN | |
import time | |
import random | |
class View(object): | |
""" Provides a view of the chessboard with specified model """ | |
def __init__(self, model, size): | |
""" Initialize with the specified model """ | |
self.model = model | |
self.screen = pygame.display.set_mode(size) | |
self.lines = [] | |
def draw(self): | |
""" Draw the game to the pygame window """ | |
self.screen.fill(pygame.Color('white')) | |
for j in self.model.chessboard: | |
for r in j: | |
pygame.draw.rect(self.screen, pygame.Color('black'), r, 1) | |
pygame.display.update() | |
def center_pixel(self, r): | |
c_pix = (r.x+(self.model.box_height/2), r.y+(self.model.box_height/2)) | |
return c_pix | |
def color_square(self, prev_square, square): | |
i = square[1] | |
j = square[0] | |
r = self.model.chessboard[i][j] | |
pygame.draw.rect(self.screen, (255, 204, 255), r) | |
pygame.draw.rect(self.screen, pygame.Color('black'), r, 1) | |
if prev_square != None: | |
i_p = prev_square[1] | |
j_p = prev_square[0] | |
r_p = self.model.chessboard[i_p][j_p] | |
c_pix_p = self.center_pixel(r_p) | |
c_pix = self.center_pixel(r) | |
self.lines.append((c_pix_p, c_pix)) | |
for l in self.lines: | |
pygame.draw.line(self.screen, pygame.Color('black'), l[0], l[1], 3) | |
pygame.display.update() | |
def animate_path(self): | |
running = True | |
while running: | |
self.draw() | |
self.color_square(None, self.model.path[0]) | |
i = 1 | |
print "LENGTH PATH: ", len(self.model.path) | |
while i < (len(self.model.path)): | |
for e in pygame.event.get(): | |
if e.type == pygame.QUIT: | |
running = False | |
if e.type == pygame.KEYDOWN and e.key == pygame.K_ESCAPE: | |
running = False | |
self.color_square(self.model.path[i-1], self.model.path[i]) | |
if i == (len(self.model.path) - 2): | |
self.color_square(self.model.path[i], self.model.path[i+1]) | |
i += 1 | |
time.sleep(0.25) | |
running = False | |
j = raw_input("Press enter to end") | |
class Model(object): | |
""" Represents the state of the chessboard""" | |
def __init__(self, w, h, path): | |
self.w = w | |
self.h = h | |
self.path = path | |
self.box_height = 60 | |
self.chessboard = [] | |
for i in range(self.w): | |
row = [] | |
for j in range(self.h): | |
r = pygame.Rect(i*self.box_height, j*self.box_height, self.box_height, self.box_height) | |
row.append(r) | |
self.chessboard.append(row) | |
if __name__ == '__main__': | |
#Define the size of grid. | |
m = int(raw_input("M dimension: ")) | |
n = int(raw_input("N dimension: ")) | |
kt = KnightsTour(m, n) | |
kt.initial_pos = (0,0) | |
#Flip the following variables depending use case | |
kt.closed_tour = True | |
kt.visualize = True | |
kt.closed_positions = kt.generate_legal_moves(kt.initial_pos) | |
kt.tour(1, [], kt.initial_pos) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment