summary refs log tree commit diff homepage
diff options
context:
space:
mode:
authorNguyễn Gia Phong <vn.mcsinyx@gmail.com>2018-05-20 20:33:50 +0700
committerNguyễn Gia Phong <vn.mcsinyx@gmail.com>2018-05-20 20:48:51 +0700
commit9dff378b57fbe16aa9edde9e3bc4eab959081e83 (patch)
treeffd643df85ca7092958c689f1a2fd8e14afc8f0c
parent92a41b3cff725da25c510b1684d9a72acd8cc0c8 (diff)
downloadbrutalmaze-9dff378b57fbe16aa9edde9e3bc4eab959081e83.tar.gz
Allow moving hero using mouse
-rw-r--r--README.rst11
-rw-r--r--brutalmaze/__init__.py2
-rw-r--r--brutalmaze/characters.py6
-rw-r--r--brutalmaze/constants.py6
-rw-r--r--brutalmaze/game.py72
-rw-r--r--brutalmaze/maze.py91
-rw-r--r--brutalmaze/misc.py14
-rw-r--r--brutalmaze/settings.ini4
-rwxr-xr-xsetup.py6
m---------wiki0
10 files changed, 150 insertions, 62 deletions
diff --git a/README.rst b/README.rst
index ec4b3d6..1d68dac 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,8 @@
 Brutal Maze
 ===========
 
-Brutal Maze is a hack and slash game with fast-paced action and a minimalist
-art style.
+Brutal Maze is a third-person shooter game with fast-paced action and a
+minimalist art style.
 
 .. image:: https://raw.githubusercontent.com/McSinyx/brutalmaze/master/screenshot.png
 
@@ -39,7 +39,8 @@ For more information, see
 page from Brutal Maze wiki.
 
 After installation, you can launch the game by running the command
-``brutalmaze``. Below are default bindings:
+``brutalmaze``. Below are the default bindings, which can be configured as
+shown in the next section:
 
 F2
    New game.
@@ -55,9 +56,11 @@ Up
    Move up.
 Down
    Move down.
+Right Mouse
+   Move the hero using mouse
 Left Mouse
    Long-range attack.
-Right Mouse
+Space
    Close-range attack, also dodge from bullets.
 
 Configuration
diff --git a/brutalmaze/__init__.py b/brutalmaze/__init__.py
index 19bf3a7..3e2e7a1 100644
--- a/brutalmaze/__init__.py
+++ b/brutalmaze/__init__.py
@@ -1 +1 @@
-"""Brutal Maze is a minimalist hack and slash game with fast-paced action"""
+"""Brutal Maze is a minimalist third-person shooter with fast-paced action"""
diff --git a/brutalmaze/characters.py b/brutalmaze/characters.py
index e814d87..ab517c1 100644
--- a/brutalmaze/characters.py
+++ b/brutalmaze/characters.py
@@ -26,7 +26,7 @@ from sys import modules
 from .constants import (
     TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY,
     ENEMY_SPEED, ENEMY_HP, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE, AROUND_HERO,
-    ADJACENT_GRIDS, EMPTY, FG_COLOR, SQRT2, MINW)
+    ADJACENTS, EMPTY, FG_COLOR, SQRT2, MINW)
 from .misc import sign, cosin, randsign, regpoly, fill_aapolygon, choices, play
 from .weapons import Bullet
 
@@ -232,8 +232,8 @@ class Enemy:
         self.move_speed = self.maze.fps / speed
         directions = [(sign(MIDDLE - self.x), 0), (0, sign(MIDDLE - self.y))]
         shuffle(directions)
-        directions.append(choice(ADJACENT_GRIDS))
-        if self.maze.hero.dead: directions = choice(ADJACENT_GRIDS),
+        directions.append(choice(ADJACENTS))
+        if self.maze.hero.dead: directions = choice(ADJACENTS),
         for x, y in directions:
             if (x or y) and self.maze.map[self.x + x][self.y + y] == EMPTY:
                 self.offsetx = round(x * (1 - self.move_speed))
diff --git a/brutalmaze/constants.py b/brutalmaze/constants.py
index cb65d88..29b0241 100644
--- a/brutalmaze/constants.py
+++ b/brutalmaze/constants.py
@@ -56,9 +56,9 @@ ATTACK_SPEED = 333.333  # ms/strike
 FIRANGE = 6     # grids
 BULLET_LIFETIME = 1000.0 * FIRANGE / (BULLET_SPEED-HERO_SPEED)  # ms
 EMPTY, WALL, HERO, ENEMY = range(4)
