home · contact · privacy
Ensure popup stays on even if background window changes.
[plomrogue2-experiments] / new / example_client.py
index 039d8de6bc134f9835c4f70a130a3a4879fb7890..eb198e0584da58e14063df126487fd1b2eaa4448 100755 (executable)
@@ -3,12 +3,13 @@ import curses
 import socket
 import threading
 from plomrogue.parser import ArgError, Parser
 import socket
 import threading
 from plomrogue.parser import ArgError, Parser
-from plomrogue.commands import cmd_MAP, cmd_THING_POS
+from plomrogue.commands import cmd_MAP, cmd_THING_POS, cmd_PLAYER_ID
 from plomrogue.game import Game, WorldBase
 from plomrogue.mapping import MapBase
 from plomrogue.io import PlomSocket
 from plomrogue.things import ThingBase
 import types
 from plomrogue.game import Game, WorldBase
 from plomrogue.mapping import MapBase
 from plomrogue.io import PlomSocket
 from plomrogue.things import ThingBase
 import types
+import queue
 
 
 class Map(MapBase):
 
 
 class Map(MapBase):
@@ -69,47 +70,57 @@ class World(WorldBase):
         """
         super().__init__(*args, **kwargs)
         self.map_ = Map()
         """
         super().__init__(*args, **kwargs)
         self.map_ = Map()
-        self.player_position = (0, 0)
+        self.player_inventory = []
+        self.player_id = 0
+        self.pickable_items = []
 
     def new_map(self, yx):
         self.map_ = Map(yx)
 
 
     def new_map(self, yx):
         self.map_ = Map(yx)
 
+    @property
+    def player(self):
+        return self.get_thing(self.player_id)
+
 
 
-def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
+def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
     if msg != "success":
     if msg != "success":
-        self.log(msg)
+        game.log(msg)
 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
 
 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
 
-def cmd_TURN_FINISHED(self, n):
+def cmd_TURN_FINISHED(game, n):
     """Do nothing. (This may be extended later.)"""
     pass
 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
 
     """Do nothing. (This may be extended later.)"""
     pass
 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
 
-def cmd_TURN(self, n):
-    """Set self.turn to n, empty self.things."""
-    self.world.turn = n
-    self.world.things = []
-    self.to_update['turn'] = False
-    self.to_update['map'] = False
+def cmd_TURN(game, n):
+    """Set game.turn to n, empty game.things."""
+    game.world.turn = n
+    game.world.things = []
+    game.world.pickable_items = []
 cmd_TURN.argtypes = 'int:nonneg'
 
 cmd_TURN.argtypes = 'int:nonneg'
 
-def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
-    self.world.map_.set_line(y, terrain_line)
+def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
+    game.world.map_.set_line(y, terrain_line)
 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
 
 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
+def cmd_GAME_STATE_COMPLETE(game):
+    game.tui.to_update['turn'] = True
+    game.tui.to_update['map'] = True
 
 def cmd_THING_TYPE(game, i, type_):
     t = game.world.get_thing(i)
     t.type_ = type_
 cmd_THING_TYPE.argtypes = 'int:nonneg string'
 
 
 def cmd_THING_TYPE(game, i, type_):
     t = game.world.get_thing(i)
     t.type_ = type_
 cmd_THING_TYPE.argtypes = 'int:nonneg string'
 
+def cmd_PLAYER_INVENTORY(game, ids):
+    game.world.player_inventory = ids  # TODO: test whether valid IDs
+cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
+
+def cmd_PICKABLE_ITEMS(game, ids):
+    game.world.pickable_items = ids
+    game.tui.to_update['map'] = True
+cmd_PICKABLE_ITEMS.argtypes = 'seq:int:nonneg'
+
 
 class Game:
 
 
 class Game:
 
@@ -121,18 +132,16 @@ class Game:
                          'TURN_FINISHED': cmd_TURN_FINISHED,
                          'TURN': cmd_TURN,
                          'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
                          'TURN_FINISHED': cmd_TURN_FINISHED,
                          'TURN': cmd_TURN,
                          'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
-                         'PLAYER_POS': cmd_PLAYER_POS,
+                         'PLAYER_ID': cmd_PLAYER_ID,
+                         'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
                          'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
                          'MAP': cmd_MAP,
                          'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
                          'MAP': cmd_MAP,
+                         'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
                          'THING_TYPE': cmd_THING_TYPE,
                          'THING_POS': cmd_THING_POS}
         self.log_text = ''
                          'THING_TYPE': cmd_THING_TYPE,
                          'THING_POS': cmd_THING_POS}
         self.log_text = ''
-        self.to_update = {
-            'log': True,
-            'map': True,
-            'turn': True,
-            }
         self.do_quit = False
         self.do_quit = False
+        self.tui = None
 
     def get_command(self, command_name):
         from functools import partial
 
     def get_command(self, command_name):
         from functools import partial
