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 LogWidget(Widget):
288 line_width = self.size[1]
289 log_lines = self.tui.game.log_text.split('\n')
291 for line in log_lines:
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 PopUpWidget(Widget):
302 self.safe_write(self.tui.popup_text)
304 def reconfigure(self):
306 size = (1, len(self.tui.popup_text))
309 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
310 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
311 self.start = (offset_y, offset_x)
312 self.win.mvwin(self.start[0], self.start[1])
315 class ItemsSelectorWidget(Widget):
317 def draw_item_selector(self, title, selection):
320 for id_ in selection:
321 pointer = '*' if counter == self.tui.item_pointer else ' '
322 t = self.tui.game.world.get_thing(id_)
323 lines += ['%s %s' % (pointer, t.type_)]
325 line_width = self.size[1]
328 to_pad = line_width - (len(line) % line_width)
329 if to_pad == line_width:
331 to_join += [line + ' '*to_pad]
332 self.safe_write((''.join(to_join), curses.color_pair(3)))
335 class InventoryWidget(ItemsSelectorWidget):
338 self.draw_item_selector('INVENTORY:',
339 self.tui.game.world.player_inventory)
341 class PickableItemsWidget(ItemsSelectorWidget):
344 self.draw_item_selector('PICKABLE:',
345 self.tui.game.world.pickable_items)
348 class MapWidget(Widget):
350 def __init__(self, *args, **kwargs):
351 super().__init__(*args, **kwargs)
352 self.examine_mode = False
353 self.examine_pos = (0, 0)
357 def annotated_terrain():
358 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
359 for t in self.tui.game.world.things:
360 pos_i = self.tui.game.world.map_.get_position_index(t.position)
361 symbol = self.tui.game.symbol_for_type(t.type_)
362 if terrain_as_list[pos_i][0] in {'i', '@', 'm'}:
363 old_symbol = terrain_as_list[pos_i][0]
364 if old_symbol in {'@', 'm'}:
366 terrain_as_list[pos_i] = (symbol, '+')
368 terrain_as_list[pos_i] = symbol
369 if self.examine_mode:
370 pos_i = self.tui.game.world.map_.\
371 get_position_index(self.examine_pos)
372 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
373 return terrain_as_list
375 def pad_or_cut_x(lines):
376 line_width = self.size[1]
377 for y in range(len(lines)):
379 if line_width > len(line):
380 to_pad = line_width - (len(line) % line_width)
381 lines[y] = line + '0' * to_pad
383 lines[y] = line[:line_width]
386 if len(lines) < self.size[0]:
387 to_pad = self.size[0] - len(lines)
388 lines += to_pad * ['0' * self.size[1]]
390 def lines_to_colored_chars(lines):
391 chars_with_attrs = []
392 for c in ''.join(lines):
394 chars_with_attrs += [(c, curses.color_pair(1))]
396 chars_with_attrs += [(c, curses.color_pair(4))]
398 chars_with_attrs += [(c, curses.color_pair(2))]
399 elif c in {'x', 'X', '#'}:
400 chars_with_attrs += [(c, curses.color_pair(3))]
402 chars_with_attrs += [c]
403 return chars_with_attrs
405 if self.tui.game.world.map_.terrain == '':
408 self.safe_write(''.join(lines))
411 annotated_terrain = annotated_terrain()
412 center = self.tui.game.world.player.position
413 if self.examine_mode:
414 center = self.examine_pos
415 lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
419 self.safe_write(lines_to_colored_chars(lines))
422 class TurnWidget(Widget):
425 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
428 class TextLineWidget(Widget):
430 def __init__(self, text_line, *args, **kwargs):
431 self.text_line = text_line
432 super().__init__(*args, **kwargs)
435 self.safe_write(self.text_line)
440 def __init__(self, plom_socket, game, q):
441 self.socket = plom_socket
445 self.parser = Parser(self.game)
447 self.item_pointer = 0
448 curses.wrapper(self.loop)
450 def loop(self, stdscr):
452 def setup_screen(stdscr):
454 self.stdscr.refresh() # will be called by getkey else, clearing screen
455 self.stdscr.timeout(10)
457 def switch_widgets(widget_1, widget_2):
458 widget_1.visible = False
459 widget_2.visible = True
460 trigger = widget_2.check_updates[0]
461 self.to_update[trigger] = True
463 def pick_or_drop_menu(action_key, widget, selectables, task,
465 if len(selectables) < self.item_pointer + 1 and\
466 self.item_pointer > 0:
467 self.item_pointer = len(selectables) - 1
469 switch_widgets(widget, map_widget)
470 map_widget.examine_mode = False
472 self.item_pointer += 1
473 elif key == 'k' and self.item_pointer > 0:
474 self.item_pointer -= 1
475 elif key == action_key and len(selectables) > 0:
476 id_ = selectables[self.item_pointer]
477 self.socket.send('TASK:%s %s' % (task, id_))
479 self.socket.send(bonus_command)
480 if self.item_pointer > 0:
481 self.item_pointer -= 1
484 trigger = widget.check_updates[0]
485 self.to_update[trigger] = True
487 def move_examiner(direction):
488 start_pos = map_widget.examine_pos
489 new_examine_pos = self.game.world.map_.move(start_pos, direction)
491 map_widget.examine_pos = new_examine_pos
492 self.to_update['map'] = True
495 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
496 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
497 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
498 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
499 curses.curs_set(False) # hide cursor
501 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
502 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
503 edit_widget.children += [edit_line_widget]
504 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
505 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
506 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
507 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
508 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
509 ['inventory'], False)
510 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
511 ['pickable_items'], False)
512 top_widgets = [edit_widget, turn_widget, log_widget, map_widget,
513 inventory_widget, pickable_items_widget]
514 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
515 self.popup_text = 'Hi bob'
517 for w in top_widgets:
518 w.ensure_freshness(True)
519 draw_popup_if_visible = True
521 for w in top_widgets:
522 did_refresh = w.ensure_freshness()
523 draw_popup_if_visible = did_refresh | draw_popup_if_visible
524 if popup_widget.visible and draw_popup_if_visible:
525 popup_widget.ensure_freshness(True)
526 draw_popup_if_visible = False
527 for k in self.to_update.keys():
528 self.to_update[k] = False
531 command = self.queue.get(block=False)
534 self.game.handle_input(command)
536 key = self.stdscr.getkey()
537 if key == 'KEY_RESIZE':
539 setup_screen(curses.initscr())
540 for w in top_widgets:
542 w.ensure_freshness(True)
543 elif key == '\t': # Tabulator key.
544 write_mode = False if write_mode else True
546 if len(key) == 1 and key in ASCII_printable and \
547 len(self.to_send) < len(edit_line_widget):
548 self.to_send += [key]
549 self.to_update['edit'] = True
550 elif key == 'KEY_BACKSPACE':
551 self.to_send[:] = self.to_send[:-1]
552 self.to_update['edit'] = True
553 elif key == '\n': # Return key
554 self.socket.send(''.join(self.to_send))
556 self.to_update['edit'] = True
558 if not popup_widget.visible:
559 self.to_update['popup'] = True
560 popup_widget.visible = True
561 popup_widget.reconfigure()
562 draw_popup_if_visible = True
564 popup_widget.visible = False
565 for w in top_widgets:
566 w.ensure_freshness(True)
567 elif map_widget.visible:
569 map_widget.examine_mode = not map_widget.examine_mode
570 map_widget.examine_pos = self.game.world.player.position
571 self.to_update['map'] = True
573 self.socket.send('GET_PICKABLE_ITEMS')
574 self.item_pointer = 0
575 switch_widgets(map_widget, pickable_items_widget)
577 self.item_pointer = 0
578 switch_widgets(map_widget, inventory_widget)
579 elif map_widget.examine_mode:
581 move_examiner('UPLEFT')
583 move_examiner('UPRIGHT')
585 move_examiner('LEFT')
587 move_examiner('RIGHT')
589 move_examiner('DOWNLEFT')
591 move_examiner('DOWNRIGHT')
593 self.socket.send('TASK:MOVE UPLEFT')
595 self.socket.send('TASK:MOVE UPRIGHT')
597 self.socket.send('TASK:MOVE LEFT')
599 self.socket.send('TASK:MOVE RIGHT')
601 self.socket.send('TASK:MOVE DOWNLEFT')
603 self.socket.send('TASK:MOVE DOWNRIGHT')
604 elif pickable_items_widget.visible:
605 pick_or_drop_menu('p', pickable_items_widget,
606 self.game.world.pickable_items,
607 'PICKUP', 'GET_PICKABLE_ITEMS')
608 elif inventory_widget.visible:
609 pick_or_drop_menu('d', inventory_widget,
610 self.game.world.player_inventory,
614 if self.game.do_quit:
618 s = socket.create_connection(('127.0.0.1', 5000))
619 plom_socket = PlomSocket(s)
622 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
624 TUI(plom_socket, game, q)