about summary refs log tree commit diff homepage
diff options
context:
space:
mode:
-rw-r--r--brutalmaze/characters.py18
-rw-r--r--brutalmaze/constants.py5
-rw-r--r--brutalmaze/main.py231
-rw-r--r--brutalmaze/maze.py25
-rw-r--r--brutalmaze/settings.ini8
-rw-r--r--brutalmaze/weapons.py14
6 files changed, 213 insertions, 88 deletions
diff --git a/brutalmaze/characters.py b/brutalmaze/characters.py
index 6201569..7aab40f 100644
--- a/brutalmaze/characters.py
+++ b/brutalmaze/characters.py
@@ -86,11 +86,12 @@ class Hero:
         if abs(self.spin_queue) > 0.5:
             self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed
             self.spin_queue -= sign(self.spin_queue)
-        else:
-            # Follow the mouse cursor
-            x, y = pygame.mouse.get_pos()
-            self.angle = atan2(y - self.y, x - self.x)
+
+    def update_angle(self, angle):
+        """Turn to the given angle if the hero is not busy slashing."""
+        if abs(self.spin_queue) <= 0.5:
             self.spin_queue = 0.0
+            self.angle = angle
 
     def draw(self):
         """Draw the hero."""
@@ -238,13 +239,18 @@ class Enemy:
         x, y = self.get_pos()
         return atan2(y - self.maze.y, x - self.maze.x)
 
+
+    def get_color(self):
+        """Return current color of the enemy."""
+        return TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR
+
+
     def draw(self):
         """Draw the enemy."""
         if get_ticks() < self.maze.next_move and not self.awake: return
         radious = self.maze.distance/SQRT2 - self.awake*2
         square = regpoly(4, radious, self.angle, *self.get_pos())
-        color = TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR
-        fill_aapolygon(self.maze.surface, square, color)
+        fill_aapolygon(self.maze.surface, square, self.get_color())
 
     def update(self):
         """Update the enemy."""
diff --git a/brutalmaze/constants.py b/brutalmaze/constants.py
index 3bd34b1..c64afa5 100644
--- a/brutalmaze/constants.py
+++ b/brutalmaze/constants.py
@@ -19,6 +19,8 @@
 
 __doc__ = 'brutalmaze module for shared constants'
 
+from string import ascii_lowercase
+
 from pkg_resources import resource_filename as pkg_file
 import pygame
 from pygame.mixer import Sound
@@ -69,6 +71,9 @@ TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)),
                        (136, 138, 133), (85, 87, 83), (46, 52, 54))}
 ENEMIES = ['Butter', 'Orange', 'Chocolate', 'Chameleon',
            'SkyBlue', 'Plum', 'ScarletRed']
+COLOR_CODE = ascii_lowercase + '0'
+COLORS = {c: COLOR_CODE[i] for i, c in enumerate(
+    color for code in ENEMIES + ['Aluminium'] for color in TANGO[code])}
 MINW, MAXW = 24, 36
 ENEMY_HP = 3
 HERO_HP = 5
diff --git a/brutalmaze/main.py b/brutalmaze/main.py
index 39dca3b..2300658 100644
--- a/brutalmaze/main.py
+++ b/brutalmaze/main.py
@@ -26,7 +26,10 @@ try:                    # Python 3
     from configparser import ConfigParser
 except ImportError:     # Python 2
     from ConfigParser import ConfigParser
+from itertools import repeat
+from math import atan2, degrees
 from os.path import join, pathsep
+from socket import socket
 from sys import stdout
 
 
@@ -35,9 +38,9 @@ from pygame import DOUBLEBUF, KEYDOWN, OPENGL, QUIT, RESIZABLE, VIDEORESIZE
 from pygame.time import Clock, get_ticks
 from appdirs import AppDirs
 
-from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED
+from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED, COLORS, WALL
 from .maze import Maze
-from .misc import sign
+from .misc import round2, sign
 
 
 class ConfigReader:
@@ -58,8 +61,33 @@ class ConfigReader:
         self.config.read(SETTINGS)  # default configuration
         self.config.read(filenames)
 
-    def parse_output(self):
-        """Parse graphics and sound configurations."""
+    # Fallback to None when attribute is missing
+    def __getattr__(self, name): return None
+
+    def parse(self):
+        """Parse configurations."""
+        self.server = self.config.getboolean('Server', 'Enable')
+        if self.server:
+            self.host = self.config.get('Server', 'Host')
+            self.port = self.config.getint('Server', 'Port')
+            self.headless = self.config.getboolean('Server', 'Headless')
+        else:
+            self.key, self.mouse = {}, {}
+            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'):
+                        raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd))
+                    self.mouse[alias] = int(i[-1]) - 1
+                    continue
+                if len(i) == 1:
+                    self.key[alias] = ord(i.lower())
+                    continue
+                try:
+                    self.key[alias] = getattr(pygame, 'K_{}'.format(i.upper()))
+                except AttributeError:
+                    raise ValueError(self.INVALID_CONTROL_ERR.format(cmd, i))
+
         self.size = (self.config.getint('Graphics', 'Screen width'),
                      self.config.getint('Graphics', 'Screen height'))
         self.opengl = self.config.getboolean('Graphics', 'OpenGL')