@@ -147,6 +156,7 @@ class Game:
         return None
 
     def handle_input(self, msg):
         return None
 
     def handle_input(self, msg):
+        self.log(msg)
         if msg == 'BYE':
             self.do_quit = True
             return
         if msg == 'BYE':
             self.do_quit = True
             return
@@ -154,17 +164,15 @@ class Game:
             command, args = self.parser.parse(msg)
             if command is None:
                 self.log('UNHANDLED INPUT: ' + msg)
             command, args = self.parser.parse(msg)
             if command is None:
                 self.log('UNHANDLED INPUT: ' + msg)
-                self.to_update['log'] = True
             else:
                 command(*args)
         except ArgError as e:
             self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
             else:
                 command(*args)
         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."""
         self.log_text = msg + '\n' + self.log_text
 
     def log(self, msg):
         """Prefix msg plus newline to self.log_text."""
         self.log_text = msg + '\n' + self.log_text
-        self.to_update['log'] = True
+        self.tui.to_update['log'] = True
 
     def symbol_for_type(self, type_):
         symbol = '?'
 
     def symbol_for_type(self, type_):
         symbol = '?'
@@ -172,6 +180,8 @@ class Game:
             symbol = '@'
         elif type_ == 'monster':
             symbol = 'm'
             symbol = '@'
         elif type_ == 'monster':
             symbol = 'm'
+        elif type_ == 'item':
+            symbol = 'i'
         return symbol
 
 
         return symbol
 
 
@@ -179,22 +189,23 @@ ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'
                   'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
 
 
                   'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
 
 
-def recv_loop(plom_socket, game):
+def recv_loop(plom_socket, game, q):
     for msg in plom_socket.recv():
     for msg in plom_socket.recv():
-        game.handle_input(msg)
+        q.put(msg)
 
 
 class Widget:
 
 
 
 class Widget:
 
-    def __init__(self, tui, start, size, check_game=[], check_tui=[]):
-        self.check_game = check_game
-        self.check_tui = check_tui
+    def __init__(self, tui, start, size, check_updates=[]):
+        self.check_updates = check_updates
         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.do_update = True
         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.do_update = True
+        self.visible = True
+        self.children = []
 
     @property
     def size(self):
 
     @property
     def size(self):
@@ -248,20 +259,21 @@ class Widget:
                 self.win.addstr(char_with_attr[0], char_with_attr[1])
 
     def ensure_freshness(self, do_refresh=False):
                 self.win.addstr(char_with_attr[0], char_with_attr[1])
 
     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()
+        did_refresh = False
+        if self.visible:
+            if not do_refresh:
+                for key in self.check_updates:
+                    if key in self.tui.to_update and self.tui.to_update[key]:
+                        do_refresh = True
+                        break
+            if do_refresh:
+                self.win.erase()
+                self.draw()
+                self.win.refresh()
+                did_refresh = True
+            for child in self.children:
+                did_refresh = child.ensure_freshness(do_refresh) | did_refresh
+        return did_refresh
 
 
 class EditWidget(Widget):
 
 
 class EditWidget(Widget):
@@ -284,15 +296,61 @@ class LogWidget(Widget):
         self.safe_write((''.join(to_join), curses.color_pair(3)))
 
 
         self.safe_write((''.join(to_join), curses.color_pair(3)))
 
 
+class PopUpWidget(Widget):
+
+    def draw(self):
+        self.safe_write(self.tui.popup_text)
+
+    def reconfigure(self):
+        self.visible = True
+        size = (1, len(self.tui.popup_text))
+        self.size = size
+        self.size_def = size
+        offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
+        offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
+        self.start = (offset_y, offset_x)
+        self.win.mvwin(self.start[0], self.start[1])
+
+
 class MapWidget(Widget):
 
     def draw(self):
 class MapWidget(Widget):
 
     def draw(self):
+        if self.tui.view == 'map':
+            self.draw_map()
+        elif self.tui.view == 'inventory':
+            self.draw_item_selector('INVENTORY:',
+                                    self.tui.game.world.player_inventory)
+        elif self.tui.view == 'pickable_items':
+            self.draw_item_selector('PICKABLE:',
+                                    self.tui.game.world.pickable_items)
+
+    def draw_item_selector(self, title, selection):
+        lines = [title]
+        counter = 0
+        for id_ in selection:
+            pointer = '*' if counter == self.tui.item_pointer else ' '
+            t = self.tui.game.world.get_thing(id_)
+            lines += ['%s %s' % (pointer, t.type_)]
+            counter += 1
+        line_width = self.size[1]
+        to_join = []
+        for line in lines:
+            to_pad = line_width - (len(line) % line_width)
+            if to_pad == line_width:
+                to_pad = 0
+            to_join += [line + ' '*to_pad]
+        self.safe_write((''.join(to_join), curses.color_pair(3)))
+
+    def draw_map(self):
 
         def terrain_with_objects():
             terrain_as_list = list(self.tui.game.world.map_.terrain[:])
             for t in self.tui.game.world.things:
                 pos_i = self.tui.game.world.map_.get_position_index(t.position)
 
         def terrain_with_objects():
             terrain_as_list = list(self.tui.game.world.map_.terrain[:])
             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_)
+                symbol = self.tui.game.symbol_for_type(t.type_)
+                if symbol in {'i'} and terrain_as_list[pos_i] in {'@', 'm'}:
+                    continue
+                terrain_as_list[pos_i] = symbol
             return ''.join(terrain_as_list)
 
         def pad_or_cut_x(lines):
             return ''.join(terrain_as_list)
 
         def pad_or_cut_x(lines):
@@ -315,6 +373,8 @@ class MapWidget(Widget):
             for c in ''.join(lines):
                 if c in {'@', 'm'}:
                     chars_with_attrs += [(c, curses.color_pair(1))]
             for c in ''.join(lines):
                 if c in {'@', 'm'}:
                     chars_with_attrs += [(c, curses.color_pair(1))]
+                elif c == 'i':
+                    chars_with_attrs += [(c, curses.color_pair(4))]
                 elif c == '.':
                     chars_with_attrs += [(c, curses.color_pair(2))]
                 elif c in {'x', 'X', '#'}:
                 elif c == '.':
                     chars_with_attrs += [(c, curses.color_pair(2))]
                 elif c in {'x', 'X', '#'}:
@@ -330,7 +390,7 @@ class MapWidget(Widget):
             return
 
         terrain_with_objects = terrain_with_objects()
             return
 
         terrain_with_objects = terrain_with_objects()
-        center = self.tui.game.world.player_position
+        center = self.tui.game.world.player.position
         lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
                                                         center, self.size)
         pad_or_cut_x(lines)
         lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
                                                         center, self.size)
         pad_or_cut_x(lines)
@@ -344,53 +404,95 @@ class TurnWidget(Widget):
         self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
 
 
         self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
 
 
+class TextLineWidget(Widget):
+
+    def __init__(self, text_line, *args, **kwargs):
+        self.text_line = text_line
+        super().__init__(*args, **kwargs)
+
+    def draw(self):
+        self.safe_write(self.text_line)
+
+
 class TUI:
 
 class TUI:
 
-    def __init__(self, plom_socket, game):
+    def __init__(self, plom_socket, game, q):
         self.socket = plom_socket
         self.game = game
         self.socket = plom_socket
         self.game = game
+        self.game.tui = self
+        self.queue = q
         self.parser = Parser(self.game)
         self.parser = Parser(self.game)
-        self.to_update = {'edit': False}
+        self.to_update = {}
+        self.item_pointer = 0
         curses.wrapper(self.loop)
 
     def setup_screen(self, stdscr):
         self.stdscr = stdscr
         self.stdscr.refresh()  # will be called by getkey else, clearing screen
         self.stdscr.timeout(10)
         curses.wrapper(self.loop)
 
     def setup_screen(self, stdscr):
         self.stdscr = stdscr
         self.stdscr.refresh()  # will be called by getkey else, clearing screen
         self.stdscr.timeout(10)
-        self.stdscr.addstr(0, 0, 'SEND:')
-        self.stdscr.addstr(2, 0, 'TURN:')
 
     def loop(self, stdscr):
         self.setup_screen(stdscr)
         curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
         curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
         curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
 
     def loop(self, stdscr):
         self.setup_screen(stdscr)
         curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
         curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
         curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
+        curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
         curses.curs_set(False)  # hide cursor
         self.to_send = []
         curses.curs_set(False)  # hide cursor
         self.to_send = []
-        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_)
-        map_mode = False
+        edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
+        edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
+        edit_widget.children += [edit_line_widget]
+        turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
+        turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
+        log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
+        map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
+        top_widgets = [edit_widget, turn_widget, log_widget, map_widget]
+        popup_widget = PopUpWidget(self, (0, 0), (1, 1))
+        popup_widget.visible = False
+        self.popup_text = 'Hi bob'
+        write_mode = True
+        self.view = 'map'
+        for w in top_widgets:
+            w.ensure_freshness(True)
+        draw_popup_if_visible = True
         while True:
         while True:
-            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
+            for w in top_widgets:
+                did_refresh = w.ensure_freshness()
+                draw_popup_if_visible = did_refresh | draw_popup_if_visible
+            if popup_widget.visible and draw_popup_if_visible:
+                popup_widget.ensure_freshness(True)
+                draw_popup_if_visible = False
+            for k in self.to_update.keys():
+                self.to_update[k] = False
+            while True:
+                try:
+                    command = self.queue.get(block=False)
+                except queue.Empty:
+                    break
+                self.game.handle_input(command)
             try:
                 key = self.stdscr.getkey()
                 if key == 'KEY_RESIZE':
                     curses.endwin()
                     self.setup_screen(curses.initscr())
             try:
                 key = self.stdscr.getkey()
                 if key == 'KEY_RESIZE':
                     curses.endwin()
                     self.setup_screen(curses.initscr())
-                    for w in widgets:
+                    for w in top_widgets:
                         w.size = w.size_def
                         w.ensure_freshness(True)
                 elif key == '\t':  # Tabulator key.
                         w.size = w.size_def
                         w.ensure_freshness(True)
                 elif key == '\t':  # Tabulator key.
-                    map_mode = False if map_mode else True
-                elif map_mode:
+                    write_mode = False if write_mode else True
+                elif write_mode:
+                    if len(key) == 1 and key in ASCII_printable and \
+                            len(self.to_send) < len(edit_line_widget):
+                        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':  # Return key
+                        self.socket.send(''.join(self.to_send))
+                        self.to_send[:] = []
+                        self.to_update['edit'] = True
+                elif self.view == 'map':
                     if key == 'w':
                         self.socket.send('TASK:MOVE UPLEFT')
                     elif key == 'e':
                     if key == 'w':
                         self.socket.send('TASK:MOVE UPLEFT')
                     elif key == 'e':
@@ -403,18 +505,61 @@ class TUI:
                         self.socket.send('TASK:MOVE DOWNLEFT')
                     elif key == 'c':
                         self.socket.send('TASK:MOVE DOWNRIGHT')
                         self.socket.send('TASK:MOVE DOWNLEFT')
                     elif key == 'c':
                         self.socket.send('TASK:MOVE DOWNRIGHT')
-                else:
-                    if len(key) == 1 and key in ASCII_printable and \
-                            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':  # Return key
-                        self.socket.send(''.join(self.to_send))
-                        self.to_send[:] = []
-                        self.to_update['edit'] = True
+                    elif key == 't':
+                        if not popup_widget.visible:
+                            self.to_update['popup'] = True
+                            popup_widget.visible = True
+                            popup_widget.reconfigure()
+                            draw_popup_if_visible = True
+                        else:
+                            popup_widget.visible = False
+                            for w in top_widgets:
+                                w.ensure_freshness(True)
+                    elif key == 'p':
+                        self.socket.send('GET_PICKABLE_ITEMS')
+                        self.item_pointer = 0
+                        self.view = 'pickable_items'
+                    elif key == 'i':
+                        self.item_pointer = 0
+                        self.view = 'inventory'
+                        self.to_update['map'] = True
+                elif self.view == 'pickable_items':
+                    if key == 'c':
+                        self.view = 'map'
+                    elif key == 'j' and \
+                         len(self.game.world.pickable_items) > \
+                         self.item_pointer + 1:
+                        self.item_pointer += 1
+                    elif key == 'k' and self.item_pointer > 0:
+                        self.item_pointer -= 1
+                    elif key == 'p' and \
+                         len(self.game.world.pickable_items) > 0:
+                        id_ = self.game.world.pickable_items[self.item_pointer]
+                        self.socket.send('TASK:PICKUP %s' % id_)
+                        self.socket.send('GET_PICKABLE_ITEMS')
+                        if self.item_pointer > 0:
+                            self.item_pointer -= 1
+                    else:
+                        continue
+                    self.to_update['map'] = True
+                elif self.view == 'inventory':
+                    if key == 'c':
+                        self.view = 'map'
+                    elif key == 'j' and \
+                         len(self.game.world.player_inventory) > \
+                         self.item_pointer + 1:
+                        self.item_pointer += 1
+                    elif key == 'k' and self.item_pointer > 0:
+                        self.item_pointer -= 1
+                    elif key == 'd' and \
+                         len(self.game.world.player_inventory) > 0:
+                        id_ = self.game.world.player_inventory[self.item_pointer]
+                        self.socket.send('TASK:DROP %s' % id_)
+                        if self.item_pointer > 0:
+                            self.item_pointer -= 1
+                    else:
+                        continue
+                    self.to_update['map'] = True
             except curses.error:
                 pass
             if self.game.do_quit:
             except curses.error:
                 pass
             if self.game.do_quit:
@@ -424,6 +569,7 @@ class TUI:
 s = socket.create_connection(('127.0.0.1', 5000))
 plom_socket = PlomSocket(s)
 game = Game()
 s = socket.create_connection(('127.0.0.1', 5000))
 plom_socket = PlomSocket(s)
 game = Game()
-t = threading.Thread(target=recv_loop, args=(plom_socket, game))
+q = queue.Queue()
+t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
 t.start()
 t.start()
-TUI(plom_socket, game)
+TUI(plom_socket, game, q)