-ADJACENT_GRIDS = (1, 0), (0, 1), (-1, 0), (0, -1)
-AROUND_HERO = set((MIDDLE + x, MIDDLE + y) for x, y in
-                  ADJACENT_GRIDS + ((1, 1), (-1, 1), (-1, -1), (1, -1)))
+ADJACENTS = (1, 0), (0, 1), (-1, 0), (0, -1)
+CORNERS = (1, 1), (-1, 1), (-1, -1), (1, -1)
+AROUND_HERO = set((MIDDLE + x, MIDDLE + y) for x, y in ADJACENTS + CORNERS)
 
 TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)),
          'Orange': ((252, 175, 62), (245, 121, 0), (206, 92, 0)),
diff --git a/brutalmaze/game.py b/brutalmaze/game.py
index f155ad9..8c30686 100644
--- a/brutalmaze/game.py
+++ b/brutalmaze/game.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# main.py - main module, starts game and main loop
+# game.py - main module, starts game and main loop
 # Copyright (C) 2017, 2018  Nguyễn Gia Phong
 #
 # This file is part of Brutal Maze.
@@ -17,7 +17,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with Brutal Maze.  If not, see <https://www.gnu.org/licenses/>.
 
-__version__ = '0.6.5'
+__version__ = '0.7.0'
 
 import re
 from argparse import ArgumentParser, FileType, RawTextHelpFormatter
@@ -37,7 +37,7 @@ from pygame import KEYDOWN, QUIT, VIDEORESIZE
 from pygame.time import Clock, get_ticks
 from appdirs import AppDirs
 
-from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED, COLORS, WALL
+from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED, COLORS, MIDDLE, WALL
 from .maze import Maze
 from .misc import deg, round2, sign
 
@@ -50,6 +50,7 @@ class ConfigReader:
                        ('Toggle mute', 'mute'),
                        ('Move left', 'left'), ('Move right', 'right'),
                        ('Move up', 'up'), ('Move down', 'down'),
+                       ('Auto move', 'autove'),
                        ('Long-range attack', 'shot'),
                        ('Close-range attack', 'slash'))
     WEIRD_MOUSE_ERR = '{}: Mouse is not a suitable control'
@@ -81,7 +82,7 @@ class ConfigReader:
         for cmd, alias in self.CONTROL_ALIASES:
             i = self.config.get('Control', cmd)
             if re.match('mouse[1-3]$', i.lower()):
-                if alias not in ('shot', 'slash'):
+                if alias not in ('autove', 'shot', 'slash'):
                     raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd))
                 self.mouse[alias] = int(i[-1]) - 1
                 continue
@@ -219,27 +220,33 @@ class Game:
 
     def move(self, x, y):
         """Command the hero to move faster in the given direction."""
-        x, y = -x, -y # or move the maze in the reverse direction
-        velocity = self.maze.distance * HERO_SPEED / self.fps
+        maze = self.maze
+        velocity = maze.distance * HERO_SPEED / self.fps
         accel = velocity * HERO_SPEED / self.fps
 
-        if self.maze.next_move > 0 or not x:
-            self.maze.vx -= sign(self.maze.vx) * accel
-            if abs(self.maze.vx) < accel * 2: self.maze.vx = 0.0
-        elif x * self.maze.vx < 0:
-            self.maze.vx += x * 2 * accel
+        if x == y == 0:
+            maze.set_step()
+            x, y = maze.stepx, maze.stepy
         else:
-            self.maze.vx += x * accel
-            if abs(self.maze.vx) > velocity: self.maze.vx = x * velocity
-
-        if self.maze.next_move > 0 or not y:
-            self.maze.vy -= sign(self.maze.vy) * accel
-            if abs(self.maze.vy) < accel * 2: self.maze.vy = 0.0
-        elif y * self.maze.vy < 0:
-            self.maze.vy += y * 2 * accel
+            x, y = -x, -y   # or move the maze in the reverse direction
+
+        if maze.next_move > 0 or not x:
+            maze.vx -= sign(maze.vx) * accel
+            if abs(maze.vx) < accel * 2: maze.vx = 0.0
+        elif x * maze.vx < 0:
+            maze.vx += x * 2 * accel
+        else:
+            maze.vx += x * accel
+            if abs(maze.vx) > velocity: maze.vx = x * velocity
+
+        if maze.next_move > 0 or not y:
+            maze.vy -= sign(maze.vy) * accel
+            if abs(maze.vy) < accel * 2: maze.vy = 0.0
+        elif y * maze.vy < 0:
+            maze.vy += y * 2 * accel
         else:
