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'
94 def cmd_TURN_FINISHED(game, n):
95 """Do nothing. (This may be extended later.)"""
97 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
100 def cmd_TURN(game, n):
101 """Set game.turn to n, empty game.things."""
103 game.world.things = []
104 game.world.pickable_items = []
105 cmd_TURN.argtypes = 'int:nonneg'
108 def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
109 game.world.map_.set_line(y, terrain_line)
110 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
113 def cmd_GAME_STATE_COMPLETE(game):
114 game.tui.to_update['turn'] = True
115 game.tui.to_update['map'] = True
116 game.tui.to_update['inventory'] = True
119 def cmd_THING_TYPE(game, i, type_):
120 t = game.world.get_thing(i)
122 cmd_THING_TYPE.argtypes = 'int:nonneg string'
125 def cmd_PLAYER_INVENTORY(game, ids):
126 game.world.player_inventory = ids # TODO: test whether valid IDs
127 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
130 def cmd_PICKABLE_ITEMS(game, ids):
131 game.world.pickable_items = ids
132 game.tui.to_update['pickable_items'] = True
133 cmd_PICKABLE_ITEMS.argtypes = 'seq:int:nonneg'
139 self.parser = Parser(self)
140 self.world = World(self)
141 self.thing_type = ThingBase
142 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
143 'TURN_FINISHED': cmd_TURN_FINISHED,
145 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
146 'PLAYER_ID': cmd_PLAYER_ID,
147 'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
148 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
150 'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
151 'THING_TYPE': cmd_THING_TYPE,
152 'THING_POS': cmd_THING_POS}
157 def get_command(self, command_name):
158 from functools import partial
159 if command_name in self.commands:
160 f = partial(self.commands[command_name], self)
161 if hasattr(self.commands[command_name], 'argtypes'):
162 f.argtypes = self.commands[command_name].argtypes
166 def get_string_options(self, string_option_type):
169 def handle_input(self, msg):
175 command, args = self.parser.parse(msg)
177 self.log('UNHANDLED INPUT: ' + msg)
180 except ArgError as e:
181 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
184 """Prefix msg plus newline to self.log_text."""
185 self.log_text = msg + '\n' + self.log_text
186 self.tui.to_update['log'] = True
188 def symbol_for_type(self, type_):
192 elif type_ == 'monster':
194 elif type_ == 'item':
199 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
200 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
203 def recv_loop(plom_socket, game, q):
204 for msg in plom_socket.recv():
210 def __init__(self, tui, start, size, check_updates=[], visible=True):
211 self.check_updates = check_updates
214 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
215 self.size_def = size # store for re-calling .size on SIGWINCH
217 self.do_update = True
218 self.visible = visible
223 return self.win.getmaxyx()
226 def size(self, size):
227 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
228 n_lines, n_cols = size
230 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
232 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
233 self.win.resize(n_lines, n_cols)
236 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
238 def safe_write(self, foo):
240 def to_chars_with_attrs(part):
241 attr = curses.A_NORMAL
243 if not type(part) == str:
244 part_string = part[0]
246 return [(char, attr) for char in part_string]
248 chars_with_attrs = []
249 if type(foo) == str or (len(foo) == 2 and type(foo[1]) == int):
250 chars_with_attrs += to_chars_with_attrs(foo)
253 chars_with_attrs += to_chars_with_attrs(part)
255 if len(chars_with_attrs) < len(self):
256 for char_with_attr in chars_with_attrs:
257 self.win.addstr(char_with_attr[0], char_with_attr[1])
258 else: # workaround to <https://stackoverflow.com/q/7063128>
259 cut = chars_with_attrs[:len(self) - 1]
260 last_char_with_attr = chars_with_attrs[len(self) - 1]
261 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
262 last_char_with_attr[0], last_char_with_attr[1])
263 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
265 for char_with_attr in cut:
266 self.win.addstr(char_with_attr[0], char_with_attr[1])
268 def ensure_freshness(self, do_refresh=False):
272 for key in self.check_updates:
273 if key in self.tui.to_update and self.tui.to_update[key]:
281 for child in self.children:
282 did_refresh = child.ensure_freshness(do_refresh) | did_refresh
286 class EditWidget(Widget):
289 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
292 class TextLinesWidget(Widget):
295 lines = self.get_text_lines()
296 line_width = self.size[1]
299 to_pad = line_width - (len(line) % line_width)
300 if to_pad == line_width:
302 to_join += [line + ' '*to_pad]
303 self.safe_write((''.join(to_join), curses.color_pair(3)))
306 class LogWidget(TextLinesWidget):
308 def get_text_lines(self):
309 return self.tui.game.log_text.split('\n')
312 class DescriptorWidget(TextLinesWidget):
314 def get_text_lines(self):
316 pos_i = self.tui.game.world.map_.\
317 get_position_index(self.tui.examiner_position)
318 terrain = self.tui.game.world.map_.terrain[pos_i]
320 for t in self.tui.game.world.things:
321 if t.position == self.tui.examiner_position:
326 class PopUpWidget(Widget):
329 self.safe_write(self.tui.popup_text)
331 def reconfigure(self):
332 size = (1, len(self.tui.popup_text))
335 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
336 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
337 self.start = (offset_y, offset_x)
338 self.win.mvwin(self.start[0], self.start[1])
341 class ItemsSelectorWidget(Widget):
343 def draw_item_selector(self, title, selection):
346 for id_ in selection:
347 pointer = '*' if counter == self.tui.item_pointer else ' '
348 t = self.tui.game.world.get_thing(id_)
349 lines += ['%s %s' % (pointer, t.type_)]
351 line_width = self.size[1]
354 to_pad = line_width - (len(line) % line_width)
355 if to_pad == line_width:
357 to_join += [line + ' '*to_pad]
358 self.safe_write((''.join(to_join), curses.color_pair(3)))
361 class InventoryWidget(ItemsSelectorWidget):
364 self.draw_item_selector('INVENTORY:',
365 self.tui.game.world.player_inventory)
367 class PickableItemsWidget(ItemsSelectorWidget):
370 self.draw_item_selector('PICKABLE:',
371 self.tui.game.world.pickable_items)
374 class MapWidget(Widget):
378 def annotated_terrain():
379 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
380 for t in self.tui.game.world.things:
381 pos_i = self.tui.game.world.map_.get_position_index(t.position)
382 symbol = self.tui.game.symbol_for_type(t.type_)
383 if terrain_as_list[pos_i][0] in {'i', '@', 'm'}:
384 old_symbol = terrain_as_list[pos_i][0]
385 if old_symbol in {'@', 'm'}:
387 terrain_as_list[pos_i] = (symbol, '+')
389 terrain_as_list[pos_i] = symbol
390 if self.tui.examiner_mode:
391 pos_i = self.tui.game.world.map_.\
392 get_position_index(self.tui.examiner_position)
393 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
394 return terrain_as_list
396 def pad_or_cut_x(lines):
397 line_width = self.size[1]
398 for y in range(len(lines)):
400 if line_width > len(line):
401 to_pad = line_width - (len(line) % line_width)
402 lines[y] = line + '0' * to_pad
404 lines[y] = line[:line_width]
407 if len(lines) < self.size[0]:
408 to_pad = self.size[0] - len(lines)
409 lines += to_pad * ['0' * self.size[1]]
411 def lines_to_colored_chars(lines):
412 chars_with_attrs = []
413 for c in ''.join(lines):
415 chars_with_attrs += [(c, curses.color_pair(1))]
417 chars_with_attrs += [(c, curses.color_pair(4))]
419 chars_with_attrs += [(c, curses.color_pair(2))]
420 elif c in {'x', 'X', '#'}:
421 chars_with_attrs += [(c, curses.color_pair(3))]
423 chars_with_attrs += [(c, curses.color_pair(5))]
425 chars_with_attrs += [c]
426 return chars_with_attrs
428 if self.tui.game.world.map_.terrain == '':
431 self.safe_write(''.join(lines))
434 annotated_terrain = annotated_terrain()
435 center = self.tui.game.world.player.position
436 if self.tui.examiner_mode:
437 center = self.tui.examiner_position
438 lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
442 self.safe_write(lines_to_colored_chars(lines))
445 class TurnWidget(Widget):
448 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
451 class TextLineWidget(Widget):
453 def __init__(self, text_line, *args, **kwargs):
454 self.text_line = text_line
455 super().__init__(*args, **kwargs)
458 self.safe_write(self.text_line)
463 def __init__(self, plom_socket, game, q):
464 self.socket = plom_socket
468 self.parser = Parser(self.game)
470 self.item_pointer = 0
471 self.examiner_position = (0, 0)
472 self.examiner_mode = False
473 self.popup_text = 'Hi bob'
475 self.draw_popup_if_visible = True
476 curses.wrapper(self.loop)
478 def loop(self, stdscr):
480 def setup_screen(stdscr):
482 self.stdscr.refresh() # will be called by getkey else, clearing screen
483 self.stdscr.timeout(10)
485 def switch_widgets(widget_1, widget_2):
486 widget_1.visible = False
487 widget_2.visible = True
488 trigger = widget_2.check_updates[0]
489 self.to_update[trigger] = True
491 def pick_or_drop_menu(action_key, widget, selectables, task,
493 if len(selectables) < self.item_pointer + 1 and\
494 self.item_pointer > 0:
495 self.item_pointer = len(selectables) - 1
497 switch_widgets(widget, map_widget)
499 self.item_pointer += 1
500 elif key == 'k' and self.item_pointer > 0:
501 self.item_pointer -= 1
502 elif key == action_key and len(selectables) > 0:
503 id_ = selectables[self.item_pointer]
504 self.socket.send('TASK:%s %s' % (task, id_))
506 self.socket.send(bonus_command)
507 if self.item_pointer > 0:
508 self.item_pointer -= 1
511 trigger = widget.check_updates[0]
512 self.to_update[trigger] = True
514 def move_examiner(direction):
515 start_pos = self.examiner_position
516 new_examine_pos = self.game.world.map_.move(start_pos, direction)
518 self.examiner_position = new_examine_pos
519 self.to_update['map'] = True
521 def switch_to_pick_or_drop(target_widget):
522 self.item_pointer = 0
523 switch_widgets(map_widget, target_widget)
524 if self.examiner_mode:
525 self.examiner_mode = False
526 switch_widgets(descriptor_widget, log_widget)
528 def toggle_examiner_mode():
529 if self.examiner_mode:
530 self.examiner_mode = False
531 switch_widgets(descriptor_widget, log_widget)
533 self.examiner_mode = True
534 self.examiner_position = self.game.world.player.position
535 switch_widgets(log_widget, descriptor_widget)
536 self.to_update['map'] = True
539 if popup_widget.visible:
540 popup_widget.visible = False
541 for w in top_widgets:
542 w.ensure_freshness(True)
544 self.to_update['popup'] = True
545 popup_widget.visible = True
546 popup_widget.reconfigure()
547 self.draw_popup_if_visible = True
549 def try_write_keys():
550 if len(key) == 1 and key in ASCII_printable and \
551 len(self.to_send) < len(edit_line_widget):
552 self.to_send += [key]
553 self.to_update['edit'] = True
554 elif key == 'KEY_BACKSPACE':
555 self.to_send[:] = self.to_send[:-1]
556 self.to_update['edit'] = True
557 elif key == '\n': # Return key
558 self.socket.send(''.join(self.to_send))
560 self.to_update['edit'] = True
562 def try_examiner_keys():
564 move_examiner('UPLEFT')
566 move_examiner('UPRIGHT')
568 move_examiner('LEFT')
570 move_examiner('RIGHT')
572 move_examiner('DOWNLEFT')
574 move_examiner('DOWNRIGHT')
576 def try_player_move_keys():
578 self.socket.send('TASK:MOVE UPLEFT')
580 self.socket.send('TASK:MOVE UPRIGHT')
582 self.socket.send('TASK:MOVE LEFT')
584 self.socket.send('TASK:MOVE RIGHT')
586 self.socket.send('TASK:MOVE DOWNLEFT')
588 self.socket.send('TASK:MOVE DOWNRIGHT')
591 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
592 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
593 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
594 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
595 curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_WHITE)
597 # Basic curses initialization work.
599 curses.curs_set(False) # hide cursor
602 # With screen initialized, set up widgets with their curses windows.
603 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
604 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
605 edit_widget.children += [edit_line_widget]
606 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
607 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
608 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
609 descriptor_widget = DescriptorWidget(self, (4, 0), (None, 20),
611 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
612 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
613 ['inventory'], False)
614 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
615 ['pickable_items'], False)
616 top_widgets = [edit_widget, turn_widget, log_widget,
617 descriptor_widget, map_widget, inventory_widget,
618 pickable_items_widget]
619 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
621 # Ensure initial window state before loop starts.
622 for w in top_widgets:
623 w.ensure_freshness(True)
624 self.socket.send('GET_GAMESTATE')
629 for w in top_widgets:
630 if w.ensure_freshness():
631 self.draw_popup_if_visible = True
632 if popup_widget.visible and self.draw_popup_if_visible:
633 popup_widget.ensure_freshness(True)
634 self.draw_popup_if_visible = False
635 for k in self.to_update.keys():
636 self.to_update[k] = False
638 # Handle input from server.
641 command = self.queue.get(block=False)
644 self.game.handle_input(command)
646 # Handle keys (and resize event read as key).
648 key = self.stdscr.getkey()
649 if key == 'KEY_RESIZE':
651 setup_screen(curses.initscr())
652 for w in top_widgets:
654 w.ensure_freshness(True)
655 elif key == '\t': # Tabulator key.
656 write_mode = False if write_mode else True
661 elif map_widget.visible:
663 toggle_examiner_mode()
665 self.socket.send('GET_PICKABLE_ITEMS')
666 switch_to_pick_or_drop(pickable_items_widget)
668 switch_to_pick_or_drop(inventory_widget)
669 elif self.examiner_mode:
672 try_player_move_keys()
673 elif pickable_items_widget.visible:
674 pick_or_drop_menu('p', pickable_items_widget,
675 self.game.world.pickable_items,
676 'PICKUP', 'GET_PICKABLE_ITEMS')
677 elif inventory_widget.visible:
678 pick_or_drop_menu('d', inventory_widget,
679 self.game.world.player_inventory,
684 # Quit when server recommends it.
685 if self.game.do_quit:
689 s = socket.create_connection(('127.0.0.1', 5000))
690 plom_socket = PlomSocket(s)
693 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
695 TUI(plom_socket, game, q)