home · contact · privacy
Register game commands and tasks outside of game module.
[plomrogue2-experiments] / new2 / rogue_chat_curses.py
index 643023bcc6292961974de7be71d4476ce28f4c77..a6d1ca7a81e9f8632bd82d2bb9e482da8529aae8 100755 (executable)
@@ -1,14 +1,53 @@
 #!/usr/bin/env python3
 import curses
 #!/usr/bin/env python3
 import curses
-import socket
 import queue
 import threading
 import queue
 import threading
-from plomrogue.io_tcp import PlomSocket
 from plomrogue.game import GameBase
 from plomrogue.parser import Parser
 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
 from plomrogue.things import ThingBase
 from plomrogue.misc import quote
 from plomrogue.game import GameBase
 from plomrogue.parser import Parser
 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
 from plomrogue.things import ThingBase
 from plomrogue.misc import quote
+from plomrogue.errors import BrokenSocketConnection
+
+from ws4py.client import WebSocketBaseClient
+class WebSocketClient(WebSocketBaseClient):
+
+    def __init__(self, recv_handler, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.recv_handler = recv_handler
+        self.connect()
+
+    def received_message(self, message):
+        if message.is_text:
+            message = str(message)
+            self.recv_handler(message)
+
+    @property
+    def plom_closed(self):
+        return self.client_terminated
+
+from plomrogue.io_tcp import PlomSocket
+class PlomSocketClient(PlomSocket):
+
+    def __init__(self, recv_handler, url):
+        import socket
+        self.recv_handler = recv_handler
+        host, port = url.split(':')
+        super().__init__(socket.create_connection((host, port)))
+
+    def close(self):
+        self.socket.close()
+
+    def run(self):
+        import ssl
+        try:
+            for msg in self.recv():
+                if msg == 'NEED_SSL':
+                    self.socket = ssl.wrap_socket(self.socket)
+                    continue
+                self.recv_handler(msg)
+        except BrokenSocketConnection:
+            pass  # we assume socket will be known as dead by now
 
 def cmd_TURN(game, n):
     game.turn = n
 
 def cmd_TURN(game, n):
     game.turn = n
@@ -48,19 +87,19 @@ def cmd_MAP(game, geometry, size, content):
     game.map_content = content
     if type(game.map_geometry) == MapGeometrySquare:
         game.tui.movement_keys = {
     game.map_content = content
     if type(game.map_geometry) == MapGeometrySquare:
         game.tui.movement_keys = {
-            'w': 'UP',
-            'a': 'LEFT',
-            's': 'DOWN',
-            'd': 'RIGHT',
+            game.tui.keys['square_move_up']: 'UP',
+            game.tui.keys['square_move_left']: 'LEFT',
+            game.tui.keys['square_move_down']: 'DOWN',
+            game.tui.keys['square_move_right']: 'RIGHT',
         }
     elif type(game.map_geometry) == MapGeometryHex:
         game.tui.movement_keys = {
         }
     elif type(game.map_geometry) == MapGeometryHex:
         game.tui.movement_keys = {
-            'w': 'UPLEFT',
-            'e': 'UPRIGHT',
-            'd': 'RIGHT',
-            'c': 'DOWNRIGHT',
-            'x': 'DOWNLEFT',
-            's': 'LEFT',
+            game.tui.keys['hex_move_upleft']: 'UPLEFT',
+            game.tui.keys['hex_move_upright']: 'UPRIGHT',
+            game.tui.keys['hex_move_right']: 'RIGHT',
+            game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
+            game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
+            game.tui.keys['hex_move_left']: 'LEFT',
         }
 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
 
         }
 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
 
@@ -73,9 +112,7 @@ def cmd_GAME_STATE_COMPLETE(game):
         game.tui.query_info()
     player = game.get_thing(game.player_id, False)
     if player.position in game.portals:
         game.tui.query_info()
     player = game.get_thing(game.player_id, False)
     if player.position in game.portals:
-        host, port = game.portals[player.position].split(':')
-        game.tui.teleport_target_host = host
-        game.tui.teleport_target_port = port
+        game.tui.teleport_target_host = game.portals[player.position]
         game.tui.switch_mode('teleport')
     game.turn_complete = True
     game.tui.do_refresh = True
         game.tui.switch_mode('teleport')
     game.turn_complete = True
     game.tui.do_refresh = True
@@ -106,25 +143,30 @@ def cmd_ANNOTATION(game, position, msg):
         game.tui.do_refresh = True
 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
 
         game.tui.do_refresh = True
 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
 
+def cmd_PONG(game):
+    pass
+cmd_PONG.argtypes = ''
+
 class Game(GameBase):
 class Game(GameBase):
-    commands = {'LOGIN_OK': cmd_LOGIN_OK,
-                'CHAT': cmd_CHAT,
-                'PLAYER_ID': cmd_PLAYER_ID,
-                'TURN': cmd_TURN,
-                'THING_POS': cmd_THING_POS,
-                'THING_NAME': cmd_THING_NAME,
-                'MAP': cmd_MAP,
-                'PORTAL': cmd_PORTAL,
-                'ANNOTATION': cmd_ANNOTATION,
-                'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
-                'ARGUMENT_ERROR': cmd_ARGUMENT_ERROR,
-                'GAME_ERROR': cmd_GAME_ERROR,
-                'PLAY_ERROR': cmd_PLAY_ERROR}
     thing_type = ThingBase
     turn_complete = False
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
     thing_type = ThingBase
     turn_complete = False
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
+        self.register_command(cmd_LOGIN_OK)
+        self.register_command(cmd_PONG)
+        self.register_command(cmd_CHAT)
+        self.register_command(cmd_PLAYER_ID)
+        self.register_command(cmd_TURN)
+        self.register_command(cmd_THING_POS)
+        self.register_command(cmd_THING_NAME)
+        self.register_command(cmd_MAP)
+        self.register_command(cmd_PORTAL)
+        self.register_command(cmd_ANNOTATION)
+        self.register_command(cmd_GAME_STATE_COMPLETE)
+        self.register_command(cmd_ARGUMENT_ERROR)
+        self.register_command(cmd_GAME_ERROR)
+        self.register_command(cmd_PLAY_ERROR)
         self.map_content = ''
         self.player_id = -1
         self.info_db = {}
         self.map_content = ''
         self.player_id = -1
         self.info_db = {}
@@ -152,9 +194,10 @@ class TUI:
             self.shows_info = shows_info
             self.is_intro = is_intro
 
             self.shows_info = shows_info
             self.is_intro = is_intro
 
-    def __init__(self, host, port):
+    def __init__(self, host):
+        import os
+        import json
         self.host = host
         self.host = host
-        self.port = port
         self.mode_play = self.Mode('play')
         self.mode_study = self.Mode('study', shows_info=True)
         self.mode_edit = self.Mode('edit')
         self.mode_play = self.Mode('play')
         self.mode_study = self.Mode('study', shows_info=True)
         self.mode_edit = self.Mode('edit')
@@ -171,7 +214,32 @@ class TUI:
         self.log = []
         self.do_refresh = True
         self.queue = queue.Queue()
         self.log = []
         self.do_refresh = True
         self.queue = queue.Queue()
+        self.login_name = None
         self.switch_mode('waiting_for_server')
         self.switch_mode('waiting_for_server')
+        self.keys = {
+            'switch_to_chat': 't',
+            'switch_to_play': 'p',
+            'switch_to_annotate': 'm',
+            'switch_to_portal': 'P',
+            'switch_to_study': '?',
+            'switch_to_edit': 'm',
+            'flatten': 'F',
+            'hex_move_upleft': 'w',
+            'hex_move_upright': 'e',
+            'hex_move_right': 'd',
+            'hex_move_downright': 'x',
+            'hex_move_downleft': 'y',
+            'hex_move_left': 'a',
+            'square_move_up': 'w',
+            'square_move_left': 'a',
+            'square_move_down': 's',
+            'square_move_right': 'd',
+        }
+        if os.path.isfile('config.json'):
+            with open('config.json', 'r') as f:
+                keys_conf = json.loads(f.read())
+            for k in keys_conf:
+                self.keys[k] = keys_conf[k]
         curses.wrapper(self.loop)
 
     def flash(self):
         curses.wrapper(self.loop)
 
     def flash(self):
@@ -179,8 +247,10 @@ class TUI:
 
     def send(self, msg):
         try:
 
     def send(self, msg):
         try:
+            if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
+                raise BrokenSocketConnection
             self.socket.send(msg)
             self.socket.send(msg)
-        except BrokenPipeError:
+        except (BrokenPipeError, BrokenSocketConnection):
             self.log_msg('@ server disconnected :(')
             self.do_refresh = True
 
             self.log_msg('@ server disconnected :(')
             self.do_refresh = True
 
@@ -200,11 +270,13 @@ class TUI:
         if self.mode.name == 'waiting_for_server':
             self.log_msg('@ waiting for server …')
         elif self.mode.name == 'login':
         if self.mode.name == 'waiting_for_server':
             self.log_msg('@ waiting for server …')
         elif self.mode.name == 'login':
-            self.log_msg('@ enter username')
+            if self.login_name:
+                self.send('LOGIN ' + quote(self.login_name))
+            else:
+                self.log_msg('@ enter username')
         elif self.mode.name == 'teleport':
         elif self.mode.name == 'teleport':
-            self.log_msg("@ May teleport to %s:%s" % (self.teleport_target_host,
-                                                      self.teleport_target_port));
-            self.log_msg("@ Enter 'YES!' to affirm.");
+            self.log_msg("@ May teleport to %s" % (self.teleport_target_host)),
+            self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
         elif self.mode.name == 'annotate' and self.explorer in self.game.info_db:
             info = self.game.info_db[self.explorer]
             if info != '(none)':
         elif self.mode.name == 'annotate' and self.explorer in self.game.info_db:
             info = self.game.info_db[self.explorer]
             if info != '(none)':
@@ -215,23 +287,25 @@ class TUI:
     def help(self):
         self.log_msg("HELP:");
         self.log_msg("chat mode commands:");
     def help(self):
         self.log_msg("HELP:");
         self.log_msg("chat mode commands:");
-        self.log_msg("  :nick NAME - re-name yourself to NAME");
-        self.log_msg("  :msg USER TEXT - send TEXT to USER");
-        self.log_msg("  :help - show this help");
-        self.log_msg("  :p or :play - switch to play mode");
-        self.log_msg("  :? or :study - switch to study mode");
+        self.log_msg("  /nick NAME - re-name yourself to NAME");
+        self.log_msg("  /msg USER TEXT - send TEXT to USER");
+        self.log_msg("  /help - show this help");
+        self.log_msg("  /%s or /play - switch to play mode" % self.keys['switch_to_play']);
+        self.log_msg("  /%s or /study - switch to study mode" % self.keys['switch_to_study']);
         self.log_msg("commands common to study and play mode:");
         self.log_msg("commands common to study and play mode:");
-        self.log_msg("  w,a,s,d - move");
-        self.log_msg("  c - switch to chat mode");
+        self.log_msg("  %s - move" % ','.join(self.movement_keys));
+        self.log_msg("  %s - switch to chat mode" % self.keys['switch_to_chat']);
         self.log_msg("commands specific to play mode:");
         self.log_msg("commands specific to play mode:");
-        self.log_msg("  e - write following ASCII character");
-        self.log_msg("  f - flatten surroundings");
-        self.log_msg("  ? - switch to study mode");
+        self.log_msg("  %s - write following ASCII character" % self.keys['switch_to_edit']);
+        self.log_msg("  %s - flatten surroundings" % self.keys['flatten']);
+        self.log_msg("  %s - switch to study mode" % self.keys['switch_to_study']);
         self.log_msg("commands specific to study mode:");
         self.log_msg("commands specific to study mode:");
-        self.log_msg("  e - annotate terrain");
-        self.log_msg("  p - switch to play mode");
+        self.log_msg("  %s - annotate terrain" % self.keys['switch_to_annotate']);
+        self.log_msg("  %s - switch to play mode" % self.keys['switch_to_play']);
 
     def loop(self, stdscr):
 
     def loop(self, stdscr):
+        import time
+        import datetime
 
         def safe_addstr(y, x, line):
             if y < self.size.y - 1 or x + len(line) < self.size.x:
 
         def safe_addstr(y, x, line):
             if y < self.size.y - 1 or x + len(line) < self.size.x:
@@ -245,19 +319,20 @@ class TUI:
                 stdscr.addstr(y, x, cut)
 
         def connect():
                 stdscr.addstr(y, x, cut)
 
         def connect():
-            import time
 
 
-            def recv_loop():
-                for msg in self.socket.recv():
-                    if msg == 'BYE':
-                        break
+            def handle_recv(msg):
+                if msg == 'BYE':
+                    self.socket.close()
+                else:
                     self.queue.put(msg)
 
                     self.queue.put(msg)
 
+            socket_client_class = PlomSocketClient
+            if self.host.startswith('ws://') or self.host.startswith('wss://'):
+                socket_client_class = WebSocketClient
             while True:
                 try:
             while True:
                 try:
-                    s = socket.create_connection((self.host, self.port))
-                    self.socket = PlomSocket(s)
-                    self.socket_thread = threading.Thread(target=recv_loop)
+                    self.socket = socket_client_class(handle_recv, self.host)
+                    self.socket_thread = threading.Thread(target=self.socket.run)
                     self.socket_thread.start()
                     self.switch_mode('login')
                     return
                     self.socket_thread.start()
                     self.switch_mode('login')
                     return
@@ -269,6 +344,8 @@ class TUI:
 
         def reconnect():
             self.send('QUIT')
 
         def reconnect():
             self.send('QUIT')
+            time.sleep(0.1)  # FIXME necessitated by some some strange SSL race
+                             # conditions with ws4py, find out what exactly
             self.switch_mode('waiting_for_server')
             connect()
 
             self.switch_mode('waiting_for_server')
             connect()
 
@@ -293,7 +370,7 @@ class TUI:
 
         def reset_screen_size():
             self.size = YX(*stdscr.getmaxyx())
 
         def reset_screen_size():
             self.size = YX(*stdscr.getmaxyx())
-            self.size = self.size - YX(self.size.y % 2, 0)
+            self.size = self.size - YX(self.size.y % 4, 0)
             self.size = self.size - YX(0, self.size.x % 4)
             self.window_width = int(self.size.x / 2)
 
             self.size = self.size - YX(0, self.size.x % 4)
             self.window_width = int(self.size.x / 2)
 
@@ -327,10 +404,15 @@ class TUI:
         def draw_info():
             if not self.game.turn_complete:
                 return
         def draw_info():
             if not self.game.turn_complete:
                 return
+            pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
+            info = 'TERRAIN: %s\n' % self.game.map_content[pos_i]
+            for t in self.game.things:
+                if t.position == self.explorer:
+                    info += 'PLAYER @: %s\n' % t.name
             if self.explorer in self.game.portals:
             if self.explorer in self.game.portals:
-                info = 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
+                info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
             else:
             else:
-                info = 'PORTAL: (none)\n'
+                info += 'PORTAL: (none)\n'
             if self.explorer in self.game.info_db:
                 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
             else:
             if self.explorer in self.game.info_db:
                 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
             else:
@@ -377,15 +459,14 @@ class TUI:
                     indent = 0 if indent else 1
             else:
                 for line in map_lines_split:
                     indent = 0 if indent else 1
             else:
                 for line in map_lines_split:
-                    map_lines += [''.join(line)]
+                    map_lines += [' '.join(line)]
             window_center = YX(int(self.size.y / 2),
                                int(self.window_width / 2))
             player = self.game.get_thing(self.game.player_id, False)
             center = player.position
             if self.mode.shows_info:
                 center = self.explorer
             window_center = YX(int(self.size.y / 2),
                                int(self.window_width / 2))
             player = self.game.get_thing(self.game.player_id, False)
             center = player.position
             if self.mode.shows_info:
                 center = self.explorer
-            if type(self.game.map_geometry) == MapGeometryHex:
-                center = YX(center.y, center.x * 2)
+            center = YX(center.y, center.x * 2)
             offset = center - window_center
             if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
                 offset += YX(0, 1)
             offset = center - window_center
             if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
                 offset += YX(0, 1)
@@ -414,13 +495,20 @@ class TUI:
                 draw_map()
 
         curses.curs_set(False)  # hide cursor
                 draw_map()
 
         curses.curs_set(False)  # hide cursor
+        curses.use_default_colors();
         stdscr.timeout(10)
         reset_screen_size()
         self.explorer = YX(0, 0)
         self.input_ = ''
         input_prompt = '> '
         connect()
         stdscr.timeout(10)
         reset_screen_size()
         self.explorer = YX(0, 0)
         self.input_ = ''
         input_prompt = '> '
         connect()
+        last_ping = datetime.datetime.now()
+        interval = datetime.timedelta(seconds=30)
         while True:
         while True:
+            now = datetime.datetime.now()
+            if now - last_ping > interval:
+                self.send('PING')
+                last_ping = now
             if self.do_refresh:
                 draw_screen()
                 self.do_refresh = False
             if self.do_refresh:
                 draw_screen()
                 self.do_refresh = False
@@ -445,25 +533,26 @@ class TUI:
                 if len(self.input_) > max_length:
                     self.input_ = self.input_[:max_length]
             elif self.mode == self.mode_login and key == '\n':
                 if len(self.input_) > max_length:
                     self.input_ = self.input_[:max_length]
             elif self.mode == self.mode_login and key == '\n':
+                self.login_name = self.input_
                 self.send('LOGIN ' + quote(self.input_))
                 self.input_ = ""
             elif self.mode == self.mode_chat and key == '\n':
                 self.send('LOGIN ' + quote(self.input_))
                 self.input_ = ""
             elif self.mode == self.mode_chat and key == '\n':
-                if self.input_[0] == ':':
-                    if self.input_ in {':p', ':play'}:
+                if self.input_[0] == '/':
+                    if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
                         self.switch_mode('play')
                         self.switch_mode('play')
-                    elif self.input_ in {':?', ':study'}:
+                    elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
                         self.switch_mode('study')
                         self.switch_mode('study')
-                    if self.input_ == ':help':
+                    elif self.input_ == '/help':
                         self.help()
                         self.help()
-                    if self.input_ == ':reconnect':
+                    elif self.input_ == '/reconnect':
                         reconnect()
                         reconnect()
-                    elif self.input_.startswith(':nick'):
+                    elif self.input_.startswith('/nick'):
                         tokens = self.input_.split(maxsplit=1)
                         if len(tokens) == 2:
                             self.send('LOGIN ' + quote(tokens[1]))
                         else:
                             self.log_msg('? need login name')
                         tokens = self.input_.split(maxsplit=1)
                         if len(tokens) == 2:
                             self.send('LOGIN ' + quote(tokens[1]))
                         else:
                             self.log_msg('? need login name')
-                    elif self.input_.startswith(':msg'):
+                    elif self.input_.startswith('/msg'):
                         tokens = self.input_.split(maxsplit=2)
                         if len(tokens) == 3:
                             self.send('QUERY %s %s' % (quote(tokens[1]),
                         tokens = self.input_.split(maxsplit=2)
                         if len(tokens) == 3:
                             self.send('QUERY %s %s' % (quote(tokens[1]),
@@ -490,31 +579,30 @@ class TUI:
             elif self.mode == self.mode_teleport and key == '\n':
                 if self.input_ == 'YES!':
                     self.host = self.teleport_target_host
             elif self.mode == self.mode_teleport and key == '\n':
                 if self.input_ == 'YES!':
                     self.host = self.teleport_target_host
-                    self.port = self.teleport_target_port
                     reconnect()
                 else:
                     self.log_msg('@ teleport aborted')
                     self.switch_mode('play')
                 self.input_ = ''
             elif self.mode == self.mode_study:
                     reconnect()
                 else:
                     self.log_msg('@ teleport aborted')
                     self.switch_mode('play')
                 self.input_ = ''
             elif self.mode == self.mode_study:
-                if key == 'C':
+                if key == self.keys['switch_to_chat']:
                     self.switch_mode('chat')
                     self.switch_mode('chat')
-                elif key == 'P':
+                elif key == self.keys['switch_to_play']:
                     self.switch_mode('play')
                     self.switch_mode('play')
-                elif key == 'A':
+                elif key == self.keys['switch_to_annotate']:
                     self.switch_mode('annotate', keep_position=True)
                     self.switch_mode('annotate', keep_position=True)
-                elif key == 'p':
+                elif key == self.keys['switch_to_portal']:
                     self.switch_mode('portal', keep_position=True)
                 elif key in self.movement_keys:
                     move_explorer(self.movement_keys[key])
             elif self.mode == self.mode_play:
                     self.switch_mode('portal', keep_position=True)
                 elif key in self.movement_keys:
                     move_explorer(self.movement_keys[key])
             elif self.mode == self.mode_play:
-                if key == 'C':
+                if key == self.keys['switch_to_chat']:
                     self.switch_mode('chat')
                     self.switch_mode('chat')
-                elif key == '?':
+                elif key == self.keys['switch_to_study']:
                     self.switch_mode('study')
                     self.switch_mode('study')
-                if key == 'E':
+                if key == self.keys['switch_to_edit']:
                     self.switch_mode('edit')
                     self.switch_mode('edit')
-                elif key == 'f':
+                elif key == self.keys['flatten']:
                     self.send('TASK:FLATTEN_SURROUNDINGS')
                 elif key in self.movement_keys:
                     self.send('TASK:MOVE ' + self.movement_keys[key])
                     self.send('TASK:FLATTEN_SURROUNDINGS')
                 elif key in self.movement_keys:
                     self.send('TASK:MOVE ' + self.movement_keys[key])
@@ -522,4 +610,4 @@ class TUI:
                 self.send('TASK:WRITE ' + key)
                 self.switch_mode('play')
 
                 self.send('TASK:WRITE ' + key)
                 self.switch_mode('play')
 
-TUI('127.0.0.1', 5000)
+TUI('localhost:5000')