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, curses.color_pair(5))]
418 chars_with_attrs += [c]
419 return chars_with_attrs
421 if self.tui.game.world.map_.terrain == '':
424 self.safe_write(''.join(lines))
427 annotated_terrain = annotated_terrain()
428 center = self.tui.game.world.player.position
429 if self.tui.examiner_mode:
430 center = self.tui.examiner_position
431 lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
435 self.safe_write(lines_to_colored_chars(lines))
438 class TurnWidget(Widget):
441 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
444 class TextLineWidget(Widget):
446 def __init__(self, text_line, *args, **kwargs):
447 self.text_line = text_line
448 super().__init__(*args, **kwargs)
451 self.safe_write(self.text_line)
456 def __init__(self, plom_socket, game, q):
457 self.socket = plom_socket
461 self.parser = Parser(self.game)
463 self.item_pointer = 0
464 self.examiner_position = (0, 0)
465 self.examiner_mode = False
466 self.popup_text = 'Hi bob'
468 self.draw_popup_if_visible = True
469 curses.wrapper(self.loop)
471 def loop(self, stdscr):
473 def setup_screen(stdscr):
475 self.stdscr.refresh() # will be called by getkey else, clearing screen
476 self.stdscr.timeout(10)
478 def switch_widgets(widget_1, widget_2):
479 widget_1.visible = False
480 widget_2.visible = True
481 trigger = widget_2.check_updates[0]
482 self.to_update[trigger] = True
484 def pick_or_drop_menu(action_key, widget, selectables, task,
486 if len(selectables) < self.item_pointer + 1 and\
487 self.item_pointer > 0:
488 self.item_pointer = len(selectables) - 1
490 switch_widgets(widget, map_widget)
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
515 def switch_to_pick_or_drop(target_widget):
516 self.item_pointer = 0
517 switch_widgets(map_widget, target_widget)
518 if self.examiner_mode:
519 self.examiner_mode = False
520 switch_widgets(descriptor_widget, log_widget)
522 def toggle_examiner_mode():
523 if self.examiner_mode:
524 self.examiner_mode = False
525 switch_widgets(descriptor_widget, log_widget)
527 self.examiner_mode = True
528 self.examiner_position = self.game.world.player.position
529 switch_widgets(log_widget, descriptor_widget)
530 self.to_update['map'] = True
533 if popup_widget.visible:
534 popup_widget.visible = False
535 for w in top_widgets:
536 w.ensure_freshness(True)
538 self.to_update['popup'] = True
539 popup_widget.visible = True
540 popup_widget.reconfigure()
541 self.draw_popup_if_visible = True
543 def try_write_keys():
544 if len(key) == 1 and key in ASCII_printable and \
545 len(self.to_send) < len(edit_line_widget):
546 self.to_send += [key]
547 self.to_update['edit'] = True
548 elif key == 'KEY_BACKSPACE':
549 self.to_send[:] = self.to_send[:-1]
550 self.to_update['edit'] = True
551 elif key == '\n': # Return key
552 self.socket.send(''.join(self.to_send))
554 self.to_update['edit'] = True
556 def try_examiner_keys():
558 move_examiner('UPLEFT')
560 move_examiner('UPRIGHT')
562 move_examiner('LEFT')
564 move_examiner('RIGHT')
566 move_examiner('DOWNLEFT')
568 move_examiner('DOWNRIGHT')
570 def try_player_move_keys():
572 self.socket.send('TASK:MOVE UPLEFT')
574 self.socket.send('TASK:MOVE UPRIGHT')
576 self.socket.send('TASK:MOVE LEFT')
578 self.socket.send('TASK:MOVE RIGHT')
580 self.socket.send('TASK:MOVE DOWNLEFT')
582 self.socket.send('TASK:MOVE DOWNRIGHT')
585 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
586 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
587 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
588 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
589 curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_WHITE)
591 # Basic curses initialization work.
593 curses.curs_set(False) # hide cursor
596 # With screen initialized, set up widgets with their curses windows.
597 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
598 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
599 edit_widget.children += [edit_line_widget]
600 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
601 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
602 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
603 descriptor_widget = DescriptorWidget(self, (4, 0), (None, 20),
604 ['descriptor'], False)
605 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
606 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
607 ['inventory'], False)
608 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
609 ['pickable_items'], False)
610 top_widgets = [edit_widget, turn_widget, log_widget,
611 descriptor_widget, map_widget, inventory_widget,
612 pickable_items_widget]
613 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
615 # Ensure initial window state before loop starts.
616 for w in top_widgets:
617 w.ensure_freshness(True)
618 self.socket.send('GET_GAMESTATE')
623 for w in top_widgets:
624 if w.ensure_freshness():
625 self.draw_popup_if_visible = True
626 if popup_widget.visible and self.draw_popup_if_visible:
627 popup_widget.ensure_freshness(True)
628 self.draw_popup_if_visible = False
629 for k in self.to_update.keys():
630 self.to_update[k] = False
632 # Handle input from server.
635 command = self.queue.get(block=False)
638 self.game.handle_input(command)
640 # Handle keys (and resize event read as key).
642 key = self.stdscr.getkey()
643 if key == 'KEY_RESIZE':
645 setup_screen(curses.initscr())
646 for w in top_widgets:
648 w.ensure_freshness(True)
649 elif key == '\t': # Tabulator key.
650 write_mode = False if write_mode else True
655 elif map_widget.visible:
657 toggle_examiner_mode()
659 self.socket.send('GET_PICKABLE_ITEMS')
660 switch_to_pick_or_drop(pickable_items_widget)
662 switch_to_pick_or_drop(inventory_widget)
663 elif self.examiner_mode:
666 try_player_move_keys()
667 elif pickable_items_widget.visible:
668 pick_or_drop_menu('p', pickable_items_widget,
669 self.game.world.pickable_items,
670 'PICKUP', 'GET_PICKABLE_ITEMS')
671 elif inventory_widget.visible:
672 pick_or_drop_menu('d', inventory_widget,
673 self.game.world.player_inventory,
678 # Quit when server recommends it.
679 if self.game.do_quit:
683 s = socket.create_connection(('127.0.0.1', 5000))
684 plom_socket = PlomSocket(s)
687 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
689 TUI(plom_socket, game, q)