-            self.maze.vy += y * accel
-            if abs(self.maze.vy) > velocity: self.maze.vy = y * velocity
+            maze.vy += y * accel
+            if abs(maze.vy) > velocity: maze.vy = y * velocity
 
     def control(self, x, y, angle, firing, slashing):
         """Control how the hero move and attack."""
@@ -293,12 +300,12 @@ class Game:
             right = keys[self.key['right']] - keys[self.key['left']]
             down = keys[self.key['down']] - keys[self.key['up']]
 
-            # Follow the mouse cursor
-            x, y = pygame.mouse.get_pos()
-            angle = atan2(y - self.hero.y, x - self.hero.x)
-
             buttons = pygame.mouse.get_pressed()
             try:
+                autove = keys[self.key['autove']]
+            except KeyError:
+                autove = buttons[self.mouse['autove']]
+            try:
                 firing = keys[self.key['shot']]
             except KeyError:
                 firing = buttons[self.mouse['shot']]
@@ -307,6 +314,21 @@ class Game:
             except KeyError:
                 slashing = buttons[self.mouse['slash']]
 
+            # Follow the mouse cursor
+            x, y = pygame.mouse.get_pos()
+            maze = self.maze
+            if right or down:
+                maze.destx = maze.desty = MIDDLE
+                maze.stepx = maze.stepy = 0
+            elif autove:
+                maze.destx = MIDDLE + round2((x-maze.centerx) / maze.distance)
+                maze.desty = MIDDLE + round2((y-maze.centery) / maze.distance)
+                maze.set_step(lambda x: maze.rangex[0] <= x <= maze.rangex[-1],
+                              lambda y: maze.rangey[0] <= y <= maze.rangey[-1])
+                if maze.stepx == maze.stepy == 0:
+                    maze.destx = maze.desty = MIDDLE
+
+            angle = atan2(y - self.hero.y, x - self.hero.x)
             self.control(right, down, angle, firing, slashing)
 
     def __exit__(self, exc_type, exc_value, traceback):
diff --git a/brutalmaze/maze.py b/brutalmaze/maze.py
index 665d9c3..e98e421 100644
--- a/brutalmaze/maze.py
+++ b/brutalmaze/maze.py
@@ -19,7 +19,7 @@
 
 __doc__ = 'Brutal Maze module for the maze class'
 
-from collections import deque
+from collections import deque, defaultdict
 from math import pi, log
 from random import choice, getrandbits, uniform
 
@@ -28,10 +28,10 @@ import pygame
 from .characters import Hero, new_enemy
 from .constants import (
     EMPTY, WALL, HERO, ROAD_WIDTH, MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES,
-    MINW, MAXW, SQRT2, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_LOSE, ADJACENT_GRIDS,
+    MINW, MAXW, SQRT2, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_LOSE, ADJACENTS,
     BG_COLOR, FG_COLOR, CELL_WIDTH, LAST_ROW, HERO_HP, ENEMY_HP, ATTACK_SPEED,
     HERO_SPEED, BULLET_LIFETIME)
-from .misc import round2, sign, regpoly, fill_aapolygon, play
+from .misc import round2, sign, around, regpoly, fill_aapolygon, play
 from .weapons import Bullet
 
 
@@ -74,6 +74,8 @@ class Maze:
         enemy_weights (dict): probabilities of enemies to be created
         enemies (list of Enemy): alive enemies
         hero (Hero): the hero
+        destx, desty (int): the grid the hero is moving to
+        stepx, stepy (int): direction the maze is moving
         next_move (float): time until the hero gets mobilized (in ms)
         next_slashfx (float): time until next slash effect of the hero (in ms)
         slashd (float): minimum distance for slashes to be effective
@@ -105,6 +107,8 @@ class Maze:
         self.add_enemy()
         self.hero = Hero(self.surface, fps, size)
         self.map[MIDDLE][MIDDLE] = HERO
