home · contact · privacy
Register game commands and tasks outside of game module.
[plomrogue2-experiments] / new2 / rogue_chat_curses.py
index 8bd45afb3e3742a5bc796632cd8e5d07380ad250..a6d1ca7a81e9f8632bd82d2bb9e482da8529aae8 100755 (executable)
@@ -1,14 +1,53 @@
 #!/usr/bin/env python3
 import curses
-import socket
 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.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
@@ -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 = {
-            '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 = {
-            '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'
 
@@ -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:
-        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
@@ -106,25 +143,30 @@ def cmd_ANNOTATION(game, position, msg):
         game.tui.do_refresh = True
 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
 
+def cmd_PONG(game):
+    pass
+cmd_PONG.argtypes = ''
+
 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)
+        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 = {}
@@ -152,9 +194,10 @@ class TUI:
             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.port = port
         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.login_name = None
         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):
@@ -179,8 +247,10 @@ class TUI:
 
     def send(self, msg):
         try:
+            if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
+                raise BrokenSocketConnection
             self.socket.send(msg)
-        except BrokenPipeError:
+        except (BrokenPipeError, BrokenSocketConnection):
             self.log_msg('@ server disconnected :(')
             self.do_refresh = True
 
@@ -200,10 +270,12 @@ class TUI:
         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':
-            self.log_msg("@ May teleport to %s:%s" % (self.teleport_target_host,
-                                                      self.teleport_target_port));
+            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]
@@ -215,23 +287,25 @@ class TUI:
     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("  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("  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("  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):
+        import time
+        import datetime
 
         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():
-            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)
 
+            socket_client_class = PlomSocketClient
+            if self.host.startswith('ws://') or self.host.startswith('wss://'):
+                socket_client_class = WebSocketClient
             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
@@ -269,6 +344,8 @@ class TUI:
 
         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()
 
@@ -293,7 +370,7 @@ class TUI:
 
         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)
 
@@ -327,10 +404,15 @@ class TUI:
         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:
-                info = 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
+                info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
             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:
@@ -377,15 +459,14 @@ class TUI:
                     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
-            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)
@@ -414,13 +495,20 @@ class TUI:
                 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()
+        last_ping = datetime.datetime.now()
+        interval = datetime.timedelta(seconds=30)
         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
@@ -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':
+                self.login_name = self.input_
                 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')
-                    elif self.input_ in {':?', ':study'}:
+                    elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
                         self.switch_mode('study')
-                    if self.input_ == ':help':
+                    elif self.input_ == '/help':
                         self.help()
-                    if self.input_ == ':reconnect':
+                    elif self.input_ == '/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')
-                    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]),
@@ -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
-                    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:
-                if key == 'C':
+                if key == self.keys['switch_to_chat']:
                     self.switch_mode('chat')
-                elif key == 'P':
+                elif key == self.keys['switch_to_play']:
                     self.switch_mode('play')
-                elif key == 'A':
+                elif key == self.keys['switch_to_annotate']:
                     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:
-                if key == 'C':
+                if key == self.keys['switch_to_chat']:
                     self.switch_mode('chat')
-                elif key == '?':
+                elif key == self.keys['switch_to_study']:
                     self.switch_mode('study')
-                if key == 'E':
+                if key == self.keys['switch_to_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])
@@ -522,4 +610,4 @@ class TUI:
                 self.send('TASK:WRITE ' + key)
                 self.switch_mode('play')
 
-TUI('127.0.0.1', 5000)
+TUI('localhost:5000')