brutalmaze/brutalmaze/maze.py

340 lines
13 KiB
Python
Raw Normal View History

2017-10-12 15:29:55 +02:00
# -*- coding: utf-8 -*-
# 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
2017-11-02 15:39:06 +01:00
__doc__ = 'brutalmaze module for the maze class'
2017-10-12 15:29:55 +02:00
from collections import deque
2017-11-04 15:43:05 +01:00
from math import pi, atan2, log
from random import choice, getrandbits, uniform
2017-10-12 15:29:55 +02:00
import pygame
from pygame import RESIZABLE
2017-10-12 15:29:55 +02:00
2017-11-12 15:08:14 +01:00
from .characters import Hero, new_enemy
2017-10-12 15:29:55 +02:00
from .constants import *
2017-11-04 15:43:05 +01:00
from .utils import round2, sign, regpoly, fill_aapolygon
from .weapons import Bullet
2017-10-12 15:29:55 +02:00
2017-11-20 16:29:56 +01:00
def new_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)
2017-11-20 16:29:56 +01:00
upper.extend(new_cell(b))
lower.extend(new_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:
2017-11-04 15:43:05 +01:00
"""Object representing the maze, including the characters.
Attributes:
w, h: width and height of the display
fps: current frame rate
surface (pygame.Surface): the display to draw on
distance (float): distance between centers of grids (in px)
x, y (int): coordinates of the center of the hero (in px)
centerx, centery (float): center grid's center's coordinates (in px)
rangex, rangey: range of the index of the grids on display
paused (bool): flag indicates if the game is paused
score (float): current score
map (deque of deque): map of grids representing objects on the maze
vx, vy (float): velocity of the maze movement (in pixels per frame)
2017-11-04 15:43:05 +01:00
rotatex, rotatey: grids rotated
bullets (list of Bullet): bullets flying
2017-11-12 15:08:14 +01:00
enemy_weights (dict): probabilities of enemies to be created
2017-11-04 15:43:05 +01:00
enemies (list of Enemy): alive enemies
hero (Hero): the hero
next_move (int): the tick that the hero gets mobilized
2017-11-04 15:43:05 +01:00
slashd (float): minimum distance for slashes to be effective
"""
2017-10-19 10:24:56 +02:00
def __init__(self, size, fps):
2017-10-12 15:29:55 +02:00
self.w, self.h = size
2017-11-02 15:39:06 +01:00
self.fps = fps
2017-10-12 15:29:55 +02:00
self.surface = pygame.display.set_mode(size, RESIZABLE)
self.distance = (self.w * self.h / 416) ** 0.5
2017-11-04 15:43:05 +01:00
self.x, self.y = self.w // 2, self.h // 2
self.centerx, self.centery = self.w / 2.0, self.h / 2.0
2017-10-19 15:28:56 +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)
self.paused, self.score = False, INIT_SCORE
2017-10-12 15:29:55 +02:00
self.map = deque()
for _ in range(MAZE_SIZE): self.map.extend(new_column())
self.vx = self.vy = 0.0
self.rotatex = self.rotatey = 0
self.bullets, self.enemies = [], []
self.enemy_weights = {color: MINW for color in ENEMIES}
self.add_enemy()
2017-10-19 10:24:56 +02:00
self.hero = Hero(self.surface, fps)
self.map[MIDDLE][MIDDLE] = HERO
self.next_move, self.slashd = 0, self.hero.R + self.distance/SQRT2
def add_enemy(self):
"""Add enough enemies."""
2017-11-21 12:01:32 +01:00
walls = [(i, j) for i in self.rangex for j in self.rangey
if self.map[i][j] == WALL]
plums = [e for e in self.enemies if e.color == 'Plum' and e.awake]
plum = choice(plums) if plums else None
2017-11-02 15:39:06 +01:00
while walls and len(self.enemies) < log(self.score, INIT_SCORE):
x, y = choice(walls)
if all(self.map[x + a][y + b] == WALL for a, b in ADJACENT_GRIDS):
continue
2017-11-19 09:00:24 +01:00
enemy = new_enemy(self, x, y)
self.enemies.append(enemy)
2017-11-21 12:01:32 +01:00
if plum is None or not plum.clone(enemy): walls.remove((x, y))
2017-10-12 15:29:55 +02:00
2017-11-20 16:29:56 +01:00
def get_pos(self, x, y):
2017-11-02 15:39:06 +01:00
"""Return coordinate of the center of the grid (x, y)."""
2017-11-04 15:43:05 +01:00
return (self.centerx + (x - MIDDLE)*self.distance,
self.centery + (y - MIDDLE)*self.distance)
2017-11-02 15:39:06 +01:00
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:
if self.map[i][j] != WALL: continue
2017-11-20 16:29:56 +01:00
x, y = self.get_pos(i, j)
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-11-02 15:39:06 +01:00
def rotate(self):
"""Rotate the maze if needed."""
2017-11-04 15:43:05 +01:00
x = int((self.centerx-self.x) * 2 / self.distance)
y = int((self.centery-self.y) * 2 / self.distance)
2017-11-02 15:39:06 +01:00
if x == y == 0: return
for enemy in self.enemies: self.map[enemy.x][enemy.y] = EMPTY
2017-11-02 15:39:06 +01:00
self.map[MIDDLE][MIDDLE] = EMPTY
if x:
2017-11-04 15:43:05 +01:00
self.centerx -= x * self.distance
self.map.rotate(x)
self.rotatex += x
if y:
2017-11-04 15:43:05 +01:00
self.centery -= y * self.distance
for d in self.map: d.rotate(y)
self.rotatey += y
2017-11-02 15:39:06 +01:00
self.map[MIDDLE][MIDDLE] = HERO
# Respawn the enemies that fall off the display
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:
self.score += enemy.wound
enemy.die()
killist.append(i)
for i in reversed(killist): self.enemies.pop(i)
self.add_enemy()
# Regenerate the maze
if abs(self.rotatex) == CELL_WIDTH:
self.rotatex = 0
for _ in range(CELL_WIDTH): self.map.pop()
self.map.extend(new_column())
for i in range(-CELL_WIDTH, 0):
self.map[i].rotate(self.rotatey)
if abs(self.rotatey) == CELL_WIDTH:
self.rotatey = 0
for i in range(MAZE_SIZE):
b, c = getrandbits(1), (i-1)*CELL_WIDTH + self.rotatex
2017-11-21 12:01:32 +01:00
for j, grid in enumerate(new_cell(b)):
for k in range(ROAD_WIDTH):
self.map[c + k][LAST_ROW + j] = grid
c += ROAD_WIDTH
2017-11-21 12:01:32 +01:00
for j, grid in enumerate(new_cell(b, False)):
for k in range(ROAD_WIDTH):
self.map[c + k][LAST_ROW + j] = grid
2017-11-20 16:29:56 +01:00
def get_distance(self, x, y):
"""Return the distance from the center of the maze to the point
(x, y).
2017-11-02 15:39:06 +01:00
"""
return ((self.x-x)**2 + (self.y-y)**2)**0.5
2017-11-12 15:08:14 +01:00
def hit(self, wound, color):
"""Handle the hero when he loses HP."""
fx = (uniform(0, sum(self.enemy_weights.values()))
< self.enemy_weights[color])
time = pygame.time.get_ticks()
2017-11-21 12:01:32 +01:00
if (color == 'Butter' or color == 'ScarletRed') and fx:
self.hero.wound += wound * 2.5
elif color == 'Orange' and fx:
self.hero.next_heal = max(self.hero.next_heal, time) + wound*1000
elif color == 'SkyBlue' and fx:
self.next_move = max(self.next_move, time) + wound*1000
else:
self.hero.wound += wound
if self.enemy_weights[color] + wound < MAXW:
self.enemy_weights[color] += wound
if self.hero.wound > HERO_HP: self.lose()
2017-11-12 15:08:14 +01:00
2017-10-15 11:20:14 +02:00
def slash(self):
2017-11-04 15:43:05 +01:00
"""Handle close-range attacks."""
2017-11-14 02:51:18 +01:00
for enemy in self.enemies: enemy.slash()
2017-10-21 16:22:06 +02:00
if not self.hero.spin_queue: return
2017-10-19 10:24:56 +02:00
unit, killist = self.distance/SQRT2 * self.hero.spin_speed, []
2017-10-15 11:20:14 +02:00
for i, enemy in enumerate(self.enemies):
2017-11-20 16:29:56 +01:00
x, y = enemy.get_pos()
d = self.get_distance(x, y)
2017-10-15 11:20:14 +02:00
if d <= self.slashd:
2017-11-02 15:39:06 +01:00
enemy.hit((self.slashd-d) / unit)
if enemy.wound >= ENEMY_HP:
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)
self.add_enemy()
2017-10-15 11:20:14 +02:00
def track_bullets(self):
"""Handle the bullets."""
fallen, time = [], pygame.time.get_ticks()
2017-10-21 16:22:06 +02:00
if (self.hero.firing and not self.hero.slashing
and time >= self.hero.next_strike):
self.hero.next_strike = time + ATTACK_SPEED
self.bullets.append(Bullet(self.surface, self.x, self.y,
2017-11-12 15:08:14 +01:00
self.hero.angle, 'Aluminium'))
for i, bullet in enumerate(self.bullets):
wound = float(bullet.fall_time-time) / BULLET_LIFETIME
bullet.update(self.fps, self.distance)
if wound < 0:
fallen.append(i)
2017-11-12 15:08:14 +01:00
elif bullet.color == 'Aluminium':
x = MIDDLE + round2((bullet.x-self.x) / self.distance)
y = MIDDLE + round2((bullet.y-self.y) / self.distance)
if self.map[x][y] == WALL:
fallen.append(i)
continue
for j, enemy in enumerate(self.enemies):
2017-11-20 16:29:56 +01:00
x, y = enemy.get_pos()
if bullet.get_distance(x, y) < self.distance:
2017-11-02 15:39:06 +01:00
enemy.hit(wound)
if enemy.wound >= ENEMY_HP:
self.score += enemy.wound
enemy.die()
self.enemies.pop(j)
fallen.append(i)
break
2017-11-20 16:29:56 +01:00
elif bullet.get_distance(self.x, self.y) < self.distance:
2017-11-12 15:08:14 +01:00
if not self.hero.spin_queue: self.hit(wound, bullet.color)
fallen.append(i)
for i in reversed(fallen): self.bullets.pop(i)
2017-11-21 15:49:35 +01:00
def is_valid_move(self, vx=0.0, vy=0.0):
"""Return dx or dy if it it valid to move the maze in that
velocity, otherwise return 0.0.
2017-11-02 15:39:06 +01:00
"""
d = self.distance/2 + self.hero.R
herox, heroy, dx, dy = self.x - vx, self.y - vy, sign(vx), sign(vy)
2017-11-09 09:22:39 +01:00
for gridx in range(MIDDLE - dx - 1, MIDDLE - dx + 2):
for gridy in range(MIDDLE - dy - 1, MIDDLE - dy + 2):
2017-11-20 16:29:56 +01:00
x, y = self.get_pos(gridx, gridy)
2017-11-09 09:22:39 +01:00
if (max(abs(herox - x), abs(heroy - y)) < d
and self.map[gridx][gridy] == WALL):
return 0.0
2017-11-09 09:22:39 +01:00
for enemy in self.enemies:
2017-11-20 16:29:56 +01:00
x, y = self.get_pos(enemy.x, enemy.y)
2017-11-09 09:22:39 +01:00
if (max(abs(herox - x), abs(heroy - y)) * 2 < self.distance
and enemy.awake):
return 0.0
return vx or vy
2017-11-02 15:39:06 +01:00
2017-10-19 10:24:56 +02:00
def update(self, fps):
2017-10-12 15:29:55 +02:00
"""Update the maze."""
if self.paused: return
self.fps = fps
2017-11-21 15:49:35 +01:00
dx = self.is_valid_move(vx=self.vx)
2017-11-04 15:43:05 +01:00
self.centerx += dx
2017-11-21 15:49:35 +01:00
dy = self.is_valid_move(vy=self.vy)
2017-11-04 15:43:05 +01:00
self.centery += dy
2017-10-12 15:29:55 +02:00
if dx or dy:
2017-11-02 15:39:06 +01:00
self.rotate()
2017-11-04 15:43:05 +01:00
for enemy in self.enemies: enemy.wake()
2017-11-02 15:39:06 +01:00
for bullet in self.bullets: bullet.place(dx, dy)
2017-10-12 15:29:55 +02:00
2017-10-19 15:28:56 +02:00
self.draw()
for enemy in self.enemies: enemy.update()
2017-10-19 10:24:56 +02:00
self.hero.update(fps)
self.slash()
self.track_bullets()
2017-10-12 15:29:55 +02:00
pygame.display.flip()
pygame.display.set_caption('Brutal Maze - Score: {}'.format(
int(self.score - INIT_SCORE)))
2017-10-13 10:10:16 +02:00
def move(self, x, y, fps):
"""Command the hero to move faster in the given direction."""
stunned = pygame.time.get_ticks() < self.next_move
velocity = self.distance * HERO_SPEED / fps
accel = velocity * HERO_SPEED / fps
if stunned or not x:
self.vx -= sign(self.vx) * accel
2017-11-09 09:22:39 +01:00
if abs(self.vx) < accel * 2: self.vx = 0.0
elif x * self.vx < 0:
self.vx += x * 2 * accel
else:
self.vx += x * accel
if abs(self.vx) > velocity: self.vx = x * velocity
if stunned or not y:
self.vy -= sign(self.vy) * accel
2017-11-09 09:22:39 +01:00
if abs(self.vy) < accel * 2: self.vy = 0.0
elif y * self.vy < 0:
self.vy += y * 2 * accel
else:
self.vy += y * accel
if abs(self.vy) > velocity: self.vy = y * velocity
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-11-04 15:43:05 +01:00
offsetx = (self.centerx-self.x) / self.distance
offsety = (self.centery-self.y) / self.distance
self.distance = (w * h / 416) ** 0.5
2017-11-04 15:43:05 +01:00
self.x, self.y = w // 2, h // 2
self.centerx = self.x + offsetx*self.distance
self.centery = self.y + offsety*self.distance
2017-10-19 15:28:56 +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)
2017-10-19 15:28:56 +02:00
self.slashd = self.hero.R + self.distance/SQRT2
2017-10-13 10:10:16 +02:00
2017-11-09 09:22:39 +01:00
def isfast(self):
"""Return if the hero is moving faster than HERO_SPEED."""
return (self.vx**2+self.vy**2)**0.5*self.fps > HERO_SPEED*self.distance
2017-10-15 11:20:14 +02:00
def lose(self):
"""Handle loses."""
self.hero.die()
self.vx = self.vy = 0.0