diff --git a/brutalmaze/characters.py b/brutalmaze/characters.py index f7c8521..752ea21 100644 --- a/brutalmaze/characters.py +++ b/brutalmaze/characters.py @@ -22,11 +22,12 @@ __doc__ = 'brutalmaze module for hero and enemy classes' from collections import deque from math import atan, atan2, sin, pi from random import choice, randrange, shuffle +from sys import modules import pygame from .constants import * -from .utils import sign, cosin, randsign, regpoly, fill_aapolygon +from .utils import sign, cosin, randsign, regpoly, fill_aapolygon, choices from .weapons import Bullet @@ -52,7 +53,7 @@ class Hero: w, h = self.surface.get_width(), self.surface.get_height() self.x, self.y = w >> 1, h >> 1 self.angle, self.color = pi / 4, TANGO['Aluminium'] - self.R = int((w * h / sin(pi*2/3) / 624) ** 0.5) + self.R = (w * h / sin(pi*2/3) / 624) ** 0.5 self.next_strike = 0 self.slashing = self.firing = self.dead = False @@ -103,7 +104,7 @@ class Enemy: maze (Maze): the maze x, y (int): coordinates of the center of the enemy (in grids) angle (float): angle of the direction the enemy pointing (in radians) - color (tuple of pygame.Color): colors of the enemy on different HPs + color (str): enemy's color name awake (bool): flag indicates if the enemy is active next_strike (int): the tick that the enemy can do the next attack move_speed (float): speed of movement (in frames per grid) @@ -112,11 +113,11 @@ class Enemy: spin_queue (float): frames left to finish spinning wound (float): amount of wound """ - def __init__(self, maze, x, y): + def __init__(self, maze, x, y, color): self.maze = maze self.x, self.y = x, y self.maze.map[x][y] = ENEMY - self.angle, self.color = pi / 4, TANGO[choice(ENEMIES)] + self.angle, self.color = pi / 4, color self.awake = False self.next_strike = 0 @@ -138,8 +139,12 @@ class Enemy: self.maze.map[self.x][self.y] = ENEMY def wake(self): - """Wake the enemy up if it can see the hero.""" - if self.awake: return + """Wake the enemy up if it can see the hero. + + Return None if the enemy is already awake, True if the function + has just woken it, False otherwise. + """ + if self.awake: return None startx = starty = MIDDLE stopx, stopy, distance = self.x, self.y, self.maze.distance if startx > stopx: startx, stopx = stopx, startx @@ -152,11 +157,13 @@ class Enemy: for j in range(starty, stopy + 1): if self.maze.map[i][j] != WALL: continue x, y = self.maze.pos(i, j) - if length(x - self.maze.x, y - self.maze.y) <= mind: return + if length(x - self.maze.x, y - self.maze.y) <= mind: + return False self.awake = True + return True def fire(self): - """Return True if the enemy shot the hero, False otherwise.""" + """Return True if the enemy has just fired, False otherwise.""" x, y = self.pos() if (self.maze.length(x, y) > FIRANGE*self.maze.distance or self.next_strike > pygame.time.get_ticks() @@ -166,14 +173,11 @@ class Enemy: self.next_strike = pygame.time.get_ticks() + ATTACK_SPEED self.maze.bullets.append(Bullet( self.maze.surface, x, y, - atan2(self.maze.y - y, self.maze.x - x), self.color[0])) + atan2(self.maze.y - y, self.maze.x - x), self.color)) return True def move(self): - """Handle the movement of the enemy. - - Return True if it moved, False otherwise. - """ + """Return True if it has just moved, False otherwise.""" if self.offsetx: self.offsetx -= sign(self.offsetx) return True @@ -195,6 +199,13 @@ class Enemy: return True return False + def draw(self): + """Draw the enemy.""" + radious = self.maze.distance/SQRT2 - self.awake*2 + square = regpoly(4, radious, self.angle, *self.pos()) + color = TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR + fill_aapolygon(self.maze.surface, square, color) + def update(self): """Update the enemy.""" if self.awake: @@ -207,15 +218,85 @@ class Enemy: self.spin_queue -= sign(self.spin_queue) else: self.angle, self.spin_queue = pi / 4, 0.0 - radious = self.maze.distance/SQRT2 - self.awake*2 - square = regpoly(4, radious, self.angle, *self.pos()) - color = self.color[int(self.wound)] if self.awake else FG_COLOR - fill_aapolygon(self.maze.surface, square, color) + self.draw() def hit(self, wound): - """Handle the enemy when it's hit by a bullet.""" + """Handle the enemy when it's attacked.""" self.wound += wound def die(self): """Handle the enemy's death.""" - self.maze.map[self.x][self.y] = EMPTY if self.awake else WALL + if self.awake: + self.maze.map[self.x][self.y] = EMPTY + if self.maze.enemy_weights[self.color] > INIT_WEIGHT: + self.maze.enemy_weights[self.color] -= 1 + else: + self.maze.map[self.x][self.y] = WALL + + +class Butter(Enemy): + """Object representing an enemy of Butter type.""" + def __init__(self, maze, x, y): + Enemy.__init__(self, maze, x, y, 'Butter') + + +class Orange(Enemy): + """Object representing an enemy of Orange type.""" + def __init__(self, maze, x, y): + Enemy.__init__(self, maze, x, y, 'Orange') + + +class Chocolate(Enemy): + """Object representing an enemy of Chocolate type.""" + def __init__(self, maze, x, y): + Enemy.__init__(self, maze, x, y, 'Chocolate') + + +class Chameleon(Enemy): + """Object representing an enemy of Chameleon type. + + Additional attributes: + visible (int): the tick until which the Chameleon is visible + """ + def __init__(self, maze, x, y): + Enemy.__init__(self, maze, x, y, 'Chameleon') + self.visible = 0 + + def wake(self): + """Wake the Chameleon up if it can see the hero.""" + if Enemy.wake(self) is True: + self.visible = pygame.time.get_ticks() + 1000//ENEMY_SPEED + + def draw(self): + """Draw the Chameleon.""" + if not self.awake or pygame.time.get_ticks() <= self.visible: + Enemy.draw(self) + + def hit(self, wound): + """Handle the Chameleon when it's hit by a bullet.""" + self.visible = pygame.time.get_ticks() + 1000//ENEMY_SPEED + self.wound += wound + + +class SkyBlue(Enemy): + """Object representing an enemy of Sky Blue type.""" + def __init__(self, maze, x, y): + Enemy.__init__(self, maze, x, y, 'SkyBlue') + + +class Plum(Enemy): + """Object representing an enemy of Plum type.""" + def __init__(self, maze, x, y): + Enemy.__init__(self, maze, x, y, 'Plum') + + +class ScarletRed(Enemy): + """Object representing an enemy of Scarlet Red type.""" + def __init__(self, maze, x, y): + Enemy.__init__(self, maze, x, y, 'ScarletRed') + + +def new_enemy(maze, x, y): + """Return an enemy of a random type in the grid (x, y).""" + color = choices(maze.enemy_weights) + return getattr(modules[__name__], color)(maze, x, y) diff --git a/brutalmaze/constants.py b/brutalmaze/constants.py index ecb797c..9d6cacf 100644 --- a/brutalmaze/constants.py +++ b/brutalmaze/constants.py @@ -57,13 +57,14 @@ TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)), 'Orange': ((252, 175, 62), (245, 121, 0), (206, 92, 0)), 'Chocolate': ((233, 185, 110), (193, 125, 17), (143, 89, 2)), 'Chameleon': ((138, 226, 52), (115, 210, 22), (78, 154, 6)), - 'Sky Blue': ((114, 159, 207), (52, 101, 164), (32, 74, 135)), + 'SkyBlue': ((114, 159, 207), (52, 101, 164), (32, 74, 135)), 'Plum': ((173, 127, 168), (117, 80, 123), (92, 53, 102)), - 'Scarlet Red': ((239, 41, 41), (204, 0, 0), (164, 0, 0)), + 'ScarletRed': ((239, 41, 41), (204, 0, 0), (164, 0, 0)), 'Aluminium': ((238, 238, 236), (211, 215, 207), (186, 189, 182), (136, 138, 133), (85, 87, 83), (46, 52, 54))} -ENEMIES = ('Butter', 'Orange', 'Chocolate', 'Chameleon', - 'Sky Blue', 'Plum', 'Scarlet Red') +ENEMIES = ['Butter', 'Orange', 'Chocolate', 'Chameleon', + 'SkyBlue', 'Plum', 'ScarletRed'] +INIT_WEIGHT = 11.25 ENEMY_HP = 3 HERO_HP = 6 BG_COLOR = TANGO['Aluminium'][-1] diff --git a/brutalmaze/maze.py b/brutalmaze/maze.py index df83ff0..c7c7102 100644 --- a/brutalmaze/maze.py +++ b/brutalmaze/maze.py @@ -26,7 +26,7 @@ from random import choice, getrandbits import pygame from pygame import RESIZABLE -from .characters import Hero, Enemy +from .characters import Hero, new_enemy from .constants import * from .utils import round2, sign, regpoly, fill_aapolygon from .weapons import Bullet @@ -69,6 +69,7 @@ class Maze: vx, vy (float): velocity of the maze movement (in pixels per frame) rotatex, rotatey: grids rotated bullets (list of Bullet): bullets flying + enemy_weights (dict): probabilities of enemies to be created enemies (list of Enemy): alive enemies hero (Hero): the hero slashd (float): minimum distance for slashes to be effective @@ -90,6 +91,7 @@ class Maze: self.vx = self.vy = 0.0 self.rotatex = self.rotatey = 0 self.bullets, self.enemies = [], [] + self.enemy_weights = {color: INIT_WEIGHT for color in ENEMIES} self.add_enemy() self.hero = Hero(self.surface, fps) self.map[MIDDLE][MIDDLE] = HERO @@ -105,7 +107,7 @@ class Maze: x, y = choice(walls) if all(self.map[x + a][y + b] == WALL for a, b in ADJACENT_GRIDS): continue - self.enemies.append(Enemy(self, x, y)) + self.enemies.append(new_enemy(self, x, y)) walls.remove((x, y)) def pos(self, x, y): @@ -176,6 +178,11 @@ class Maze: """ return ((self.x-x)**2 + (self.y-y)**2)**0.5 + def hit(self, wound, color): + """Handle the hero when he loses HP.""" + self.hero.wound += wound + self.enemy_weights[color] += wound + def slash(self): """Handle close-range attacks.""" for enemy in self.enemies: @@ -183,7 +190,7 @@ class Maze: x, y = enemy.pos() d = self.slashd - self.length(x, y) if d >= 0: - self.hero.wound += d / self.hero.R / enemy.spin_speed + self.hit(d / self.hero.R / enemy.spin_speed, enemy.color) if not self.hero.spin_queue: return unit, killist = self.distance/SQRT2 * self.hero.spin_speed, [] @@ -206,13 +213,13 @@ class Maze: and time >= self.hero.next_strike): self.hero.next_strike = time + ATTACK_SPEED self.bullets.append(Bullet(self.surface, self.x, self.y, - self.hero.angle, FG_COLOR)) + 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) - elif bullet.color == FG_COLOR: + 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: @@ -229,7 +236,7 @@ class Maze: fallen.append(i) break elif bullet.length(self.x, self.y) < self.distance: - if not self.hero.spin_queue: self.hero.wound += wound + if not self.hero.spin_queue: self.hit(wound, bullet.color) fallen.append(i) for i in reversed(fallen): self.bullets.pop(i) diff --git a/brutalmaze/utils.py b/brutalmaze/utils.py index fdf893f..387fea3 100644 --- a/brutalmaze/utils.py +++ b/brutalmaze/utils.py @@ -22,6 +22,7 @@ __doc__ = 'brutalmaze module for hero and enemy classes' from functools import reduce from math import cos, sin, pi from operator import or_ +from random import uniform import pygame from pygame.gfxdraw import filled_polygon, aapolygon @@ -68,3 +69,15 @@ def sign(n): def cosin(x): """Return the sum of cosine and sine of x (measured in radians).""" return cos(x) + sin(x) + + +def choices(d): + """Choose a random key from a dict which has values being relative + weights of the coresponding keys. + """ + population, weights = tuple(d.keys()), tuple(d.values()) + cum_weights = [weights[0]] + for weight in weights[1:]: cum_weights.append(cum_weights[-1] + weight) + num = uniform(0, cum_weights[-1]) + for i, w in enumerate(cum_weights): + if num <= w: return population[i] diff --git a/brutalmaze/weapons.py b/brutalmaze/weapons.py index 7504457..1112c2b 100644 --- a/brutalmaze/weapons.py +++ b/brutalmaze/weapons.py @@ -23,7 +23,7 @@ from math import cos, sin from pygame.time import get_ticks -from .constants import BULLET_LIFETIME, BULLET_SPEED +from .constants import BULLET_LIFETIME, BULLET_SPEED, ENEMY_HP, TANGO from .utils import regpoly, fill_aapolygon @@ -34,7 +34,7 @@ class Bullet: surface (pygame.Surface): the display to draw on x, y (int): coordinates of the center of the bullet (in pixels) angle (float): angle of the direction the bullet pointing (in radians) - color (pygame.Color): color of the bullet + color (str): bullet's color name fall_time (int): the tick that the bullet will fall down """ def __init__(self, surface, x, y, angle, color): @@ -47,8 +47,12 @@ class Bullet: s = distance * BULLET_SPEED / fps self.x += s * cos(self.angle) self.y += s * sin(self.angle) - hexagon = regpoly(5, distance // 4, self.angle, self.x, self.y) - fill_aapolygon(self.surface, hexagon, self.color) + pentagon = regpoly(5, distance // 4, self.angle, self.x, self.y) + value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP) + try: + fill_aapolygon(self.surface, pentagon, TANGO[self.color][value]) + except IndexError: + pass def place(self, x, y): """Move the bullet by (x, y) (in pixels).""" diff --git a/setup.py b/setup.py index a72ea9c..24b057d 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open('README.rst') as f: setup( name='brutalmaze', - version='0.0.4', + version='0.1.0', description='A hash and slash game with fast-paced action and a minimalist art style', long_description=long_description, url='https://github.com/McSinyx/brutalmaze',