changeset 486:2f53be1b2f60

Merge netplay branch.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Fri, 27 Sep 2013 19:01:47 +0200
parents ca22df9e70bc (current diff) c099802e2435 (diff)
children 711c75115675
files eosd pytouhou/game/enemy.pyx pytouhou/game/game.pxd pytouhou/game/game.pyx pytouhou/ui/gamerunner.pyx
diffstat 6 files changed, 147 insertions(+), 23 deletions(-) [+]
line wrap: on
line diff
--- a/eosd
+++ b/eosd
@@ -46,6 +46,8 @@ parser.add_argument('--no-music', action
 parser.add_argument('--hints', metavar='HINTS', default=None, help='Hints file, to display text while playing.')
 parser.add_argument('--verbosity', metavar='VERBOSITY', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Select the wanted logging level.')
 parser.add_argument('--game', metavar='GAME', choices=['EoSD'], default='EoSD', help='Select the game engine to use.')
+parser.add_argument('--port', metavar='PORT', type=int, default=0, help='Port to use for netplay')
+parser.add_argument('--remote', metavar='REMOTE', default=None, help='Remote address')
 
 args = parser.parse_args()
 
@@ -62,6 +64,7 @@ from pytouhou.formats.t6rp import T6RP, 
 from pytouhou.utils.random import Random
 from pytouhou.vm.msgrunner import NextStage
 from pytouhou.formats.hint import Hint
+from pytouhou.network import Network
 
 
 if args.game == 'EoSD':
@@ -98,7 +101,7 @@ class GameBossRush(Game):
 
 def main(window, path, data, stage_num, rank, character, replay, save_filename,
          skip_replay, boss_rush, debug, enable_background, enable_particles,
-         enable_music, hints, verbosity):
+         enable_music, hints, verbosity, port, remote):
 
     resource_loader = Loader(path)
 
@@ -136,6 +139,22 @@ def main(window, path, data, stage_num, 
         save_replay.rank = rank
         save_replay.character = character
 
+    if port != 0:
+        players = [PlayerState(character=0), PlayerState(character=2)]
+
+        if remote:
+            remote_addr, remote_port = remote.split(':')
+            addr = remote_addr, int(remote_port)
+            selected_player = 0
+        else:
+            addr = None
+            selected_player = 1
+
+        prng = Random(0)
+        con = Network(port, addr, selected_player)
+    else:
+        con = None
+
     if hints:
         with open(hints, 'rb') as file:
             hints = Hint.read(file)
@@ -147,7 +166,7 @@ def main(window, path, data, stage_num, 
     game_class = GameBossRush if boss_rush else Game
 
     common = Common(resource_loader)
-    runner = GameRunner(window, resource_loader, skip=skip_replay)
+    runner = GameRunner(window, resource_loader, skip=skip_replay, con=con)
     while True:
         if replay:
             level = replay.levels[stage_num - 1]
@@ -167,7 +186,7 @@ def main(window, path, data, stage_num, 
             states[0].lives = level.lives
             states[0].bombs = level.bombs
             difficulty = level.difficulty
-        else:
+        elif port == 0:
             prng = Random()
 
         if save_filename:
@@ -231,7 +250,7 @@ with SDL():
     main(window, args.path, tuple(args.data), args.stage, args.rank,
          args.character, args.replay, args.save_replay, args.skip_replay,
          args.boss_rush, args.debug, args.no_background, args.no_particles,
-         args.no_music, args.hints, args.verbosity)
+         args.no_music, args.hints, args.verbosity, args.port, args.remote)
 
     import gc
     gc.collect()
--- a/pytouhou/game/enemy.pyx
+++ b/pytouhou/game/enemy.pyx
@@ -213,7 +213,7 @@ cdef class Enemy(Element):
     cpdef Player select_player(self, list players=None):
         if players is None:
             players = self._game.players
-        return players[0] #TODO
+        return min(players, key=self.select_player_key)
 
 
     cpdef double get_player_angle(self, tuple pos=None, Player player=None):
@@ -540,3 +540,6 @@ cdef class Enemy(Element):
 
         self.frame += 1
 
+
+    def select_player_key(self, p):
+        return ((p.x - self.x) ** 2 + (p.y - self.y) ** 2, p.state.character)
--- a/pytouhou/game/game.pxd
+++ b/pytouhou/game/game.pxd
@@ -34,11 +34,11 @@ cdef class Game:
     cpdef NativeText new_native_text(self, tuple pos, unicode text, align=*)
     cpdef Text new_hint(self, hint)
     cpdef new_face(self, side, effect)
-    cpdef run_iter(self, long keystate)
+    cpdef run_iter(self, list keystates)
     cdef void update_background(self) except *
     cdef void update_enemies(self) except *
     cdef void update_msg(self, long keystate) except *
-    cdef void update_players(self, long keystate) except *
+    cdef void update_players(self, list keystates) except *
     cdef void update_effects(self) except *
     cdef void update_hints(self) except *
     cdef void update_faces(self) except *
--- a/pytouhou/game/game.pyx
+++ b/pytouhou/game/game.pyx
@@ -164,7 +164,7 @@ cdef class Game:
         cdef Bullet bullet
         cdef Laser laser
 
-        player = self.players[0] #TODO
+        player = min(self.players, key=select_player_key)
         item_type = self.item_types[6]
         self.items.extend([Item((bullet.x, bullet.y), 6, item_type, self, player=player)
                              for bullet in self.bullets])
@@ -259,8 +259,9 @@ cdef class Game:
         return face
 
 
-    cpdef run_iter(self, long keystate):
+    cpdef run_iter(self, list keystates):
         cdef Laser laser
+
         # 1. VMs.
         for runner in self.ecl_runners:
             runner.run_iter()
@@ -283,9 +284,10 @@ cdef class Game:
         # Pri 6 is background
         self.update_background() #TODO: Pri unknown
         if self.msg_runner is not None:
-            self.update_msg(keystate) # Pri ?
-            keystate &= ~3 # Remove the ability to attack (keystates 1 and 2).
-        self.update_players(keystate) # Pri 7
+            self.update_msg(keystates[0]) # Pri ?
+            for i in xrange(len(keystates)):
+                keystates[i] &= ~3 # Remove the ability to attack (keystates 1 and 2).
+        self.update_players(keystates) # Pri 7
         self.update_enemies() # Pri 9
         self.update_effects() # Pri 10
         self.update_bullets() # Pri 11
@@ -337,9 +339,10 @@ cdef class Game:
         self.msg_runner.run_iteration()
 
 
-    cdef void update_players(self, long keystate):
+    cdef void update_players(self, list keystates):
         cdef Bullet bullet
         cdef Player player
+        cdef long keystate
 
         if self.time_stop:
             return
@@ -347,13 +350,13 @@ cdef class Game:
         for bullet in self.players_bullets:
             bullet.update()
 
-        for player in self.players:
+        for player, keystate in zip(self.players, keystates):
             player.update(keystate) #TODO: differentiate keystates (multiplayer mode)
 
-        #XXX: Why 78910? Is it really the right value?
-        player.state.effective_score = min(player.state.effective_score + 78910,
-                                           player.state.score)
-        #TODO: give extra lives to the player
+            #XXX: Why 78910? Is it really the right value?
+            player.state.effective_score = min(player.state.effective_score + 78910,
+                                               player.state.score)
+            #TODO: give extra lives to the player
 
 
     cdef void update_effects(self):
@@ -540,6 +543,7 @@ cdef class Game:
         if self.boss and self.boss._enemy.removed:
             self.boss = None
 
+
 cdef list filter_removed(list elements):
     cdef Element element
 
@@ -548,3 +552,7 @@ cdef list filter_removed(list elements):
         if not element.removed:
             filtered.append(element)
     return filtered
+
+
+def select_player_key(player):
+    return (player.state.score, player.state.character)
new file mode 100644
--- /dev/null
+++ b/pytouhou/network.py
@@ -0,0 +1,90 @@
+import socket
+import struct
+from select import select
+import time
+
+MSG_STRUCT = struct.Struct('!HHH')
+
+class Network(object):
+    def __init__(self, port=8080, dest=None, selected_player=0):
+        self.frame = 0
+        self.keystate = 0
+        self.old_keystate = 0
+        self.remote_keystate = 0
+
+        self.selected_player = selected_player
+
+        self.remote_addr = dest
+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+        self.sock.bind(('', port))
+
+
+    def read_message(self):
+        message = None
+
+        start_time = time.time()
+        delta = 1./60.
+
+        rlist, wlist, xlist = select([self.sock], [], [], delta)
+        while rlist:
+            msg, addr = rlist[0].recvfrom(MSG_STRUCT.size)
+            # Check whether the message comes from the right address
+            if self.frame == 0 or addr == self.remote_addr:
+                self.remote_addr = addr
+
+                frame, keystate, old_keystate = MSG_STRUCT.unpack(msg)
+
+                # Check for well-formedness
+                if frame in (self.frame, self.frame + 1):
+                    message = (frame, keystate, old_keystate)
+            else:
+                print('Mismatch', self.remote_addr, addr)
+
+            # If no valid message has been read, wait for one as long as possible
+            # else, read as much as we can without blocking.
+            delta = 0 if message else max(0, 1./60. - (time.time() - start_time))
+            rlist, wlist, xlist = select(rlist, [], [], delta)
+
+        return message
+
+
+    def send_message(self):
+        frame, keystate, old_keystate = self.frame, self.keystate, self.old_keystate
+        if self.remote_addr is not None:
+            self.sock.sendto(MSG_STRUCT.pack(frame, keystate, old_keystate), self.remote_addr)
+
+
+    def run_game_iter(self, game, keystate, other_keystate):
+        keystates = [other_keystate, other_keystate]
+        keystates[self.selected_player] = keystate
+        game.run_iter(keystates)
+
+
+    def run_iter(self, game, keystate):
+        if game.frame % 3 == 0:
+            # Phase 1: Update game with old data
+            self.run_game_iter(game, self.keystate, self.remote_keystate)
+        elif game.frame % 3 == 1:
+            # Phase 2: Update data, send new data, update game with old data
+            self.old_keystate, self.keystate = self.keystate, keystate
+            self.frame = game.frame // 3
+            self.send_message()
+            self.run_game_iter(game, self.old_keystate, self.remote_keystate)
+        elif game.frame % 3 == 2:
+            # Phase 3: Send new data, get remote data, update game with new data
+            self.send_message()
+            # Follow one valid update
+            message = self.read_message()
+            if message:
+                frame, keystate, old_keystate = message
+                if frame == self.frame:
+                    self.remote_keystate = keystate
+                elif frame == self.frame + 1:
+                    self.remote_keystate = old_keystate
+                else:
+                    raise Exception #TODO
+                self.run_game_iter(game, self.keystate, self.remote_keystate)
+            elif game.frame > 2:
+                print('ARGH')
+
+
--- a/pytouhou/ui/gamerunner.pyx
+++ b/pytouhou/ui/gamerunner.pyx
@@ -20,20 +20,20 @@ from .music import MusicPlayer, SFXPlaye
 
 
 cdef class GameRunner(Runner):
-    cdef object game, background
+    cdef object game, background, con
     cdef GameRenderer renderer
     cdef Window window
     cdef object replay_level, save_keystates
-    cdef long keystate
     cdef bint skip
 
-    def __init__(self, Window window, resource_loader, bint skip=False):
+    def __init__(self, Window window, resource_loader, bint skip=False,
+                 con=None):
         self.renderer = GameRenderer(resource_loader, window.use_fixed_pipeline)
 
         self.window = window
         self.replay_level = None
         self.skip = skip
-        self.keystate = 0
+        self.con = con
 
         self.width = window.width #XXX
         self.height = window.height #XXX
@@ -134,7 +134,11 @@ cdef class GameRunner(Runner):
             if self.save_keystates is not None:
                 self.save_keystates.append(keystate)
 
-            self.game.run_iter(keystate)
+            if self.con:
+                self.con.run_iter(self.game, keystate)
+            else:
+                self.game.run_iter([keystate])
+
             self.game.interface.labels['framerate'].set_text('%.2ffps' % self.window.get_fps())
         if not self.skip:
             self.renderer.render(self.game, self.window)