@@ -67,24 +95,6 @@ class ConfigReader:
         self.muted = self.config.getboolean('Sound', 'Muted')
         self.musicvol = self.config.getfloat('Sound', 'Music volume')
 
-    def parse_control(self):
-        """Parse control configurations."""
-        self.key, self.mouse = {}, {}
-        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'):
-                    raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd))
-                self.mouse[alias] = int(i[-1]) - 1
-                continue
-            if len(i) == 1:
-                self.key[alias] = ord(i.lower())
-                continue
-            try:
-                self.key[alias] = getattr(pygame, 'K_{}'.format(i.upper()))
-            except AttributeError:
-                raise ValueError(self.INVALID_CONTROL_ERR.format(cmd, i))
-
     def read_args(self, arguments):
         """Read and parse a ArgumentParser.Namespace."""
         for option in 'size', 'opengl', 'max_fps', 'muted', 'musicvol':
@@ -94,29 +104,108 @@ class ConfigReader:
 
 class Game:
     """Object handling main loop and IO."""
-    def __init__(self, size, scrtype, max_fps, muted, musicvol, key, mouse):
+    def __init__(self, config):
         pygame.mixer.pre_init(frequency=44100)
         pygame.init()
-        if muted:
+        if config.muted or config.headless:
             pygame.mixer.quit()
         else:
             pygame.mixer.music.load(MUSIC)
-            pygame.mixer.music.set_volume(musicvol)
+            pygame.mixer.music.set_volume(config.musicvol)
             pygame.mixer.music.play(-1)
         pygame.display.set_icon(ICON)
-        pygame.fastevent.init()
+
+        if config.server:
+            self.socket = socket()
+            self.socket.bind((config.host, config.port))
+            self.socket.listen()
+        else:
+            pygame.fastevent.init()
+
+        self.server, self.headless = config.server, config.headless
         # self.fps is a float to make sure floordiv won't be used in Python 2
-        self.max_fps, self.fps = max_fps, float(max_fps)
-        self.musicvol = musicvol
-        self.key, self.mouse = key, mouse
-        self.maze = Maze(max_fps, size, scrtype)
+        self.max_fps, self.fps = config.max_fps, float(config.max_fps)
+        self.musicvol = config.musicvol
+        self.key, self.mouse = config.key, config.mouse
+        scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
+        self.maze = Maze(config.max_fps, config.size, scrtype, config.headless)
         self.hero = self.maze.hero
         self.clock, self.paused = Clock(), False
 
     def __enter__(self): return self
 
+    def expos(self, x, y):
+        """Return position of the given coordinates in rounded percent."""
+        cx = (x+self.maze.x-self.maze.centerx) / self.maze.distance * 100
+        cy = (y+self.maze.y-self.maze.centery) / self.maze.distance * 100
+        return round2(cx), round2(cy)
+
+    def export(self):
+        """Export maze data to a bytes object."""
+        maze, hero, tick, ne = self.maze, self.hero, get_ticks(), 0
+        walls = [[1 if maze.map[x][y] == WALL else 0 for x in maze.rangex]
+                 for y in maze.rangey]
+
+        x, y = self.expos(maze.x, maze.y)
+        lines = deque(['{} {} {:.0f} {:d} {:d} {:d}'.format(
+            x, y, hero.wound * 100, hero.next_strike <= tick,
+            hero.next_heal <= tick, maze.next_move <= tick)])
+
+        for enemy in maze.enemies:
+            if not enemy.awake:
+                walls[enemy.y-maze.rangey[0]][enemy.x-maze.rangex[0]] = WALL
+                continue
+            elif enemy.color == 'Chameleon' and maze.next_move <= tick:
+                continue
+            x, y = self.expos(*enemy.get_pos())
+            lines.append('{} {} {} {:.0f}'.format(COLORS[enemy.get_color()],
+                                                  x, y, degrees(enemy.angle)))
+            ne += 1
+
+        if maze.next_move <= tick:
+            rows = (''.join(str(cell) for cell in row) for row in walls)
+        else:
+            rows = repeat('0' * len(maze.rangex), len(maze.rangey))
+        lines.appendleft('\n'.join(rows))
+
+        for bullet in maze.bullets:
+            x, y = self.expos(bullet.x, bullet.y)
+            lines.append('{} {} {} {:.0f}'.format(COLORS[bullet.get_color()],
+                                                  x, y, degrees(bullet.angle)))
+
+        lines.appendleft('{} {} {}'.format(len(walls), ne, len(maze.bullets)))
+        return '\n'.join(lines).encode()
+
+    def meta(self):
+        """Handle meta events on Pygame window.
+
+        Return False if QUIT event is captured, True otherwise.
+        """
+        events = pygame.fastevent.get()
+        for event in events:
+            if event.type == QUIT:
+                return False
+            elif event.type == VIDEORESIZE:
+                self.maze.resize((event.w, event.h))
+            elif event.type == KEYDOWN and not self.server:
+                if event.key == self.key['new']:
+                    self.maze.__init__(self.fps)
+                elif event.key == self.key['pause'] and not self.hero.dead:
+                    self.paused ^= True
+                elif event.key == self.key['mute']:
+                    if pygame.mixer.get_init() is None:
+                        pygame.mixer.init(frequency=44100)
+                        pygame.mixer.music.load(MUSIC)
+                        pygame.mixer.music.set_volume(self.musicvol)
+                        pygame.mixer.music.play(-1)
+                    else:
+                        pygame.mixer.quit()
+        if not self.headless: self.maze.draw()
+        return True
+
     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
         stunned = pygame.time.get_ticks() < self.maze.next_move
         velocity = self.maze.distance * HERO_SPEED / self.fps
         accel = velocity * HERO_SPEED / self.fps
