summary refs log tree commit diff homepage
diff options
context:
space:
mode:
-rw-r--r--brutalmaze/main.py119
-rw-r--r--brutalmaze/maze.py39
2 files changed, 93 insertions, 65 deletions
diff --git a/brutalmaze/main.py b/brutalmaze/main.py
index 2300658..a47abf0 100644
--- a/brutalmaze/main.py
+++ b/brutalmaze/main.py
@@ -27,10 +27,11 @@ try:                    # Python 3
 except ImportError:     # Python 2
     from ConfigParser import ConfigParser
 from itertools import repeat
-from math import atan2, degrees
+from math import atan2, degrees, radians
 from os.path import join, pathsep
-from socket import socket
+from socket import socket, SOL_SOCKET, SO_REUSEADDR
 from sys import stdout
+from threading import Thread
 
 
 import pygame
@@ -115,14 +116,18 @@ class Game:
             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()
+            self.server = socket()
+            self.server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
+            self.server.bind((config.host, config.port))
+            self.server.listen(1)
+            print('Socket server is listening on {}:{}'.format(config.host,
+                                                               config.port))
         else:
-            pygame.fastevent.init()
+            self.server = None
 
-        self.server, self.headless = config.server, config.headless
+        self.headless = config.headless
         # self.fps is a float to make sure floordiv won't be used in Python 2
         self.max_fps, self.fps = config.max_fps, float(config.max_fps)
         self.musicvol = config.musicvol
@@ -142,17 +147,13 @@ class Game:
 
     def export(self):
         """Export maze data to a bytes object."""
-        maze, hero, tick, ne = self.maze, self.hero, get_ticks(), 0
+        maze, hero, tick = self.maze, self.hero, get_ticks()
         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 y in maze.rangey] if maze.next_move <= tick else []
+        lines, ne, nb = deque(), 0, 0
 
         for enemy in maze.enemies:
-            if not enemy.awake:
+            if not enemy.awake and walls:
                 walls[enemy.y-maze.rangey[0]][enemy.x-maze.rangex[0]] = WALL
                 continue
             elif enemy.color == 'Chameleon' and maze.next_move <= tick:
@@ -162,25 +163,35 @@ class Game:
                                                   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)))
+            color, angle = COLORS[bullet.get_color()], degrees(bullet.angle)
+            if color != '0':
+                lines.append('{} {} {} {:.0f}'.format(color, x, y, angle))
+                nb += 1
 
-        lines.appendleft('{} {} {}'.format(len(walls), ne, len(maze.bullets)))
+        if walls: lines.appendleft('\n'.join(''.join(str(cell) for cell in row)
+                                             for row in walls))
+        x, y = self.expos(maze.x, maze.y)
+        lines.appendleft('{} {} {} {} {} {} {:.0f} {:d} {:d}'.format(
+            len(walls), ne, nb, maze.get_score(), x, y, hero.wound * 100,
+            hero.next_strike <= tick, hero.next_heal <= tick))
         return '\n'.join(lines).encode()
 
-    def meta(self):
-        """Handle meta events on Pygame window.
+    def update(self):
+        """Draw and handle meta events on Pygame window.
 
         Return False if QUIT event is captured, True otherwise.
         """
+        # Compare current FPS with the average of the last 10 frames
+        new_fps = self.clock.get_fps()
+        if new_fps < self.fps:
+            self.fps -= 1
+        elif self.fps < self.max_fps and not self.paused:
+            self.fps += 5
+        if not self.paused: self.maze.update(self.fps)
+
+        self.clock.tick(self.fps)
         events = pygame.fastevent.get()
         for event in events:
             if event.type == QUIT:
@@ -189,7 +200,7 @@ class Game:
                 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)
+                    self.maze.reinit()
                 elif event.key == self.key['pause'] and not self.hero.dead:
                     self.paused ^= True
                 elif event.key == self.key['mute']:
@@ -235,18 +246,28 @@ class Game:
         self.hero.firing = firing
         self.hero.slashing = slashing
 
-    def remote_control(self, connection):
+    def remote_control(self):
         """Handle remote control though socket server.
 
