5 from plomrogue.parser import ArgError, Parser
6 from plomrogue.commands import (cmd_MAP, cmd_THING_POS, cmd_PLAYER_ID,
8 from plomrogue.game import Game, WorldBase
9 from plomrogue.mapping import MapHex
10 from plomrogue.io import PlomSocket
11 from plomrogue.things import ThingBase
16 class ClientMap(MapHex):
18 def y_cut(self, map_lines, center_y, view_height):
19 map_height = len(map_lines)
20 if map_height > view_height and center_y > view_height / 2:
21 if center_y > map_height - view_height / 2:
22 map_lines[:] = map_lines[map_height - view_height:]
24 start = center_y - int(view_height / 2) - 1
25 map_lines[:] = map_lines[start:start + view_height]
27 def x_cut(self, map_lines, center_x, view_width, map_width):
28 if map_width > view_width and center_x > view_width / 2:
29 if center_x > map_width - view_width / 2:
30 cut_start = map_width - view_width
33 cut_start = center_x - int(view_width / 2)
34 cut_end = cut_start + view_width
35 map_lines[:] = [line[cut_start:cut_end] for line in map_lines]
37 def format_to_view(self, map_cells, center, size):
39 def map_cells_to_lines(map_cells):
40 map_view_chars = ['0']
43 for cell in map_cells:
45 map_view_chars += [cell, ' ']
47 map_view_chars += [cell[0], cell[1]]
50 map_view_chars += ['\n']
54 map_view_chars += ['0']
56 map_view_chars = map_view_chars[:-1]
57 map_view_chars = map_view_chars[:-1]
58 return ''.join(map_view_chars).split('\n')
60 map_lines = map_cells_to_lines(map_cells)
61 self.y_cut(map_lines, center[0], size[0])
62 map_width = self.size[1] * 2 + 1
63 self.x_cut(map_lines, center[1] * 2, size[1], map_width)
67 class World(WorldBase):
69 def __init__(self, *args, **kwargs):
70 """Extend original with local classes and empty default map.
72 We need the empty default map because we draw the map widget
73 on any update, even before we actually receive map data.
75 super().__init__(*args, **kwargs)
76 self.map_ = ClientMap()
77 self.player_inventory = []
79 self.pickable_items = []
81 def new_map(self, yx):
82 self.map_ = ClientMap(yx)
86 return self.get_thing(self.player_id)
89 def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
92 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
95 def cmd_TURN_FINISHED(game, n):
96 """Do nothing. (This may be extended later.)"""
98 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
101 def cmd_TURN(game, n):
102 """Set game.turn to n, empty game.things."""
104 game.world.things = []
105 game.world.pickable_items = []
106 cmd_TURN.argtypes = 'int:nonneg'
109 def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
110 game.world.map_.set_line(y, terrain_line)
111 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
114 def cmd_GAME_STATE_COMPLETE(game):
115 game.tui.to_update['turn'] = True
116 game.tui.to_update['map'] = True
117 game.tui.to_update['inventory'] = True
120 def cmd_THING_TYPE(game, i, type_):
121 t = game.world.get_thing(i)
123 cmd_THING_TYPE.argtypes = 'int:nonneg string'
126 def cmd_PLAYER_INVENTORY(game, ids):
127 game.world.player_inventory = ids # TODO: test whether valid IDs
128 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
131 def cmd_PICKABLE_ITEMS(game, ids):
132 game.world.pickable_items = ids
133 game.tui.to_update['pickable_items'] = True
134 cmd_PICKABLE_ITEMS.argtypes = 'seq:int:nonneg'
140 self.parser = Parser(self)
141 self.world = World(self)
142 self.thing_type = ThingBase
143 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
144 'TURN_FINISHED': cmd_TURN_FINISHED,
146 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
147 'PLAYER_ID': cmd_PLAYER_ID,
148 'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
149 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
151 'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
152 'THING_TYPE': cmd_THING_TYPE,
153 'THING_HEALTH': cmd_THING_HEALTH,
154 'THING_POS': cmd_THING_POS}
159 def get_command(self, command_name):
160 from functools import partial
161 if command_name in self.commands:
162 f = partial(self.commands[command_name], self)
163 if hasattr(self.commands[command_name], 'argtypes'):
164 f.argtypes = self.commands[command_name].argtypes
168 def get_string_options(self, string_option_type):
171 def handle_input(self, msg):
177 command, args = self.parser.parse(msg)
179 self.log('UNHANDLED INPUT: ' + msg)
182 except ArgError as e:
183 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
186 """Prefix msg plus newline to self.log_text."""
187 self.log_text = msg + '\n' + self.log_text
188 self.tui.to_update['log'] = True
190 def symbol_for_type(self, type_):
194 elif type_ == 'monster':
196 elif type_ == 'item':
201 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
202 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
205 def recv_loop(plom_socket, game, q):
206 for msg in plom_socket.recv():
212 def __init__(self, tui, start, size, check_updates=[], visible=True):
213 self.check_updates = check_updates
216 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
217 self.size_def = size # store for re-calling .size on SIGWINCH
219 self.do_update = True
220 self.visible = visible
225 return self.win.getmaxyx()
228 def size(self, size):
229 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
230 n_lines, n_cols = size
232 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
234 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
235 self.win.resize(n_lines, n_cols)
238 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
240 def safe_write(self, foo):
242 def to_chars_with_attrs(part):
243 attr = curses.A_NORMAL
245 if not type(part) == str:
246 part_string = part[0]
248 return [(char, attr) for char in part_string]
250 chars_with_attrs = []
251 if type(foo) == str or (len(foo) == 2 and type(foo[1]) == int):
252 chars_with_attrs += to_chars_with_attrs(foo)
255 chars_with_attrs += to_chars_with_attrs(part)
257 if len(chars_with_attrs) < len(self):
258 for char_with_attr in chars_with_attrs:
259 self.win.addstr(char_with_attr[0], char_with_attr[1])
260 else: # workaround to <https://stackoverflow.com/q/7063128>
261 cut = chars_with_attrs[:len(self) - 1]
262 last_char_with_attr = chars_with_attrs[len(self) - 1]
263 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
264 last_char_with_attr[0], last_char_with_attr[1])
265 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
267 for char_with_attr in cut:
268 self.win.addstr(char_with_attr[0], char_with_attr[1])
270 def ensure_freshness(self, do_refresh=False):
274 for key in self.check_updates:
275 if key in self.tui.to_update and self.tui.to_update[key]:
283 for child in self.children:
284 did_refresh = child.ensure_freshness(do_refresh) | did_refresh
288 class EditWidget(Widget):
291 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
294 class TextLinesWidget(Widget):
297 lines = self.get_text_lines()
298 line_width = self.size[1]
301 to_pad = line_width - (len(line) % line_width)
302 if to_pad == line_width:
304 to_join += [line + ' '*to_pad]
305 self.safe_write((''.join(to_join), curses.color_pair(3)))
308 class LogWidget(TextLinesWidget):
310 def get_text_lines(self):
311 return self.tui.game.log_text.split('\n')
314 class DescriptorWidget(TextLinesWidget):
316 def get_text_lines(self):
318 pos_i = self.tui.game.world.map_.\
319 get_position_index(self.tui.examiner_position)
320 terrain = self.tui.game.world.map_.terrain[pos_i]
322 for t in self.tui.game.world.things:
323 if t.position == self.tui.examiner_position:
328 class PopUpWidget(Widget):
331 self.safe_write(self.tui.popup_text)
333 def reconfigure(self):
334 size = (1, len(self.tui.popup_text))
337 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
338 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
339 self.start = (offset_y, offset_x)
340 self.win.mvwin(self.start[0], self.start[1])
343 class ItemsSelectorWidget(Widget):
345 def draw_item_selector(self, title, selection):
348 for id_ in selection:
349 pointer = '*' if counter == self.tui.item_pointer else ' '
350 t = self.tui.game.world.get_thing(id_)
351 lines += ['%s %s' % (pointer, t.type_)]
353 line_width = self.size[1]
356 to_pad = line_width - (len(line) % line_width)
357 if to_pad == line_width:
359 to_join += [line + ' '*to_pad]
360 self.safe_write((''.join(to_join), curses.color_pair(3)))
363 class InventoryWidget(ItemsSelectorWidget):
366 self.draw_item_selector('INVENTORY:',
367 self.tui.game.world.player_inventory)
369 class PickableItemsWidget(ItemsSelectorWidget):
372 self.draw_item_selector('PICKABLE:',
373 self.tui.game.world.pickable_items)
376 class MapWidget(Widget):
380 def annotated_terrain():
381 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
382 for t in self.tui.game.world.things:
383 pos_i = self.tui.game.world.map_.get_position_index(t.position)
384 symbol = self.tui.game.symbol_for_type(t.type_)
385 if terrain_as_list[pos_i][0] in {'i', '@', 'm'}:
386 old_symbol = terrain_as_list[pos_i][0]
387 if old_symbol in {'@', 'm'}:
389 terrain_as_list[pos_i] = (symbol, '+')
391 terrain_as_list[pos_i] = symbol
392 if self.tui.examiner_mode:
393 pos_i = self.tui.game.world.map_.\
394 get_position_index(self.tui.examiner_position)
395 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
396 return terrain_as_list
398 def pad_or_cut_x(lines):
399 line_width = self.size[1]
400 for y in range(len(lines)):
402 if line_width > len(line):
403 to_pad = line_width - (len(line) % line_width)
404 lines[y] = line + '0' * to_pad
406 lines[y] = line[:line_width]
409 if len(lines) < self.size[0]:
410 to_pad = self.size[0] - len(lines)
411 lines += to_pad * ['0' * self.size[1]]
413 def lines_to_colored_chars(lines):
414 chars_with_attrs = []
415 for c in ''.join(lines):
417 chars_with_attrs += [(c, curses.color_pair(1))]
419 chars_with_attrs += [(c, curses.color_pair(4))]
421 chars_with_attrs += [(c, curses.color_pair(2))]
422 elif c in {'x', 'X', '#'}:
423 chars_with_attrs += [(c, curses.color_pair(3))]
425 chars_with_attrs += [(c, curses.color_pair(5))]
427 chars_with_attrs += [c]
428 return chars_with_attrs
430 if self.tui.game.world.map_.terrain == '':
433 self.safe_write(''.join(lines))
436 annotated_terrain = annotated_terrain()
437 center = self.tui.game.world.player.position
438 if self.tui.examiner_mode:
439 center = self.tui.examiner_position
440 lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
444 self.safe_write(lines_to_colored_chars(lines))
447 class TurnWidget(Widget):
450 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
453 class HealthWidget(Widget):
456 if hasattr(self.tui.game.world.player, 'health'):
457 self.safe_write((str(self.tui.game.world.player.health),
458 curses.color_pair(2)))
461 class TextLineWidget(Widget):
463 def __init__(self, text_line, *args, **kwargs):
464 self.text_line = text_line
465 super().__init__(*args, **kwargs)
468 self.safe_write(self.text_line)
473 def __init__(self, plom_socket, game, q):
474 self.socket = plom_socket
478 self.parser = Parser(self.game)
480 self.item_pointer = 0
481 self.examiner_position = (0, 0)
482 self.examiner_mode = False
483 self.popup_text = 'Hi bob'
485 self.draw_popup_if_visible = True
486 curses.wrapper(self.loop)
488 def loop(self, stdscr):
490 def setup_screen(stdscr):
492 self.stdscr.refresh() # will be called by getkey else, clearing screen
493 self.stdscr.timeout(10)
495 def switch_widgets(widget_1, widget_2):
496 widget_1.visible = False
497 widget_2.visible = True
498 trigger = widget_2.check_updates[0]
499 self.to_update[trigger] = True
501 def pick_or_drop_menu(action_key, widget, selectables, task,
503 if len(selectables) < self.item_pointer + 1 and\
504 self.item_pointer > 0:
505 self.item_pointer = len(selectables) - 1
507 switch_widgets(widget, map_widget)
509 self.item_pointer += 1
510 elif key == 'k' and self.item_pointer > 0:
511 self.item_pointer -= 1
512 elif key == action_key and len(selectables) > 0:
513 id_ = selectables[self.item_pointer]
514 self.socket.send('TASK:%s %s' % (task, id_))
516 self.socket.send(bonus_command)
517 if self.item_pointer > 0:
518 self.item_pointer -= 1
521 trigger = widget.check_updates[0]
522 self.to_update[trigger] = True
524 def move_examiner(direction):
525 start_pos = self.examiner_position
526 new_examine_pos = self.game.world.map_.move(start_pos, direction)
528 self.examiner_position = new_examine_pos
529 self.to_update['map'] = True
531 def switch_to_pick_or_drop(target_widget):
532 self.item_pointer = 0
533 switch_widgets(map_widget, target_widget)
534 if self.examiner_mode:
535 self.examiner_mode = False
536 switch_widgets(descriptor_widget, log_widget)
538 def toggle_examiner_mode():
539 if self.examiner_mode:
540 self.examiner_mode = False
541 switch_widgets(descriptor_widget, log_widget)
543 self.examiner_mode = True
544 self.examiner_position = self.game.world.player.position
545 switch_widgets(log_widget, descriptor_widget)
546 self.to_update['map'] = True
549 if popup_widget.visible:
550 popup_widget.visible = False
551 for w in top_widgets:
552 w.ensure_freshness(True)
554 self.to_update['popup'] = True
555 popup_widget.visible = True
556 popup_widget.reconfigure()
557 self.draw_popup_if_visible = True
559 def try_write_keys():
560 if len(key) == 1 and key in ASCII_printable and \
561 len(self.to_send) < len(edit_line_widget):
562 self.to_send += [key]
563 self.to_update['edit'] = True
564 elif key == 'KEY_BACKSPACE':
565 self.to_send[:] = self.to_send[:-1]
566 self.to_update['edit'] = True
567 elif key == '\n': # Return key
568 self.socket.send(''.join(self.to_send))
570 self.to_update['edit'] = True
572 def try_examiner_keys():
574 move_examiner('UPLEFT')
576 move_examiner('UPRIGHT')
578 move_examiner('LEFT')
580 move_examiner('RIGHT')
582 move_examiner('DOWNLEFT')
584 move_examiner('DOWNRIGHT')
586 def try_player_move_keys():
588 self.socket.send('TASK:MOVE UPLEFT')
590 self.socket.send('TASK:MOVE UPRIGHT')
592 self.socket.send('TASK:MOVE LEFT')
594 self.socket.send('TASK:MOVE RIGHT')
596 self.socket.send('TASK:MOVE DOWNLEFT')
598 self.socket.send('TASK:MOVE DOWNRIGHT')
601 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
602 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
603 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
604 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
605 curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_WHITE)
607 # Basic curses initialization work.
609 curses.curs_set(False) # hide cursor
612 # With screen initialized, set up widgets with their curses windows.
613 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
614 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
615 edit_widget.children += [edit_line_widget]
616 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
617 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
618 health_widget = TextLineWidget('HEALTH:', self, (3, 0), (1, 20))
619 health_widget.children += [HealthWidget(self, (3, 8), (1, 12), ['turn'])]
620 log_widget = LogWidget(self, (5, 0), (None, 20), ['log'])
621 descriptor_widget = DescriptorWidget(self, (5, 0), (None, 20),
623 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
624 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
625 ['inventory'], False)
626 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
627 ['pickable_items'], False)
628 top_widgets = [edit_widget, turn_widget, health_widget, log_widget,
629 descriptor_widget, map_widget, inventory_widget,
630 pickable_items_widget]
631 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
633 # Ensure initial window state before loop starts.
634 for w in top_widgets:
635 w.ensure_freshness(True)
636 self.socket.send('GET_GAMESTATE')
641 for w in top_widgets:
642 if w.ensure_freshness():
643 self.draw_popup_if_visible = True
644 if popup_widget.visible and self.draw_popup_if_visible:
645 popup_widget.ensure_freshness(True)
646 self.draw_popup_if_visible = False
647 for k in self.to_update.keys():
648 self.to_update[k] = False
650 # Handle input from server.
653 command = self.queue.get(block=False)
656 self.game.handle_input(command)
658 # Handle keys (and resize event read as key).
660 key = self.stdscr.getkey()
661 if key == 'KEY_RESIZE':
663 setup_screen(curses.initscr())
664 for w in top_widgets:
666 w.ensure_freshness(True)
667 elif key == '\t': # Tabulator key.
668 write_mode = False if write_mode else True
673 elif map_widget.visible:
675 toggle_examiner_mode()
677 self.socket.send('GET_PICKABLE_ITEMS')
678 switch_to_pick_or_drop(pickable_items_widget)
680 switch_to_pick_or_drop(inventory_widget)
681 elif self.examiner_mode:
684 try_player_move_keys()
685 elif pickable_items_widget.visible:
686 pick_or_drop_menu('p', pickable_items_widget,
687 self.game.world.pickable_items,
688 'PICKUP', 'GET_PICKABLE_ITEMS')
689 elif inventory_widget.visible:
690 pick_or_drop_menu('d', inventory_widget,
691 self.game.world.player_inventory,
696 # Quit when server recommends it.
697 if self.game.do_quit:
701 s = socket.create_connection(('127.0.0.1', 5000))
702 plom_socket = PlomSocket(s)
705 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
707 TUI(plom_socket, game, q)