@@ -139,42 +228,51 @@ class Game:
             self.maze.vy += y * accel
             if abs(self.maze.vy) > velocity: self.maze.vy = y * velocity
 
-    def loop(self):
-        """Start and handle main loop."""
-        events = pygame.fastevent.get()
-        for event in events:
-            if event.type == QUIT:
-                return False
-            elif event.type == VIDEORESIZE:
-                self.maze.resize((event.w, event.h))
-            elif event.type == KEYDOWN:
-                if event.key == self.key['new']:
-                    self.maze.__init__(self.fps)
-                elif event.key == self.key['pause'] and not self.hero.dead:
-                    self.paused ^= True
-                elif event.key == self.key['mute']:
-                    if pygame.mixer.get_init() is None:
-                        pygame.mixer.init(frequency=44100)
-                        pygame.mixer.music.load(MUSIC)
-                        pygame.mixer.music.set_volume(self.musicvol)
-                        pygame.mixer.music.play(-1)
-                    else:
-                        pygame.mixer.quit()
+    def control(self, x, y, angle, firing, slashing):
+        """Control how the hero move and attack."""
+        self.move(x, y)
+        self.hero.update_angle(angle)
+        self.hero.firing = firing
+        self.hero.slashing = slashing
+
+    def remote_control(self, connection):
+        """Handle remote control though socket server.
 
+        Return False if client disconnect, True otherwise.
+        """
+        data = self.export()
+        connection.send('{:06}'.format(len(data)))
+        connection.send(data)
+        buf = connection.recv(8)
+        if not buf: return False
+        x, y, angle, attack = (int(i) for i in buf.decode().split())
+        self.control(x, y, angle, attack & 1, attack >> 1)
+
+    def user_control(self):
+        """Handle direct control from user's mouse and keyboard."""
         if not self.hero.dead:
             keys = pygame.key.get_pressed()
-            self.move(keys[self.key['left']] - keys[self.key['right']],
-                      keys[self.key['up']] - keys[self.key['down']])
+            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:
-                self.hero.firing = keys[self.key['shot']]
+                firing = keys[self.key['shot']]
             except KeyError:
-                self.hero.firing = buttons[self.mouse['shot']]
+                firing = buttons[self.mouse['shot']]
             try:
-                self.hero.slashing = keys[self.key['slash']]
+                slashing = keys[self.key['slash']]
             except KeyError:
-                self.hero.slashing = buttons[self.mouse['slash']]
+                slashing = buttons[self.mouse['slash']]
+
+            self.control(right, down, angle, firing, slashing)
 
+    def update(self):
+        """Update fps and the maze."""
         # Compare current FPS with the average of the last 10 frames
         new_fps = self.clock.get_fps()
         if new_fps < self.fps:
@@ -183,7 +281,6 @@ class Game:
             self.fps += 5
         if not self.paused: self.maze.update(self.fps)
         self.clock.tick(self.fps)
-        return True
 
     def __exit__(self, exc_type, exc_value, traceback): pygame.quit()
 
@@ -196,7 +293,7 @@ def main():
     parents.append(dirs.user_config_dir)
     filenames = [join(parent, 'settings.ini') for parent in parents]
     config = ConfigReader(filenames)
-    config.parse_output()
+    config.parse()
 
     # Parse command-line arguments
     parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
