### Author: Leiden Tech 
### Description: Leiden Tech
### Category: Games
### License: MIT
### Appname: Maze3D
### Built-in: no

import display
import buttons
import machine
import math
import random
import mch22

fgcolor = display.BLACK
bgcolor = display.WHITE
font = "Roboto_Black22"
grid = []
#size of maze
w = 5
h = 8
#size of screen - should be able to get this from the system
screenX = 320
screenY = 240
#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 = 6
pixelH = 6
#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
    random.seed(int(microseconds))
    clearbg()
    makeMaze()
    clearStatus()
    posX = random.randrange(1, w * 2 - 1, 2)
    posY = random.randrange(1, h * 2 - 1, 2)
    grid[posX][posY][1] = 1
    display.flush()
    drawDisplay()
"""
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
    display.drawFill(bgcolor)
    #compass - has to be set before drawView because that relies on this value
    compass = dirOffset[direction][2]
    display.drawText(screenX - (pixelW * (w * 2 + 1)), 100, compass, fgcolor, font)
    #There is room for status messages here
    #display.drawText(screenX - (pixelW * (w * 2 + 1)) + 20, 100, str(posX), font, fgcolor)
    #display.drawText(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
              display.drawRect(startX + (pixelW * x), startY + (pixelH * y), pixelW, pixelH,True, fgcolor)
            if matrix[x][y][1] == 1: #char pos triangle pointing in direction
              polyArray = resizePoly(dirOffset[fixed][3], charSize)

              display.drawTri(
                  startX + (pixelW * x) + math.floor(pixelH/2) + polyArray[0][0],
                  startY + (pixelH * y) + math.floor(pixelH/2) + polyArray[0][1],
                  startX + (pixelW * x) + math.floor(pixelH/2) + polyArray[1][0],
                  startY + (pixelH * y) + math.floor(pixelH/2) + polyArray[1][1],
                  startX + (pixelW * x) + math.floor(pixelH/2) + polyArray[2][0],
                  startY + (pixelH * y) + math.floor(pixelH/2) + polyArray[2][1],
                  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 - for debugging

    #box around view
    display.drawRect(0, 0, viewX, viewY, False, 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
                display.drawLine(endLTX, endLTY, endLBX, endLBY, fgcolor)
        else: #left is wall
            #draw left vertical line
            display.drawLine(endLTX, endLTY, endLBX, endLBY, fgcolor)
        #draw two lines for that position
        display.drawLine(startLTX, startLTY, endLTX, endLTY, fgcolor)
        display.drawLine(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
                display.drawLine(endRTX, endRTY, endRBX, endRBY, fgcolor)
        else:
            #draw right vertical line
            display.drawLine(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
                display.drawLine(endRTX, endRTY, endLTX, endLTY, fgcolor)
                display.drawLine(endRBX, endRBY, endLBX, endLBY, fgcolor)
                done = True
        #draw right lines
        display.drawLine(startRTX, startRTY, endRTX, endRTY, fgcolor)
        display.drawLine(startRBX, startRBY, endRBX, endRBY, fgcolor)
        if done:
            break
        row = row + 1
    return

def drawDisplay():
    drawCompass()
    drawMaze(grid, screenX - (pixelW * (w * 2 + 1)), 0)
    draw3dView()
    display.flush()

def quit():
  display.clear(display.WHITE)
  display.flush()
  display.drawText(50, 50, "Quitting", fgcolor, font)
  display.flush()

def clearbg():
  global bgcolor
  display.drawFill(bgcolor)
  display.flush()

def on_action_btn_a(pressed):
  global bgcolor
  if pressed:
    display.drawFill(bgcolor)
    display.flush()
    drawDisplay()

def on_action_btn_b(pressed):
  if pressed:
    drawDisplay()

def on_action_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
    drawDisplay()

def on_action_btn_down(pressed):
  global direction
  if pressed:
    for i in range(2):
      direction = direction + 1
      if direction >= len(dirOffset):
        direction = 0
    drawDisplay()

def on_action_btn_left(pressed):
  global direction
  if pressed:
    direction = direction - 1
    if direction < 0:
      direction = len(dirOffset) - 1
    drawDisplay()

def on_action_btn_right(pressed):
  global direction
  if pressed:
    direction = direction+1
    if direction >= len(dirOffset):
      direction = 0
    drawDisplay()

def on_action_btn_start(pressed):
  if pressed:
    mch22.exit_python()

def on_action_btn_select(pressed):
  if pressed:
    Maze3D()

########
# MAIN #
########
buttons.attach(buttons.BTN_UP, on_action_btn_up)
buttons.attach(buttons.BTN_DOWN, on_action_btn_down)
buttons.attach(buttons.BTN_LEFT, on_action_btn_left)
buttons.attach(buttons.BTN_RIGHT, on_action_btn_right)
buttons.attach(buttons.BTN_SELECT, on_action_btn_select)
buttons.attach(buttons.BTN_START, on_action_btn_start)
buttons.attach(buttons.BTN_A, on_action_btn_a)
buttons.attach(buttons.BTN_B, on_action_btn_b)
#Default random seed doesn't seem to work - does it work for anyone?
[year, month, mday, wday, hour, minute, second, microseconds] = machine.RTC().datetime()
Maze3D()