pythonarrayslist2dgame-development

How should I configure a pathfinding algororithim for my new level generation program


my problem is that I have a 2D array like this:

    [["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
     ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"]]

the player starts in the to right (represented as "X") and the goal is to get to the door ("[") . I've already made the game and player movement, but I'm trying to make a level generator so that I don't have to manually make levels, Ive already made the level gen, I just need an algorithm to check whether or not the level is possible to play, (sometimes the door isn't reachable)

i tried to make my own (quite janky) pathfinding algorithm and it really just didn't work.

How do I go about making such a function, to check the levels for playability?

code for the game below:

import sys
import tty
import termios
import os
import random
#instalize base variables for the game
levelnum = 1
op = 1
atk = 1
hp = 20
ehp = 5
XX = 0
XY = 0
keytf = False
level = [["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "["],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "["],
         ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"]]
#key getting function
def get_key():  # get the key pressed
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        ch = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch
#print level function
def printlevel():  # Print the current level
    level[XY][XX] = "X"
    print("\033[H", end="")  # Clear the terminal
    for i in range(10):
        print(" ".join(level[i]))
    print("Stats:")
    print("HP: " + str(hp))
    print("ATK: " + str(atk))
    print("Enemy HP: " + str(ehp))
#level storage
def newlevel(levelnum):
    global level
    if levelnum == 1:
        level = [["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "#", "#", "#", "#", "#", "["],
                 ["#", "#", "#", "#", "#", "#", "#", "#", "#", "["],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"]]
    elif levelnum == 2:
        level = [["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "|", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "#", "e", "#", "#", "#", "["],
                 ["#", "#", "#", "#", "|", "|", "#", "#", "#", "["],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"]]
    elif levelnum == 3:
        level = [["#", "|", "#", "e", "#", "|", "#", "e", "#", "#"],
                 ["#", "|", "#", "|", "#", "|", "#", "|", "#", "#"],
                 ["#", "|", "#", "|", "#", "|", "#", "|", "#", "#"],
                 ["#", "|", "#", "|", "#", "|", "#", "|", "#", "#"],
                 ["H", "|", "H", "|", "H", "|", "H", "|", "#", "["],
                 ["#", "|", "#", "|", "#", "|", "#", "|", "#", "["],
                 ["#", "|", "#", "|", "#", "|", "#", "|", "#", "#"],
                 ["#", "|", "#", "|", "#", "|", "#", "|", "#", "#"],
                 ["#", "|", "#", "|", "#", "|", "#", "|", "#", "#"],
                 ["#", "e", "#", "|", "#", "e", "#", "|", "#", "#"]]
    elif levelnum == 4:
        level = [["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "#", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "|", "|", "#", "#", "#", "#"],
                 ["#", "#", "#", "#", "#", "D", "#", "#", "#", "["],
                 ["#", "|", "|", "|", "|", "|", "#", "#", "#", "["],
                 ["#", "#", "#", "|", "|", "#", "#", "#", "#", "#"],
                 ["|", "|", "#", "|", "|", "#", "#", "#", "#", "#"],
                 ["|", "K", "#", "|", "|", "#", "#", "#", "#", "#"],
                 ["|", "|", "|", "|", "|", "#", "#", "#", "#", "#"]]
#check your next move
def movechecker(move, op):
    global XY, XX, levelnum, ehp, atk, hp, keytf
    level[XY][XX] = "#"
    
    if move == "[":
        XX = 0
        XY = 0
        levelnum += 1
        newlevel(levelnum)
    elif move == "e" and ehp >= 1:
        ehp -= atk
        hp -= 1
    elif move == "H":
        hp += 10
        moveit(op)
    elif move == "K":
        keytf = True
        moveit(op)
    elif move == "D" and keytf == True:
        moveit(op)
    elif ehp < 1:
        moveit(op)
        ehp = 5
    else:
        moveit(op)

    printlevel()
#move the player   
def moveit(op):
    global XY, XX
    
    if op == 1:  # Move right
        XX += 1
    elif op == 2:  # Move left
        XX -= 1
    elif op == 3:  # Move up
        XY -= 1
    elif op == 4:  # Move down
        XY += 1
    level[XY][XX] = "X"
    printlevel()

printlevel()
#main game loop
while True:  # Main game loop
    key = get_key()
    if key == "d" and XX < 9 and level[XY][XX+1] != "|":
        move = level[XY][XX+1]
        op = 1
        movechecker(move, op)
    elif key == "a" and XX > 0 and level[XY][XX-1] != "|":
        move = level[XY][XX-1]
        op = 2
        movechecker(move, op)
    elif key == "w" and XY > 0 and level[XY-1][XX] != "|":
        move = level[XY-1][XX]
        op = 3
        movechecker(move, op)
    elif key == "s" and XY < 9 and level[XY+1][XX] != "|":
        move = level[XY+1][XX]
        op = 4
        movechecker(move, op)

level gen:

    import random

level = [["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
         ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#"]]
def getwall():
    if random.randint(1, 100) < 35:
        return "|"
    return "#"
def genlevel():
    for i in range (10):
        for j in range (10):
            level [i][j] = getwall()
    level [0][0] = " "
    level [9][9] = "["

Solution

  • Your approach is to generate a random grid, and then verify whether it is one where you can reach the exit from the entry point. I suppose if this is not possible, you will generate another random grid, and continue like that until you have found a valid one.

    I would suggest to approach this differently. You can enhance the generation part to always generate a grid that has a solution. I would also suggest to avoid cycles, which also means you'd avoid trivial "spaces" that have no walls, like this:

    # #
    # #
    

    You could use one of the algorithms suggested on Wikipedia - Maze generation algorithm, such as the depth-first traversal, using an explicit stack:

    from random import choice, randrange
    
    WALL = "|"
    FREE = " "  # A temporary value. Will not occur in a fully generated level
    REACHABLE = "#"
    ENTRY = "["  # The single cell where the player starts
    EXIT = "]"   # The target cell that the player should reach
    
    def gen_level(size):
        size |= 1 # ensure it is odd
        # start with all cells surrounded by walls
        level = [
            [
                (WALL, FREE)[(x & y) % 2]
                for x in range(size)
            ]
            for y in range(size)
        ]
        stack = [(1, 1)]
        level[1][1] = REACHABLE
        while stack:
            x1, y1 = stack[-1]
            try:
                x2, y2 = choice([
                    (x2, y2)
                    for dx, dy in ((-2, 0), (2, 0), (0, -2), (0, 2))
                    for x2, y2 in ((x1 + dx, y1 + dy), )
                    if 0 <= x2 < size and 0 <= y2 < size and level[y2][x2] == FREE
                ])
                level[y2][x2] = level[(y1+y2)//2][(x1+x2)//2] = REACHABLE
                stack.append((x2, y2))
            except IndexError:
                stack.pop()
                
        level[1 + randrange(size//2) * 2][0] = ENTRY
        level[1 + randrange(size//2) * 2][-1] = EXIT
        return level
    

    Here is how you would call the above function:

    level = gen_level(20)
    for line in level:
        print(*line)
    

    And here is one possible output that this produces:

    | | | | | | | | | | | | | | | | | | | | |
    | # # # # # # # | # # # # # # # # # | # |
    | | | | | | | # | | | # | | | | | # | # |
    | # | # # # | # # # | # # # | # # # | # |
    | # | # | # | | | # | | | # | # | | | # |
    | # # # | # # # | # # # | # | # # # | # |
    | # | | | # | | | | | # | # | | | # | # |
    | # # # | # # # # # # # | # # # | # # # ]
    | | | # | | | | | | | | | # | # | | | # |
    [ # | # # # # # | # # # # # | # # # | # |
    | # | | | | | # | # | | | | | | | # | # |
    | # # # # # | # | # # # # # # # | # | # |
    | # | | | # | # | | | | | | | # | # | | |
    | # # # | # # # | # # # # # | # | # # # |
    | | | # | | | | | # | | | # | | | | | # |
    | # | # | # # # | # # # | # # # # # # # |
    | # | # | # | # | | | # | | | | | | | # |
    | # | # # # | # # # | # # # | # | # # # |
    | # | | | | | | | # | | | # | # | # | | |
    | # # # # # # # # # # # # # | # # # # # |
    | | | | | | | | | | | | | | | | | | | | |
    

    Note that the actual height/width is odd (not 20, but 21): this is a consequence of the choice to have all (x, y) reachable that have odd x and odd y.

    Unrelated, but the output is a bit more "readable" when you use block-characters for walls, and a very light character (like a dot) for the reachable cells, like using:

    WALL = "█"
    REACHABLE = "·"
    

    ...and then format the output as follows:

    for line in level:
        print(" ".join(line).replace(WALL + " " + WALL, WALL * 3)
                            .replace(WALL + " " + WALL, WALL * 3))
    

    Then you get an output like this:

    █████████████████████████████████████████
    █ · · · █ · · · · · · · · · · · · · █ · █
    █████ · █████████ · █████████ · █ · █ · █
    █ · █ · █ · · · █ · █ · · · █ · █ · · · █
    █ · █ · █ · █ · █ · █████ · █ · █████████
    █ · █ · · · █ · █ · · · · · █ · █ · · · █
    █ · █████████ · █████████ · █ · █ · █ · █
    [ · · · · · █ · █ · · · █ · █ · · · █ · █
    █ · █████████ · █ · █ · █████████████ · █
    █ · · · · · █ · · · █ · █ · · · · · · · █
    █ · █████ · █████████ · █ · █████████████
    █ · █ · · · █ · · · · · █ · █ · · · · · █
    █████ · █████ · █████████ · █ · █████ · █
    █ · · · █ · · · █ · · · · · █ · █ · █ · █
    █ · █████ · █████ · █████████ · █ · █ · █
    █ · █ · · · █ · · · · · · · · · · · █ · █
    █ · █ · █████ · █████████████████ · █ · █
    █ · █ · █ · · · · · · · █ · · · █ · █ · █
    █ · █ · █████████████████ · █ · █████ · █
    █ · · · · · · · · · · · · · █ · · · · · ]
    █████████████████████████████████████████