I've been working on a text based adventure game, in the process of doing the skeleton for the game building system. I got the movement working by typing NESW originally but after removing the debugging code, its been broken for over a day. Thought I would reach out for help...
Here's the main issue:
A grid is automatically generated with coordinate objects containing a dictionary value of places it "canGo" corresponding for a single letter key and a bool, defaulted to False. After initializing the grid, I pass a list of coordinates I want to activate to the grid that executes the following function: (1)it activates all the grids in the list passed; (2)it runs a function that generates a list of active squares; (3)it passes that list to a function that checks one square away from each of the active coords and sees if it is active, if so then it marks that direction bool as True (example canGo["N"] = True.
Issue: (Debug below) the correctly sets the attributes when initializing, after completion the keys come back WRONG but were seemingly not set again to something else in my code. Further, it sets all of keys to the same thing (in this case all of the keys in all coords are ["S"] = True)
-SD--------
SETTING DIRECTIONG debug
(1, 1) >> N
-SD--------
(1, 1) directions = N
(1, 1){'N': True, 'S': False, 'E': False, 'W': False}
-------------------
-SD--------
SETTING DIRECTIONG debug
(1, 2) >> S
-SD--------
(1, 2) directions = S
(1, 2){'N': False, 'S': True, 'E': False, 'W': False}
-------------------
(1,1): {'N': False, 'S': True, 'E': False, 'W': False}
Which direction would you like to move? <NESW>:
Debug so far: I have debug code inside that shows me it correctly recognizes the activated grids, correctly passes them to the list, and that the list correctly sets the ["canGo"] coordinate attribute. No other code is executed after initializing and processing the activated coords and before it asks you what direction you would like to go.
Code below...
Main
from coordinates import Coordinates as Coords
from grid import Grid
from player import Player
def main():
gridSizeX = 5
gridSizeY = 5
game = Grid(gridSizeX,gridSizeY, Player("John Doe", 1, 1))
#game.setActiveGridSquares([(1,1),(1,2),(1,3)])
game.grid[1][1].active = True
game.grid[1][2].active = True
game.grid[1][1].setDirections("N")
game.grid[1][2].setDirections("S")
while(True):
x,y = game.player.getLocation()
print("({x},{y}): {canGo}".format(x=x,y=y,canGo=game.grid[x][y].canGo))
move = input("Which direction would you like to move? <NESW>: ")
if(move.upper() == "EXIT"):
break
game.movePlayer(move.upper())
if __name__ == "__main__":
main()
Grid
In here, I am passing an immutable list of tuples (x,y) that then iterates through, and breaks apart. Main processing of the directions in the error occurs under Grid class in directionsProcessing()
from coordinates import Coordinates as Coords
from player import Player
class Grid:
"""Builds Grid and takes Player Object to move along grid"""
playerLocation = None
def __init__(self, gridSizeX, gridSizeY, player):
self.grid = self.buildGrid(gridSizeX,gridSizeY)
self.player = player
def buildGrid(self, gridSizeX, gridSizeY):
"""Builds and returns a grid object as a dictionary of [x][y]
Starts at 1 and ends at gridSize(X/Y)
gridSize(X/Y) will be the (x/y) max. -> 5 would be 1 to 5
"""
Grid = {}
for x in range(1, gridSizeX+1):
Grid[x] = {}
for y in range(1, gridSizeY+1):
Grid[x][y] = Coords(x, y)
return Grid
def copyGrid(self):
"""Returns a copy of grid dictionary"""
return self.grid
def setPlayer(self, playerToSet):
"""Sets player object into grid class to allow for player tracking"""
self.player = playerToSet
def setActiveGridSquares(self, squares):
"""Sets a list of grid squares to active"""
for t in squares:
x,y = t
self.grid[x][y].isActive = True
self.solveGridDirections(self.getAllActiveSquares())
def getAllActiveSquares(self):
"""Returns list of all active grid squares"""
activeGridSquares = []
for x in self.grid:
for y in self.grid[x]:
if(self.grid[x][y].isActive):
activeGridSquares.append((self.grid[x][y].x,self.grid[x][y].y))
return activeGridSquares
def solveGridDirections(self, activeSquares):
"""Resolves all active grid squares direction components to
allow movement into nearby active squares"""
for t in activeSquares:
adjoiningDirections = []
x,y = t
#print("Evaluating ActiveSquare: ("+str(x)+","+str(y)+")")
if((x,y+1) in activeSquares):
adjoiningDirections.append("N")
if((x,y-1) in activeSquares):
adjoiningDirections.append("S")
if((x+1,y) in activeSquares):
adjoiningDirections.append("E")
if((x-1,y) in activeSquares):
adjoiningDirections.append("W")
self.grid[x][y].setDirections("".join(adjoiningDirections)) #Sets allowed move directions inside grid
def movePlayer(self, direction):
"""Moves player in direction, preforms coordinate check if player can move"""
if(len(direction) > 1):
print("Lenght must be 1 character ONLY <NESW>")
return
x,y = self.player.getLocation()
print("-MP-------------") #####
print("({x},{y})Can I move in {direction} direction? {tf}".format(x=str(x),y=str(y),direction=direction,tf=str(self.grid[x][y].canGo[direction])))
print("-MP-------------") #####
if(self.grid[x][y].canGo[direction]):
self.player.movePlayer(direction)
else:
print("Player cannot move in that direciton on this grid square.")
Coordinates
class Coordinates:
"""Set coordinates of path squards in the world"""
actionOnEnter = None
choices = {"a":None,"b":None,"c":None}
canGo = {"N": False,"S": False,"E": False,"W": False}
isActive = False
def __init__(self, x, y):
self.x = x #set x coords
self.y = y #set y coords
def directionsProcessing(self, directions):
"""Directions are processed into a specific order and canGo is amended"""
listOfDirections = ["N", "S", "E", "W"]
verifiedDirections = []
coordsDir = str(self.getCoords())
print(coordsDir+" directions = "+directions) #####
for m in listOfDirections:
if(directions.find(m) != -1):
self.canGo[m] = True
verifiedDirections.append(m)
else:
self.canGo[m] = False
print(coordsDir+str(self.canGo)) #####
print("-------------------") #####
def setDirections(self, direcitons):
"""Sets directions a player can move, updates from initialization"""
print("-SD--------")#####
coordsDir = str(self.getCoords())#####
print("SETTING DIRECTIONG debug")#####
print(coordsDir+" >> "+direcitons)#####
print("-SD--------")#####
self.directionsProcessing(direcitons)
def getCoords(self):
"""Return x,y coordinate duple"""
return self.x,self.y
Player (less important but still plays a roll in motion)
class Player:
"""Player class used to hold player data.
X and Y coords are player's starting coordinates
Control inventory, keys, etc.
"""
inventory = {"sword": False}
keys = {"rectangle": False, "half moon": False}
def __init__(self, name, x, y):
self.name = name
self.x = x
self.y = y
def movePlayer(self, direction):
"""Moves player toards certain coordinates, <NESW>"""
if (len(direction) != 1):
raise Exception("Only 1 letter may be used.")
updown = {"N": 1, "S": -1, "W": -1, "E": 1}
validDirections = ["N","S","E","W"]
if(direction.upper() in validDirections):
if (direction in ["N","S"]):
self.y += updown[direction]
if (direction in ["W","E"]):
self.x += updown[direction]
else:
Exception("Direction is invalid")
def getLocation(self):
"""Returns tuple of player location x,y"""
return self.x,self.y
def setLocation(self, locationTuple):
"""Sets location based on input tuple
Syntax -> Player.setLocation((x,y))
"""
x,y = locationTuple
self.x = x
self.y = y
def toString(self):
"""Converts player data into string"""
return "Player: {name} Location: {x},{y}".format(name=self.name, x=self.x, y=self.y)
Thanks to #barny for your help.
This can be solved in two ways after reading your comments
(1) [better] move canGo to the init() method
(2) create an empty instance of "canGoTemp" inside the directionsProcession() method and pass the values to that, then pass the dict to the canGo attribute (this is without moving canGo to init())
both of these worked but I landed on (1)...
Thanks again man, I know it was a mess
Related
I am still learning Python programming and currently struggling to achieve one goal. I got a class Dot that is used to create coordinates and compare them later on. Also, I got a class Player with two other child classes that are inherited from the Parent class.
class Dot:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __repr__(self):
return {self.x, self.y}
class Player:
def __init__(self, board, enemy):
self.board = board
self.enemy = enemy
def ask(self):
raise NotImplementedError()
def turn(self):
while True:
try:
target = self.ask()
repeat = self.enemy.shoot(target)
return repeat
except BoardExceptionError as e:
print(e)
class Viki(Player):
def ask(self):
answer = Dot(randint(0, 5), randint(0, 5))
time.sleep(3)
print(f'Turn of Viki: {answer.x} {answer.y}')
return answer
class Human(Player):
def ask(self):
while True:
h = input('Your turn: ').split()
if len(h) != 2:
print('Add 2 coordinates...')
continue
x, y = h
if not (x.isdigit()) or not (y.isdigit()):
print('Add numbers from 0 to 6...')
continue
x, y = int(x), int(y)
return Dot(x - 1, y - 1)
What I would like to expect is that class "Viki(Player)" kind of an AI, forcing it to not use the same coordinates(Dots) that are already used(generated) previously. So, every time it should use none used cells on the board.
I understand that it might help in this case logical operators or count function. For example,
Example 1:
a = Dot(1, 2)
b = Dot(1, 3)
c = Dot(1, 4)
abc_list = [Dot(1, 2), Dot(2, 2), Dot(2, 3)]
print(a in abc_list)
Output
True
Example 2:
print(abc_list.count(a))
Output
1
I tried to play with both options but gettings different types of errors when I try to use loops and blocks. I understand that the bottleneck here is my knowledge :) Your help is much appreciated if someone can help me to sort this out. Thanks in advance!
Here is a generator that produces all the dots in random order (no repeats):
from itertools import product
from random import shuffle
def random_dots():
dots = [Dot(*p) for p in product(range(6), repeat=2)]
shuffle(dots)
yield from dots
rd = random_dots()
Now, you can use it in you code:
dot = next(rd)
If pre-generating all dots is not an option because there are too many, you could use the following which is lighter on memory/time:
dots = set()
def random_dot():
while (tpl := (randint(0, 5), randint(0, 5))) in dots:
pass
dots.add(tpl)
return Dot(*tpl)
And use like:
dot = random_dot()
I have this code. [enemy.py].
# Enemy behaviour.
# Entities.
from ursina import Entity
# Colors.
from ursina.color import red
# 3D Vectors.
from ursina import Vec3
# Capsule model.
from ursina import Capsule
# Random.
from random import randint, choice
# Sequences and tools for it.
from ursina.sequence import Sequence, Wait, Func
# Some enemy constants.
ENEMY_MODEL = Capsule() # NOTE (TODO): Maybe later can be replaced on new enemy 3D model.
ENEMY_SCALE = (2, 2, 2)
ENEMY_COLLIDER = 'mesh'
ENEMY_COLLISION = True
ENEMY_COLOR = red
ENEMY_SPAWN_POS = (16.328, 1.500, 16.773) # XXX: Generated by console.
# Enemy class.
class Enemy(Entity):
def __init__(self) -> None:
"""Enemy class."""
super().__init__(
model=ENEMY_MODEL,
scale=Vec3(ENEMY_SCALE),
collider=ENEMY_COLLIDER,
collision=ENEMY_COLLISION,
color=ENEMY_COLOR,
position=ENEMY_SPAWN_POS
)
self.ai_enabled = False
self.friendly = False
self.move_every_secs = 5
self.sequences = [
Sequence(
Func(self.simulate_moving),
Wait(self.move_every_secs),
loop=True
)
]
self.sequences_started = False
# Enemy update function.
def update(self):
if self.ai_enabled:
if not self.sequences_started:
for sequence in self.sequences:
sequence.start()
self.sequences_started = True
elif not self.ai_enabled:
if self.sequences_started:
for sequence in self.sequences:
sequence.pause()
self.sequences_started = False
def simulate_moving(self):
"""Simulate enemy moving."""
move_by = ['x', 'z']
move_by_random = choice(move_by)
move_on = randint(5, 10)
if move_by_random == 'x':
for _ in range(int(self.position.x + move_on)):
self.position.x += 1
elif move_by_random == 'z':
for _ in range(int(self.position.z + move_on)):
self.position.z += 1
self.ai_log('Moved.')
def enable_ai(self) -> None:
"""Enable enemy AI."""
self.ai_enabled = True
def disable_ai(self) -> None:
"""Disable enemy AI."""
self.ai_enabled = False
def set_friendly(self) -> None:
"""Make enemy friendly."""
self.friendly = True
def set_not_friendly(self) -> None:
"""Make enemy not friendly."""
self.friendly = False
def update_moving_per_secs(self, new_val: int) -> None:
"""Update moving activity per seconds."""
self.move_every_secs = new_val
def ai_log(self, message) -> None:
"""Create AI log into console."""
print(f'AI (Core) : {message}')
[game.py] (Not full code, but anyway that's what we need to know).
from enemy import Enemy
enemy = Enemy()
enemy.enable_ai()
And every 5 seconds it must move, but it's doesn't move at all.
Note, that function get called.
What to do?
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
Oh, i solved it.
We need to use self.x, not self.position.x.
Your friend bought you a present for the New Year, it's a puzzle! The puzzle consists of a number of
wooden rectangular pieces of varying lengths and widths and a board. The goal is to position the
wooden pieces on the board in a way such that all of the pieces will fit.
I have this program and I need help fixing my breadth first search algorithm.
Right now it is very slow and using a lot of memory. I think it is because I deep copy multiple times. The solve function is the main function and will do the heavy work.
I added a text file that has the first line as the dimensions of the puzzle and the rest of the lines are pieceID, pieceWidth and pieceLength respectively.
This is the Input File. Thank you so much.
10,10
1,10,1
2,1,10
3,1,5
4,3,5
5,20,2
6,1,5
7,1,5
8,2,5
import argparse, copy
import queue
import copy
import numpy as np
class PuzzleBoard():
def __init__(self, board_length, board_width ):
self.l = board_length
self.w = board_width
self.state = [[0 for _ in range(board_width)] for _ in range(board_length)]
self.used_piece = []
# Input: point - tuple cotaining (row_index, col_index) of point in self.state
# Returns true if point is out of bounds; otherwise, returns false
def __out_of_bounds(self, point):
# TODO: Implement this function
if(point < 0 or point > (len(self.state)) or (point > (self.state[0]))):
return True
return False
# Finds the next available open space in the PuzzleBoard (looking from the top-left in row-major order)
def __next(self):
for i in range(len(self.state)) :
for j in range(len(self.state[0])):
if (self.state[i][j] == 0):
return (i, j)
return False
# Input: piece - PuzzlePiece object
# Check if piece fits in the next available space (determined by __next method above)
def fits(self, piece):
position = self.__next()
if not position:
return False
#TODO: Check if any part of the piece is out of bounds
#if piece will be out bounds when place rotate to see if that helps
if((( piece.w + position[0] ) > len( self.state )) or (( piece.l + position[1] )> len( self.state[0] ))):
piece.rotate()
if((( piece.w + position[0] ) > len( self.state )) or (( piece.l + position[1] )> len( self.state[0] ))):
return False
#TODO: Check if piece can be placed without intersecting another placed piece
return True
# Input: piece - PuzzlePiece object
# Insert piece into the next available position on the board and update state
def place(self, piece):
# TODO: Bug in this function. Pieces not being placed correctly.
position = self.__next()
if self.fits(piece):
for i in range(position[0], position[0] + piece.w ):
for j in range(position[1], position[1] + piece.l):
if((( piece.w + position[0] ) > len( self.state )) or (( piece.l + position[1] )> len( self.state[0] ))):
return
if(self.state[i][j]== 0):
#self.used_piece.append(piece)
self.state[i][j] = piece.id
else:
continue
return position
def check(self, piece):
position = self.__next()
if(position[0] + piece.w > self.w or position[1] + piece.l > self.l):
return False
return True
# Returns whether the board has been filledwith pieces
def completed(self):
return True if not self.__next() else False
def copy(self):
copied = PuzzleBoard(self.l, self.w)
copied.state = copy.deepcopy(self.state)
return copied
class PuzzlePiece():
def __init__(self, pid, length, width):
self.id = pid
self.l = length
self.w = width
itfits = False
def rotate(self):
#TODO: Bug in this function. Pieces are not rotating correctly
temp = self.l
self.l = self.w
self.w = temp
def orientation(self):
return "H" if self.w >= self.l else "V"
def __str__(self):
return f"ID: {self.id}, LENGTH: {self.l}, WIDTH: {self.w}, ROTATED: {self.rotated}"
def parse_input(filepath) :
#TODO: Bug in this function. Error raised when called
parsed = {'board' : {}, 'pieces' : {}}
with open(filepath, 'r') as f:
file_contents = f.read().strip().split("\n")
board_length, board_width = file_contents[0].strip().split(",")
parsed['board']['length'] = int(board_length)
parsed['board']['width'] = int(board_width)
for i in range(1, len(file_contents)):
#FIX: the issue was fix
pid, l, w = file_contents[i].strip().split(",")
pid, l, w = int(pid), int(l), int(w)
parsed['pieces'][pid] = {}
parsed['pieces'][pid]['length'] = l
parsed['pieces'][pid]['width'] = w
return parsed
def helper(board, piece):
unused = []
#for piece in pieces:
if board.fits(piece):
position = board.place(piece)
board.used_piece.append((piece, position))
return board
def solve(board, remaining, used_pieces=[]):
# TODO: Implement a solution for a variable amount of pieces and puzzle board size.
# HINT: Recursion might help.7
poss = queue.Queue()
poss.put(board)
currboard = PuzzleBoard(len(board.state), len(board.state[0]))
while not currboard.completed():
currboard = poss.get()
#print(currboard.state)
for piece in remaining:
fakeboard = copy.deepcopy(currboard)
if(not (piece.id in np.array(fakeboard.state))):
#if( fakeboard.check(piece)):
poss.put(helper(fakeboard, piece))
print("Suff done")
return currboard
'''if(len(remaining) != 0):
board, used_pieces, unused_pieces = helper(board, remaining, used_pieces)
if board.completed():
return board, used_pieces
for i in board.state:
print(i)
print("\n \n")
return solve(board, unused_pieces, used_pieces)
return board'''
def main():
#TODO: Bug in this function. Positions are not correct after solution is found.
parser = argparse.ArgumentParser()
parser.add_argument('input')
args = parser.parse_args()
parsed = parse_input(args.input)
board = PuzzleBoard(parsed['board']['length'], parsed['board']['width'])
pieces = []
for k, v in parsed['pieces'].items():
pieces.append(PuzzlePiece(k, v['length'], v['width']))
solved = solve(board, pieces)
if not solved:
print("No solution found for given input.")
else:
print("Solution found.")
board = solved
for u, position in solved.used_piece:
print(f"Piece ID: {u.id}, Position:{position}, Orientation: {u.orientation()}")
if __name__ == "__main__":
main()
I'm writing a very simple farming game in Python using curses. At this point I have been successful in allowing the player (just a "#" character) to move around within a window.
I have a few files with ascii-art that I print to the window as things to populate the world in which the player can move around. For example, I have a file, named "house", that contains:
_ . ^ . _
/____.____\
| |
| ## _ ## |
|_""_H_""_|
I have a Thing class as follows:
class Thing(object):
def __init__(self, Xstart, Ystart, looksLike, list, window):
self.Xstart = Xstart
self.Ystart = Ystart
self.X = Xstart
self.Y = Ystart
self.looksLike = looksLike
self.boundries = []
self.find_boundries()
list.append(self)
self.draw(window)
def find_boundries(self):
file = open(self.looksLike).readlines()
for line in file:
for char in line:
if char == '\n':
pass
elif char == ' ': # skip all whitespace
self.X += 1
else:
self.boundries.append([self.X, self.Y])
self.X += 1
self.Y += 1
self.X = self.Xstart
self.X = self.Xstart # reset x & y to starting coordinates
self.Y = self.Ystart
def draw(self, window):
#file = open(self.looksLike).readlines()
#for line in file:
# window.addstr(self.Y, self.X, line)
# self.Y += 1
#self.Y = self.Ystart
file = open(self.looksLike).read()
for char in file:
window.addch(self.Y, self.X, char)
if char == '\n':
self.Y += 1
self.X = self.Xstart
else:
self.X += 1
self.X = self.Xstart
self.Y = self.Ystart
Thus the constructor for my Thing class takes a filename as an argument (looksLike) and the draw method opens the file, reads it, and prints its contents to the window. I can, then, create a house object, pass my "house" file as an argument, and my ascii house will be printed to the window.
The problem is that once the object is printed to the window, when I move the player to the right of the printed object the player disappears. Above, below, and to the left of the printed object, however, the player stays in view. For example,
_ . ^ . _
/____.____\
| |
| ## _ ## |
|_""_H_""_|
#
In this position the "#" character is visible, but if I move one space up, it disappears. If I continue moving the player up, the "#" will reappear after it moves beyond the topmost character of the house.
I assume this issue is due to the nature of both addstr() and addch() ( I have tried both) printing whitespace until the end of the window, but I have been unable to find any documentation on this.
I have considered creating a new window for each object printed, but this seems like it would get quite cumbersome when more than a few objects are printed to the window. Also, I hope to define borders around the objects printed to the screen that are shapes other than just squares or rectangles.
Is there anyway to print from a file to the window without the trailing whitespace and without creating a new window for each printed object?
I assume this issue is due to the nature of both addstr() and addch() ( I have tried both) printing whitespace until the end of the window,
Why do you assume that?
First, you never actually call addstr in the code you've shown us, so that can't be it.
As for addch, it very definitely should not do that—as you could see by just, e.g., drawing from right to left. Or by running this trivial test code:
# usual curses setup
stdscr.addch(10, 10, 'a')
stdscr.addch(10, 9, 'b')
If you're not seeing the a in that test program, there's something wrong with your terminal. But if you are, then your assumption is wrong, and it has nothing to do with addch.
Almost certainly the problem is that you actually have spaces in the house file. In curses, if you draw a character on top of another character, it replaces the old character, it doesn't try to merge them or overstrike them or anything like that. (Which is good, because most consoles don't have any way to do any such thing…)
If the new character is a space, it just replaces the old character with a space. Exactly like you're seeing.
So, the fix is to remove all those spaces on the end of each line, right? Well, you could do that. Or you could just rstrip() each line. (You don't need the \n; you can tell that you've gotten to the end of a line by the fact that you've finished iterating over the whole line, right? Well, you can in your file = open(…).readlines() code, or in code that didn't bother with the readlines() and just looped over the file itself; you can't in your different file = open(…).read() code, but I don't know why you're doing it differently there in the first place.)
Or, since your find_boundries function very carefully skips over spaces, maybe you wanted to do the same thing in draw but just forgot to? If so, just write the code you intended to skip over the spaces.
But there's a much simpler solution to the whole problem: Just draw the # after the house instead of before it, and this won't even be an issue in the first place. Of course that means that if the player is in the same place as the house, he'll show up "outside" of it rather than being hidden "inside"—but you already appear to have code to prevent that from ever happening, so it shouldn't matter who it would look like if it happened.
Onfortunately I have never used curses, and I cannot see your player class.
Nevertheless maybe this snippet might give you some ideas (press 'x' to exit game) (use WASD to move the player) (requires ANSI-enabled console:
#! /usr/bin/python3
class _GetchUnix:
def __init__(self):
import tty, sys
def __call__(self):
import sys, tty, termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
getch = _GetchUnix ()
house = ''' _ . ^ . _
/____.____\\
| |
| ## _ ## |
|_""_H_""_|'''
class Screen:
def __init__ (self, width, height, background):
self.width = width
self.height = height
self.bg = '\x1b[{}m'.format (40 + background)
self.clear = '\x1b[0m'
self.objects = []
def __iadd__ (self, obj):
self.objects.append (obj)
obj.screen = self
return self
def render (self):
print ('\x1b[1;1H', end = '')
for y in range (self.height):
for x in range (self.width):
print (self.bg + ' ' + self.clear, end = '')
print ()
for obj in self.objects: obj.render ()
print ('\x1b[{};1H'.format (self.height) )
class Object:
def __init__ (self, graphics, foreground, background, x, y):
self.graphics = graphics.split ('\n')
self.fg = '\x1b[{}m'.format (30 + foreground)
self.bg = '\x1b[{}m'.format (40 + background)
self.clear = '\x1b[0m'
self.x = x
self.y = y
def render (self):
for y, line in enumerate (self.graphics):
print ('\x1b[{};{}H'.format (self.y + y, self.x), end = '')
print (self.fg + self.bg + line + self.clear)
def collide (self, x, y):
if y < self.y: return False
if x < self.x: return False
if y > self.y + len (self.graphics) - 1: return False
if x > self.x + len (self.graphics [y - self.y] ): return False
return True
def move (self, dx, dy):
nx, ny = self.x + dx, self.y + dy
if ny < 1: return
if ny > self.screen.height: return
if nx < 1: return
if nx > self.screen.width: return
for obj in self.screen.objects:
if obj == self: continue
if obj.collide (nx, ny): return
self.x, self.y = nx, ny
house = Object (house, 0, 7, 6, 3)
player = Object ('#', 1, 3, 10, 10)
s = Screen (40, 20, 3)
s += house
s += player
while True:
c = getch ()
if c == 'x': break
if c == 'w': player.move (0, -1)
if c == 's': player.move (0, 1)
if c == 'a': player.move (-1, 0)
if c == 'd': player.move (1, 0)
s.render ()
Here a screen shot:
I'm trying to implementing A* from the pseudo code from wikipedia however I'm getting some weird results.
The implementation finds what at first looks like a good path, but with a further look it always produces the same path!
Can anyone spot anything wrong? The code is written in python 3.1 and uses pygame.
import pygame
import sys, traceback
import random
import math
TILE_WIDTH = 30
TILE_HEIGHT = 30
NUM_TILES_X = 30
NUM_TILES_Y = 30
NUM_TILES = NUM_TILES_X * NUM_TILES_Y
GRID_WIDTH = TILE_WIDTH * NUM_TILES_X
GRID_HEIGHT = TILE_HEIGHT * NUM_TILES_Y
# h(x,y)
def heuristic_dist(source,dest):
return int(( (source.x - dest.x)**2 + (source.y - dest.y)**2 ) **0.5)
def a_star(nodes,start,goal):
# Set up data structures
closedset = []
openset = [start]
came_from={}
g_score = {}
g_score[start.index] = 0
h_score = {}
h_score[start.index] = heuristic_dist(start,goal)
f_score = {}
f_score[start.index] = h_score[start.index]
while len(openset) > 0:
# Find node with least f_score in openset
x = min(openset,key=lambda el:f_score[el.index])
# We have reached our goal!
if x.index == goal.index:
path = reconstruct_path(came_from,goal.index)
# Mark the path with green color
for node in path:
nodes[node].color=(0,255,0)
print( "Yihaaa!" )
return True
# Filter out x from openset and add it to closedset
openset = list(filter(lambda y:y.index!=x.index,openset))
closedset.append(x)
# Go through all neighbours
for y in x.get_neighbours():
# If this neighbour has been closed, skip it
if y in closedset: continue
# Not sure that this is correct.
tentative_g_score = g_score[x.index] + heuristic_dist(x,y)
if y not in openset:
openset.append(y)
tentative_is_better = True
elif tentative_g_score < g_score[y.index]:
tentative_is_better = True
else:
tentative_is_better = False
if tentative_is_better:
if y.index in came_from:
if f_score[x.index] < f_score[came_from[y].index]:
came_from[y.index] = x
else:
came_from[y.index] = x
g_score[y.index] = tentative_g_score
h_score[y.index] = heuristic_dist(y, goal)
f_score[y.index] = g_score[y.index] + h_score[y.index]
print("Couldn't find a path!")
return False
# Traverse the path backwards
def reconstruct_path(came_from,current_node,depth=0):
if current_node in came_from:
p = reconstruct_path(came_from,came_from[current_node].index)
return p + [current_node]
else:
return [current_node]
def draw_string(surface,string,x,y):
s = font.render(string,True,(0,0,0))
surface.blit(s,(x,y))
# Tile or Node that has a cuple of attributes: color, cost and x,y
class Tile:
def __init__(self,x,y,cost,index):
self.x=x
self.y=y
self.cost=cost
self.index=index
self.color = (255,255,255)
def draw(self,surface):
surface.fill(self.color,pygame.Rect(self.x*TILE_WIDTH,self.y*TILE_HEIGHT,TILE_WIDTH,TILE_HEIGHT))
pygame.draw.rect(surface,(255, 180, 180),pygame.Rect(self.x*TILE_WIDTH,self.y*TILE_HEIGHT,TILE_WIDTH,TILE_HEIGHT),2)
draw_string(surface,str(self.cost),self.x*TILE_WIDTH+TILE_WIDTH//3,self.y*TILE_HEIGHT+TILE_HEIGHT//3)
def get_neighbours(self):
nbs = []
# Where are our neighbours?
offsets = [(0,-1),(-1,0),(1,0),(0,1)]
for offset in offsets:
x = self.x + offset[0]
y = self.y + offset[1]
try: # coord_to_tile throws exception if no such neighbour exists (out of bounds for example)
nbs.append(coord_to_tile(x,y))
except Exception as e:
pass
return nbs
def __eq__(self,other):
return self.x == other.x and self.y==other.y
# Small helper function to convert x,y coords to a tile instance
nodes_lookup={}
def coord_to_tile(x,y):
return nodes_lookup[(x,y)]
def main():
global nodes_lookup
screen = pygame.display.set_mode((GRID_WIDTH, GRID_HEIGHT))
tiles = []
for x in range(NUM_TILES_X):
for y in range(NUM_TILES_Y):
# Create a random distribution where max grows
cost = random.randint(1,min(x*y,98)+1)
# Let the bottom line cost 1 as well
if y == NUM_TILES_Y-1: cost = 1
t = Tile(x,y,cost,len(tiles))
nodes_lookup[(x,y)] = t
tiles.append(t)
# Do a*
a_star(tiles,tiles[0],tiles[len(tiles)-1])
while True:
event = pygame.event.wait()
if event.type == pygame.QUIT:
break
for tile in tiles:
tile.draw(screen)
pygame.display.flip()
pygame.init()
font = pygame.font.SysFont("Times New Roman",18)
try:
main()
except Exception as e:
tb = sys.exc_info()[2]
traceback.print_exception(e.__class__, e, tb)
pygame.quit()
I really have no clue, since I think I have pretty much implemented the pseudo code statement by statement.
Here's a screenshot as well:
http://andhen.mine.nu/uploads/astar.dib
Thanks!
You access came_from on time with y, and one time with y.index in
if tentative_is_better:
if y.index in came_from:
if f_score[x.index] < f_score[came_from[y].index]: // index by y
came_from[y.index] = x // index by y.index
else:
You probably meant
if f_score[x.index] < f_score[came_from[y.index].index]:
in the first line.
Besides that, the code looks ok.
Anyway, what do you mean by always produces the same path? The algorithm is supposed to return the optimal path which should always be the same... (or did you mean, it always produces the same path independently of start and goal?)`
EDIT:
You don't use your random cost anywhere in the algorithm. The 'costs' the algorithm is using are always the distance between two adjacent nodes: They are defined in heuristic_distance and used in the line
tentative_g_score = g_score[x.index] + heuristic_dist(x,y)
If you want to define random costs, you must first realize that this algorithm assigns costs to edges, not to vertices. You'll have to define some function real_costs(x,y) which calculates the costs for going from node x to node y and use this cost function instead of heuristic_dist in the above line.