### Author: Leiden Tech 
### Description: Leiden Tech
### Category: Games
### License: MIT
### Appname: Maze3D
### Built-in: no
#TODO - this crashes for no apparent reason after playing for a while, or just letting it sit, I don't see the problem
import badge
import ugfx
import machine
import math
import random
import appglue

fgcolor = ugfx.BLACK
bgcolor = ugfx.WHITE
font = "Roboto_Black22"
grid = []
#size of maze - TODO can't be bigger than 4x5 otherwise fails with:
# maximum recursion depth exceeded
# have to figure a non-recursive way to generate the maze - sigh
w = 3
h = 4
#size of screen
screenX = 296
screenY = 128
#TODO size of wall block - should maybe be abs((screenX - viewX) / (w * 2 + 1)) for variable sized mazes but then viewX, viewY has to be hard-coded
pixelW = 10
pixelH = 10
#size of view screen
viewX = screenX - (pixelW * (w * 2 + 1) ) - 6 #200
viewY = screenY - 3 #125

#defines x,y compass and array of 3 points for the poly char object
dirOffset = [[0, 1, "N", [[-1, 1], [0, -1], [1, 1]]], [-1, 0, "E", [[-1, -1], [1, 0], [-1, 1]]], [0, -1, "S", [[-1, -1], [0, 1], [1, -1]]], [1, 0, "W", [[1, -1], [-1, 0], [1, 1]]]]

#character direction - posX,posY set after maze is built
direction = 0
posX = 0
posY = 0
compass = dirOffset[direction][2]

def Maze3D():
    global posX, posY
    badge.init()
    badge.eink_init()
    ugfx.init()
    ugfx.input_init()
    ugfx.input_attach(ugfx.JOY_UP, lambda pressed: btn_up(pressed))
    ugfx.input_attach(ugfx.JOY_DOWN, lambda pressed: btn_down(pressed))
    ugfx.input_attach(ugfx.JOY_LEFT, lambda pressed: btn_left(pressed))
    ugfx.input_attach(ugfx.JOY_RIGHT, lambda pressed: btn_right(pressed))
    ugfx.input_attach(ugfx.BTN_SELECT, lambda pressed: btn_select(pressed))
    ugfx.input_attach(ugfx.BTN_START, lambda pressed: btn_start(pressed))
    ugfx.input_attach(ugfx.BTN_A, lambda pressed: btn_a(pressed))
    ugfx.input_attach(ugfx.BTN_B, lambda pressed: btn_b(pressed))
    #Default random seed doesn't seem to work - does it work for anyone?
    [year, month, mday, wday, hour, minute, second, microseconds] = machine.RTC().datetime()
    random.seed(int(microseconds))
    clearScreen()
    makeMaze()
    clearStatus()
    posX = random.randrange(1, w * 2 - 1, 2)
    posY = random.randrange(1, h * 2 - 1, 2)
    grid[posX][posY][1] = 1
    ugfx.flush()
    display()
"""
Start at a random cell.
Mark the current cell as visited, and get a list of its neighbors.
    starting with a random neighbor and stepping through neighbors until a valid unvisited one is found:
            remove the wall between the current cell and that neighbor, 
            and then recurse with that neighbor as the start point.
  when you hit the end of the recursion, start over with the next valid neighbor
"""
def makeMaze():
    global grid
    """
    sets up an array of WxH grid[x][y] = [wall,status]
    wall is True/False status is numeric 
    1) position of char
    2) position of exit
    3) position of monster
    4) position of treasure
    5) etc?
    """
    #Set all positions in grid to wall
    grid = [[[True, 0] for i in range(h * 2 + 1)] for j in range(w * 2 + 1)]
    #Remove every other one to set up grid         grid[x][y] = [wall,status]
    for x in range(1, w * 2 + 1, 2):                      #########
        for y in range(1, h * 2 + 1, 2):                  # # # # #
            grid[x][y] = [False, 0]                       #########
    #start at random cell and walk                        # # # # #
    startw = random.randrange(1, w * 2 - 1, 2)            #########
    starth = random.randrange(1, h * 2 - 1, 2)            # # # # #
    walk(startw, starth, w * 2 - 1, h * 2 - 1)            #########

