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):
325 size = (1, len(self.tui.popup_text))
328 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
329 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
330 self.start = (offset_y, offset_x)
331 self.win.mvwin(self.start[0], self.start[1])
334 class ItemsSelectorWidget(Widget):
336 def draw_item_selector(self, title, selection):
339 for id_ in selection:
340 pointer = '*' if counter == self.tui.item_pointer else ' '
341 t = self.tui.game.world.get_thing(id_)
342 lines += ['%s %s' % (pointer, t.type_)]
344 line_width = self.size[1]
347 to_pad = line_width - (len(line) % line_width)
348 if to_pad == line_width:
350 to_join += [line + ' '*to_pad]
351 self.safe_write((''.join(to_join), curses.color_pair(3)))
354 class InventoryWidget(ItemsSelectorWidget):
357 self.draw_item_selector('INVENTORY:',
358 self.tui.game.world.player_inventory)
360 class PickableItemsWidget(ItemsSelectorWidget):
363 self.draw_item_selector('PICKABLE:',
364 self.tui.game.world.pickable_items)
367 class MapWidget(Widget):
371 def annotated_terrain():
372 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
373 for t in self.tui.game.world.things:
374 pos_i = self.tui.game.world.map_.get_position_index(t.position)
375 symbol = self.tui.game.symbol_for_type(t.type_)
376 if terrain_as_list[pos_i][0] in {'i', '@', 'm'}:
377 old_symbol = terrain_as_list[pos_i][0]
378 if old_symbol in {'@', 'm'}:
380 terrain_as_list[pos_i] = (symbol, '+')
382 terrain_as_list[pos_i] = symbol
383 if self.tui.examiner_mode:
384 pos_i = self.tui.game.world.map_.\
385 get_position_index(self.tui.examiner_position)
386 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
387 return terrain_as_list
389 def pad_or_cut_x(lines):
390 line_width = self.size[1]
391 for y in range(len(lines)):
393 if line_width > len(line):
394 to_pad = line_width - (len(line) % line_width)
395 lines[y] = line + '0' * to_pad
397 lines[y] = line[:line_width]
400 if len(lines) < self.size[0]:
401 to_pad = self.size[0] - len(lines)
402 lines += to_pad * ['0' * self.size[1]]
404 def lines_to_colored_chars(lines):
405 chars_with_attrs = []
406 for c in ''.join(lines):
408 chars_with_attrs += [(c, curses.color_pair(1))]
410 chars_with_attrs += [(c, curses.color_pair(4))]
412 chars_with_attrs += [(c, curses.color_pair(2))]
413 elif c in {'x', 'X', '#'}:
414 chars_with_attrs += [(c, curses.color_pair(3))]
416 chars_with_attrs += [c]
417 return chars_with_attrs
419 if self.tui.game.world.map_.terrain == '':
422 self.safe_write(''.join(lines))
425 annotated_terrain = annotated_terrain()
426 center = self.tui.game.world.player.position
427 if self.tui.examiner_mode:
428 center = self.tui.examiner_position
429 lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
433 self.safe_write(lines_to_colored_chars(lines))
436 class TurnWidget(Widget):
439 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
442 class TextLineWidget(Widget):
444 def __init__(self, text_line, *args, **kwargs):
445 self.text_line = text_line
446 super().__init__(*args, **kwargs)
449 self.safe_write(self.text_line)
454 def __init__(self, plom_socket, game, q):
455 self.socket = plom_socket
459 self.parser = Parser(self.game)
461 self.item_pointer = 0
462 self.examiner_position = (0, 0)
463 self.examiner_mode = False
464 self.popup_text = 'Hi bob'
466 self.draw_popup_if_visible = True
467 curses.wrapper(self.loop)
469 def loop(self, stdscr):
471 def setup_screen(stdscr):
473 self.stdscr.refresh() # will be called by getkey else, clearing screen
474 self.stdscr.timeout(10)
476 def switch_widgets(widget_1, widget_2):
477 widget_1.visible = False
478 widget_2.visible = True
479 trigger = widget_2.check_updates[0]
480 self.to_update[trigger] = True
482 def pick_or_drop_menu(action_key, widget, selectables, task,
484 if len(selectables) < self.item_pointer + 1 and\
485 self.item_pointer > 0:
486 self.item_pointer = len(selectables) - 1
488 switch_widgets(widget, map_widget)
490 self.item_pointer += 1
491 elif key == 'k' and self.item_pointer > 0:
492 self.item_pointer -= 1
493 elif key == action_key and len(selectables) > 0:
494 id_ = selectables[self.item_pointer]
495 self.socket.send('TASK:%s %s' % (task, id_))
497 self.socket.send(bonus_command)
498 if self.item_pointer > 0:
499 self.item_pointer -= 1
502 trigger = widget.check_updates[0]
503 self.to_update[trigger] = True
505 def move_examiner(direction):
506 start_pos = self.examiner_position
507 new_examine_pos = self.game.world.map_.move(start_pos, direction)
509 self.examiner_position = new_examine_pos
510 self.to_update['map'] = True
511 self.to_update['descriptor'] = True
513 def switch_to_pick_or_drop(target_widget):
514 self.item_pointer = 0
515 switch_widgets(map_widget, target_widget)
516 if self.examiner_mode:
517 self.examiner_mode = False
518 switch_widgets(descriptor_widget, log_widget)
520 def toggle_examiner_mode():
521 if self.examiner_mode:
522 self.examiner_mode = False
523 switch_widgets(descriptor_widget, log_widget)
525 self.examiner_mode = True
526 self.examiner_position = self.game.world.player.position
527 switch_widgets(log_widget, descriptor_widget)
528 self.to_update['map'] = True
531 if popup_widget.visible:
532 popup_widget.visible = False
533 for w in top_widgets:
534 w.ensure_freshness(True)
536 self.to_update['popup'] = True
537 popup_widget.visible = True
538 popup_widget.reconfigure()
539 self.draw_popup_if_visible = True
541 def try_write_keys():
542 if len(key) == 1 and key in ASCII_printable and \
543 len(self.to_send) < len(edit_line_widget):
544 self.to_send += [key]
545 self.to_update['edit'] = True
546 elif key == 'KEY_BACKSPACE':
547 self.to_send[:] = self.to_send[:-1]
548 self.to_update['edit'] = True
549 elif key == '\n': # Return key
550 self.socket.send(''.join(self.to_send))
552 self.to_update['edit'] = True
554 def try_examiner_keys():
556 move_examiner('UPLEFT')
558 move_examiner('UPRIGHT')
560 move_examiner('LEFT')
562 move_examiner('RIGHT')
564 move_examiner('DOWNLEFT')
566 move_examiner('DOWNRIGHT')
568 def try_player_move_keys():
570 self.socket.send('TASK:MOVE UPLEFT')
572 self.socket.send('TASK:MOVE UPRIGHT')
574 self.socket.send('TASK:MOVE LEFT')
576 self.socket.send('TASK:MOVE RIGHT')
578 self.socket.send('TASK:MOVE DOWNLEFT')
580 self.socket.send('TASK:MOVE DOWNRIGHT')
583 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
584 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
585 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
586 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
588 # Basic curses initialization work.
590 curses.curs_set(False) # hide cursor
593 # With screen initialized, set up widgets with their curses windows.
594 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
595 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
596 edit_widget.children += [edit_line_widget]
597 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
598 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
599 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
600 descriptor_widget = DescriptorWidget(self, (4, 0), (None, 20),
601 ['descriptor'], False)
602 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
603 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
604 ['inventory'], False)
605 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
606 ['pickable_items'], False)
607 top_widgets = [edit_widget, turn_widget, log_widget,
608 descriptor_widget, map_widget, inventory_widget,
609 pickable_items_widget]
610 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
612 # Ensure initial window state before loop starts.
613 for w in top_widgets:
614 w.ensure_freshness(True)
615 self.socket.send('GET_GAMESTATE')
620 for w in top_widgets:
621 if w.ensure_freshness():
622 self.draw_popup_if_visible = True
623 if popup_widget.visible and self.draw_popup_if_visible:
624 popup_widget.ensure_freshness(True)
625 self.draw_popup_if_visible = False
626 for k in self.to_update.keys():
627 self.to_update[k] = False
629 # Handle input from server.
632 command = self.queue.get(block=False)
635 self.game.handle_input(command)
637 # Handle keys (and resize event read as key).
639 key = self.stdscr.getkey()
640 if key == 'KEY_RESIZE':
642 setup_screen(curses.initscr())
643 for w in top_widgets:
645 w.ensure_freshness(True)
646 elif key == '\t': # Tabulator key.
647 write_mode = False if write_mode else True
652 elif map_widget.visible:
654 toggle_examiner_mode()
656 self.socket.send('GET_PICKABLE_ITEMS')
657 switch_to_pick_or_drop(pickable_items_widget)
659 switch_to_pick_or_drop(inventory_widget)
660 elif self.examiner_mode:
663 try_player_move_keys()
664 elif pickable_items_widget.visible:
665 pick_or_drop_menu('p', pickable_items_widget,
666 self.game.world.pickable_items,
667 'PICKUP', 'GET_PICKABLE_ITEMS')
668 elif inventory_widget.visible:
669 pick_or_drop_menu('d', inventory_widget,
670 self.game.world.player_inventory,
675 # Quit when server recommends it.
676 if self.game.do_quit:
680 s = socket.create_connection(('127.0.0.1', 5000))
681 plom_socket = PlomSocket(s)
684 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
686 TUI(plom_socket, game, q)