2017-10-12 15:29:55 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2017-10-16 17:12:32 +02:00
|
|
|
# maze.py - module for the maze class
|
2017-10-12 15:29:55 +02:00
|
|
|
# This file is part of brutalmaze
|
|
|
|
#
|
|
|
|
# brutalmaze is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# brutalmaze is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with brutalmaze. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
# Copyright (C) 2017 Nguyễn Gia Phong
|
|
|
|
|
|
|
|
from collections import deque
|
2017-10-15 12:50:06 +02:00
|
|
|
from math import pi, atan, cos, sin, log
|
|
|
|
from random import choice, getrandbits
|
2017-10-12 15:29:55 +02:00
|
|
|
|
|
|
|
import pygame
|
|
|
|
|
2017-10-13 16:56:03 +02:00
|
|
|
from .characters import pos, sign, regpoly, fill_aapolygon, Hero, Enemy
|
2017-10-12 15:29:55 +02:00
|
|
|
from .constants import *
|
|
|
|
|
2017-10-16 17:12:32 +02:00
|
|
|
__doc__ = 'brutalmaze module for the maze class'
|
|
|
|
|
2017-10-12 15:29:55 +02:00
|
|
|
|
2017-10-13 10:10:16 +02:00
|
|
|
def cosin(x):
|
|
|
|
"""Return the sum of cosine and sine of x (measured in radians)."""
|
|
|
|
return cos(x) + sin(x)
|
|
|
|
|
|
|
|
|
2017-10-15 11:20:14 +02:00
|
|
|
def length(x0, y0, x1, y1):
|
|
|
|
"""Return the length of the line segment joining the two points
|
|
|
|
(x0, y0) and (x1, y1).
|
|
|
|
"""
|
|
|
|
return ((x0-x1)**2 + (y0-y1)**2)**0.5
|
|
|
|
|
|
|
|
|
2017-10-16 17:12:32 +02:00
|
|
|
def cell(bit, upper=True):
|
|
|
|
"""Return a half of a cell of the maze based on the given bit."""
|
|
|
|
if bit: return deque([WALL]*ROAD_WIDTH + [EMPTY]*ROAD_WIDTH)
|
|
|
|
if upper: return deque([WALL] * (ROAD_WIDTH<<1))
|
|
|
|
return deque([EMPTY] * (ROAD_WIDTH<<1))
|
|
|
|
|
|
|
|
|
|
|
|
def new_column():
|
|
|
|
"""Return a newly generated column of the maze."""
|
|
|
|
column = deque()
|
|
|
|
upper, lower = deque(), deque()
|
|
|
|
for _ in range(MAZE_SIZE):
|
|
|
|
b = getrandbits(1)
|
|
|
|
upper.extend(cell(b))
|
|
|
|
lower.extend(cell(b, False))
|
|
|
|
for _ in range(ROAD_WIDTH): column.append(upper.__copy__())
|
|
|
|
for _ in range(ROAD_WIDTH): column.append(lower.__copy__())
|
|
|
|
return column
|
|
|
|
|
|
|
|
|
2017-10-12 15:29:55 +02:00
|
|
|
class Maze:
|
|
|
|
"""Object representing the maze, including the characters."""
|
|
|
|
def __init__(self, size):
|
|
|
|
self.w, self.h = size
|
|
|
|
self.surface = pygame.display.set_mode(size, RESIZABLE)
|
2017-10-16 17:12:32 +02:00
|
|
|
self.distance = (self.w * self.h / 416) ** 0.5
|
|
|
|
self.step = self.distance / MOVE_SPEED
|
2017-10-13 10:10:16 +02:00
|
|
|
self.middlex, self.middley = self.x, self.y = self.w >> 1, self.h >> 1
|
2017-10-16 17:12:32 +02:00
|
|
|
w, h = (int((i/self.distance+2) / 2) for i in size)
|
2017-10-12 15:29:55 +02:00
|
|
|
self.rangex = range(MIDDLE - w, MIDDLE + w + 1)
|
|
|
|
self.rangey = range(MIDDLE - h, MIDDLE + h + 1)
|
2017-10-15 12:50:06 +02:00
|
|
|
self.right = self.down = self.offsetx = self.offsety = 0
|
|
|
|
self.score = INIT_SCORE
|
2017-10-12 15:29:55 +02:00
|
|
|
|
|
|
|
self.map = deque()
|
2017-10-16 17:12:32 +02:00
|
|
|
for _ in range(MAZE_SIZE): self.map.extend(new_column())
|
|
|
|
self.rotatex = self.rotatey = 0
|
2017-10-14 16:49:37 +02:00
|
|
|
self.enemies = []
|
2017-10-15 12:50:06 +02:00
|
|
|
self.add_enemy()
|
|
|
|
self.hero = Hero(self.surface)
|
|
|
|
self.map[MIDDLE][MIDDLE] = HERO
|
|
|
|
self.slashd = self.hero.R + self.distance/SQRT2
|
|
|
|
self.draw()
|
|
|
|
|
|
|
|
def add_enemy(self):
|
|
|
|
"""Add enough enemies."""
|
2017-10-16 17:12:32 +02:00
|
|
|
walls, length = [], log(self.score, GOLDEN_MEAN)
|
|
|
|
for i in self.rangex:
|
|
|
|
for j in self.rangey:
|
|
|
|
if self.map[i][j] == WALL: walls.append((i, j))
|
|
|
|
while walls and len(self.enemies) < length:
|
|
|
|
x, y = choice(walls)
|
2017-10-14 16:49:37 +02:00
|
|
|
if all(self.map[x + a][y + b] == WALL for a, b in ADJACENT_GRIDS):
|
|
|
|
continue
|
|
|
|
self.enemies.append(
|
|
|
|
Enemy(self.surface, self.map, choice(ENEMIES), x, y))
|
2017-10-16 17:12:32 +02:00
|
|
|
walls.remove((x, y))
|
2017-10-12 15:29:55 +02:00
|
|
|
|
|
|
|
def draw(self):
|
|
|
|
"""Draw the maze."""
|
|
|
|
self.surface.fill(BG_COLOR)
|
|
|
|
for i in self.rangex:
|
|
|
|
for j in self.rangey:
|
2017-10-14 16:49:37 +02:00
|
|
|
if self.map[i][j] != WALL: continue
|
2017-10-13 10:10:16 +02:00
|
|
|
x, y = pos(i, j, self.distance, self.middlex, self.middley)
|
2017-10-16 17:12:32 +02:00
|
|
|
square = regpoly(4, self.distance / SQRT2, pi / 4, x, y)
|
2017-10-12 15:29:55 +02:00
|
|
|
fill_aapolygon(self.surface, square, FG_COLOR)
|
|
|
|
|
2017-10-13 10:10:16 +02:00
|
|
|
def wake(self, enemy):
|
|
|
|
"""Wake the enemy up if it can see the hero."""
|
|
|
|
dx = (enemy.x - MIDDLE)*self.distance + self.offsetx*self.step
|
|
|
|
dy = (enemy.y - MIDDLE)*self.distance + self.offsety*self.step
|
|
|
|
mind = cosin(abs(atan(dy / dx)) if dx else 0) * self.distance
|
|
|
|
startx = starty = MIDDLE
|
|
|
|
stopx, stopy = enemy.x, enemy.y
|
|
|
|
if startx > stopx : startx, stopx = stopx, startx
|
|
|
|
if starty > stopy : starty, stopy = stopy, starty
|
|
|
|
for i in range(startx, stopx + 1):
|
|
|
|
for j in range(starty, stopy + 1):
|
2017-10-14 16:49:37 +02:00
|
|
|
if self.map[i][j] != WALL: continue
|
2017-10-13 10:10:16 +02:00
|
|
|
x, y = pos(i, j, self.distance, self.middlex, self.middley)
|
|
|
|
d = abs(dy*(x-self.x) - dx*(y-self.y)) / (dy**2 + dx**2)**0.5
|
|
|
|
if d <= mind: return
|
|
|
|
enemy.awake = True
|
2017-10-12 15:29:55 +02:00
|
|
|
|
2017-10-14 16:49:37 +02:00
|
|
|
def rotate(self, x, y):
|
|
|
|
"""Rotate the maze by (x, y)."""
|
|
|
|
for enemy in self.enemies: self.map[enemy.x][enemy.y] = EMPTY
|
|
|
|
if x:
|
|
|
|
self.offsetx = 0
|
|
|
|
self.map.rotate(x)
|
2017-10-16 17:12:32 +02:00
|
|
|
self.rotatex = (self.rotatex+x) % (ROAD_WIDTH*2)
|
2017-10-14 16:49:37 +02:00
|
|
|
if y:
|
|
|
|
self.offsety = 0
|
|
|
|
for d in self.map: d.rotate(y)
|
2017-10-16 17:12:32 +02:00
|
|
|
self.rotatey = (self.rotatey+y) % (ROAD_WIDTH*2)
|
|
|
|
if not self.rotatex and not self.rotatey:
|
|
|
|
for _ in range(ROAD_WIDTH * 2): self.map.pop()
|
|
|
|
self.map.extend(new_column())
|
|
|
|
for i in range(MAZE_SIZE):
|
|
|
|
b = getrandbits(1)
|
|
|
|
for j, grid in enumerate(cell(b)):
|
|
|
|
for k in range(ROAD_WIDTH):
|
|
|
|
self.map[i*2*ROAD_WIDTH+k][LAST_ROW + j] = grid
|
|
|
|
for j, grid in enumerate(cell(b, False)):
|
|
|
|
for k in range(ROAD_WIDTH):
|
|
|
|
self.map[(i*2+1)*ROAD_WIDTH+k][LAST_ROW + j] = grid
|
|
|
|
|
2017-10-15 12:50:06 +02:00
|
|
|
killist = []
|
|
|
|
for i, enemy in enumerate(self.enemies):
|
|
|
|
enemy.place(x, y)
|
|
|
|
if enemy.x not in self.rangex or enemy.y not in self.rangey:
|
|
|
|
enemy.die()
|
|
|
|
killist.append(i)
|
|
|
|
for i in reversed(killist): self.enemies.pop(i)
|
|
|
|
self.add_enemy()
|
2017-10-14 16:49:37 +02:00
|
|
|
|
2017-10-15 11:20:14 +02:00
|
|
|
def slash(self):
|
|
|
|
"""Slash the enemies."""
|
|
|
|
unit, killist = self.distance/SQRT2 * self.hero.speed, []
|
|
|
|
for i, enemy in enumerate(self.enemies):
|
|
|
|
x, y = enemy.pos(self.distance, self.middlex, self.middley)
|
|
|
|
d = length(x, y, self.x, self.y)
|
|
|
|
if d <= self.slashd:
|
|
|
|
enemy.wound += (self.slashd-d) / unit
|
2017-10-15 12:50:06 +02:00
|
|
|
if enemy.wound >= len(enemy.color):
|
2017-10-16 17:12:32 +02:00
|
|
|
self.score += enemy.wound
|
2017-10-15 11:20:14 +02:00
|
|
|
enemy.die()
|
|
|
|
killist.append(i)
|
|
|
|
for i in reversed(killist): self.enemies.pop(i)
|
2017-10-15 12:50:06 +02:00
|
|
|
self.add_enemy()
|
2017-10-15 11:20:14 +02:00
|
|
|
|
2017-10-12 15:29:55 +02:00
|
|
|
def update(self):
|
|
|
|
"""Update the maze."""
|
2017-10-15 12:50:06 +02:00
|
|
|
modified, d = False, self.distance*1.5 - self.hero.R
|
2017-10-12 15:29:55 +02:00
|
|
|
self.offsetx += self.right
|
|
|
|
s = sign(self.offsetx) * 2
|
|
|
|
if ((self.map[MIDDLE - s][MIDDLE - 1]
|
|
|
|
or self.map[MIDDLE - s][MIDDLE]
|
|
|
|
or self.map[MIDDLE - s][MIDDLE + 1])
|
2017-10-15 12:50:06 +02:00
|
|
|
and abs(self.offsetx*self.step) > d):
|
2017-10-12 15:29:55 +02:00
|
|
|
self.offsetx -= self.right
|
|
|
|
else:
|
|
|
|
modified = True
|
|
|
|
|
|
|
|
self.offsety += self.down
|
|
|
|
s = sign(self.offsety) * 2
|
|
|
|
if ((self.map[MIDDLE - 1][MIDDLE - s]
|
|
|
|
or self.map[MIDDLE][MIDDLE - s]
|
|
|
|
or self.map[MIDDLE + 1][MIDDLE - s])
|
2017-10-15 12:50:06 +02:00
|
|
|
and abs(self.offsety*self.step) > d):
|
2017-10-12 15:29:55 +02:00
|
|
|
self.offsety -= self.down
|
|
|
|
else:
|
|
|
|
modified = True
|
|
|
|
|
|
|
|
if modified:
|
2017-10-14 16:49:37 +02:00
|
|
|
self.map[MIDDLE][MIDDLE] = EMPTY
|
2017-10-15 12:50:06 +02:00
|
|
|
self.rotate(sign(self.offsetx) * (abs(self.offsetx)==MOVE_SPEED),
|
|
|
|
sign(self.offsety) * (abs(self.offsety)==MOVE_SPEED))
|
2017-10-14 16:49:37 +02:00
|
|
|
self.map[MIDDLE][MIDDLE] = HERO
|
2017-10-13 10:10:16 +02:00
|
|
|
self.middlex = self.x + self.offsetx*self.step
|
|
|
|
self.middley = self.y + self.offsety*self.step
|
2017-10-12 15:29:55 +02:00
|
|
|
self.draw()
|
2017-10-13 10:10:16 +02:00
|
|
|
for enemy in self.enemies:
|
|
|
|
if not enemy.awake: self.wake(enemy)
|
2017-10-12 15:29:55 +02:00
|
|
|
|
|
|
|
for enemy in self.enemies:
|
2017-10-13 10:10:16 +02:00
|
|
|
enemy.update(self.distance, self.middlex, self.middley)
|
2017-10-12 15:29:55 +02:00
|
|
|
self.hero.update()
|
2017-10-15 11:20:14 +02:00
|
|
|
if self.hero.slashing: self.slash()
|
|
|
|
for enemy in self.enemies:
|
2017-10-15 12:50:06 +02:00
|
|
|
if not enemy.spin_queue: continue
|
2017-10-15 11:20:14 +02:00
|
|
|
x, y = enemy.pos(self.distance, self.middlex, self.middley)
|
|
|
|
d = length(x, y, self.x, self.y)
|
|
|
|
if d <= self.slashd:
|
|
|
|
self.hero.wound += (self.slashd-d) / self.hero.R / enemy.speed
|
2017-10-12 15:29:55 +02:00
|
|
|
pygame.display.flip()
|
2017-10-15 11:20:14 +02:00
|
|
|
if self.hero.wound + 1 >= len(self.hero.color): self.lose()
|
2017-10-13 10:10:16 +02:00
|
|
|
|
|
|
|
def resize(self, w, h):
|
|
|
|
"""Resize the maze."""
|
|
|
|
size = self.w, self.h = w, h
|
|
|
|
self.surface = pygame.display.set_mode(size, RESIZABLE)
|
|
|
|
self.hero.resize()
|
|
|
|
|
2017-10-16 17:12:32 +02:00
|
|
|
self.distance = (w * h / 416) ** 0.5
|
|
|
|
self.step = self.distance / MOVE_SPEED
|
2017-10-13 10:10:16 +02:00
|
|
|
self.middlex = self.x + self.offsetx*self.step
|
|
|
|
self.middley = self.y + self.offsety*self.step
|
|
|
|
self.x, self.y = w >> 1, h >> 1
|
2017-10-16 17:12:32 +02:00
|
|
|
w, h = int((w/self.distance+2) / 2), int((h/self.distance+2) / 2)
|
2017-10-13 10:10:16 +02:00
|
|
|
self.rangex = range(MIDDLE - w, MIDDLE + w + 1)
|
|
|
|
self.rangey = range(MIDDLE - h, MIDDLE + h + 1)
|
|
|
|
self.draw()
|
|
|
|
|
|
|
|
def move(self, x, y):
|
|
|
|
"""Command the maze to move x step/frame faster to the left and
|
|
|
|
y step/frame faster upward so the hero will move in the reverse
|
|
|
|
direction.
|
|
|
|
"""
|
|
|
|
self.right += x
|
|
|
|
self.down += y
|
|
|
|
self.right, self.down = sign(self.right), sign(self.down)
|
2017-10-15 11:20:14 +02:00
|
|
|
|
|
|
|
def lose(self):
|
2017-10-15 12:50:06 +02:00
|
|
|
"""Handle loses."""
|
|
|
|
quit()
|