I am trying to simulate Rubik's Cube. In order to detect if user has actually solved the Cube I would like to remember all initial position vectors and then just compare it.
However, when you start my program and mess the cube, then press 'k' to solve it you can see in the console that values in fact are the same, however they have different precision. For example z-value is -0.99999 instead of -1. The result of this flaw is that even though the values are quite the same, the program still won't consider the Cube solved. I guess that while rotating, when calculations are performed on the vector, the precision changes and as a result in the end the values are different. How can I solve this problem? To print out the initial position vectors and current vectors press 'a' key anytime you wish to :)
from visual import *
import string
import random
class Cube:
def __init__(self):
self.surfaces = { 'r': (color.red, (1, 0, 0)),
'o': (color.orange, (-1, 0, 0)),
'y': (color.yellow, (0, 1, 0)),
'b': (color.blue, (0, -1, 0)),
'w': (color.white, (0, 0, 1)),
'g': (color.green, (0, 0, -1))}
self.fps = 30
self.wholeSurfaces = []
self.initialPosition = []
self.commandsList = []
def createCube(self):
for colour, axis in self.surfaces.itervalues():
for x in (-1, 0, 1):
for y in (-1, 0, 1):
# Start with all powierzchniaBoczna on the top face, then rotate them "down"
# to the appropriate face.
powBoczna = box(color=colour, pos=(x, y, 1.5),
length=0.98, height=0.98, width=0.05)
cos_kat = dot((0, 0, 1), axis)
if cos_kat == 0: #alfa = 90 + kPI
obliczonaOsObrotu = cross((0, 0, 1), axis) #iloczyn wektorowy
else:
obliczonaOsObrotu=(1, 0, 0)
powBoczna.rotate(angle=acos(cos_kat), axis=obliczonaOsObrotu, origin=(0, 0, 0))
self.wholeSurfaces.append(powBoczna)
#remember initial position
v = (float(powBoczna.pos.x), float(powBoczna.pos.y), float(powBoczna.pos.z))
self.initialPosition.append(v)
def solveCube(self):
print self.commandsList
self.commandsList.reverse()
print self.commandsList
for i in self.commandsList:
self.rotateCube(self.reverseCommand(i), 10000)
self.commandsList = []
def reverseCommand(self, key):
if (key.islower()): return key.upper()
else: return key.lower()
def rotateCube(self, key, refreshRate):
colour, axis = self.surfaces[key.lower()]
if (key.isupper()): kat = (pi / 2.0)
else: kat = -pi/2.0
for r in arange(0, kat, kat / self.fps):
rate(refreshRate)
for surface in self.wholeSurfaces:
if dot(surface.pos, axis) > 0.5:
surface.rotate(angle=kat / self.fps, axis=axis, origin=(0, 0, 0))
def beginLoop(self):
while True:
key = scene.kb.getkey()
if (key.lower() in self.surfaces):
self.commandsList.append(key)
self.rotateCube(key, self.fps)
elif key == "k":
self.solveCube()
elif key == "a":
i = 0
print "================="
for surface in self.wholeSurfaces:
print "%s\n(%s,%s,%s)" % (self.initialPosition[i], surface.pos.x, surface.pos.y, surface.pos.z)
if self.initialPosition[i][0] == float(surface.pos.x) and self.initialPosition[i][1] == float(surface.pos.y) and self.initialPosition[i][2] == float(surface.pos.z): print "equal"
else: print "not equal"
print ""
i+=1
if __name__ == "__main__":
myCube = Cube()
myCube.createCube()
myCube.beginLoop()
The solution is simple, you need to use numpy.allclose method with given precision.
for surface in self.wholeSurfaces:
print "%s\n%s" % (self.initialPosition[i], powierzchnia.pos)
if np.allclose(self.initialPosition[i], surface.pos.astuple(), 1e-5, 1e-5): print "are equal"
else: print "arent equal"
i+=1
Related
I have a tic tac toe game using a minimax algorithm for the computer "player." The Tkinter portion works, and the actual person player works correctly. However, when the computer is supposed to play, it gives the error: "NoneType is not subscriptable." I'm not sure why. Am I missing an input for one of my variables? Thank you in advance.
Here is my code:
from tkinter import *
import customtkinter
import random
import minimax
customtkinter.set_appearance_mode("Dark")
#creating CTk window for app
root = customtkinter.CTk()
#setting window width and height
root.geometry('500x300')
#Creating label
label = customtkinter.CTkLabel(master=root,
text="Tic Tac Toe",
width=120,
height=50,
font=("normal", 20),
corner_radius=8)
label.place(relx=0.25, rely=0.8, anchor=CENTER)
#Handling clicks
DEPTH=8
def clickbutton(r, c):
buttons[r][c]["text"]="X"
board[r][c]="X"
buttons[r][c]['state']=DISABLED
label = customtkinter.CTkLabel(master=root,
text=checkwin(board),
width=120,
height=25,
corner_radius=8)
label.place(relx=0.25, rely=0.9, anchor=CENTER)
computerplay()
DEPTH=DEPTH-1
#Button matrix
buttons = [
[0,0,0],
[0,0,0],
[0,0,0]]
#Matrix identifying whether buttons are active or inactive
board=[[0,0,0],[0,0,0],[0,0,0]]
for i in range(3):
for j in range(3):
buttons[i][j] = Button(height = 3, width = 6, font = ("Normal", 20),
command = lambda r = i, c = j : clickbutton(r,c))
buttons[i][j].grid(row = i, column = j)
def computerplay():
bestmove=minimax.minimax(board, DEPTH, 1)
buttons[bestmove[0]][bestmove[1]]['text']="O"
buttons[bestmove[0]][bestmove[1]]['state']=DISABLED
board[bestmove[0]][bestmove[1]]="O"
def checkwin(b):
score=minimax.evaluate(b)
if score==10:
return 'Computer won!'
elif score==-10:
return 'You won!'
else:
return 'Player vs. Computer'
root.mainloop()
My minimax code:
import math
def change_board(board):
#changes board into -1, 0, and 1s instead of X, O, and 0 (zero).
new_board = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
for i in range(3):
for j in range(3):
if board[i][j]=='X':
new_board[i][j]==-1
elif board[i][j]=='O':
new_board[i][j]==1
return new_board
def empty_cells(board):
cells=[]
for i in range(3):
for j in range(3):
if board[i][j]==0:
cells.append([i, j])
return False
def game_over(board):
#check for wins using evaluate
if evaluate(board)==10 or evaluate(board)==-10:
return True
#check for full board
if (not empty_cells(board)):
return True
return False
def evaluate(board):
#check score
if board[0][0]==board[1][1] and board[1][1]==board[2][2]:
if board[0][0]==-1:
return -10
elif board[0][0]==1:
return 10
if board[0][2]==board[1][1] and board[1][1]==board[2][0]:
if board[0][2]==-1:
return -10
elif board[0][2]==1:
return 10
for row in range(3):
if board[row][0]==board[row][1] and board[row][1]==board[row][2]:
if board[row][0]==-1:
return -10
elif board[row][0]==1:
return 10
for col in range(3):
if board[0][col]==board[1][col] and board[1][col]==board[2][col]:
if board[0][col]==-1:
return -10
elif board[0][col]==1:
return 10
def minimax(board, depth, player):
if player==1:
#1 is computer. -1 is human player.
best=[-1, -1, -math.inf]
else:
best=[-1, -1, math.inf]
if depth==0 or game_over(board):
score=evaluate(board)
return score
#checking scores of valid moves
for cell in empty_cells(board):
x, y = cell[0], cell[1]
board[x][y] = player
score = minimax(board, depth - 1, -player)
board[x][y] = 0
score[0], score[1] = x, y
if player == 1:
if score[2] > best[2]:
best = score
else:
if score[2] < best[2]:
best = score
return best
There are these issues with your minimax code:
In change_board you are not assigning anything to new_board cells, as you perform a comparison, not an assignment. Change new_board[i][j]==-1 to new_board[i][j]=-1 and similarly in the else clause.
In empty_cells you are not doing anything with the work done on cells but just return False. Surely the caller needs the cells list, so you should return it.
In evaluate you should also return a numerical value when no winning line was detected: the caller needs a number to compare it with the best score so far, so leaving it to just return None is not good. Add a return 0 at the bottom of the function.
In minimax you should always return the same structure, i.e. a list with three members. This is not done in the base case where you just return a number. To fix that, that base case should return [-1, -1, score]. Related to this, you should consider using a different name for when the score is a number (like here), or when the score is that list, like you do elsewhere in the code. This is confusing.
With those fixes it will work.
Still, there are some things you could improve, including:
Where you return True or return False based on a boolean condition, you can just return the evaluation of that condition. For instance, game_over can do return abs(evaluate(board))==10 or not empty_cells(board)
You can chain comparison operators, so like board[0][0]==board[1][1]==board[2][2]
Avoid code repetition in game_over: put the information of where the winning lines are in a data structure and then loop over it to only have the three-cell comparisons coded once.
Use tuples instead of lists when there is no need to mutate it, like for x/y coordinates.
With math.inf you're using a float where otherwise the score would be an integer. It is better practice to avoid that type mix. In this case you can use the value 11 instead.
Here is how it could look:
def change_board(board):
return [
[(cell == "O") - (cell == "X") for cell in row]
for row in board
]
def empty_cells(board):
return [(i, j) for i in range(3) for j in range(3) if board[i][j] == 0]
def game_over(board):
return abs(evaluate(board)) == 10 or not empty_cells(board)
lines = (
(0, 0, 1, 1, 2, 2),
(0, 2, 1, 1, 2, 0),
(0, 0, 0, 1, 0, 2),
(1, 0, 1, 1, 1, 2),
(2, 0, 2, 1, 2, 2),
(0, 0, 1, 0, 2, 0),
(0, 1, 1, 1, 2, 1),
(0, 2, 1, 2, 2, 2),
)
def evaluate(board):
def iswin(line):
arow, acol, brow, bcol, crow, ccol = line
if board[arow][acol] == board[brow][bcol] == board[crow][ccol]:
return board[arow][acol]*10
return next((win for win in map(iswin, lines) if win), 0)
def minimax(board, depth, player):
best = (-1, -1, -11*player)
if depth <= 0 or game_over(board):
return (-1, -1, evaluate(board)) # must return same structure
for x, y in empty_cells(board):
board[x][y] = player
score = minimax(board, depth - 1, -player)[2] # only get the score
board[x][y] = 0
if (player == 1) == (score > best[2]):
best = (x, y, score) # only inject x, y when move is best
return best
More optimisation is possible when you use a bit representation of the board instead of a 2D matrix.
I am trying to create a tic tac toe game with an adjustable game size and a computer that uses the minimax algorithm. The game sizes can only be odd numbers, to make sure that diagonal wins are always possible. The game runs with no errors, but I know that the minimax algorithm isn't working 100% correct because I can still beat the computer. I've looked extensively over my code and cannot find where the algorithm is going wrong. Here is my code:
Main.py
import TicTacToe
import Minimax
if (__name__ == "__main__"):
t = TicTacToe.ttt(3)
m = Minimax.Minimax(3, t)
while (t.winner == None):
if (t.turn == 1):
playerInputI = int(input("Input row: "))
playerInputJ = int(input("Input column: "))
bestIndex = (playerInputI, playerInputJ)
else:
winner, bestIndex = m.minimax(t.grid, (-1, -1), 15, -1)
t.winner = None
t.findWinner(bestIndex, t.grid)
t.updateGameGrid(bestIndex)
print(t.grid)
print(t.grid)
Minimax.py
import Minimax
if (__name__ == "__main__"):
t = TicTacToe.ttt(3)
m = Minimax.Minimax(3, t)
while (t.winner == None):
if (t.turn == 1):
playerInputI = int(input("Input row: "))
playerInputJ = int(input("Input column: "))
bestIndex = (playerInputI, playerInputJ)
else:
winner, bestIndex = m.minimax(t.grid, (-1, -1), 15, -1)
t.winner = None
t.findWinner(bestIndex, t.grid)
t.updateGameGrid(bestIndex)
print(t.grid)
print(t.grid)
Tictactoe.py
from random import randint
class ttt:
def __init__(self, size):
self.gridSize = size
self.grid = self.createGrid()
# If using minimax algorithm, user is maximizer(1) and computer is minimizer(-1)
# If single player, then user is 1, computer is -1
# If multiplayer, user1 is 1, user2 = -1
self.turn = 1
self.winner = None
def createGrid(self):
grid = []
for i in range(self.gridSize):
grid.append([])
for j in range(self.gridSize):
grid[i].append(0)
# grid = [[-1, 1, 0], [0, -1, 0], [0, 0, 0]]
return grid
def updateGameGrid(self, index):
if (self.grid[index[0]][index[1]] != 0):
return
self.grid[index[0]][index[1]] = self.turn
winner = self.findWinner(index, self.grid)
self.turn = -self.turn
def randomIndex(self):
x = randint(0, self.gridSize-1)
y = randint(0, self.gridSize-1)
while (self.grid[x][y] != 0):
x = randint(0, self.gridSize-1)
y = randint(0, self.gridSize-1)
return (x, y)
def findWinner(self, index, grid):
# Row
found = True
for j in range(self.gridSize-1):
if (grid[index[0]][j] != grid[index[0]][j+1] or grid[index[0]][j] == 0):
found = False
break
if (found):
self.winner = self.turn
return self.turn
# Column
found = True
for i in range(self.gridSize-1):
if (grid[i][index[1]] != grid[i+1][index[1]] or grid[i][index[1]] == 0):
found = False
break
if (found):
self.winner = self.turn
return self.turn
# Top Left to Bottom Right Diagonal
if (index[0] == index[1]):
found = True
for i in range(self.gridSize-1):
if (grid[i][i] != grid[i+1][i+1] or grid[i][i] == 0):
found = False
break
if (found):
self.winner = self.turn
return self.turn
# Top Right to Bottom Left Diagonal
if (index[0] + index[1] == self.gridSize-1):
found = True
for i in range(self.gridSize-1):
if (grid[self.gridSize-i-1][i] != grid[self.gridSize-i-2][i+1] or grid[self.gridSize-i-1][i] == 0):
found = False
break
if (found):
self.winner = self.turn
return self.turn
tie = True
for i in range(self.gridSize):
for j in range(self.gridSize):
if (grid[i][j] == 0):
tie = False
if (tie):
self.winner = 0
return 0
return None
The grid is represented as a 2d array, with each element being either a -1 for O, 1 for X and 0 for nothing. The player is 1 and the computer is -1. Who's turn it is is represented as a -1 or 1, corresponding to O or X. If anyone is able to find where the error in my code is that would be a great.
EDIT 30/03/2021: Question was really poorly-worded, reformulating it
I implemented an Alpha-Beta Prunning algorithm in Python and I was wondering if it is normal for it not to go for the fastest victory route (sometimes it will go for a victory in 2 moves while it could have won in 1).
import math
from collections import Counter
from copy import copy, deepcopy
""" Board Class Definition """
class Board:
""" constructor """
def __init__(self):
# init data
self.data = [ "." for i in range(9) ]
""" copy constructor equivalent """
#staticmethod
def copy(board):
return deepcopy(board)
""" play at given coordinates """
def play_at(self, position, color):
# check if you can play
if self.data[position] == ".":
# make the move
self.data[position] = color
return True
# did not play
return False
""" get coordinates of empty pieces on the board """
def get_playable_coord(self):
# define coordinates of empty tiles
return [ i for i in range(9) if self.data[i] == "." ]
""" board is full """
def is_full(self):
# define tile counter
c = Counter( [ self.data[i] for i in range(9) ] )
return ( c["x"] + c["o"] == 9 )
""" get winner of the board """
def get_winner(self):
# straight lines to check
straightLines = [ (0, 1, 2) , (3, 4, 5) , (6, 7, 8) , (0, 3, 6) , (1, 4, 7) , (2, 5, 8) , (0, 4, 8) , (2, 4, 6) ]
# check straight lines - 8 in total
for i in range(8):
# get counter of line of tiles
c = Counter( [ self.data[j] for j in straightLines[i] ] )
# different scenarii
if c["x"] == 3:
return "x"
elif c["o"] == 3:
return "o"
# if board is full, game is a draw
if self.is_full():
return "draw"
# return None by default
return None
""" get heuristic value of board - for "x" if 'reverse' == False """
def get_heuristic_value(self, reverse):
# init variable
value = 0
# straight lines to check
straightLines = [ (0, 1, 2) , (3, 4, 5) , (6, 7, 8) , (0, 3, 6) , (1, 4, 7) , (2, 5, 8) , (0, 4, 8) , (2, 4, 6) ]
# check straight lines - 8 in total
for i in range(8):
# get counter of line of tiles
c = Counter( [ self.data[j] for j in straightLines[i] ] )
# different scenarii
if c["x"] == 3:
value += 100
elif c["x"] == 2 and c["."] == 1:
value += 10
elif c["x"] == 1 and c["."] == 2:
value += 1
elif c["o"] == 3:
value -= 100
elif c["o"] == 2 and c["."] == 1:
value -= 10
elif c["o"] == 1 and c["."] == 2:
value -= 1
# return heuristic value
if reverse:
return -value
else:
return value
""" Model Class Definition """
class Model:
""" constructor """
def __init__(self, color):
# define parameters
self.color = color
self.other = self.get_opponent(color)
# define board
self.board = Board()
# define winner
self.winner = None
# 'x' plays first
if self.other == "x":
self.make_ai_move()
""" get opponent """
def get_opponent(self, player):
if player == "x":
return "o"
return "x"
""" player makes a move in given position """
def make_player_move(self, pos):
if self.winner is None:
# get result of board method
res = self.board.play_at(pos, self.color)
# check end of game <?>
self.winner = self.board.get_winner()
if res and self.winner is None:
# make AI move
self.make_ai_move()
""" AI makes a move by using alphabeta pruning on all child nodes """
def make_ai_move(self):
# init variables
best, bestValue = None, - math.inf
for i in self.board.get_playable_coord():
# copy board as child
copie = Board.copy(self.board)
copie.play_at(i, self.other)
# use alpha beta && (potentially) register play
value = self.alphabeta(copie, 10, - math.inf, math.inf, False)
if value > bestValue:
best, bestValue = i, value
# play at best coordinates
self.board.play_at(best, self.other)
# check end of game <?>
self.winner = self.board.get_winner()
""" alpha beta function (minimax optimization) """
def alphabeta(self, node, depth, alpha, beta, maximizingPlayer):
# ending condition
if depth == 0 or node.get_winner() is not None:
return node.get_heuristic_value(self.other == "o")
# recursive part initialization
if maximizingPlayer:
value = - math.inf
for pos in node.get_playable_coord():
# copy board as child
child = Board.copy(node)
child.play_at(pos, self.other)
value = max(value, self.alphabeta(child, depth-1, alpha, beta, False))
# update alpha
alpha = max(alpha, value)
if alpha >= beta:
break
return value
else:
value = math.inf
for pos in node.get_playable_coord():
# copy board as child
child = Board.copy(node)
child.play_at(pos, self.color)
value = min(value, self.alphabeta(child, depth-1, alpha, beta, True))
# update beta
beta = min(beta, value)
if beta <= alpha:
break
return value
My conclusion on the question:
Alpha-Beta Pruning is a depth-first search algorithm, not a breadth-first search algorithm, so I think it is natural for it to pick the first route it finds no matter its depth, and not search for the quickest one...
I know it's not the answer to the question, but I would like to suggest perhaps simplier approach for AI tac-tac-toe player, which involves calculating whether the position is winning or losing. This will require considering all valid positions that may happen at any time in the game, but since the field is 3x3, number of valid positions is less than 3^9 = 19683 (every position is either 'x', 'o' or ' '). This is not a hard bound, since lots of positions are invalid from the game rules perspective. I suggest you start from here, because the algorithm you are talking about are mainly used in harder games where full search is infeasible.
Hence, all you need to do is to calculate winning/losing metric for every position once after you start the program and then make a decision in O(1). This is acceptable for 3x3 field, but perhaps not much more.
The general approach is described here: https://cp-algorithms.com/game_theory/games_on_graphs.html. In a nutshell you build a tree of possible moves, mark the leaves as winning or losing and work your way up by considering all children transitions (for example, if every transition lead to a winning position for the opposite player, the position in losing).
In case you understand russian, here is a link to the original page: http://e-maxx.ru/algo/games_on_graphs
P.S. I was also playing with this game at some point in the past and implementing this approach. Here is my repo in case you want to investigate: https://github.com/yuuurchyk/cpp_tic_tac_toe. Fair warning: it's written in C++ and the code is a bit ugly
I am trying to write a program that takes in a matrix representing a board filled with 0s and 1s.
The goal is to find a path from the top left position to target position, using backtracking. You can only move up, down, left and right one space at a time. The code below raises
RecursionError: maximum recursion depth exceeded in comparison
Why is it causing the error? Is there a way to move both up, down, right and left without causing this error?
class Maze:
def __init__(self, maze, target):
self.x, self.y = target
self.b = maze
self.n = len(self.b)
self.sb = [[0 for _ in range(self.n)] for _ in range(self.n)]
def is_safe(self, row, col):
if 0 <= row < self.n and 0 <= col < self.n and self.b[row][col] == 1:
return True
return False
def find_path(self):
move_x = [1, -1, 0, 0]
move_y = [0, 0, 1, -1]
if not self.find_path_rec(move_x, move_y, 0, 0):
print("No path")
else:
self.print_maze()
def find_path_rec(self, move_x, move_y, curr_x, curr_y):
if curr_y == self.y and curr_x == self.x and self.b[self.x][self.y] == 1:
self.sb[curr_x][curr_y] = 1
return True
for i in range(4):
new_x = move_x[i] + curr_x
new_y = move_y[i] + curr_y
if self.is_safe(new_x, new_y):
self.sb[new_x][new_y] = 1
if self.find_path_rec(move_x, move_y, new_x, new_y):
return True
self.sb[new_x][new_y] = 0
return False
def print_maze(self):
for i in range(self.n):
for j in range(self.n):
print(self.sb[i][j], end="")
print()
maze = [[1, 1, 1, 1],
[1, 1, 0, 1],
[1, 0, 0, 1],
[1, 1, 0, 1]]
The Wikipedia maze generation algorithm page https://en.wikipedia.org/wiki/Maze_generation_algorithm has your answer
A disadvantage of the [recursive] approach is a large depth of recursion – in the worst case, the routine may need to recur on every cell of the area being processed, which may exceed the maximum recursion stack depth in many environments.
The alternative is to a use a stack to store your path so far rather than using recursion. Appending each move to the stack, and back-tracking when you hit a dead end.
I believe the problem is in this line:
if self.is_safe(new_x, new_y):
which should be:
if self.is_safe(new_x, new_y) and self.sb[new_x][new_y] == 0:
That is, don't revisit positions as you'll get into a circular motion and thus recursive stack overflow. Below is my rework of your code in my own thinking about your problem:
COL, ROW = (0, 1)
class Maze:
def __init__(self, maze, target):
self.target = target
self.maze = maze
self.visited = set()
def is_safe(self, position):
col, row = position
return 0 <= row < len(self.maze) and 0 <= col < len(self.maze[row]) and self.maze[row][col] == 1
def find_path(self):
start = (0, 0)
moves = [(1, 0), (-1, 0), (0, 1), (0, -1)]
self.visited.add(start)
return self.find_path_recursive(moves, start)
def find_path_recursive(self, moves, current):
if current == self.target and self.maze[self.target[ROW]][self.target[COL]] == 1:
return True
for move in moves:
new = move[COL] + current[COL], move[ROW] + current[ROW]
if self.is_safe(new) and new not in self.visited:
self.visited.add(new)
if self.find_path_recursive(moves, new):
return True
self.visited.remove(new)
return False
def print_maze(self):
for row in range(len(self.maze)):
for col in range(len(self.maze[row])):
print(1 if (col, row) in self.visited else 0, end="")
print()
if __name__ == '__main__':
maze = [
[1, 1, 1, 1],
[1, 1, 0, 1],
[1, 0, 0, 1],
[1, 1, 0, 1]
]
game = Maze(maze, (3, 3))
if game.find_path():
game.print_maze()
else:
print("No path")
My sense of coordinates my be different than yours, so take that into account when evaluating results.
OUTPUT
> python3 test.py
1111
0001
0001
0001
>
If we move the target to (1, 3), then we get:
> python3 test.py
1100
1100
1000
1100
>
I also changed your logic to potentially handle rectangular mazes instead of just square ones.
The error is in this function:
def is_safe(self, row, col):
if 0 <= row < self.n and 0 <= col < self.n and self.b[row][col] == 1:
return True
return False
The condition should include a check that the cell has not yet been visited:
if (0 <= row < self.n and 0 <= col < self.n and self.b[row][col] == 1
and self.sb[row][col] == 0):
# ^^^^^^^^^^^^^^^^^^^^^^^^^^
I would also advise to use names that are meaningful:
self.b should better be named self.maze
self.sb should better be named self.visited
My tests of my implementations of Dijkstra and A-Star have revealed that my A-star implementation is approximately 2 times SLOWER. Usually equivalent implementations of Dijkstra and A-star should see A-star beating out Dijkstra. But that isn't the case here and so it has led me to question my implementation of A-star. So I want someone to tell me what I am doing wrong in my implementation of A-star.
Here is my code:
from copy import deepcopy
from math import inf, sqrt
import maze_builderV2 as mb
if __name__ == '__main__':
order = 10
space = ['X']+['_' for x in range(order)]+['X']
maze = [deepcopy(space) for x in range(order)]
maze.append(['X' for x in range(order+2)])
maze.insert(0, ['X' for x in range(order+2)])
finalpos = (order, order)
pos = (1, 1)
maze[pos[0]][pos[1]] = 'S' # Initializing a start position
maze[finalpos[0]][finalpos[1]] = 'O' # Initializing a end position
mb.mazebuilder(maze=maze)
def spit():
for x in maze:
print(x)
spit()
print()
mazemap = {}
def scan(): # Converts raw map/maze into a suitable datastructure.
for x in range(1, order+1):
for y in range(1, order+1):
mazemap[(x, y)] = {}
t = [(x-1, y), (x+1, y), (x, y-1), (x, y+1)]
for z in t:
if maze[z[0]][z[1]] == 'X':
pass
else:
mazemap[(x, y)][z] = [sqrt((pos[0]-z[0])**2+(pos[1]-z[1])**2),
sqrt((finalpos[0]-z[0])**2+(finalpos[1]-z[1])**2)] # Euclidean distance to destination (Heuristic)
scan()
unvisited = deepcopy(mazemap)
distances = {}
paths = {}
# Initialization of distances:
for node in unvisited:
if node == pos:
distances[node] = [0, sqrt((finalpos[0]-node[0])**2+(finalpos[1]-node[1])**2)]
else:
distances[node] = [inf, inf]
while unvisited != {}:
curnode = None
for node in unvisited:
if curnode == None:
curnode = node
elif (distances[node][0]+distances[node][1]) < (distances[curnode][0]+distances[curnode][1]):
curnode = node
else:
pass
for childnode, lengths in mazemap[curnode].items():
# Length to nearby childnode - G length, Euclidean (Heuristic) length from curnode to finalpos - H length
# G length + H length < Euclidean length to reach that childnode directly + Euclidean length to finalpos from that childnode = Better path found, update known distance and paths
if lengths[0] + lengths[1] < distances[childnode][0] + distances[childnode][1]:
distances[childnode] = [lengths[0], lengths[1]]
paths[childnode] = curnode
unvisited.pop(curnode)
def shortestroute(paths, start, end):
shortestpath = []
try:
def rec(start, end):
if end == start:
shortestpath.append(end)
return shortestpath[::-1]
else:
shortestpath.append(end)
return rec(start, paths[end])
return rec(start, end)
except KeyError:
return False
finalpath = shortestroute(paths, pos, finalpos)
if finalpath:
for x in finalpath:
if x == pos or x == finalpos:
pass
else:
maze[x[0]][x[1]] = 'W'
else:
print("This maze not solvable, Blyat!")
print()
spit()
For those who find my code too messy and can't bother to read the comments I added to help with the reading... Here is a gist of my code:
Creates a mazemap (all the coordinates and its connected neighbors along with their euclidean distances from that neighboring point to the start position (G Cost) as well as to the final position (H Cost)... in a dictionary)
start position is selected as the current node. All distances to other nodes is initialised as infinity.
For every node we compare the total path cost i.e is the G cost + H cost. The one with least total cost is selected as then next current node. Each time we select new current node, we add that node to a dictionary that keeps track of through which node it was reached, so that it is easier to backtrack and find our path.
Process continues until current node is the final position.
If anyone can help me out on this, that would be great!
EDIT: On account of people asking for the maze building algorithm, here it is:
# Maze generator - v2: Generates mazes that look like city streets (more or less...)
from copy import deepcopy
from random import randint, choice
if __name__ == "__main__":
order = 10
space = ['X']+['_' for x in range(order)]+['X']
maze = [deepcopy(space) for x in range(order)]
maze.append(['X' for x in range(order+2)])
maze.insert(0, ['X' for x in range(order+2)])
pos = (1, 1)
finalpos = (order, order)
maze[pos[0]][pos[1]] = 'S' # Initializing a start position
maze[finalpos[1]][finalpos[1]] = 'O' # Initializing a end position
def spit():
for x in maze:
print(x)
blocks = []
freespaces = [(x, y) for x in range(1, order+1) for y in range(1, order+1)]
def blockbuilder(kind):
param1 = param2 = 0
double = randint(0, 1)
if kind == 0:
param2 = randint(3, 5)
if double:
param1 = 2
else:
param1 = 1
else:
param1 = randint(3, 5)
if double:
param2 = 2
else:
param2 = 1
for a in range(blockstarter[0], blockstarter[0]+param2):
for b in range(blockstarter[1], blockstarter[1]+param1):
if (a+1, b) in blocks or (a-1, b) in blocks or (a, b+1) in blocks or (a, b-1) in blocks or (a, b) in blocks or (a+1, b+1) in blocks or (a-1, b+1) in blocks or (a+1, b-1) in blocks or (a-1, b-1) in blocks:
pass
else:
if a > order+1 or b > order+1:
pass
else:
if maze[a][b] == 'X':
blocks.append((a, b))
else:
spaces = [(a+1, b), (a-1, b), (a, b+1), (a, b-1)]
for c in spaces:
if maze[c[0]][c[1]] == 'X':
break
else:
maze[a][b] = 'X'
blocks.append((a, b))
for x in range(1, order+1):
for y in range(1, order+1):
if (x, y) in freespaces:
t = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)]
i = 0
while i < len(t):
if maze[t[i][0]][t[i][1]] == 'X' or (t[i][0], t[i][1]) == pos or (t[i][0], t[i][1]) == finalpos:
del t[i]
else:
i += 1
if len(t) > 2:
blockstarter = t[randint(0, len(t)-1)]
kind = randint(0, 1) # 0 - vertical, 1 - horizontal
blockbuilder(kind)
else:
pass
# rch = choice(['d', 'u', 'r', 'l'])
b = 0
while b < len(blocks):
block = blocks[b]
t = {'d': (block[0]+2, block[1]), 'u': (block[0]-2, block[1]),
'r': (block[0], block[1]+2), 'l': (block[0], block[1]-2)}
rch = choice(['d', 'u', 'r', 'l'])
z = t[rch]
# if z[0] > order+1 or z[1] > order+1 or z[0] < 1 or z[1] < 1:
# Decreased chance of having non solvable maze being generated...
if z[0] > order-2 or z[1] > order-2 or z[0] < 2+2 or z[1] < 2+2:
pass
else:
if maze[z[0]][z[1]] == 'X':
if randint(0, 1):
set = None
if rch == 'u':
set = (z[0]+1, z[1])
elif rch == 'd':
set = (z[0]-1, z[1])
elif rch == 'r':
set = (z[0], z[1]-1)
elif rch == 'l':
set = (z[0], z[1]+1)
else:
pass
if maze[set[0]][set[1]] == '_':
# Checks so that no walls that block the entire way are formed
# Makes sure maze is solvable
sets, count = [
(set[0]+1, set[1]), (set[0]-1, set[1]), (set[0], set[1]+1), (set[0], set[1]-1)], 0
for blyat in sets:
while blyat[0] != 0 and blyat[1] != 0 and blyat[0] != order+1 and blyat[1] != order+1:
ch = [(blyat[0]+1, blyat[1]), (blyat[0]-1, blyat[1]),
(blyat[0], blyat[1]+1), (blyat[0], blyat[1]-1)]
suka = []
for i in ch:
if ch not in suka:
if maze[i[0]][i[1]] == 'X':
blyat = i
break
else:
pass
suka.append(ch)
else:
pass
else:
blyat = None
if blyat == None:
break
else:
pass
else:
count += 1
if count < 1:
maze[set[0]][set[1]] = 'X'
blocks.append(set)
else:
pass
else:
pass
else:
pass
b += 1
mazebuilder(maze, order)
spit()
Sorry for leaving this out!
Just at a quick glance, it looks like you don't have a closed set at all?? Your unvisited structure appears to contain every node in the map. This algorithm is not A* at all.
Once you fix that, make sure to change unvisited from a list to a priority queue also.