home · contact · privacy
Curses loop won't delay socket input processing.
[plomrogue2-experiments] / client-curses.py
index a79afa0bf557574d6416e35b35fd9daa63aabec9..a2441f64e39e0c1386327150db007fd55be04922 100755 (executable)
@@ -9,7 +9,7 @@ import game_common
 
 class MapSquare(game_common.Map):
 
-    def list_terrain_to_lines(self, terrain_as_list):
+    def list_terrain_to_lines(self, terrain_as_list, center, size):
         terrain = ''.join(terrain_as_list)
         map_lines = []
         start_cut = 0
@@ -17,12 +17,27 @@ class MapSquare(game_common.Map):
             limit = start_cut + self.size[1]
             map_lines += [terrain[start_cut:limit]]
             start_cut = limit
+        if len(map_lines) > size[0] and center[0] > size[0] / 2:
+            diff = len(map_lines) - size[0]
+            if center[0] > len(map_lines) - size[0] / 2:
+                map_lines = map_lines[diff:]
+            else:
+                start = center[0] - int(size[0] / 2)
+                map_lines = map_lines[start:start + size[0]]
+        if self.size[1] > size[1] and center[1] > size[1] / 2:
+            if center[1] > self.size[1] - size[1] / 2:
+                cut_start = self.size[1] - size[1]
+                cut_end = None
+            else:
+                cut_start = center[1] - int(size[1] / 2)
+                cut_end = cut_start + size[1]
+            map_lines = [line[cut_start:cut_end] for line in map_lines]
         return map_lines
 
 
 class MapHex(game_common.Map):
 
-    def list_terrain_to_lines(self, terrain_as_list):
+    def list_terrain_to_lines(self, terrain_as_list, center, size):
         new_terrain_list = [' ']
         x = 0
         y = 0
@@ -35,7 +50,23 @@ class MapHex(game_common.Map):
                 y += 1
                 if y % 2 == 0:
                     new_terrain_list += [' ']
-        return ''.join(new_terrain_list).split('\n')
+        map_lines = ''.join(new_terrain_list).split('\n')
+        if len(map_lines) > size[0] and center[0] > size[0] / 2:
+            diff = len(map_lines) - size[0]
+            if center[0] > len(map_lines) - size[0] / 2:
+                map_lines = map_lines[diff:]
+            else:
+                start = center[0] - int(size[0] / 2)
+                map_lines = map_lines[start:start + size[0]]
+        if self.size[1]*2 > size[1] and center[1]*4 > size[1]:
+            if center[1]*2 > self.size[1]*2 - size[1] / 2:
+                cut_start = self.size[1] * 2 - size[1]
+                cut_end = None
+            else:
+                cut_start = center[1]*2 - int(size[1] / 2)
+                cut_end = cut_start + size[1]
+            map_lines = [line[cut_start:cut_end] for line in map_lines]
+        return map_lines
 
 
 map_manager = game_common.MapManager(globals())
@@ -52,6 +83,7 @@ class World(game_common.World):
         super().__init__(*args, **kwargs)
         self.game = game
         self.map_ = self.game.map_manager.get_map_class('Hex')()
+        self.player_position = (0, 0)
 
 
 class Game(game_common.CommonCommandsMixin):
@@ -61,6 +93,27 @@ class Game(game_common.CommonCommandsMixin):
         self.parser = Parser(self)
         self.world = World(self)
         self.log_text = ''
+        self.to_update = {
+            'log': False,
+            'map': False,
+            'turn': False,
+            }
+        self.do_quit = False
+
+    def handle_input(self, msg):
+        if msg == 'BYE':
+            self.do_quit = True
+            return
+        try:
+            command = self.parser.parse(msg)
+            if command is None:
+                self.log('UNHANDLED INPUT: ' + msg)
+                self.to_update['log'] = True
+            else:
+                command()
+        except ArgError as e:
+                self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
+                self.to_update['log'] = True
 
     def log(self, msg):
         """Prefix msg plus newline to self.log_text."""
@@ -77,6 +130,7 @@ class Game(game_common.CommonCommandsMixin):
     def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
         if msg != "success":
             self.log(msg)
+            self.to_update['log'] = True
     cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
 
     def cmd_TURN_FINISHED(self, n):
@@ -94,27 +148,35 @@ class Game(game_common.CommonCommandsMixin):
         self.world.map_.set_line(y, terrain_line)
     cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
 
+    def cmd_PLAYER_POS(self, yx):
+        self.world.player_position = yx
+    cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
+
+    def cmd_GAME_STATE_COMPLETE(self):
+        self.to_update['turn'] = True
+        self.to_update['map'] = True
+
 
 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
                   'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
 
 
-def recv_loop(server_output):
+def recv_loop(socket, game):
     for msg in plom_socket_io.recv(s):
-        while len(server_output) > 0:
-            pass
-        server_output += [msg]
+        game.handle_input(msg)
 
 
 class Widget:
 
-    def __init__(self, tui, start, size):
+    def __init__(self, tui, start, size, check_game=[], check_tui=[]):
+        self.check_game = check_game
+        self.check_tui = check_tui
         self.tui = tui
         self.start = start
         self.win = curses.newwin(1, 1, self.start[0], self.start[1])
         self.size_def = size  # store for re-calling .size on SIGWINCH
         self.size = size
-        self.update = True
+        self.do_update = True
 
     @property
     def size(self):
@@ -142,10 +204,7 @@ class Widget:
                 part_string = part[0]
                 attr = part[1]
             if len(part_string) > 0:
