about summary refs log tree commit diff homepage
path: root/brutalmaze/main.py
diff options
context:
space:
mode:
Diffstat (limited to 'brutalmaze/main.py')
-rw-r--r--brutalmaze/main.py231
1 files changed, 166 insertions, 65 deletions
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()