536 lines
20 KiB
Python
536 lines
20 KiB
Python
import curses
|
|
import random
|
|
import time
|
|
from collections import defaultdict
|
|
from itertools import tee
|
|
from math import cos, exp, fabs, pi
|
|
|
|
import maze.modules.maze_saveandload as sl
|
|
import maze.modules.PlayerBase_func as database
|
|
from maze.menu import menu
|
|
|
|
from .about import about
|
|
|
|
WON = 0
|
|
PAUSED = False
|
|
CONNECTED = {"N": 1, "S": 2, "E": 4, "W": 8}
|
|
DIRECTIONS = {"N": (-1, 0), "S": (1, 0), "E": (0, 1), "W": (0, -1)}
|
|
ANTIPODES = {"N": "S", "S": "N", "W": "E", "E": "W"}
|
|
|
|
WALL = {
|
|
12: "═",
|
|
3: "║",
|
|
10: "╗",
|
|
5: "╚",
|
|
9: "╝",
|
|
6: "╔",
|
|
7: "╠",
|
|
11: "╣",
|
|
14: "╦",
|
|
13: "╩",
|
|
15: "╬",
|
|
0: " ",
|
|
4: "═",
|
|
8: "═",
|
|
1: "║",
|
|
2: "║",
|
|
}
|
|
|
|
VISITED = 16
|
|
|
|
|
|
class Maze:
|
|
"""This class handles the entirety of maze generation and returns the maze as a string"""
|
|
|
|
def __init__(self, height, width, start=(0, 0)):
|
|
self.height = height
|
|
self.width = width - 11
|
|
self.stack = []
|
|
self.cells = {(y, x): 0 for y in range(self.height) for x in range(self.width)}
|
|
self.build(start)
|
|
|
|
def eligible_neighbours(self, y, x):
|
|
return [
|
|
((y + i, x + j), d)
|
|
for d, (i, j) in DIRECTIONS.items()
|
|
if (y + i, x + j) in self.cells.keys()
|
|
and not self.cells[(y + i, x + j)] & VISITED
|
|
]
|
|
|
|
def connected_cells(self, y, x):
|
|
cell_directions = [d for (d, v) in CONNECTED.items() if v & self.cells[(y, x)]]
|
|
return {
|
|
(y + i, x + j): d
|
|
for d, (i, j) in DIRECTIONS.items()
|
|
if d in cell_directions
|
|
}
|
|
|
|
def build(self, start):
|
|
current_cell = start
|
|
while [c for c in self.cells.values() if not c & VISITED]:
|
|
self.cells[current_cell] |= VISITED
|
|
eligible_neighbours = self.eligible_neighbours(*current_cell)
|
|
if not eligible_neighbours:
|
|
next_cell = self.stack.pop()
|
|
else:
|
|
self.stack.append(current_cell)
|
|
next_cell, direction = random.choice(eligible_neighbours)
|
|
self.cells[current_cell] |= CONNECTED[direction]
|
|
self.cells[next_cell] |= CONNECTED[ANTIPODES[direction]]
|
|
current_cell = next_cell
|
|
|
|
def track(self, start=(0, 0)):
|
|
yield start
|
|
current_cell = start
|
|
self.stack = []
|
|
for coord in self.cells.keys():
|
|
self.cells[coord] &= ~VISITED
|
|
while [c for c in self.cells.values() if not c & VISITED]:
|
|
self.cells[current_cell] |= VISITED
|
|
eligible_neighbours = [
|
|
(c, d)
|
|
for (c, d) in self.connected_cells(*current_cell).items()
|
|
if not self.cells[c] & VISITED
|
|
]
|
|
if not eligible_neighbours:
|
|
next_cell = self.stack.pop()
|
|
else:
|
|
self.stack.append(current_cell)
|
|
next_cell, direction = random.choice(eligible_neighbours)
|
|
yield next_cell
|
|
current_cell = next_cell
|
|
|
|
def __repr__(self):
|
|
buffer = [
|
|
[0 for _ in range(2 * self.width + 1)] for _ in range(2 * self.height + 1)
|
|
]
|
|
for row in range(self.height): # 0 -> 3
|
|
for col in range(self.width): # 0 -> 21
|
|
if row != 0:
|
|
buffer[2 * row][2 * col + 1] = (
|
|
~self.cells[row, col] & CONNECTED["N"]
|
|
) << 3
|
|
if col != 0:
|
|
buffer[2 * row + 1][2 * col] = (
|
|
~self.cells[row, col] & CONNECTED["W"]
|
|
) >> 3
|
|
if (row and col) != 0:
|
|
buffer[2 * row][2 * col] = (
|
|
buffer[2 * row][2 * col - 1]
|
|
| (buffer[2 * row][2 * col + 1] >> 1)
|
|
| buffer[2 * row - 1][2 * col]
|
|
| (buffer[2 * row + 1][2 * col] << 1)
|
|
)
|
|
|
|
for row in range(1, 2 * self.height):
|
|
buffer[row][0] = CONNECTED["N"] | CONNECTED["S"] | (buffer[row][1] >> 1)
|
|
buffer[row][2 * self.width] = (
|
|
CONNECTED["N"] | CONNECTED["S"] | buffer[row][2 * self.width - 1]
|
|
)
|
|
for col in range(1, 2 * self.width):
|
|
buffer[0][col] = CONNECTED["E"] | CONNECTED["W"] | (buffer[1][col] << 1)
|
|
buffer[2 * self.height][col] = (
|
|
CONNECTED["E"] | CONNECTED["W"] | buffer[2 * self.height - 1][col]
|
|
)
|
|
buffer[0][0] = CONNECTED["S"] | CONNECTED["E"]
|
|
buffer[0][2 * self.width] = CONNECTED["S"] | CONNECTED["W"]
|
|
buffer[2 * self.height][0] = CONNECTED["N"] | CONNECTED["E"]
|
|
buffer[2 * self.height][2 * self.width] = CONNECTED["N"] | CONNECTED["W"]
|
|
finalstr = "\n".join(["".join(WALL[cell] for cell in row) for row in buffer])
|
|
broken = list(finalstr)
|
|
broken[len(broken) - 2 - (2 * self.width + 1)] = " "
|
|
finalstr = "".join(broken)
|
|
return finalstr
|
|
|
|
|
|
def path(maze, start, finish):
|
|
"""This is a pathfinding logic used in loading of a stored maze"""
|
|
heuristic = lambda node: abs(node[0] - finish[0]) + abs(node[1] - finish[1])
|
|
nodes_to_explore = [start]
|
|
explored_nodes = set()
|
|
parent = {}
|
|
global_score = defaultdict(lambda: float("inf"))
|
|
global_score[start] = 0
|
|
local_score = defaultdict(lambda: float("inf"))
|
|
local_score[start] = heuristic(start)
|
|
|
|
def retrace_path(current):
|
|
total_path = [current]
|
|
while current in parent.keys():
|
|
current = parent[current]
|
|
total_path.append(current)
|
|
return reversed(total_path)
|
|
|
|
while nodes_to_explore:
|
|
nodes_to_explore.sort(key=lambda n: local_score[n])
|
|
current = nodes_to_explore.pop()
|
|
if current == finish:
|
|
return retrace_path(current)
|
|
explored_nodes.add(current)
|
|
for neighbour in maze.connected_cells(*current).keys():
|
|
tentative_global_score = global_score[current] + 1
|
|
if tentative_global_score < global_score[neighbour]:
|
|
parent[neighbour] = current
|
|
global_score[neighbour] = tentative_global_score
|
|
local_score[neighbour] = global_score[neighbour] + heuristic(neighbour)
|
|
if neighbour not in explored_nodes:
|
|
nodes_to_explore.append(neighbour)
|
|
|
|
|
|
def draw_path(
|
|
path, screen, delay=0, head=None, trail=None, skip_first=True, calledby=None
|
|
):
|
|
"""This is a pathfinding logic used in loading of a stored maze"""
|
|
if not head:
|
|
head = ("█", curses.color_pair(2))
|
|
if not trail:
|
|
trail = ("█", curses.color_pair(2))
|
|
current_cell = next(path)
|
|
old_cell = current_cell
|
|
for idx, next_cell in enumerate(path):
|
|
first = (not idx) and skip_first
|
|
if calledby != "reset" and screen.getch() == ord(" "):
|
|
break
|
|
screen.refresh()
|
|
for last, cell in enumerate(
|
|
[
|
|
(
|
|
current_cell[0] + t * (next_cell[0] - current_cell[0]),
|
|
current_cell[1] + t * (next_cell[1] - current_cell[1]),
|
|
)
|
|
for t in [0, 1 / 2]
|
|
]
|
|
):
|
|
time.sleep(delay)
|
|
if not first:
|
|
screen.addstr(*coords(cell), *head)
|
|
if last:
|
|
if not first:
|
|
screen.addstr(*coords(current_cell), *trail)
|
|
old_cell = cell
|
|
elif not first:
|
|
screen.addstr(*coords(old_cell), *trail)
|
|
screen.refresh()
|
|
current_cell = next_cell
|
|
|
|
|
|
def coords(node):
|
|
return (int(2 * node[0]) + 1, int(2 * node[1]) + 1)
|
|
|
|
|
|
def construction_demo(maze, screen):
|
|
"""Loading screen"""
|
|
head = (".", curses.color_pair(5) | curses.A_BOLD)
|
|
trail = (".", curses.color_pair(2) | curses.A_BOLD)
|
|
draw_path(
|
|
maze.track(), screen, delay=0.01, head=head, trail=trail, skip_first=False
|
|
)
|
|
screen.nodelay(False)
|
|
|
|
|
|
def pathfinding_demo(
|
|
maze, screen, start_ts, won_coords, loadedcoords=None, loadedtime=0
|
|
):
|
|
"""Movement controls and win detection. Handles pausing, time for score
|
|
loadedcoords and loadedtime are optional args which handles loading saved maze"""
|
|
start = []
|
|
finish = []
|
|
solution = None
|
|
old_solution = None
|
|
|
|
def reset(start_or_finish, cell, colour):
|
|
nonlocal solution, old_solution
|
|
if start_or_finish:
|
|
screen.addstr(*coords(start_or_finish.pop()), " ")
|
|
screen.addstr(*coords(cell), "█", colour)
|
|
screen.refresh()
|
|
if old_solution:
|
|
draw_path(old_solution, screen, head=" ", trail=" ", calledby="reset")
|
|
start_or_finish.append(cell)
|
|
if start and finish:
|
|
solution, old_solution = tee(path(maze, start[0], finish[0]))
|
|
draw_path(solution, screen, calledby="reset")
|
|
|
|
maxy, maxx = screen.getmaxyx()
|
|
|
|
if loadedcoords:
|
|
current_coords = list(loadedcoords)
|
|
cell = (int(current_coords[0] / 2), int(current_coords[1] / 2))
|
|
reset(finish, cell, curses.color_pair(2))
|
|
reset(start, (0, 0), curses.color_pair(2))
|
|
else:
|
|
# current_coords = [maxy - 5, maxx - 27]
|
|
current_coords = [1, 1]
|
|
screen.addstr(current_coords[0], current_coords[1], "█", curses.color_pair(2))
|
|
WALL = ["═", "║", "╗", "╚", "╝", "╔", "╠", "╣", "╦", "╩", "╬", "═", "═", "║", "║"]
|
|
pause_elapsed = 0
|
|
|
|
while True:
|
|
global PAUSED
|
|
if PAUSED:
|
|
start_paused_ts = time.time()
|
|
screen.addstr(4, maxx - 17, "PAUSED")
|
|
screen.refresh()
|
|
while True:
|
|
pausekey = screen.getch()
|
|
if pausekey == ord("r"):
|
|
end_paused_ts = time.time()
|
|
screen.addstr(4, maxx - 17, " ")
|
|
PAUSED = False
|
|
break
|
|
pause_elapsed += int(end_paused_ts - start_paused_ts)
|
|
actual_elapsed = str(
|
|
int(time.time() - start_ts - -1 * loadedtime) - pause_elapsed
|
|
)
|
|
screen.addstr(5, maxx - 17, actual_elapsed + " sec")
|
|
screen.refresh()
|
|
key = screen.getch()
|
|
# print("Max=",maxy, maxx, "Current=", current_coords[0], current_coords[1])
|
|
if key == 27:
|
|
break
|
|
elif key == ord("p"):
|
|
PAUSED = True
|
|
continue
|
|
elif key == ord("m"):
|
|
sl.save(screen, maze, current_coords, float(actual_elapsed))
|
|
elif current_coords[0] == won_coords[0] and current_coords[1] == won_coords[1]:
|
|
screen.clear()
|
|
screen.refresh()
|
|
screen.border()
|
|
winmsg = """
|
|
\t\t\t██ ██ ██████ ██ ██ ██ ██ ██████ ███ ██ ██
|
|
\t\t\t ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██
|
|
\t\t\t ████ ██ ██ ██ ██ ██ █ ██ ██ ██ ██ ██ ██ ██
|
|
\t\t\t ██ ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██
|
|
\t\t\t ██ ██████ ██████ ███ ███ ██████ ██ ████ ██"""
|
|
screen.addstr(maxy // 2 - 5, maxx // 2 - 31, winmsg, curses.color_pair(5))
|
|
screen.border()
|
|
screen.refresh()
|
|
global WON
|
|
WON = WON + 1
|
|
time.sleep(3)
|
|
break
|
|
# Mouse hacks
|
|
# elif key == curses.KEY_MOUSE:
|
|
# _, x, y, _, state = curses.getmouse()
|
|
# cell = (int(y / 2), int(x / 2))
|
|
# if state & curses.BUTTON3_PRESSED:
|
|
# reset(finish, cell, curses.color_pair(2))
|
|
# elif state & curses.BUTTON1_PRESSED:
|
|
# reset(start, cell, curses.color_pair(1))
|
|
|
|
elif key == curses.KEY_UP:
|
|
if (
|
|
screen.instr(current_coords[0] - 1, current_coords[1], 1).decode(
|
|
"utf-8"
|
|
)
|
|
== "█"
|
|
):
|
|
screen.addstr(current_coords[0], current_coords[1], " ")
|
|
screen.refresh()
|
|
current_coords = [current_coords[0] - 1, current_coords[1]]
|
|
elif (
|
|
screen.instr(current_coords[0] - 1, current_coords[1], 1).decode(
|
|
"utf-8"
|
|
)
|
|
not in WALL
|
|
):
|
|
# if current_coords[0]-1 != 0:
|
|
current_coords = [current_coords[0] - 1, current_coords[1]]
|
|
screen.addstr(
|
|
current_coords[0], current_coords[1], "█", curses.color_pair(2)
|
|
)
|
|
# else:
|
|
# print(screen.instr(current_coords[0]-1,current_coords[1],1).decode("utf-8"), "UP PRESS")
|
|
elif key == curses.KEY_DOWN:
|
|
if (
|
|
screen.instr(current_coords[0] + 1, current_coords[1], 1).decode(
|
|
"utf-8"
|
|
)
|
|
== "█"
|
|
):
|
|
screen.addstr(current_coords[0], current_coords[1], " ")
|
|
screen.refresh()
|
|
current_coords = [current_coords[0] + 1, current_coords[1]]
|
|
elif (
|
|
screen.instr(current_coords[0] + 1, current_coords[1], 1).decode(
|
|
"utf-8"
|
|
)
|
|
not in WALL
|
|
):
|
|
current_coords = [current_coords[0] + 1, current_coords[1]]
|
|
screen.addstr(
|
|
current_coords[0], current_coords[1], "█", curses.color_pair(2)
|
|
)
|
|
# else:
|
|
# print(screen.instr(current_coords[0]+1,current_coords[1],1).decode("utf-8"), "DOWN PRESS")
|
|
elif key == curses.KEY_LEFT:
|
|
if (
|
|
screen.instr(current_coords[0], current_coords[1] - 1, 1).decode(
|
|
"utf-8"
|
|
)
|
|
== "█"
|
|
):
|
|
screen.addstr(current_coords[0], current_coords[1], " ")
|
|
screen.refresh()
|
|
current_coords = [current_coords[0], current_coords[1] - 1]
|
|
elif (
|
|
screen.instr(current_coords[0], current_coords[1] - 1, 1).decode(
|
|
"utf-8"
|
|
)
|
|
not in WALL
|
|
):
|
|
current_coords = [current_coords[0], current_coords[1] - 1]
|
|
screen.addstr(
|
|
current_coords[0], current_coords[1], "█", curses.color_pair(2)
|
|
)
|
|
# else:
|
|
# print(screen.instr(current_coords[0],current_coords[1]-1,1).decode("utf-8"), "SIDE PRESS")
|
|
elif key == curses.KEY_RIGHT:
|
|
if (
|
|
screen.instr(current_coords[0], current_coords[1] + 1, 1).decode(
|
|
"utf-8"
|
|
)
|
|
== "█"
|
|
):
|
|
screen.addstr(current_coords[0], current_coords[1], " ")
|
|
screen.refresh()
|
|
current_coords = [current_coords[0], current_coords[1] + 1]
|
|
elif (
|
|
screen.instr(current_coords[0], current_coords[1] + 1, 1).decode(
|
|
"utf-8"
|
|
)
|
|
not in WALL
|
|
):
|
|
current_coords = [current_coords[0], current_coords[1] + 1]
|
|
screen.addstr(
|
|
current_coords[0], current_coords[1], "█", curses.color_pair(2)
|
|
)
|
|
# else:
|
|
# print(screen.instr(current_coords[0],current_coords[1]+1,1).decode("utf-8"), "RSIDE PRESS")
|
|
|
|
|
|
def play(
|
|
screen,
|
|
loadedmaze=None,
|
|
loadedcoords=None,
|
|
loadedtime=0,
|
|
executeguest=False,
|
|
outerscore=0,
|
|
outergame=False,
|
|
):
|
|
"""Displays side menu, handles win, lose of maze
|
|
loadedmaze, loadedcoords and loadedtime are used for loading saved maze
|
|
executeguest, outerscore and outergame are for calling the bounded func guestswitch()"""
|
|
y, x = screen.getmaxyx()
|
|
height, width = int((y - 2) / 2), int((x - 2) / 2)
|
|
|
|
def guestswitch(score, game):
|
|
"""For checking if guest, else updating score"""
|
|
screen.clear()
|
|
screen.refresh()
|
|
screen.border()
|
|
screen.addstr(
|
|
y // 2 - 5,
|
|
x // 2 - 7,
|
|
str("Your score is: " + str(int(score))),
|
|
curses.color_pair(3) | curses.A_BOLD,
|
|
)
|
|
res = database.Update_score(int(score), game)
|
|
if res == "guest":
|
|
screen.addstr(
|
|
height - 1,
|
|
31,
|
|
"You are not signed in. You will lose your score if you proceed.",
|
|
curses.color_pair(1) | curses.A_BOLD,
|
|
)
|
|
screen.addstr(
|
|
height,
|
|
37,
|
|
"Do you want to login and save your progress? (y/n)",
|
|
curses.color_pair(1) | curses.A_BOLD,
|
|
)
|
|
while True:
|
|
key = screen.getch()
|
|
if key == ord("y"):
|
|
database.login(screen, calledby=(int(score), game))
|
|
break
|
|
elif key == ord("n"):
|
|
break
|
|
screen.clear()
|
|
screen.refresh()
|
|
menu(screen)
|
|
return
|
|
|
|
if executeguest:
|
|
guestswitch(outerscore, outergame)
|
|
return
|
|
|
|
screen.clear()
|
|
screen.refresh()
|
|
if not loadedmaze:
|
|
maze = Maze(height, width)
|
|
else:
|
|
maze = loadedmaze
|
|
screen.addstr(0, 0, str(maze))
|
|
won_coords = screen.getyx()
|
|
won_coords = list(won_coords)
|
|
won_coords[0] = won_coords[0] - 1
|
|
won_coords = tuple(won_coords)
|
|
screen.refresh()
|
|
sx = x - 22 # x - 23
|
|
screen.addstr(0, sx, "LABYRINTH")
|
|
screen.addstr(5, sx, "Time:")
|
|
screen.addstr(8, sx, "esc - Quit")
|
|
screen.addstr(9, sx, "Up - Move up")
|
|
screen.addstr(10, sx, "Down - Move down")
|
|
screen.addstr(11, sx, "Left - Move left")
|
|
screen.addstr(12, sx, "Right - Move right")
|
|
screen.addstr(13, sx, "p - Pause")
|
|
screen.addstr(14, sx, "r - Resume")
|
|
screen.addstr(15, sx, "m - save")
|
|
screen.refresh()
|
|
start_ts = time.time()
|
|
pathfinding_demo(maze, screen, start_ts, won_coords, loadedcoords, loadedtime)
|
|
end_ts = time.time()
|
|
came_out = 1
|
|
while True:
|
|
if came_out != 0:
|
|
global WON
|
|
if WON != 0:
|
|
tt = (start_ts - end_ts) / 300
|
|
score = fabs(cos(tt * pi))
|
|
score *= 10000
|
|
WON = 0
|
|
else:
|
|
score = 0
|
|
|
|
guestswitch(score, game="maze")
|
|
|
|
|
|
def main(screen):
|
|
"""Main function initialising screen settings, display loading screen and creates a maze"""
|
|
screen.nodelay(True)
|
|
curses.curs_set(False)
|
|
curses.mousemask(curses.ALL_MOUSE_EVENTS)
|
|
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
|
curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
|
|
curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
|
screen.clear()
|
|
screen.refresh()
|
|
y, x = screen.getmaxyx()
|
|
height, width = int((y - 2) / 2), int((x - 2) / 2)
|
|
database.databaseinit()
|
|
database.tableinit()
|
|
maze = Maze(height, width)
|
|
screen.addstr(0, 0, str(maze))
|
|
screen.refresh()
|
|
screen.addstr(2, x - 22, "Press space to skip")
|
|
screen.addstr(3, x - 22, "the loading screen...")
|
|
construction_demo(maze, screen)
|
|
screen.clear()
|
|
screen.refresh() # 70x 15y
|
|
menu(screen)
|
|
exit()
|