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_string, center, size):
38 def map_string_to_lines(map_string):
39 map_view_chars = ['0']
43 map_view_chars += [c, ' ']
46 map_view_chars += ['\n']
50 map_view_chars += ['0']
52 map_view_chars = map_view_chars[:-1]
53 map_view_chars = map_view_chars[:-1]
54 return ''.join(map_view_chars).split('\n')
56 map_lines = map_string_to_lines(map_string)
57 self.y_cut(map_lines, center[0], size[0])
58 map_width = self.size[1] * 2 + 1
59 self.x_cut(map_lines, center[1] * 2, size[1], map_width)
63 class World(WorldBase):
65 def __init__(self, *args, **kwargs):
66 """Extend original with local classes and empty default map.
68 We need the empty default map because we draw the map widget
69 on any update, even before we actually receive map data.
71 super().__init__(*args, **kwargs)
73 self.player_inventory = []
75 self.pickable_items = []
77 def new_map(self, yx):
82 return self.get_thing(self.player_id)
85 def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
88 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
90 def cmd_TURN_FINISHED(game, n):
91 """Do nothing. (This may be extended later.)"""
93 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
95 def cmd_TURN(game, n):
96 """Set game.turn to n, empty game.things."""
98 game.world.things = []
99 game.world.pickable_items = []
100 cmd_TURN.argtypes = 'int:nonneg'
102 def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
103 game.world.map_.set_line(y, terrain_line)
104 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
106 def cmd_GAME_STATE_COMPLETE(game):
107 game.tui.to_update['turn'] = True
108 game.tui.to_update['map'] = True
109 game.tui.to_update['inventory'] = True
111 def cmd_THING_TYPE(game, i, type_):
112 t = game.world.get_thing(i)
114 cmd_THING_TYPE.argtypes = 'int:nonneg string'
116 def cmd_PLAYER_INVENTORY(game, ids):
117 game.world.player_inventory = ids # TODO: test whether valid IDs
118 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
120 def cmd_PICKABLE_ITEMS(game, ids):
121 game.world.pickable_items = ids
122 game.tui.to_update['pickable_items'] = True
123 cmd_PICKABLE_ITEMS.argtypes = 'seq:int:nonneg'
129 self.parser = Parser(self)
130 self.world = World(self)
131 self.thing_type = ThingBase
132 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
133 'TURN_FINISHED': cmd_TURN_FINISHED,
135 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
136 'PLAYER_ID': cmd_PLAYER_ID,
137 'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
138 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
140 'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
141 'THING_TYPE': cmd_THING_TYPE,
142 'THING_POS': cmd_THING_POS}
147 def get_command(self, command_name):
148 from functools import partial
149 if command_name in self.commands:
150 f = partial(self.commands[command_name], self)
151 if hasattr(self.commands[command_name], 'argtypes'):
152 f.argtypes = self.commands[command_name].argtypes
156 def get_string_options(self, string_option_type):
159 def handle_input(self, msg):
165 command, args = self.parser.parse(msg)
167 self.log('UNHANDLED INPUT: ' + msg)
170 except ArgError as e:
171 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
174 """Prefix msg plus newline to self.log_text."""
175 self.log_text = msg + '\n' + self.log_text
176 self.tui.to_update['log'] = True
178 def symbol_for_type(self, type_):
182 elif type_ == 'monster':
184 elif type_ == 'item':
189 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
190 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
193 def recv_loop(plom_socket, game, q):
194 for msg in plom_socket.recv():
200 def __init__(self, tui, start, size, check_updates=[], visible=True):
201 self.check_updates = check_updates
204 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
205 self.size_def = size # store for re-calling .size on SIGWINCH
207 self.do_update = True
208 self.visible = visible
213 return self.win.getmaxyx()
216 def size(self, size):
217 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
218 n_lines, n_cols = size
220 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
222 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
223 self.win.resize(n_lines, n_cols)
226 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
228 def safe_write(self, foo):
230 def to_chars_with_attrs(part):
231 attr = curses.A_NORMAL
233 if not type(part) == str:
234 part_string = part[0]
236 if len(part_string) > 0:
237 return [(char, attr) for char in part_string]
238 elif len(part_string) == 1:
242 chars_with_attrs = []
243 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
244 chars_with_attrs += to_chars_with_attrs(foo)
247 chars_with_attrs += to_chars_with_attrs(part)
249 if len(chars_with_attrs) < len(self):
250 for char_with_attr in chars_with_attrs:
251 self.win.addstr(char_with_attr[0], char_with_attr[1])
252 else: # workaround to <https://stackoverflow.com/q/7063128>
253 cut = chars_with_attrs[:len(self) - 1]
254 last_char_with_attr = chars_with_attrs[len(self) - 1]
255 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
256 last_char_with_attr[0], last_char_with_attr[1])
257 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
259 for char_with_attr in cut:
260 self.win.addstr(char_with_attr[0], char_with_attr[1])
262 def ensure_freshness(self, do_refresh=False):
266 for key in self.check_updates:
267 if key in self.tui.to_update and self.tui.to_update[key]:
275 for child in self.children:
276 did_refresh = child.ensure_freshness(do_refresh) | did_refresh
280 class EditWidget(Widget):
283 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
286 class LogWidget(Widget):
289 line_width = self.size[1]
290 log_lines = self.tui.game.log_text.split('\n')
292 for line in log_lines:
293 to_pad = line_width - (len(line) % line_width)
294 if to_pad == line_width:
296 to_join += [line + ' '*to_pad]
297 self.safe_write((''.join(to_join), curses.color_pair(3)))
300 class PopUpWidget(Widget):
303 self.safe_write(self.tui.popup_text)
305 def reconfigure(self):
307 size = (1, len(self.tui.popup_text))
310 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
311 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
312 self.start = (offset_y, offset_x)
313 self.win.mvwin(self.start[0], self.start[1])
316 class ItemsSelectorWidget(Widget):
318 def draw_item_selector(self, title, selection):
321 for id_ in selection:
322 pointer = '*' if counter == self.tui.item_pointer else ' '
323 t = self.tui.game.world.get_thing(id_)
324 lines += ['%s %s' % (pointer, t.type_)]
326 line_width = self.size[1]
329 to_pad = line_width - (len(line) % line_width)
330 if to_pad == line_width:
332 to_join += [line + ' '*to_pad]
333 self.safe_write((''.join(to_join), curses.color_pair(3)))
336 class InventoryWidget(ItemsSelectorWidget):
339 self.draw_item_selector('INVENTORY:',
340 self.tui.game.world.player_inventory)
342 class PickableItemsWidget(ItemsSelectorWidget):
345 self.draw_item_selector('PICKABLE:',
346 self.tui.game.world.pickable_items)
349 class MapWidget(Widget):
353 def terrain_with_objects():
354 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
355 for t in self.tui.game.world.things:
356 pos_i = self.tui.game.world.map_.get_position_index(t.position)
357 symbol = self.tui.game.symbol_for_type(t.type_)
358 if symbol in {'i'} and terrain_as_list[pos_i] in {'@', 'm'}:
360 terrain_as_list[pos_i] = symbol
361 return ''.join(terrain_as_list)
363 def pad_or_cut_x(lines):
364 line_width = self.size[1]
365 for y in range(len(lines)):
367 if line_width > len(line):
368 to_pad = line_width - (len(line) % line_width)
369 lines[y] = line + '0' * to_pad
371 lines[y] = line[:line_width]
374 if len(lines) < self.size[0]:
375 to_pad = self.size[0] - len(lines)
376 lines += to_pad * ['0' * self.size[1]]
378 def lines_to_colored_chars(lines):
379 chars_with_attrs = []
380 for c in ''.join(lines):
382 chars_with_attrs += [(c, curses.color_pair(1))]
384 chars_with_attrs += [(c, curses.color_pair(4))]
386 chars_with_attrs += [(c, curses.color_pair(2))]
387 elif c in {'x', 'X', '#'}:
388 chars_with_attrs += [(c, curses.color_pair(3))]
390 chars_with_attrs += [c]
391 return chars_with_attrs
393 if self.tui.game.world.map_.terrain == '':
396 self.safe_write(''.join(lines))
399 terrain_with_objects = terrain_with_objects()
400 center = self.tui.game.world.player.position
401 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
405 self.safe_write(lines_to_colored_chars(lines))
408 class TurnWidget(Widget):
411 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
414 class TextLineWidget(Widget):
416 def __init__(self, text_line, *args, **kwargs):
417 self.text_line = text_line
418 super().__init__(*args, **kwargs)
421 self.safe_write(self.text_line)
426 def __init__(self, plom_socket, game, q):
427 self.socket = plom_socket
431 self.parser = Parser(self.game)
433 self.item_pointer = 0
434 curses.wrapper(self.loop)
436 def loop(self, stdscr):
438 def setup_screen(stdscr):
440 self.stdscr.refresh() # will be called by getkey else, clearing screen
441 self.stdscr.timeout(10)
443 def switch_widgets(widget_1, widget_2):
444 widget_1.visible = False
445 widget_2.visible = True
446 trigger = widget_2.check_updates[0]
447 self.to_update[trigger] = True
449 def pick_or_drop_menu(action_key, widget, selectables, task,
451 if len(selectables) < self.item_pointer + 1 and\
452 self.item_pointer > 0:
453 self.item_pointer = len(selectables) - 1
455 switch_widgets(widget, map_widget)
457 self.item_pointer += 1
458 elif key == 'k' and self.item_pointer > 0:
459 self.item_pointer -= 1
460 elif key == action_key and len(selectables) > 0:
461 id_ = selectables[self.item_pointer]
462 self.socket.send('TASK:%s %s' % (task, id_))
464 self.socket.send(bonus_command)
465 if self.item_pointer > 0:
466 self.item_pointer -= 1
469 trigger = widget.check_updates[0]
470 self.to_update[trigger] = True
473 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
474 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
475 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
476 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
477 curses.curs_set(False) # hide cursor
479 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
480 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
481 edit_widget.children += [edit_line_widget]
482 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
483 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
484 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
485 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
486 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
487 ['inventory'], False)
488 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
489 ['pickable_items'], False)
490 top_widgets = [edit_widget, turn_widget, log_widget, map_widget,
491 inventory_widget, pickable_items_widget]
492 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
493 self.popup_text = 'Hi bob'
495 for w in top_widgets:
496 w.ensure_freshness(True)
497 draw_popup_if_visible = True
499 for w in top_widgets:
500 did_refresh = w.ensure_freshness()
501 draw_popup_if_visible = did_refresh | draw_popup_if_visible
502 if popup_widget.visible and draw_popup_if_visible:
503 popup_widget.ensure_freshness(True)
504 draw_popup_if_visible = False
505 for k in self.to_update.keys():
506 self.to_update[k] = False
509 command = self.queue.get(block=False)
512 self.game.handle_input(command)
514 key = self.stdscr.getkey()
515 if key == 'KEY_RESIZE':
517 setup_screen(curses.initscr())
518 for w in top_widgets:
520 w.ensure_freshness(True)
521 elif key == '\t': # Tabulator key.
522 write_mode = False if write_mode else True
524 if len(key) == 1 and key in ASCII_printable and \
525 len(self.to_send) < len(edit_line_widget):
526 self.to_send += [key]
527 self.to_update['edit'] = True
528 elif key == 'KEY_BACKSPACE':
529 self.to_send[:] = self.to_send[:-1]
530 self.to_update['edit'] = True
531 elif key == '\n': # Return key
532 self.socket.send(''.join(self.to_send))
534 self.to_update['edit'] = True
535 elif map_widget.visible:
537 self.socket.send('TASK:MOVE UPLEFT')
539 self.socket.send('TASK:MOVE UPRIGHT')
541 self.socket.send('TASK:MOVE LEFT')
543 self.socket.send('TASK:MOVE RIGHT')
545 self.socket.send('TASK:MOVE DOWNLEFT')
547 self.socket.send('TASK:MOVE DOWNRIGHT')
549 if not popup_widget.visible:
550 self.to_update['popup'] = True
551 popup_widget.visible = True
552 popup_widget.reconfigure()
553 draw_popup_if_visible = True
555 popup_widget.visible = False
556 for w in top_widgets:
557 w.ensure_freshness(True)
559 self.socket.send('GET_PICKABLE_ITEMS')
560 self.item_pointer = 0
561 switch_widgets(map_widget, pickable_items_widget)
563 self.item_pointer = 0
564 switch_widgets(map_widget, inventory_widget)
565 elif pickable_items_widget.visible:
566 pick_or_drop_menu('p', pickable_items_widget,
567 self.game.world.pickable_items,
568 'PICKUP', 'GET_PICKABLE_ITEMS')
569 elif inventory_widget.visible:
570 pick_or_drop_menu('d', inventory_widget,
571 self.game.world.player_inventory,
575 if self.game.do_quit:
579 s = socket.create_connection(('127.0.0.1', 5000))
580 plom_socket = PlomSocket(s)
583 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
585 TUI(plom_socket, game, q)