-        Return False if client disconnect, True otherwise.
+        This function is supposed to be run in a Thread.
         """
-        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)
+        while True:
+            connection, address = self.server.accept()
+            print('Connected to {}:{}'.format(*address))
+            self.maze.reinit()
+            while not self.hero.dead:
+                data = self.export()
+                connection.send('{:06}'.format(len(data)).encode())
+                connection.send(data)
+                buf = connection.recv(8)
+                if not buf: break
+                move, angle, attack = (int(i) for i in buf.decode().split())
+                y, x = (i - 1 for i in divmod(move, 3))
+                self.control(x, y, radians(angle), attack & 1, attack >> 1)
+            self.maze.lose()
+            print('{1}:{2} scored {0} points'.format(
+                self.maze.get_score(), *address))
+            connection.close()
 
     def user_control(self):
         """Handle direct control from user's mouse and keyboard."""
@@ -271,18 +292,9 @@ class Game:
 
             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:
-            self.fps -= 1
-        elif self.fps < self.max_fps and not self.paused:
-            self.fps += 5
-        if not self.paused: self.maze.update(self.fps)
-        self.clock.tick(self.fps)
-
-    def __exit__(self, exc_type, exc_value, traceback): pygame.quit()
+    def __exit__(self, exc_type, exc_value, traceback):
+        if self.server is not None: self.server.close()
+        pygame.quit()
 
 
 def main():
@@ -340,10 +352,9 @@ def main():
     # Main loop
     with Game(config) as game:
         if config.server:
-            while game.meta():
-                game.remote_control()
-                game.update()
+            socket_thread = Thread(target=game.remote_control)
+            socket_thread.daemon = True     # make it disposable
+            socket_thread.start()
+            while game.update(): pass
         else:
-            while game.meta():
-                game.user_control()
-                game.update()
+            while game.update(): game.user_control()
diff --git a/brutalmaze/maze.py b/brutalmaze/maze.py
index f94af16..387c7c6 100644
--- a/brutalmaze/maze.py
+++ b/brutalmaze/maze.py
@@ -78,14 +78,11 @@ 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, headless=False):
+    def __init__(self, fps, size, scrtype, headless=False):
         self.fps = fps
         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.w, self.h = size
+            self.scrtype = scrtype
             self.surface = pygame.display.set_mode(size, self.scrtype)
 
         self.distance = (self.w * self.h / 416) ** 0.5
@@ -135,6 +132,10 @@ class Maze:
         return (self.centerx + (x - MIDDLE)*self.distance,
                 self.centery + (y - MIDDLE)*self.distance)
 
+    def get_score(self):
+        """Return the current score."""
+        return int(self.score - INIT_SCORE)
+
     def draw(self):
         """Draw the maze."""
         self.surface.fill(BG_COLOR)
@@ -151,8 +152,8 @@ class Maze:
         bullet_radius = self.distance / 4
         for bullet in self.bullets: bullet.draw(bullet_radius)
         pygame.display.flip()
-        pygame.display.set_caption('Brutal Maze - Score: {}'.format(
-            int(self.score - INIT_SCORE)))
+        pygame.display.set_caption(
+            'Brutal Maze - Score: {}'.format(self.get_score()))
 
     def rotate(self):
         """Rotate the maze if needed."""
@@ -335,8 +336,8 @@ class Maze:
         self.centerx = self.x + offsetx*self.distance
         self.centery = self.y + offsety*self.distance
         w, h = int(self.w/self.distance/2 + 2), int(self.h/self.distance/2 + 2)
-        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.slashd = self.hero.R + self.distance/SQRT2
 
     def isfast(self):
@@ -349,4 +350,20 @@ 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)))
+
+    def reinit(self):
+        """Open new game."""
+        self.centerx, self.centery = self.w / 2.0, self.h / 2.0
+        self.score = INIT_SCORE
+        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()
+
+        self.next_move = self.next_slashfx = 0
+        self.hero.next_heal = self.hero.next_beat = self.hero.next_strike = 0
+        self.hero.slashing = self.hero.firing = self.hero.dead = False
+        self.hero.spin_queue = self.hero.wound = 0.0