def walk(x, y, ww, hh):
    global grid
    #mark current cell as visited
    grid[x][y][1] = 1
    #get list of neighbors
    neighbors = [(x - 2, y), (x, y + 2), (x + 2, y), (x, y - 2)]
    #For each random neighbor
    #going to have to implement my own shuffle here, sigh
    neighbors = shuffle(neighbors)
    for (xx, yy) in neighbors:
        #skip if out of range
        if xx <= 0 or xx > ww or yy <= 0 or yy > hh:
            continue
        #Skip if already visited
        if grid[xx][yy][1] == 1:
            continue
        #if not previously visited, remove the connecting wall
        if xx == x:
            if grid[xx][yy][1] == 0:
                grid[xx][max(y, yy) - 1] = [False, 0]
        if yy == y:
            if grid[xx][yy][1] == 0:
                grid[max(x, xx) - 1][yy] = [False, 0]
        #recurse using neighbor as start point
        walk(xx, yy, ww, hh)

def shuffle(target):
    #doesn't exist in micropython apparently, gotta roll my own
    length = len(target)
    secondary = -1
    if length < 2:
        #what are you going to shuffle then moron?
        return
    for i in range(length): #maybe math.ceil(length/2)+1):
        primary = random.randint(0, length - 1)
        while secondary == primary: #no point in shuffling to itself
            secondary = random.randint(0, length - 1)
        tmp = target[primary]
        target[primary] = target[secondary]
        target[secondary] = tmp
    return target

def clearStatus():
    global grid
    for i in range(0, (w * 2 + 1)):
        for j in range(0, (h * 2 + 1)):
            grid[i][j][1] = 0

def drawCompass():
    global compass
    #area is MUCH quicker than clear... hmm
    ugfx.area(0, 0, screenX, screenY, bgcolor)
    #compass - has to be set before drawView because that relies on this value
    compass = dirOffset[direction][2]
    ugfx.string(screenX - (pixelW * (w * 2 + 1)), 100, compass, font, fgcolor)
    #There is room for status messages here
    #ugfx.string(screenX - (pixelW * (w * 2 + 1)) + 20, 100, str(posX), font, fgcolor)
    #ugfx.string(screenX - (pixelW * (w * 2 + 1)) + 40, 100, str(posY), font, fgcolor)

def drawMaze(matrix, startX, startY, fixed = None):
    charSize = 4
    if fixed is None:
        fixed = direction
    #offset where to start drawing map
    for y in range(len(matrix[0])):
        for x in range(len(matrix)):
            if matrix[x][y][0] is True: #wall
                ugfx.area(startX + (pixelW * x), startY + (pixelH * y), pixelW, pixelH, fgcolor)
            if matrix[x][y][1] == 1: #char pos triangle pointing in direction
                polyArray = resizePoly(dirOffset[fixed][3], charSize)
                ugfx.fill_polygon(startX + (pixelW * x) + math.floor(pixelH/2), startY + ( pixelH * y ) + math.floor(pixelH/2)-1, polyArray, fgcolor)

def resizePoly(array, size):
    #for variable sized char indicator
    polyArray = []
    for point in array:
        polyArray.append([point[0]*size, point[1]*size])
    return polyArray

#These set up the array of what we can see
def chunkN():
    array = []
    column = []
    for row in grid[posX-1:posX+2]:
        column.append(row[:posY+1][::-1])
    for i in range(len(column[0])):
        t = [e[i] for e in column]
        array.append(t)
    return array

def chunkE():
    array = []
    for row in grid[posX:]:
        array.append(row[posY-1:posY+2])
    return array

def chunkS():
    array = []
    column = []
    for row in grid[posX-1:posX+2]:
        column.append(row[posY:])
    for i in range(len(column[0])):
        t = [e[i] for e in column]
        array.append(t[::-1])
    return array

def chunkW():
    array = []
    for row in grid[:posX+1]:
        array.append(row[posY-1:posY+2][::-1])
    return array[::-1]

