5 from plomrogue.parser import ArgError, Parser
6 from plomrogue.commands import cmd_MAP, cmd_THING_POS, cmd_PLAYER_ID
7 from plomrogue.game import Game, WorldBase
8 from plomrogue.mapping import MapHex
9 from plomrogue.io import PlomSocket
10 from plomrogue.things import ThingBase
15 class ClientMap(MapHex):
17 def y_cut(self, map_lines, center_y, view_height):
18 map_height = len(map_lines)
19 if map_height > view_height and center_y > view_height / 2:
20 if center_y > map_height - view_height / 2:
21 map_lines[:] = map_lines[map_height - view_height:]
23 start = center_y - int(view_height / 2) - 1
24 map_lines[:] = map_lines[start:start + view_height]
26 def x_cut(self, map_lines, center_x, view_width, map_width):
27 if map_width > view_width and center_x > view_width / 2:
28 if center_x > map_width - view_width / 2:
29 cut_start = map_width - view_width
32 cut_start = center_x - int(view_width / 2)
33 cut_end = cut_start + view_width
34 map_lines[:] = [line[cut_start:cut_end] for line in map_lines]
36 def format_to_view(self, map_cells, center, size):
38 def map_cells_to_lines(map_cells):
39 map_view_chars = ['0']
42 for cell in map_cells:
44 map_view_chars += [cell, ' ']
46 map_view_chars += [cell[0], cell[1]]
49 map_view_chars += ['\n']
53 map_view_chars += ['0']
55 map_view_chars = map_view_chars[:-1]
56 map_view_chars = map_view_chars[:-1]
57 return ''.join(map_view_chars).split('\n')
59 map_lines = map_cells_to_lines(map_cells)
60 self.y_cut(map_lines, center[0], size[0])
61 map_width = self.size[1] * 2 + 1
62 self.x_cut(map_lines, center[1] * 2, size[1], map_width)
66 class World(WorldBase):
68 def __init__(self, *args, **kwargs):
69 """Extend original with local classes and empty default map.
71 We need the empty default map because we draw the map widget
72 on any update, even before we actually receive map data.
74 super().__init__(*args, **kwargs)
75 self.map_ = ClientMap()
76 self.player_inventory = []
78 self.pickable_items = []
80 def new_map(self, yx):
81 self.map_ = ClientMap(yx)
85 return self.get_thing(self.player_id)
88 def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
91 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
93 def cmd_TURN_FINISHED(game, n):
94 """Do nothing. (This may be extended later.)"""
96 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
98 def cmd_TURN(game, n):
99 """Set game.turn to n, empty game.things."""
101 game.world.things = []
102 game.world.pickable_items = []
103 cmd_TURN.argtypes = 'int:nonneg'
105 def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
106 game.world.map_.set_line(y, terrain_line)
107 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
109 def cmd_GAME_STATE_COMPLETE(game):
110 game.tui.to_update['turn'] = True
111 game.tui.to_update['map'] = True
112 game.tui.to_update['inventory'] = True
114 def cmd_THING_TYPE(game, i, type_):
115 t = game.world.get_thing(i)
117 cmd_THING_TYPE.argtypes = 'int:nonneg string'
119 def cmd_PLAYER_INVENTORY(game, ids):
120 game.world.player_inventory = ids # TODO: test whether valid IDs
121 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
123 def cmd_PICKABLE_ITEMS(game, ids):
124 game.world.pickable_items = ids
125 game.tui.to_update['pickable_items'] = True
126 cmd_PICKABLE_ITEMS.argtypes = 'seq:int:nonneg'
132 self.parser = Parser(self)
133 self.world = World(self)
134 self.thing_type = ThingBase
135 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
136 'TURN_FINISHED': cmd_TURN_FINISHED,
138 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
139 'PLAYER_ID': cmd_PLAYER_ID,
140 'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
141 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
143 'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
144 'THING_TYPE': cmd_THING_TYPE,
145 'THING_POS': cmd_THING_POS}
150 def get_command(self, command_name):
151 from functools import partial
152 if command_name in self.commands:
153 f = partial(self.commands[command_name], self)
154 if hasattr(self.commands[command_name], 'argtypes'):
155 f.argtypes = self.commands[command_name].argtypes
159 def get_string_options(self, string_option_type):
162 def handle_input(self, msg):
168 command, args = self.parser.parse(msg)
170 self.log('UNHANDLED INPUT: ' + msg)
173 except ArgError as e:
174 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
177 """Prefix msg plus newline to self.log_text."""
178 self.log_text = msg + '\n' + self.log_text
179 self.tui.to_update['log'] = True
181 def symbol_for_type(self, type_):
185 elif type_ == 'monster':
187 elif type_ == 'item':
192 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
193 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
196 def recv_loop(plom_socket, game, q):
197 for msg in plom_socket.recv():
203 def __init__(self, tui, start, size, check_updates=[], visible=True):
204 self.check_updates = check_updates
207 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
208 self.size_def = size # store for re-calling .size on SIGWINCH
210 self.do_update = True
211 self.visible = visible
216 return self.win.getmaxyx()
219 def size(self, size):
220 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
221 n_lines, n_cols = size
223 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
225 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
226 self.win.resize(n_lines, n_cols)
229 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
231 def safe_write(self, foo):
233 def to_chars_with_attrs(part):
234 attr = curses.A_NORMAL
236 if not type(part) == str:
237 part_string = part[0]
239 return [(char, attr) for char in part_string]
241 chars_with_attrs = []
242 if type(foo) == str or (len(foo) == 2 and type(foo[1]) == int):
243 chars_with_attrs += to_chars_with_attrs(foo)
246 chars_with_attrs += to_chars_with_attrs(part)
248 if len(chars_with_attrs) < len(self):
249 for char_with_attr in chars_with_attrs:
250 self.win.addstr(char_with_attr[0], char_with_attr[1])
251 else: # workaround to <https://stackoverflow.com/q/7063128>
252 cut = chars_with_attrs[:len(self) - 1]
253 last_char_with_attr = chars_with_attrs[len(self) - 1]
254 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
255 last_char_with_attr[0], last_char_with_attr[1])
256 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
258 for char_with_attr in cut:
259 self.win.addstr(char_with_attr[0], char_with_attr[1])
261 def ensure_freshness(self, do_refresh=False):
265 for key in self.check_updates:
266 if key in self.tui.to_update and self.tui.to_update[key]:
274 for child in self.children:
275 did_refresh = child.ensure_freshness(do_refresh) | did_refresh
279 class EditWidget(Widget):
282 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
285 class TextLinesWidget(Widget):
288 lines = self.get_text_lines()
289 line_width = self.size[1]
292 to_pad = line_width - (len(line) % line_width)
293 if to_pad == line_width:
295 to_join += [line + ' '*to_pad]
296 self.safe_write((''.join(to_join), curses.color_pair(3)))
299 class LogWidget(TextLinesWidget):
301 def get_text_lines(self):
302 return self.tui.game.log_text.split('\n')
305 class DescriptorWidget(TextLinesWidget):
307 def get_text_lines(self):
309 pos_i = self.tui.game.world.map_.\
310 get_position_index(self.tui.examiner_position)
311 terrain = self.tui.game.world.map_.terrain[pos_i]
313 for t in self.tui.game.world.things:
314 if t.position == self.tui.examiner_position:
319 class PopUpWidget(Widget):
322 self.safe_write(self.tui.popup_text)
324 def reconfigure(self):
326 size = (1, len(self.tui.popup_text))
329 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
330 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
331 self.start = (offset_y, offset_x)
332 self.win.mvwin(self.start[0], self.start[1])
335 class ItemsSelectorWidget(Widget):
337 def draw_item_selector(self, title, selection):
340 for id_ in selection:
341 pointer = '*' if counter == self.tui.item_pointer else ' '
342 t = self.tui.game.world.get_thing(id_)
343 lines += ['%s %s' % (pointer, t.type_)]
345 line_width = self.size[1]
348 to_pad = line_width - (len(line) % line_width)
349 if to_pad == line_width:
351 to_join += [line + ' '*to_pad]
352 self.safe_write((''.join(to_join), curses.color_pair(3)))
355 class InventoryWidget(ItemsSelectorWidget):
358 self.draw_item_selector('INVENTORY:',
359 self.tui.game.world.player_inventory)
361 class PickableItemsWidget(ItemsSelectorWidget):
364 self.draw_item_selector('PICKABLE:',
365 self.tui.game.world.pickable_items)
368 class MapWidget(Widget):
370 def __init__(self, *args, **kwargs):
371 super().__init__(*args, **kwargs)
372 self.examine_mode = False
376 def annotated_terrain():
377 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
378 for t in self.tui.game.world.things:
379 pos_i = self.tui.game.world.map_.get_position_index(t.position)
380 symbol = self.tui.game.symbol_for_type(t.type_)
381 if terrain_as_list[pos_i][0] in {'i', '@', 'm'}:
382 old_symbol = terrain_as_list[pos_i][0]
383 if old_symbol in {'@', 'm'}:
385 terrain_as_list[pos_i] = (symbol, '+')
387 terrain_as_list[pos_i] = symbol
388 if self.examine_mode:
389 pos_i = self.tui.game.world.map_.\
390 get_position_index(self.tui.examiner_position)
391 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
392 return terrain_as_list
394 def pad_or_cut_x(lines):
395 line_width = self.size[1]
396 for y in range(len(lines)):
398 if line_width > len(line):
399 to_pad = line_width - (len(line) % line_width)
400 lines[y] = line + '0' * to_pad
402 lines[y] = line[:line_width]
405 if len(lines) < self.size[0]:
406 to_pad = self.size[0] - len(lines)
407 lines += to_pad * ['0' * self.size[1]]
409 def lines_to_colored_chars(lines):
410 chars_with_attrs = []
411 for c in ''.join(lines):
413 chars_with_attrs += [(c, curses.color_pair(1))]
415 chars_with_attrs += [(c, curses.color_pair(4))]
417 chars_with_attrs += [(c, curses.color_pair(2))]
418 elif c in {'x', 'X', '#'}:
419 chars_with_attrs += [(c, curses.color_pair(3))]
421 chars_with_attrs += [c]
422 return chars_with_attrs
424 if self.tui.game.world.map_.terrain == '':
427 self.safe_write(''.join(lines))
430 annotated_terrain = annotated_terrain()
431 center = self.tui.game.world.player.position
432 if self.examine_mode:
433 center = self.tui.examiner_position
434 lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
438 self.safe_write(lines_to_colored_chars(lines))
441 class TurnWidget(Widget):
444 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
447 class TextLineWidget(Widget):
449 def __init__(self, text_line, *args, **kwargs):
450 self.text_line = text_line
451 super().__init__(*args, **kwargs)
454 self.safe_write(self.text_line)
459 def __init__(self, plom_socket, game, q):
460 self.socket = plom_socket
464 self.parser = Parser(self.game)
466 self.item_pointer = 0
467 self.examiner_position = (0, 0)
468 curses.wrapper(self.loop)
470 def loop(self, stdscr):
472 def setup_screen(stdscr):
474 self.stdscr.refresh() # will be called by getkey else, clearing screen
475 self.stdscr.timeout(10)
477 def switch_widgets(widget_1, widget_2):
478 widget_1.visible = False
479 widget_2.visible = True
480 trigger = widget_2.check_updates[0]
481 self.to_update[trigger] = True
483 def pick_or_drop_menu(action_key, widget, selectables, task,
485 if len(selectables) < self.item_pointer + 1 and\
486 self.item_pointer > 0:
487 self.item_pointer = len(selectables) - 1
489 switch_widgets(widget, map_widget)
490 map_widget.examine_mode = False
492 self.item_pointer += 1
493 elif key == 'k' and self.item_pointer > 0:
494 self.item_pointer -= 1
495 elif key == action_key and len(selectables) > 0:
496 id_ = selectables[self.item_pointer]
497 self.socket.send('TASK:%s %s' % (task, id_))
499 self.socket.send(bonus_command)
500 if self.item_pointer > 0:
501 self.item_pointer -= 1
504 trigger = widget.check_updates[0]
505 self.to_update[trigger] = True
507 def move_examiner(direction):
508 start_pos = self.examiner_position
509 new_examine_pos = self.game.world.map_.move(start_pos, direction)
511 self.examiner_position = new_examine_pos
512 self.to_update['map'] = True
513 self.to_update['descriptor'] = True
516 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
517 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
518 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
519 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
520 curses.curs_set(False) # hide cursor
522 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
523 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
524 edit_widget.children += [edit_line_widget]
525 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
526 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
527 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
528 descriptor_widget = DescriptorWidget(self, (4, 0), (None, 20),
529 ['descriptor'], False)
530 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
531 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
532 ['inventory'], False)
533 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
534 ['pickable_items'], False)
535 top_widgets = [edit_widget, turn_widget, log_widget,
536 descriptor_widget, map_widget, inventory_widget,
537 pickable_items_widget]
538 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
539 self.popup_text = 'Hi bob'
541 for w in top_widgets:
542 w.ensure_freshness(True)
543 draw_popup_if_visible = True
545 for w in top_widgets:
546 did_refresh = w.ensure_freshness()
547 draw_popup_if_visible = did_refresh | draw_popup_if_visible
548 if popup_widget.visible and draw_popup_if_visible:
549 popup_widget.ensure_freshness(True)
550 draw_popup_if_visible = False
551 for k in self.to_update.keys():
552 self.to_update[k] = False
555 command = self.queue.get(block=False)
558 self.game.handle_input(command)
560 key = self.stdscr.getkey()
561 if key == 'KEY_RESIZE':
563 setup_screen(curses.initscr())
564 for w in top_widgets:
566 w.ensure_freshness(True)
567 elif key == '\t': # Tabulator key.
568 write_mode = False if write_mode else True
570 if len(key) == 1 and key in ASCII_printable and \
571 len(self.to_send) < len(edit_line_widget):
572 self.to_send += [key]
573 self.to_update['edit'] = True
574 elif key == 'KEY_BACKSPACE':
575 self.to_send[:] = self.to_send[:-1]
576 self.to_update['edit'] = True
577 elif key == '\n': # Return key
578 self.socket.send(''.join(self.to_send))
580 self.to_update['edit'] = True
582 if not popup_widget.visible:
583 self.to_update['popup'] = True
584 popup_widget.visible = True
585 popup_widget.reconfigure()
586 draw_popup_if_visible = True
588 popup_widget.visible = False
589 for w in top_widgets:
590 w.ensure_freshness(True)
591 elif map_widget.visible:
593 map_widget.examine_mode = not map_widget.examine_mode
594 if map_widget.examine_mode:
595 self.examiner_position = self.game.world.\
597 switch_widgets(log_widget, descriptor_widget)
599 switch_widgets(descriptor_widget, log_widget)
600 self.to_update['map'] = True
602 self.socket.send('GET_PICKABLE_ITEMS')
603 self.item_pointer = 0
604 switch_widgets(map_widget, pickable_items_widget)
606 self.item_pointer = 0
607 switch_widgets(map_widget, inventory_widget)
608 elif map_widget.examine_mode:
610 move_examiner('UPLEFT')
612 move_examiner('UPRIGHT')
614 move_examiner('LEFT')
616 move_examiner('RIGHT')
618 move_examiner('DOWNLEFT')
620 move_examiner('DOWNRIGHT')
622 self.socket.send('TASK:MOVE UPLEFT')
624 self.socket.send('TASK:MOVE UPRIGHT')
626 self.socket.send('TASK:MOVE LEFT')
628 self.socket.send('TASK:MOVE RIGHT')
630 self.socket.send('TASK:MOVE DOWNLEFT')
632 self.socket.send('TASK:MOVE DOWNRIGHT')
633 elif pickable_items_widget.visible:
634 pick_or_drop_menu('p', pickable_items_widget,
635 self.game.world.pickable_items,
636 'PICKUP', 'GET_PICKABLE_ITEMS')
637 elif inventory_widget.visible:
638 pick_or_drop_menu('d', inventory_widget,
639 self.game.world.player_inventory,
643 if self.game.do_quit:
647 s = socket.create_connection(('127.0.0.1', 5000))
648 plom_socket = PlomSocket(s)
651 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
653 TUI(plom_socket, game, q)