@@ -238,11 +335,15 @@ def main():
     # Manipulate config
     if args.config: config.config.read(args.config)
     config.read_args(args)
-    config.parse_output()
-    config.parse_control()
+    config.parse()
 
     # Main loop
-    scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
-    with Game(config.size, scrtype, config.max_fps, config.muted,
-              config.musicvol, config.key, config.mouse) as game:
-        while game.loop(): pass
+    with Game(config) as game:
+        if config.server:
+            while game.meta():
+                game.remote_control()
+                game.update()
+        else:
+            while game.meta():
+                game.user_control()
+                game.update()
diff --git a/brutalmaze/maze.py b/brutalmaze/maze.py
index 66c4c81..f94af16 100644
--- a/brutalmaze/maze.py
+++ b/brutalmaze/maze.py
@@ -63,7 +63,7 @@ class Maze:
         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): range of the index of the grids on display
+        rangex, rangey (list): range of the index of the grids on display
         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)
@@ -78,21 +78,22 @@ class Maze:
         sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy
         sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose
     """
-    def __init__(self, fps, size=None, scrtype=None):
+    def __init__(self, fps, size=None, scrtype=None, headless=False):
         self.fps = fps
-        if size is not None:
-            self.w, self.h = size
-        else:
-            size = self.w, self.h
-        if scrtype is not None: self.scrtype = scrtype
+        if not headless:
+            if size is not None:
+                self.w, self.h = size
+            else:
+                size = self.w, self.h
+            if scrtype is not None: self.scrtype = scrtype
+            self.surface = pygame.display.set_mode(size, self.scrtype)
 
-        self.surface = pygame.display.set_mode(size, self.scrtype)
         self.distance = (self.w * self.h / 416) ** 0.5
         self.x, self.y = self.w // 2, self.h // 2
         self.centerx, self.centery = self.w / 2.0, self.h / 2.0
         w, h = (int(i/self.distance/2 + 2) for i in size)
-        self.rangex = range(MIDDLE - w, MIDDLE + w + 1)
-        self.rangey = range(MIDDLE - h, MIDDLE + h + 1)
+        self.rangex = list(range(MIDDLE - w, MIDDLE + w + 1))
+        self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1))
         self.score = INIT_SCORE
 
         self.map = deque()
@@ -146,7 +147,7 @@ class Maze:
                     fill_aapolygon(self.surface, square, FG_COLOR)
 
         for enemy in self.enemies: enemy.draw()
-        self.hero.draw()
+        if not self.hero.dead: self.hero.draw()
         bullet_radius = self.distance / 4
         for bullet in self.bullets: bullet.draw(bullet_radius)
         pygame.display.flip()
@@ -320,7 +321,6 @@ class Maze:
             self.hero.update(fps)
             self.slash()
             self.track_bullets()
-        self.draw()
 
     def resize(self, size):
         """Resize the maze."""
@@ -349,3 +349,4 @@ class Maze:
         self.hero.slashing = self.hero.firing = False
         self.vx = self.vy = 0.0
         play(self.sfx_lose)
+        print('Game over. Your score: {}'.format(int(self.score - INIT_SCORE)))
diff --git a/brutalmaze/settings.ini b/brutalmaze/settings.ini
index 2baeedd..67357a0 100644
--- a/brutalmaze/settings.ini
+++ b/brutalmaze/settings.ini
@@ -1,3 +1,11 @@
+[Server]
+# Enabling remote control will disable control via keyboard and mouse.
+Enable: no
+Host: localhost
+Port: 8089
+# Disable graphics and sounds (only if socket server is enabled).
+Headless: no
+
 [Graphics]
 Screen width: 640
 Screen height: 480
diff --git a/brutalmaze/weapons.py b/brutalmaze/weapons.py
index 3979d30..be5dd57 100644
--- a/brutalmaze/weapons.py
+++ b/brutalmaze/weapons.py
@@ -55,14 +55,18 @@ class Bullet:
         self.x += s * cos(self.angle)
         self.y += s * sin(self.angle)
 
-    def draw(self, radius):
-        """Draw the bullet."""
-        pentagon = regpoly(5, radius, self.angle, self.x, self.y)
+    def get_color(self):
+        """Return current color of the enemy."""
         value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP)
         try:
-            fill_aapolygon(self.surface, pentagon, TANGO[self.color][value])
+            return TANGO[self.color][value]
         except IndexError:
-            pass
+            return BG_COLOR
+
+    def draw(self, radius):
+        """Draw the bullet."""
+        pentagon = regpoly(5, radius, self.angle, self.x, self.y)
+        fill_aapolygon(self.surface, pentagon, self.get_color())
 
     def place(self, x, y):
         """Move the bullet by (x, y) (in pixels)."""