def draw3dView():
    done = False
    row = 0
    maxDepth = max(w*2,h*2)
    #This could be calculated on the fly but it's  probably not worth the effort
    offsetArray = [[0, 0], [28, 18], [48, 30], [62, 40], [77, 49], [84, 54], [90, 60]]
    maxDepth = min(len(offsetArray)-1, maxDepth)
    posGrid = []

    #rotate and slice grid for view matrix
    if ( compass == "N"):
       posGrid = chunkN()
    if ( compass == "E"):
       posGrid = chunkE()
    if ( compass == "S"):
       posGrid = chunkS()
    if ( compass == "W"):
       posGrid = chunkW()
    #drawMaze(posGrid, 0, 0, 1) #draws what view is based on

    #box around view
    ugfx.box(0, 0, viewX, viewY, fgcolor)

    while not done and row < maxDepth:
        #foreach depth
        #calculate the default points based on offset array
        offsetX  = offsetArray[row + 1][0]
        offsetY  = offsetArray[row + 1][1]

        startLTX = offsetArray[row][0]
        startLTY = offsetArray[row][1]
        endLTX   = offsetX
        endLTY   = offsetY

        #previous loop end numbers
        startLBX = offsetArray[row][0]
        startLBY = viewY - offsetArray[row][1]
        endLBX   = offsetX
        endLBY   = viewY - offsetY

        startRTX = viewX - offsetArray[row][0]
        startRTY = offsetArray[row][1]
        endRTX   = viewX - offsetX
        endRTY   = offsetY

        startRBX = viewX - offsetArray[row][0]
        startRBY = viewY - offsetArray[row][1]
        endRBX   = viewX - offsetX
        endRBY   = viewY - offsetY

        #if left is not wall, lower the start position
        if posGrid[row][0][0] is False:
            startLTY = offsetY
            startLBY = viewY - offsetY
            #if front is a wall move start right
            if posGrid[row + 1][1][0] is True:
                endLTX = viewX - offsetX
                endLBX = viewX - offsetX
                #if right is a wall move start to right wall and skip right
                if posGrid[row][2][0] is False:
                    endLTX = viewX - offsetArray[row][0]
                    endLBX = viewX - offsetArray[row][0]
                    done = True
            else:
                #draw left vertical line
                ugfx.line(endLTX, endLTY, endLBX, endLBY, fgcolor)
        else: #left is wall
            #draw left vertical line
            ugfx.line(endLTX, endLTY, endLBX, endLBY, fgcolor)
        #draw two lines for that position
        ugfx.line(startLTX, startLTY, endLTX, endLTY, fgcolor)
        ugfx.line(startLBX, startLBY, endLBX, endLBY, fgcolor)
        if done:
            break

        #starting right side
        #if right is not a wall, lower the start position
        if posGrid[row][2][0] is False:
            startRTY = offsetY
            startRBY = viewY - offsetY
            #if front is a wall move end left
            if posGrid[row + 1][1][0] is True:
                endRTX = offsetX
                endRBX = offsetX
                done = True
            else:
                #draw right vertical line
                ugfx.line(endRTX, endRTY, endRBX, endRBY, fgcolor)
        else:
            #draw right vertical line
            ugfx.line(endRTX, endRTY, endRBX, endRBY, fgcolor)
            if posGrid[row + 1][1][0] is True:
                #draw end wall and finish
                #this is going to be redundant in some cases I think
                ugfx.line(endRTX, endRTY, endLTX, endLTY, fgcolor)
                ugfx.line(endRBX, endRBY, endLBX, endLBY, fgcolor)
                done = True
        #draw right lines
        ugfx.line(startRTX, startRTY, endRTX, endRTY, fgcolor)
        ugfx.line(startRBX, startRBY, endRBX, endRBY, fgcolor)
        if done:
            break
        row = row + 1
    return


def display():
    drawCompass()
    drawMaze(grid, screenX - (pixelW * (w * 2 + 1)), 0)
    draw3dView()
    ugfx.flush()

def quit():
    ugfx.clear(ugfx.WHITE)
    ugfx.flush()
    ugfx.string(50, 50, "Quitting", font, fgcolor)
    ugfx.flush()
    appglue.start_app("launcher", False)

def clearfg():
    ugfx.clear(fgcolor)
    ugfx.flush()

def clearbg():
    ugfx.clear(bgcolor)
    ugfx.flush()

def clearScreen():
    clearfg()
    clearbg()

def btn_a(pressed):
    if pressed:
        ugfx.clear(ugfx.WHITE)
        ugfx.flush()
        display()

def btn_b(pressed):
    if pressed:
        display()

def btn_up(pressed):
    global posX, posY
    if pressed:
        newX=posX - dirOffset[direction][0]
        newY=posY - dirOffset[direction][1]
        if grid[newX][newY][0] is False:
            grid[posX][posY][1] = 0
            grid[newX][newY][1] = 1
            posX = newX
            posY = newY
        display()

def btn_down(pressed):
    global direction
    if pressed:
        for i in range(2):
            direction = direction + 1
            if direction >= len(dirOffset):
                direction = 0
        display()

def btn_left(pressed):
    global direction
    if pressed:
        direction = direction - 1
        if direction < 0:
            direction = len(dirOffset) - 1
        display()

def btn_right(pressed):
    global direction
    if pressed:
        direction = direction+1
        if direction >= len(dirOffset):
            direction = 0
        display()

def btn_start(pressed):
    if pressed:
        quit()

def btn_select(pressed):
    if pressed:
        Maze3D()

Maze3D()
