home · contact · privacy
In client, ensure item pointer is always within range.
[plomrogue2-experiments] / new / example_client.py
index aae66cf9367ccfabbacabdc6aeb390c1a17b7c71..61f9233d60dea76c1a1a83ab13e7aa7f2a1ebc59 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,12 +70,17 @@ 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_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(game, msg):
     if msg != "success":
 
 def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
     if msg != "success":
@@ -90,21 +96,16 @@ def cmd_TURN(game, n):
     """Set game.turn to n, empty game.things."""
     game.world.turn = n
     game.world.things = []
     """Set game.turn to n, empty game.things."""
     game.world.turn = n
     game.world.things = []
-    game.to_update['turn'] = False
-    game.to_update['map'] = False
+    game.world.pickable_items = []
 cmd_TURN.argtypes = 'int:nonneg'
 
 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_TURN.argtypes = 'int:nonneg'
 
 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'
 
-def cmd_PLAYER_POS(game, yx):
-    game.world.player_position = yx
-cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
-
 def cmd_GAME_STATE_COMPLETE(game):
 def cmd_GAME_STATE_COMPLETE(game):
-    game.to_update['turn'] = True
-    game.to_update['map'] = True
+    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)
 
 def cmd_THING_TYPE(game, i, type_):
     t = game.world.get_thing(i)
@@ -112,9 +113,14 @@ def cmd_THING_TYPE(game, i, type_):
 cmd_THING_TYPE.argtypes = 'int:nonneg string'
 
 def cmd_PLAYER_INVENTORY(game, ids):
 cmd_THING_TYPE.argtypes = 'int:nonneg string'
 
 def cmd_PLAYER_INVENTORY(game, ids):
-    game.world.player_inventory = [ids]  # TODO: test whether valid IDs
+    game.world.player_inventory = ids  # TODO: test whether valid IDs
 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
 
 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:
 
@@ -126,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
@@ -152,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
@@ -159,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 = '?'
@@ -186,16 +189,15 @@ 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.tui = tui
         self.start = start
         self.win = curses.newwin(1, 1, self.start[0], self.start[1])
@@ -203,6 +205,7 @@ class Widget:
         self.size = size
         self.do_update = True
         self.visible = True
         self.size = size
         self.do_update = True
         self.visible = True
+        self.children = []
 
     @property
     def size(self):
 
     @property
     def size(self):
@@ -256,22 +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 self.visible:
-            return
-        if not do_refresh:
-            for key in self.check_game:
-                if key in self.tui.game.to_update and self.tui.game.to_update[key]:
-                    do_refresh = True
-                    break
-        if not do_refresh:
-            for key in self.check_tui:
-                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 = 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):
@@ -308,13 +310,38 @@ class PopUpWidget(Widget):
         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])
         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])
-        self.ensure_freshness(True)
-
 
 
 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[:])
 
         def terrain_with_objects():
             terrain_as_list = list(self.tui.game.world.map_.terrain[:])
@@ -363,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)
@@ -377,24 +404,32 @@ 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)
 
         curses.wrapper(self.loop)
 
-    def draw_screen(self):
-        self.stdscr.addstr(0, 0, 'SEND:')
-        self.stdscr.addstr(2, 0, 'TURN:')
-
     def setup_screen(self, stdscr):
         self.stdscr = stdscr
         self.stdscr.refresh()  # will be called by getkey else, clearing screen
         self.stdscr.timeout(10)
     def setup_screen(self, stdscr):
         self.stdscr = stdscr
         self.stdscr.refresh()  # will be called by getkey else, clearing screen
         self.stdscr.timeout(10)
-        self.draw_screen()
 
     def loop(self, stdscr):
         self.setup_screen(stdscr)
 
     def loop(self, stdscr):
         self.setup_screen(stdscr)
@@ -404,33 +439,60 @@ class TUI:
         curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
         curses.curs_set(False)  # hide cursor
         self.to_send = []
         curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
         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'])
-        self.popup = PopUpWidget(self, (0, 0), (1, 1), ['popup'])
-        self.popup.visible = 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'
         self.popup_text = 'Hi bob'
-        widgets = (self.edit, self.turn, self.log, self.map_, self.popup)
-        map_mode = False
+        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':
@@ -444,29 +506,64 @@ class TUI:
                     elif key == 'c':
                         self.socket.send('TASK:MOVE DOWNRIGHT')
                     elif key == 't':
                     elif key == 'c':
                         self.socket.send('TASK:MOVE DOWNRIGHT')
                     elif key == 't':
-                        if not self.popup.visible:
+                        if not popup_widget.visible:
                             self.to_update['popup'] = True
                             self.to_update['popup'] = True
-                            self.popup.visible = True
-                            self.popup.reconfigure()
+                            popup_widget.visible = True
+                            popup_widget.reconfigure()
+                            draw_popup_if_visible = True
                         else:
                         else:
-                            self.popup.visible = False
-                            self.stdscr.erase()    # we'll call refresh here so
-                            self.stdscr.refresh()  # getkey doesn't, erasing screen
-                            self.draw_screen()
-                            for w in widgets:
+                            popup_widget.visible = False
+                            for w in top_widgets:
                                 w.ensure_freshness(True)
                                 w.ensure_freshness(True)
-                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 == '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 len(self.game.world.pickable_items) < self.item_pointer + 1\
+                       and self.item_pointer > 0:
+                        self.item_pointer = len(self.game.world.pickable_items) - 1
+                    while len(self.game.world.pickable_items) <= self.item_pointer:
+                        self.item_pointer -= 1
+                    if key == 'c':
+                        self.view = 'map'
+                    elif key == 'j':
+                        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 len(self.game.world.player_inventory) < self.item_pointer + 1\
+                       and self.item_pointer > 0:
+                        self.item_pointer = len(self.game.world.player_inventory) - 1
+                    if key == 'c':
+                        self.view = 'map'
+                    elif key == 'j':
+                        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:
@@ -476,6 +573,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)