+        self.destx = self.desty = MIDDLE
+        self.stepx = self.stepy = 0
         self.next_move = self.next_slashfx = 0.0
         self.slashd = self.hero.R + self.distance/SQRT2
 
@@ -121,7 +125,7 @@ class Maze:
         num = log(self.score, INIT_SCORE)
         while walls and len(self.enemies) < num:
             x, y = choice(walls)
-            if all(self.map[x + a][y + b] == WALL for a, b in ADJACENT_GRIDS):
+            if all(self.map[x + a][y + b] == WALL for a, b in ADJACENTS):
                 continue
             enemy = new_enemy(self, x, y)
             self.enemies.append(enemy)
@@ -164,22 +168,26 @@ class Maze:
         y = int((self.centery-self.y) * 2 / self.distance)
         if x == y == 0: return
         for enemy in self.enemies: self.map[enemy.x][enemy.y] = EMPTY
+
         self.map[MIDDLE][MIDDLE] = EMPTY
-        if x:
-            self.centerx -= x * self.distance
-            self.map.rotate(x)
-            self.rotatex += x
-        if y:
-            self.centery -= y * self.distance
-            for d in self.map: d.rotate(y)
-            self.rotatey += y
+        self.centerx -= x * self.distance
+        self.map.rotate(x)
+        self.rotatex += x
+        self.centery -= y * self.distance
+        for d in self.map: d.rotate(y)
+        self.rotatey += y
         self.map[MIDDLE][MIDDLE] = HERO
+        if self.map[self.destx][self.desty] != HERO:
+            self.destx += x
+            self.desty += y
+        self.stepx = self.stepy = 0
 
         # 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:
+            if not (self.rangex[0] <= enemy.x <= self.rangex[-1]
+                    and self.rangey[0] <= enemy.y <= self.rangey[-1]):
                 self.score += enemy.wound
                 enemy.die()
                 killist.append(i)
@@ -307,26 +315,25 @@ class Maze:
                     return 0.0
         for enemy in self.enemies:
             x, y = self.get_pos(enemy.x, enemy.y)
-            if (max(abs(herox - x), abs(heroy - y)) * 2 < self.distance
-                and enemy.awake):
+            if max(abs(herox - x), abs(heroy - y)) * 2 < self.distance:
                 return 0.0
         return vx or vy
 
     def update(self, fps):
         """Update the maze."""
         self.fps = fps
-        dx = self.is_valid_move(vx=self.vx)
-        self.centerx += dx
-        dy = self.is_valid_move(vy=self.vy)
-        self.centery += dy
+        self.vx = self.is_valid_move(vx=self.vx)
+        self.centerx += self.vx
+        self.vy = self.is_valid_move(vy=self.vy)
+        self.centery += self.vy
 
         self.next_move -= 1000.0 / self.fps
         self.next_slashfx -= 1000.0 / self.fps
 
         self.rotate()
-        if dx or dy:
+        if self.vx or self.vy:
             for enemy in self.enemies: enemy.wake()
-            for bullet in self.bullets: bullet.place(dx, dy)
+            for bullet in self.bullets: bullet.place(self.vx, self.vy)
 
         for enemy in self.enemies: enemy.update()
         if not self.hero.dead:
@@ -351,6 +358,44 @@ class Maze:
         self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1))
         self.slashd = self.hero.R + self.distance/SQRT2
 
+    def set_step(self, xcheck=(lambda _: True), ycheck=(lambda _: True)):
+        """Return direction on the shortest path to the destination."""
+        if self.stepx or self.stepy and self.vx == self.vy == 0.0:
+            x, y = MIDDLE - self.stepx, MIDDLE - self.stepy
+            if self.stepx and not self.stepy:
+                nextx = x - self.stepx
+                n = self.map[x][y - 1] == EMPTY == self.map[nextx][y - 1]
+                s = self.map[x][y + 1] == EMPTY == self.map[nextx][y + 1]
+                self.stepy = n - s
+            elif not self.stepx and self.stepy:
+                nexty = y - self.stepy
+                w = self.map[x - 1][y] == EMPTY == self.map[x - 1][nexty]
+                e = self.map[x + 1][y] == EMPTY == self.map[x + 1][nexty]
+                self.stepx = w - e
+            return
+
+        queue = defaultdict(list, {0: [(self.destx, self.desty)]})
+        visited, count, distance = set(), 1, 0
+        while count:
+            # Hashes of small intergers are themselves so queue is sorted
+            if not queue[distance]: distance += 1
+            x, y = queue[distance].pop()
+            count -= 1
+            if (x, y) not in visited:
+                visited.add((x, y))
+            else:
+                continue
+
+            dx, dy = MIDDLE - x, MIDDLE - y
+            if dx**2 + dy**2 <= 2:
+                self.stepx, self.stepy = dx, dy
+                return
+            for i, j in around(x, y):
+                if self.map[i][j] == EMPTY and xcheck(i) and ycheck(j):
+                    queue[distance + 1].append((i, j))
+                    count += 1
+        self.stepx, self.stepy = 0, 0
+
     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
