labyrinth-cs-proj/maze/modules/maze.py

543 lines
20 KiB
Python
Raw Normal View History

2023-08-18 16:40:45 +00:00
#
# Copyright © 2023 adithyagenie
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
2022-11-02 20:02:13 +00:00
import curses
import random
2022-11-04 11:25:22 +00:00
import time
2022-11-02 20:02:13 +00:00
from collections import defaultdict
from itertools import tee
2022-11-04 11:25:22 +00:00
from math import cos, exp, fabs, pi
import maze.modules.maze_saveandload as sl
2022-11-02 20:02:13 +00:00
import maze.modules.PlayerBase_func as database
2022-11-24 07:46:04 +00:00
from maze.menu import menu
2022-11-04 11:25:22 +00:00
2022-11-02 20:55:36 +00:00
from .about import about
2022-11-02 20:02:13 +00:00
WON = 0
PAUSED = False
2022-11-02 20:02:13 +00:00
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"}
2022-11-02 20:02:13 +00:00
WALL = {
12: "",
3: "",
10: "",
5: "",
9: "",
6: "",
7: "",
11: "",
14: "",
13: "",
15: "",
0: " ",
4: "",
8: "",
1: "",
2: "",
}
2022-11-02 20:02:13 +00:00
VISITED = 16
class Maze:
2022-11-30 15:57:28 +00:00
"""This class handles the entirety of maze generation and returns the maze as a string"""
2022-11-02 20:02:13 +00:00
def __init__(self, height, width, start=(0, 0)):
self.height = height
self.width = width - 11
2022-11-02 20:02:13 +00:00
self.stack = []
self.cells = {(y, x): 0 for y in range(self.height) for x in range(self.width)}
2022-11-02 20:02:13 +00:00
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)
2022-11-02 20:02:13 +00:00
return finalstr
2022-11-23 18:03:16 +00:00
def path(maze, start, finish):
2022-11-30 15:57:28 +00:00
"""This is a pathfinding logic used in loading of a stored maze"""
2022-11-02 20:02:13 +00:00
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)
2022-11-23 18:03:16 +00:00
def draw_path(
path, screen, delay=0, head=None, trail=None, skip_first=True, calledby=None
):
2022-11-30 15:57:28 +00:00
"""This is a pathfinding logic used in loading of a stored maze"""
2022-11-02 20:02:13 +00:00
if not head:
2022-11-22 10:35:23 +00:00
head = ("", curses.color_pair(2))
2022-11-02 20:02:13 +00:00
if not trail:
2022-11-22 10:35:23 +00:00
trail = ("", curses.color_pair(2))
2022-11-02 20:02:13 +00:00
current_cell = next(path)
old_cell = current_cell
for idx, next_cell in enumerate(path):
first = (not idx) and skip_first
2022-11-22 10:35:23 +00:00
if calledby != "reset" and screen.getch() == ord(" "):
2022-11-02 20:02:13 +00:00
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):
2022-11-30 15:57:28 +00:00
"""Loading screen"""
head = (".", curses.color_pair(5) | curses.A_BOLD)
trail = (".", curses.color_pair(2) | curses.A_BOLD)
2022-11-02 20:02:13 +00:00
draw_path(
maze.track(), screen, delay=0.01, head=head, trail=trail, skip_first=False
)
screen.nodelay(False)
2022-11-23 18:03:16 +00:00
def pathfinding_demo(
maze, screen, start_ts, won_coords, loadedcoords=None, loadedtime=0
):
2022-11-30 15:57:28 +00:00
"""Movement controls and win detection. Handles pausing, time for score
loadedcoords and loadedtime are optional args which handles loading saved maze"""
2022-11-02 20:02:13 +00:00
start = []
finish = []
solution = None
old_solution = None
2022-11-23 18:03:16 +00:00
2022-11-22 10:35:23 +00:00
def reset(start_or_finish, cell, colour):
2022-11-02 20:02:13 +00:00
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:
2022-11-22 10:35:23 +00:00
draw_path(old_solution, screen, head=" ", trail=" ", calledby="reset")
2022-11-02 20:02:13 +00:00
start_or_finish.append(cell)
if start and finish:
solution, old_solution = tee(path(maze, start[0], finish[0]))
2022-11-23 18:03:16 +00:00
draw_path(solution, screen, calledby="reset")
2022-11-02 20:02:13 +00:00
maxy, maxx = screen.getmaxyx()
2022-11-23 18:03:16 +00:00
2022-11-22 10:35:23 +00:00
if loadedcoords:
current_coords = list(loadedcoords)
cell = (int(current_coords[0] / 2), int(current_coords[1] / 2))
reset(finish, cell, curses.color_pair(2))
2022-11-23 18:03:16 +00:00
reset(start, (0, 0), curses.color_pair(2))
2022-11-22 10:35:23 +00:00
else:
2022-11-30 12:57:18 +00:00
# current_coords = [maxy - 5, maxx - 27]
current_coords = [1, 1]
2022-11-02 20:02:13 +00:00
screen.addstr(current_coords[0], current_coords[1], "", curses.color_pair(2))
WALL = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]
pause_elapsed = 0
2022-11-02 20:02:13 +00:00
while True:
global PAUSED
if PAUSED:
start_paused_ts = time.time()
2022-11-03 19:35:23 +00:00
screen.addstr(4, maxx - 17, "PAUSED")
screen.refresh()
while True:
pausekey = screen.getch()
if pausekey == ord("r"):
end_paused_ts = time.time()
2022-11-03 19:35:23 +00:00
screen.addstr(4, maxx - 17, " ")
PAUSED = False
break
2022-11-22 10:35:23 +00:00
pause_elapsed += int(end_paused_ts - start_paused_ts)
2022-11-23 18:03:16 +00:00
actual_elapsed = str(
int(time.time() - start_ts - -1 * loadedtime) - pause_elapsed
)
2022-11-03 19:35:23 +00:00
screen.addstr(5, maxx - 17, actual_elapsed + " sec")
2022-11-02 20:40:09 +00:00
screen.refresh()
2022-11-02 20:02:13 +00:00
key = screen.getch()
# print("Max=",maxy, maxx, "Current=", current_coords[0], current_coords[1])
2022-11-02 20:40:09 +00:00
if key == 27:
2022-11-02 20:02:13 +00:00
break
elif key == ord("p"):
PAUSED = True
continue
2022-11-03 19:35:23 +00:00
elif key == ord("m"):
2022-11-22 10:35:23 +00:00
sl.save(screen, maze, current_coords, float(actual_elapsed))
elif current_coords[0] == won_coords[0] and current_coords[1] == won_coords[1]:
2022-11-02 20:02:13 +00:00
screen.clear()
screen.refresh()
screen.border()
winmsg = """
\t\t\t
\t\t\t
\t\t\t
\t\t\t
\t\t\t """
2022-11-30 12:57:18 +00:00
screen.addstr(maxy // 2 - 5, maxx // 2 - 31, winmsg, curses.color_pair(5))
screen.border()
2022-11-02 20:02:13 +00:00
screen.refresh()
global WON
WON = WON + 1
time.sleep(3)
break
2022-11-22 10:35:23 +00:00
# Mouse hacks
2022-11-02 20:02:13 +00:00
# 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))
2022-11-02 20:02:13 +00:00
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")
2022-11-24 12:13:45 +00:00
def play(
screen,
loadedmaze=None,
loadedcoords=None,
loadedtime=0,
executeguest=False,
outerscore=0,
2022-11-30 12:57:18 +00:00
outergame=False,
2022-11-24 12:13:45 +00:00
):
2022-11-30 15:57:28 +00:00
"""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()"""
2022-11-02 20:02:13 +00:00
y, x = screen.getmaxyx()
2022-11-24 07:46:04 +00:00
height, width = int((y - 2) / 2), int((x - 2) / 2)
2022-11-24 12:13:45 +00:00
def guestswitch(score, game):
2022-11-30 15:57:28 +00:00
"""For checking if guest, else updating score"""
2022-11-24 07:46:04 +00:00
screen.clear()
screen.refresh()
2022-11-24 12:13:45 +00:00
screen.border()
2022-11-30 12:57:18 +00:00
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)
2022-11-24 07:46:04 +00:00
if res == "guest":
screen.addstr(
height - 1,
31,
2022-11-24 07:46:04 +00:00
"You are not signed in. You will lose your score if you proceed.",
2022-11-30 12:57:18 +00:00
curses.color_pair(1) | curses.A_BOLD,
2022-11-24 07:46:04 +00:00
)
screen.addstr(
2022-11-30 12:57:18 +00:00
height,
37,
"Do you want to login and save your progress? (y/n)",
curses.color_pair(1) | curses.A_BOLD,
2022-11-24 07:46:04 +00:00
)
while True:
2022-11-24 07:46:04 +00:00
key = screen.getch()
if key == ord("y"):
database.login(screen, calledby=(int(score), game))
2022-11-24 07:46:04 +00:00
break
elif key == ord("n"):
break
screen.clear()
screen.refresh()
menu(screen)
return
2022-11-24 12:13:45 +00:00
2022-11-24 07:46:04 +00:00
if executeguest:
guestswitch(outerscore, outergame)
2022-11-24 07:46:04 +00:00
return
2022-11-02 20:02:13 +00:00
screen.clear()
screen.refresh()
2022-11-03 19:35:23 +00:00
if not loadedmaze:
maze = Maze(height, width)
else:
maze = loadedmaze
2022-11-02 20:02:13 +00:00
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)
2022-11-02 20:02:13 +00:00
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")
2022-11-02 20:40:09 +00:00
screen.refresh()
2022-11-02 20:02:13 +00:00
start_ts = time.time()
2022-11-22 10:35:23 +00:00
pathfinding_demo(maze, screen, start_ts, won_coords, loadedcoords, loadedtime)
2022-11-02 20:02:13 +00:00
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))
2022-11-02 20:40:09 +00:00
score *= 10000
2022-11-02 20:02:13 +00:00
WON = 0
else:
score = 0
2022-11-24 07:46:04 +00:00
guestswitch(score, game="maze")
2022-11-02 20:02:13 +00:00
2022-11-30 12:57:18 +00:00
2022-11-02 20:02:13 +00:00
def main(screen):
2022-11-30 15:57:28 +00:00
"""Main function initialising screen settings, display loading screen and creates a maze"""
2022-11-23 18:03:16 +00:00
screen.nodelay(True)
2022-11-02 20:02:13 +00:00
curses.curs_set(False)
curses.mousemask(curses.ALL_MOUSE_EVENTS)
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
2022-11-02 20:02:13 +00:00
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)
2022-11-02 20:02:13 +00:00
screen.clear()
screen.refresh()
y, x = screen.getmaxyx()
height, width = int((y - 2) / 2), int((x - 2) / 2)
2022-11-02 20:02:13 +00:00
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...")
2022-11-02 20:02:13 +00:00
construction_demo(maze, screen)
screen.clear()
screen.refresh() # 70x 15y
menu(screen)
exit()