-                chars_with_attrs = []
-                for char in part_string:
-                   chars_with_attrs += [(char, attr)]
-                return chars_with_attrs
+                return [(char, attr) for char in part_string]
             elif len(part_string) == 1:
                 return [part]
             return []
@@ -170,10 +229,21 @@ class Widget:
             for char_with_attr in cut:
                 self.win.addstr(char_with_attr[0], char_with_attr[1])
 
-    def draw_and_refresh(self):
-        self.win.erase()
-        self.draw()
-        self.win.refresh()
+    def ensure_freshness(self, do_refresh=False):
+        if not do_refresh:
+            for key in self.check_game:
+                if self.tui.game.to_update[key]:
+                    do_refresh = True
+                    break
+        if not do_refresh:
+            for key in self.check_tui:
+                if self.tui.to_update[key]:
+                    do_refresh = True
+                    break
+        if do_refresh:
+            self.win.erase()
+            self.draw()
+            self.win.refresh()
 
 
 class EditWidget(Widget):
@@ -205,9 +275,10 @@ class MapWidget(Widget):
             for t in self.tui.game.world.things:
                 pos_i = self.tui.game.world.map_.get_position_index(t.position)
                 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
-            text = self.tui.game.world.map_.list_terrain_to_lines(terrain_as_list)
+            center = self.tui.game.world.player_position
+            lines = self.tui.game.world.map_.list_terrain_to_lines(terrain_as_list, center, self.size)
             line_width = self.size[1]
-            for line in text:
+            for line in lines:
                 if line_width > len(line):
                     to_pad = line_width - (len(line) % line_width)
                     to_join += [line + '0' * to_pad]
@@ -216,7 +287,18 @@ class MapWidget(Widget):
         if len(to_join) < self.size[0]:
             to_pad = self.size[0] - len(to_join)
             to_join += to_pad * ['0' * self.size[1]]
-        self.safe_write(''.join(to_join))
+        text = ''.join(to_join)
+        text_as_list = []
+        for c in text:
+            if c in {'@', 'm'}:
+                text_as_list += [(c, curses.color_pair(1))]
+            elif c == '.':
+                text_as_list += [(c, curses.color_pair(2))]
+            elif c in {'x', 'X', '#'}:
+                text_as_list += [(c, curses.color_pair(3))]
+            else:
+                text_as_list += [c]
+        self.safe_write(text_as_list)
 
 
 class TurnWidget(Widget):
@@ -227,10 +309,11 @@ class TurnWidget(Widget):
 
 class TUI:
 
-    def __init__(self, server_output):
-        self.server_output = server_output
-        self.game = Game()
+    def __init__(self, socket, game):
+        self.socket = socket
+        self.game = game
         self.parser = Parser(self.game)
+        self.to_update = {'edit': False}
         curses.wrapper(self.loop)
 
     def setup_screen(self, stdscr):
@@ -247,61 +330,45 @@ class TUI:
         curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
         curses.curs_set(False)  # hide cursor
         self.to_send = []
-        edit_line = EditWidget(self, (0, 6), (1, 14))
-        turn_line = TurnWidget(self, (2, 6), (1, 14))
-        log_display = LogWidget(self, (4, 0), (None, 20))
-        map_view = MapWidget(self, (0, 21), (None, None))
-        map_view.update = True
-        widgets = [edit_line, turn_line, log_display, map_view]
-        do_update = True
+        self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
+        self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
+        self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
+        self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
+        widgets = (self.edit, self.turn, self.log, self.map_)
         while True:
-            if do_update:
-                for w in widgets:
-                    w.draw_and_refresh()
-                do_update = False
+            for w in widgets:
+                w.ensure_freshness()
+            for key in self.game.to_update:
+                self.game.to_update[key] = False
+            for key in self.to_update:
+                self.to_update[key] = False
             try:
                 key = self.stdscr.getkey()
-                do_update = True
                 if len(key) == 1 and key in ASCII_printable and \
-                        len(self.to_send) < len(edit_line):
+                        len(self.to_send) < len(self.edit):
                     self.to_send += [key]
+                    self.to_update['edit'] = True
                 elif key == 'KEY_BACKSPACE':
                     self.to_send[:] = self.to_send[:-1]
+                    self.to_update['edit'] = True
                 elif key == '\n':
-                    plom_socket_io.send(s, ''.join(self.to_send))
+                    plom_socket_io.send(self.socket, ''.join(self.to_send))
                     self.to_send[:] = []
+                    self.to_update['edit'] = True
                 elif key == 'KEY_RESIZE':
                     curses.endwin()
                     self.setup_screen(curses.initscr())
                     for w in widgets:
                         w.size = w.size_def
-                else:
-                    do_update = False
+                        w.ensure_freshness(True)
             except curses.error:
                 pass
-            if len(self.server_output) > 0:
-                do_quit = self.handle_input(self.server_output[0])
-                if do_quit:
-                    break
-                self.server_output[:] = []
-                do_update = True
-
-    def handle_input(self, msg):
-        if msg == 'BYE':
-            return True
-        try:
-            command = self.parser.parse(msg)
-            if command is None:
-                self.game.log('UNHANDLED INPUT: ' + msg)
-            else:
-                command()
-        except ArgError as e:
-                self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
-        return False
+            if self.game.do_quit:
+                break
 
 
-server_output = []
 s = socket.create_connection(('127.0.0.1', 5000))
-t = threading.Thread(target=recv_loop, args=(server_output,))
+game = Game()
+t = threading.Thread(target=recv_loop, args=(s, game))
 t.start()
-TUI(server_output)
+TUI(s, game)