home · contact · privacy
Ensure rough feature parity between clients.
authorChristian Heller <c.heller@plomlompom.de>
Thu, 5 Nov 2020 21:16:08 +0000 (22:16 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Thu, 5 Nov 2020 21:16:08 +0000 (22:16 +0100)
new2/plomrogue/game.py
new2/rogue_chat_curses.py

index 1cb6e2232b7d59542a37e08e9264a58da538efde..da148460d0c244b3f350bd6c0ada8c407ec5e4e3 100755 (executable)
@@ -16,6 +16,7 @@ class GameBase:
     def __init__(self):
         self.turn = 0
         self.things = []
+        self.map_geometry = MapGeometrySquare(YX(24, 40))
 
     def get_thing(self, id_, create_unfound):
         # No default for create_unfound because every call to get_thing
@@ -42,7 +43,6 @@ class Game(GameBase):
                       'MOVE': Task_MOVE,
                       'WRITE': Task_WRITE,
                       'FLATTEN_SURROUNDINGS': Task_FLATTEN_SURROUNDINGS}
-        self.map_geometry = MapGeometrySquare(YX(24, 40))
         self.commands = {'QUERY': cmd_QUERY,
                          'ALL': cmd_ALL,
                          'LOGIN': cmd_LOGIN,
index ad6f4f98e349bd5eb6e7b006e0b852c2ee7913fc..6d0f8b77f8518f16b224c0afb761438793a63180 100755 (executable)
@@ -14,11 +14,12 @@ def cmd_TURN(game, n):
     game.turn = n
     game.things = []
     game.portals = {}
+    game.turn_complete = False
 cmd_TURN.argtypes = 'int:nonneg'
 
 def cmd_LOGIN_OK(game):
     game.tui.switch_mode('post_login_wait')
-    game.tui.socket.send('GET_GAMESTATE')
+    game.tui.send('GET_GAMESTATE')
     game.tui.log_msg('@ welcome')
 cmd_LOGIN_OK.argtypes = ''
 
@@ -42,7 +43,7 @@ def cmd_THING_NAME(game, thing_id, name):
 cmd_THING_NAME.argtypes = 'int:nonneg string'
 
 def cmd_MAP(game, size, content):
-    game.map_size = size
+    game.map_geometry.size = size
     game.map_content = content
 cmd_MAP.argtypes = 'yx_tuple:pos string'
 
@@ -50,8 +51,16 @@ def cmd_GAME_STATE_COMPLETE(game):
     game.info_db = {}
     if game.tui.mode.name == 'post_login_wait':
         game.tui.switch_mode('play')
+        game.tui.help()
     if game.tui.mode.shows_info:
         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.switch_mode('teleport')
+    game.turn_complete = True
     game.tui.do_refresh = True
 cmd_GAME_STATE_COMPLETE.argtypes = ''
 
@@ -60,10 +69,15 @@ def cmd_PORTAL(game, position, msg):
 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
 
 def cmd_PLAY_ERROR(game, msg):
-    game.tui.log_msg('imagine the screen flicker (TODO)')
+    game.tui.flash()
     game.tui.do_refresh = True
 cmd_PLAY_ERROR.argtypes = 'string'
 
+def cmd_GAME_ERROR(game, msg):
+    game.tui.log_msg('? game error: ' + msg)
+    game.tui.do_refresh = True
+cmd_GAME_ERROR.argtypes = 'string'
+
 def cmd_ARGUMENT_ERROR(game, msg):
     game.tui.log_msg('? syntax error: ' + msg)
     game.tui.do_refresh = True
@@ -75,10 +89,6 @@ def cmd_ANNOTATION(game, position, msg):
         game.tui.do_refresh = True
 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
 
-def recv_loop(plom_socket, q):
-    for msg in plom_socket.recv():
-        q.put(msg)
-
 class Game(GameBase):
     commands = {'LOGIN_OK': cmd_LOGIN_OK,
                 'CHAT': cmd_CHAT,
@@ -91,12 +101,13 @@ class Game(GameBase):
                 '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.map_size = YX(0, 0)
         self.map_content = ''
         self.player_id = -1
         self.info_db = {}
@@ -110,46 +121,134 @@ class Game(GameBase):
 
 class TUI:
 
-    def __init__(self, socket, q, game):
-        self.game = game
+    class Mode:
+
+        def __init__(self, name, has_input_prompt=False, shows_info=False,
+                     is_intro = False):
+            self.name = name
+            self.has_input_prompt = has_input_prompt
+            self.shows_info = shows_info
+            self.is_intro = is_intro
+
+    def __init__(self, host, port):
+        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_annotate = self.Mode('annotate', has_input_prompt=True, shows_info=True)
+        self.mode_portal = self.Mode('portal', has_input_prompt=True, shows_info=True)
+        self.mode_chat = self.Mode('chat', has_input_prompt=True)
+        self.mode_waiting_for_server = self.Mode('waiting_for_server', is_intro=True)
+        self.mode_login = self.Mode('login', has_input_prompt=True, is_intro=True)
+        self.mode_post_login_wait = self.Mode('post_login_wait', is_intro=True)
+        self.mode_teleport = self.Mode('teleport', has_input_prompt=True)
+        self.game = Game()
         self.game.tui = self
         self.parser = Parser(self.game)
-        self.queue = q
-        self.socket = socket
         self.log = []
         self.do_refresh = True
+        self.queue = queue.Queue()
+        self.switch_mode('waiting_for_server')
         curses.wrapper(self.loop)
 
+    def flash(self):
+        curses.flash()
+
+    def send(self, msg):
+        try:
+            self.socket.send(msg)
+        except BrokenPipeError:
+            self.log_msg('@ server disconnected :(')
+            self.do_refresh = True
+
     def log_msg(self, msg):
         self.log += [msg]
         if len(self.log) > 100:
             self.log = self.log[-100:]
 
     def query_info(self):
-        self.socket.send('GET_ANNOTATION ' + str(self.explorer))
+        self.send('GET_ANNOTATION ' + str(self.explorer))
 
     def switch_mode(self, mode_name, keep_position = False):
         self.mode = getattr(self, 'mode_' + mode_name)
         if self.mode.shows_info and not keep_position:
             player = self.game.get_thing(self.game.player_id, False)
             self.explorer = YX(player.position.y, player.position.x)
-        if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
+        if self.mode.name == 'waiting_for_server':
+            self.log_msg('@ waiting for server …')
+        elif self.mode.name == 'login':
+            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("@ Enter 'YES!' to affirm.");
+        elif self.mode.name == 'annotate' and self.explorer in self.game.info_db:
             info = self.game.info_db[self.explorer]
             if info != '(none)':
                 self.input_ = info
         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
             self.input_ = self.game.portals[self.explorer]
 
+    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("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("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("commands specific to study mode:");
+        self.log_msg("  e - annotate terrain");
+        self.log_msg("  p - switch to play mode");
+
     def loop(self, stdscr):
 
-        class Mode:
+        def safe_addstr(y, x, line):
+            if y < self.size.y - 1 or x + len(line) < self.size.x:
+                stdscr.addstr(y, x, line)
+            else:  # workaround to <https://stackoverflow.com/q/7063128>
+                cut_i = self.size.x - x - 1
+                cut = line[:cut_i]
+                last_char = line[cut_i]
+                stdscr.addstr(y, self.size.x - 2, last_char)
+                stdscr.insstr(y, self.size.x - 2, ' ')
+                stdscr.addstr(y, x, cut)
+
+        def connect():
+            import time
+
+            def recv_loop():
+                for msg in self.socket.recv():
+                    if msg == 'BYE':
+                        break
+                    self.queue.put(msg)
 
-            def __init__(self, name, has_input_prompt=False, shows_info=False,
-                         is_intro = False):
-                self.name = name
-                self.has_input_prompt = has_input_prompt
-                self.shows_info = shows_info
-                self.is_intro = is_intro
+            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_thread.start()
+                    self.switch_mode('login')
+                    return
+                except ConnectionRefusedError:
+                    self.log_msg('@ server connect failure, trying again …')
+                    draw_screen()
+                    stdscr.refresh()
+                    time.sleep(1)
+
+        def reconnect():
+            self.send('QUIT')
+            self.switch_mode('waiting_for_server')
+            connect()
 
         def handle_input(msg):
             command, args = self.parser.parse(msg)
@@ -160,19 +259,18 @@ class TUI:
             lines = []
             x = 0
             for i in range(len(msg)):
-                x += 1
                 if x >= width or msg[i] == "\n":
                     lines += [chunk]
                     chunk = ''
                     x = 0
                 if msg[i] != "\n":
                     chunk += msg[i]
+                x += 1
             lines += [chunk]
             return lines
 
         def reset_screen_size():
             self.size = YX(*stdscr.getmaxyx())
-            self.size = self.size - YX(0, 1) # ugly TODO ncurses bug workaround, FIXME
             self.size = self.size - YX(self.size.y % 2, 0)
             self.size = self.size - YX(0, self.size.x % 4)
             self.window_width = int(self.size.x / 2)
@@ -185,16 +283,12 @@ class TUI:
                                                            self.window_width)
 
         def move_explorer(direction):
-            # TODO movement constraints
-            if direction == 'up':
-                self.explorer += YX(-1, 0)
-            elif direction == 'left':
-                self.explorer += YX(0, -1)
-            elif direction == 'down':
-                self.explorer += YX(1, 0)
-            elif direction == 'right':
-                self.explorer += YX(0, 1)
-            self.query_info()
+            target = self.game.map_geometry.move(self.explorer, direction)
+            if target:
+                self.explorer = target
+                self.query_info()
+            else:
+                self.flash()
 
         def draw_history():
             lines = []
@@ -206,9 +300,11 @@ class TUI:
             for i in range(len(lines)):
                 if (i >= max_y - height_header):
                     break
-                stdscr.addstr(max_y - i - 1, self.window_width, lines[i])
+                safe_addstr(max_y - i - 1, self.window_width, lines[i])
 
         def draw_info():
+            if not self.game.turn_complete:
+                return
             if self.explorer in self.game.portals:
                 info = 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
             else:
@@ -223,29 +319,29 @@ class TUI:
                 y = height_header + i
                 if y >= self.size.y - len(self.input_lines):
                     break
-                stdscr.addstr(y, self.window_width, lines[i])
+                safe_addstr(y, self.window_width, lines[i])
 
         def draw_input():
             y = self.size.y - len(self.input_lines)
             for i in range(len(self.input_lines)):
-                stdscr.addstr(y, self.window_width, self.input_lines[i])
+                safe_addstr(y, self.window_width, self.input_lines[i])
                 y += 1
 
         def draw_turn():
-            stdscr.addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
+            if not self.game.turn_complete:
+                return
+            safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
 
         def draw_mode():
-            stdscr.addstr(1, self.window_width, 'MODE: ' + self.mode.name)
+            safe_addstr(1, self.window_width, 'MODE: ' + self.mode.name)
 
         def draw_map():
-            player = self.game.get_thing(self.game.player_id, False)
-            if not player:
-                # catches race conditions where game.things still empty
+            if not self.game.turn_complete:
                 return
             map_lines_split = []
-            for y in range(self.game.map_size.y):
-                start = self.game.map_size.x * y
-                end = start + self.game.map_size.x
+            for y in range(self.game.map_geometry.size.y):
+                start = self.game.map_geometry.size.x * y
+                end = start + self.game.map_geometry.size.x
                 map_lines_split += [list(self.game.map_content[start:end])]
             for t in self.game.things:
                 map_lines_split[t.position.y][t.position.x] = '@'
@@ -254,11 +350,12 @@ class TUI:
             map_lines = []
             for line in map_lines_split:
                 map_lines += [''.join(line)]
-            map_center = YX(int(self.game.map_size.y / 2),
-                            int(self.game.map_size.x / 2))
+            map_center = YX(int(self.game.map_geometry.size.y / 2),
+                            int(self.game.map_geometry.size.x / 2))
             window_center = YX(int(self.size.y / 2),
                                int(self.window_width / 2))
-            center = player
+            player = self.game.get_thing(self.game.player_id, False)
+            center = player.position
             if self.mode.shows_info:
                 center = self.explorer
             offset = center - window_center
@@ -266,9 +363,9 @@ class TUI:
             term_x = max(0, -offset.x)
             map_y = max(0, offset.y)
             map_x = max(0, offset.x)
-            while (term_y < self.size.y and map_y < self.game.map_size.y):
+            while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
                 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
-                stdscr.addstr(term_y, term_x, to_draw)
+                safe_addstr(term_y, term_x, to_draw)
                 term_y += 1
                 map_y += 1
 
@@ -286,21 +383,13 @@ class TUI:
                 draw_turn()
                 draw_map()
 
-        self.mode_play = Mode('play')
-        self.mode_study = Mode('study', shows_info=True)
-        self.mode_edit = Mode('edit')
-        self.mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
-        self.mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
-        self.mode_chat = Mode('chat', has_input_prompt=True)
-        self.mode_login = Mode('login', has_input_prompt=True, is_intro=True)
-        self.mode_post_login_wait = Mode('post_login_wait', is_intro=True)
         curses.curs_set(False)  # hide cursor
         stdscr.timeout(10)
         reset_screen_size()
-        self.mode = self.mode_login
         self.explorer = YX(0, 0)
         self.input_ = ''
         input_prompt = '> '
+        connect()
         while True:
             if self.do_refresh:
                 draw_screen()
@@ -322,50 +411,61 @@ class TUI:
                 self.input_ = self.input_[:-1]
             elif self.mode.has_input_prompt and key != '\n':  # Return key
                 self.input_ += key
-                # TODO find out why - 1 is necessary here
                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
                 if len(self.input_) > max_length:
                     self.input_ = self.input_[:max_length]
             elif self.mode == self.mode_login and key == '\n':
-                self.socket.send('LOGIN ' + quote(self.input_))
+                self.send('LOGIN ' + quote(self.input_))
                 self.input_ = ""
             elif self.mode == self.mode_chat and key == '\n':
-                # TODO: query, nick, help, reconnect, unknown command
                 if self.input_[0] == ':':
                     if self.input_ in {':p', ':play'}:
                         self.switch_mode('play')
                     elif self.input_ in {':?', ':study'}:
                         self.switch_mode('study')
+                    if self.input_ == ':help':
+                        self.help()
+                    if self.input_ == ':reconnect':
+                        reconnect()
                     elif self.input_.startswith(':nick'):
                         tokens = self.input_.split(maxsplit=1)
                         if len(tokens) == 2:
-                            self.socket.send('LOGIN ' + quote(tokens[1]))
+                            self.send('LOGIN ' + quote(tokens[1]))
                         else:
                             self.log_msg('? need login name')
                     elif self.input_.startswith(':msg'):
                         tokens = self.input_.split(maxsplit=2)
                         if len(tokens) == 3:
-                            self.socket.send('QUERY %s %s' % (quote(tokens[1]),
+                            self.send('QUERY %s %s' % (quote(tokens[1]),
                                                               quote(tokens[2])))
                         else:
                             self.log_msg('? need message target and message')
                     else:
                         self.log_msg('? unknown command')
                 else:
-                    self.socket.send('ALL ' + quote(self.input_))
+                    self.send('ALL ' + quote(self.input_))
                 self.input_ = ""
             elif self.mode == self.mode_annotate and key == '\n':
-                if (self.input_ == ''):
+                if self.input_ == '':
                     self.input_ = ' '
-                self.socket.send('ANNOTATE %s %s' % (self.explorer, quote(self.input_)))
+                self.send('ANNOTATE %s %s' % (self.explorer, quote(self.input_)))
                 self.input_ = ""
                 self.switch_mode('study', keep_position=True)
             elif self.mode == self.mode_portal and key == '\n':
-                if (self.input_ == ''):
+                if self.input_ == '':
                     self.input_ = ' '
-                self.socket.send('PORTAL %s %s' % (self.explorer, quote(self.input_)))
+                self.send('PORTAL %s %s' % (self.explorer, quote(self.input_)))
                 self.input_ = ""
                 self.switch_mode('study', keep_position=True)
+            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':
                     self.switch_mode('chat')
@@ -376,13 +476,13 @@ class TUI:
                 elif key == 'P':
                     self.switch_mode('portal', keep_position=True)
                 elif key == 'w':
-                    move_explorer('up')
+                    move_explorer('UP')
                 elif key == 'a':
-                    move_explorer('left')
+                    move_explorer('LEFT')
                 elif key == 's':
-                    move_explorer('down')
+                    move_explorer('DOWN')
                 elif key == 'd':
-                    move_explorer('right')
+                    move_explorer('RIGHT')
             elif self.mode == self.mode_play:
                 if key == 'c':
                     self.switch_mode('chat')
@@ -391,22 +491,17 @@ class TUI:
                 if key == 'e':
                     self.switch_mode('edit')
                 elif key == 'f':
-                    self.socket.send('TASK:FLATTEN_SURROUNDINGS')
+                    self.send('TASK:FLATTEN_SURROUNDINGS')
                 elif key == 'w':
-                    self.socket.send('TASK:MOVE UP')
+                    self.send('TASK:MOVE UP')
                 elif key == 'a':
-                    self.socket.send('TASK:MOVE LEFT')
+                    self.send('TASK:MOVE LEFT')
                 elif key == 's':
-                    self.socket.send('TASK:MOVE DOWN')
+                    self.send('TASK:MOVE DOWN')
                 elif key == 'd':
-                    self.socket.send('TASK:MOVE RIGHT')
+                    self.send('TASK:MOVE RIGHT')
             elif self.mode == self.mode_edit:
-                self.socket.send('TASK:WRITE ' + key)
+                self.send('TASK:WRITE ' + key)
                 self.switch_mode('play')
 
-s = socket.create_connection(('127.0.0.1', 5000))
-plom_socket = PlomSocket(s)
-q = queue.Queue()
-t = threading.Thread(target=recv_loop, args=(plom_socket, q))
-t.start()
-TUI(plom_socket, q, Game())
+TUI('127.0.0.1', 5000)