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 MapBase
9 from plomrogue.io import PlomSocket
10 from plomrogue.things import ThingBase
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)
76 self.player_inventory = []
78 self.pickable_items = []
80 def new_map(self, 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):
352 def annotated_terrain():
353 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
354 for t in self.tui.game.world.things:
355 pos_i = self.tui.game.world.map_.get_position_index(t.position)
356 symbol = self.tui.game.symbol_for_type(t.type_)
357 if terrain_as_list[pos_i][0] in {'i', '@', 'm'}:
358 old_symbol = terrain_as_list[pos_i][0]
359 if old_symbol in {'@', 'm'}:
361 terrain_as_list[pos_i] = (symbol, '+')
363 terrain_as_list[pos_i] = symbol
364 return terrain_as_list
366 def pad_or_cut_x(lines):
367 line_width = self.size[1]
368 for y in range(len(lines)):
370 if line_width > len(line):
371 to_pad = line_width - (len(line) % line_width)
372 lines[y] = line + '0' * to_pad
374 lines[y] = line[:line_width]
377 if len(lines) < self.size[0]:
378 to_pad = self.size[0] - len(lines)
379 lines += to_pad * ['0' * self.size[1]]
381 def lines_to_colored_chars(lines):
382 chars_with_attrs = []
383 for c in ''.join(lines):
385 chars_with_attrs += [(c, curses.color_pair(1))]
387 chars_with_attrs += [(c, curses.color_pair(4))]
389 chars_with_attrs += [(c, curses.color_pair(2))]
390 elif c in {'x', 'X', '#'}:
391 chars_with_attrs += [(c, curses.color_pair(3))]
393 chars_with_attrs += [c]
394 return chars_with_attrs
396 if self.tui.game.world.map_.terrain == '':
399 self.safe_write(''.join(lines))
402 annotated_terrain = annotated_terrain()
403 center = self.tui.game.world.player.position
404 lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
408 self.safe_write(lines_to_colored_chars(lines))
411 class TurnWidget(Widget):
414 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
417 class TextLineWidget(Widget):
419 def __init__(self, text_line, *args, **kwargs):
420 self.text_line = text_line
421 super().__init__(*args, **kwargs)
424 self.safe_write(self.text_line)
429 def __init__(self, plom_socket, game, q):
430 self.socket = plom_socket
434 self.parser = Parser(self.game)
436 self.item_pointer = 0
437 curses.wrapper(self.loop)
439 def loop(self, stdscr):
441 def setup_screen(stdscr):
443 self.stdscr.refresh() # will be called by getkey else, clearing screen
444 self.stdscr.timeout(10)
446 def switch_widgets(widget_1, widget_2):
447 widget_1.visible = False
448 widget_2.visible = True
449 trigger = widget_2.check_updates[0]
450 self.to_update[trigger] = True
452 def pick_or_drop_menu(action_key, widget, selectables, task,
454 if len(selectables) < self.item_pointer + 1 and\
455 self.item_pointer > 0:
456 self.item_pointer = len(selectables) - 1
458 switch_widgets(widget, map_widget)
460 self.item_pointer += 1
461 elif key == 'k' and self.item_pointer > 0:
462 self.item_pointer -= 1
463 elif key == action_key and len(selectables) > 0:
464 id_ = selectables[self.item_pointer]
465 self.socket.send('TASK:%s %s' % (task, id_))
467 self.socket.send(bonus_command)
468 if self.item_pointer > 0:
469 self.item_pointer -= 1
472 trigger = widget.check_updates[0]
473 self.to_update[trigger] = True
476 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
477 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
478 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
479 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
480 curses.curs_set(False) # hide cursor
482 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
483 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
484 edit_widget.children += [edit_line_widget]
485 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
486 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
487 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
488 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
489 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
490 ['inventory'], False)
491 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
492 ['pickable_items'], False)
493 top_widgets = [edit_widget, turn_widget, log_widget, map_widget,
494 inventory_widget, pickable_items_widget]
495 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
496 self.popup_text = 'Hi bob'
498 for w in top_widgets:
499 w.ensure_freshness(True)
500 draw_popup_if_visible = True
502 for w in top_widgets:
503 did_refresh = w.ensure_freshness()
504 draw_popup_if_visible = did_refresh | draw_popup_if_visible
505 if popup_widget.visible and draw_popup_if_visible:
506 popup_widget.ensure_freshness(True)
507 draw_popup_if_visible = False
508 for k in self.to_update.keys():
509 self.to_update[k] = False
512 command = self.queue.get(block=False)
515 self.game.handle_input(command)
517 key = self.stdscr.getkey()
518 if key == 'KEY_RESIZE':
520 setup_screen(curses.initscr())
521 for w in top_widgets:
523 w.ensure_freshness(True)
524 elif key == '\t': # Tabulator key.
525 write_mode = False if write_mode else True
527 if len(key) == 1 and key in ASCII_printable and \
528 len(self.to_send) < len(edit_line_widget):
529 self.to_send += [key]
530 self.to_update['edit'] = True
531 elif key == 'KEY_BACKSPACE':
532 self.to_send[:] = self.to_send[:-1]
533 self.to_update['edit'] = True
534 elif key == '\n': # Return key
535 self.socket.send(''.join(self.to_send))
537 self.to_update['edit'] = True
539 if not popup_widget.visible:
540 self.to_update['popup'] = True
541 popup_widget.visible = True
542 popup_widget.reconfigure()
543 draw_popup_if_visible = True
545 popup_widget.visible = False
546 for w in top_widgets:
547 w.ensure_freshness(True)
548 elif map_widget.visible:
550 self.socket.send('TASK:MOVE UPLEFT')
552 self.socket.send('TASK:MOVE UPRIGHT')
554 self.socket.send('TASK:MOVE LEFT')
556 self.socket.send('TASK:MOVE RIGHT')
558 self.socket.send('TASK:MOVE DOWNLEFT')
560 self.socket.send('TASK:MOVE DOWNRIGHT')
562 self.socket.send('GET_PICKABLE_ITEMS')
563 self.item_pointer = 0
564 switch_widgets(map_widget, pickable_items_widget)
566 self.item_pointer = 0
567 switch_widgets(map_widget, inventory_widget)
568 elif pickable_items_widget.visible:
569 pick_or_drop_menu('p', pickable_items_widget,
570 self.game.world.pickable_items,
571 'PICKUP', 'GET_PICKABLE_ITEMS')
572 elif inventory_widget.visible:
573 pick_or_drop_menu('d', inventory_widget,
574 self.game.world.player_inventory,
578 if self.game.do_quit:
582 s = socket.create_connection(('127.0.0.1', 5000))
583 plom_socket = PlomSocket(s)
586 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
588 TUI(plom_socket, game, q)