@@ -359,6 +404,8 @@ class Maze:
         """Handle loses."""
         self.hero.dead = True
         self.hero.slashing = self.hero.firing = False
+        self.destx = self.desty = MIDDLE
+        self.stepx = self.stepy = 0
         self.vx = self.vy = 0.0
         play(self.sfx_lose)
 
@@ -368,6 +415,8 @@ class Maze:
         self.score = INIT_SCORE
         self.map = deque()
         for _ in range(MAZE_SIZE): self.map.extend(new_column())
+        self.destx = self.desty = MIDDLE
+        self.stepx = self.stepy = 0
         self.vx = self.vy = 0.0
         self.rotatex = self.rotatey = 0
         self.bullets, self.enemies = [], []
diff --git a/brutalmaze/misc.py b/brutalmaze/misc.py
index e6da56c..d26edf4 100644
--- a/brutalmaze/misc.py
+++ b/brutalmaze/misc.py
@@ -19,12 +19,15 @@
 
 __doc__ = 'Brutal Maze module for miscellaneous functions'
 
+from itertools import chain
 from math import degrees, cos, sin, pi
-from random import uniform
+from random import shuffle, uniform
 
 import pygame
 from pygame.gfxdraw import filled_polygon, aapolygon
 
+from .constants import ADJACENTS, CORNERS
+
 
 def round2(number):
     """Round a number to an int."""
@@ -69,6 +72,15 @@ def cosin(x):
     return cos(x) + sin(x)
 
 
+def around(x, y):
+    """Return grids around the given one in random order."""
+    a = [(x + i, y + j) for i, j in ADJACENTS]
+    shuffle(a)
+    c = [(x + i, y + j) for i, j in CORNERS]
+    shuffle(c)
+    return chain(a, c)
+
+
 def choices(d):
     """Choose a random key from a dict which has values being relative
     weights of the coresponding keys.
diff --git a/brutalmaze/settings.ini b/brutalmaze/settings.ini
index 0641aad..aaa6d98 100644
--- a/brutalmaze/settings.ini
+++ b/brutalmaze/settings.ini
@@ -22,8 +22,10 @@ Move left: Left
 Move right: Right
 Move up: Up
 Move down: Down
+# Move hero using mouse
+Auto move: Mouse3
 Long-range attack: Mouse1
-Close-range attack: Mouse3
+Close-range attack: Space
 
 [Server]
 # Enabling remote control will disable control via keyboard and mouse.
diff --git a/setup.py b/setup.py
index b2b83be..5588f06 100755
--- a/setup.py
+++ b/setup.py
@@ -7,8 +7,8 @@ with open('README.rst') as f:
 
 setup(
     name='brutalmaze',
-    version='0.6.5',
-    description='A minimalist hack and slash game with fast-paced action',
+    version='0.7.0',
+    description='A minimalist TPS game with fast-paced action',
     long_description=long_description,
     url='https://github.com/McSinyx/brutalmaze',
     author='Nguyễn Gia Phong',
@@ -25,7 +25,7 @@ setup(
         'Operating System :: OS Independent',
         'Programming Language :: Python',
         'Topic :: Games/Entertainment :: Arcade'],
-    keywords='pygame action-game arcade-game maze socket-server ai-challenges',
+    keywords='pygame third-person-shooter arcade-game maze ai-challenges',
     packages=['brutalmaze'],
     install_requires=['appdirs', 'pygame>=1.9'],
     package_data={'brutalmaze': ['icon.png', 'soundfx/*.ogg', 'settings.ini']},
diff --git a/wiki b/wiki
-Subproject 8f40eb7b3d368076bb2b9fc4d268472af62e288
+Subproject b4169d8f16a5f99b11f41e4823ca67065788cba