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 setup_screen(self, stdscr):
438 self.stdscr.refresh() # will be called by getkey else, clearing screen
439 self.stdscr.timeout(10)
441 def switch_widgets(self, widget_1, widget_2):
442 widget_1.visible = False
443 widget_2.visible = True
444 x = widget_2.check_updates[0]
445 self.to_update[x] = True
447 def loop(self, stdscr):
448 self.setup_screen(stdscr)
449 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
450 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
451 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
452 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
453 curses.curs_set(False) # hide cursor
455 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
456 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
457 edit_widget.children += [edit_line_widget]
458 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
459 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
460 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
461 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
462 inventory_widget = InventoryWidget(self, (0, 21), (None, None),
463 ['inventory'], False)
464 pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
465 ['pickable_items'], False)
466 top_widgets = [edit_widget, turn_widget, log_widget, map_widget,
467 inventory_widget, pickable_items_widget]
468 popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
469 self.popup_text = 'Hi bob'
471 for w in top_widgets:
472 w.ensure_freshness(True)
473 draw_popup_if_visible = True
475 for w in top_widgets:
476 did_refresh = w.ensure_freshness()
477 draw_popup_if_visible = did_refresh | draw_popup_if_visible
478 if popup_widget.visible and draw_popup_if_visible:
479 popup_widget.ensure_freshness(True)
480 draw_popup_if_visible = False
481 for k in self.to_update.keys():
482 self.to_update[k] = False
485 command = self.queue.get(block=False)
488 self.game.handle_input(command)
490 key = self.stdscr.getkey()
491 if key == 'KEY_RESIZE':
493 self.setup_screen(curses.initscr())
494 for w in top_widgets:
496 w.ensure_freshness(True)
497 elif key == '\t': # Tabulator key.
498 write_mode = False if write_mode else True
500 if len(key) == 1 and key in ASCII_printable and \
501 len(self.to_send) < len(edit_line_widget):
502 self.to_send += [key]
503 self.to_update['edit'] = True
504 elif key == 'KEY_BACKSPACE':
505 self.to_send[:] = self.to_send[:-1]
506 self.to_update['edit'] = True
507 elif key == '\n': # Return key
508 self.socket.send(''.join(self.to_send))
510 self.to_update['edit'] = True
511 elif map_widget.visible:
513 self.socket.send('TASK:MOVE UPLEFT')
515 self.socket.send('TASK:MOVE UPRIGHT')
517 self.socket.send('TASK:MOVE LEFT')
519 self.socket.send('TASK:MOVE RIGHT')
521 self.socket.send('TASK:MOVE DOWNLEFT')
523 self.socket.send('TASK:MOVE DOWNRIGHT')
525 if not popup_widget.visible:
526 self.to_update['popup'] = True
527 popup_widget.visible = True
528 popup_widget.reconfigure()
529 draw_popup_if_visible = True
531 popup_widget.visible = False
532 for w in top_widgets:
533 w.ensure_freshness(True)
535 self.socket.send('GET_PICKABLE_ITEMS')
536 self.item_pointer = 0
537 self.switch_widgets(map_widget, pickable_items_widget)
539 self.item_pointer = 0
540 self.switch_widgets(map_widget, inventory_widget)
541 elif pickable_items_widget.visible:
542 if len(self.game.world.pickable_items) < self.item_pointer + 1\
543 and self.item_pointer > 0:
544 self.item_pointer = len(self.game.world.pickable_items) - 1
545 while len(self.game.world.pickable_items) <= self.item_pointer:
546 self.item_pointer -= 1
548 self.switch_widgets(pickable_items_widget, map_widget)
550 self.item_pointer += 1
551 elif key == 'k' and self.item_pointer > 0:
552 self.item_pointer -= 1
553 elif key == 'p' and \
554 len(self.game.world.pickable_items) > 0:
555 id_ = self.game.world.pickable_items[self.item_pointer]
556 self.socket.send('TASK:PICKUP %s' % id_)
557 self.socket.send('GET_PICKABLE_ITEMS')
558 if self.item_pointer > 0:
559 self.item_pointer -= 1
562 self.to_update['pickable_items'] = True
563 elif inventory_widget.visible:
564 if len(self.game.world.player_inventory) < self.item_pointer + 1\
565 and self.item_pointer > 0:
566 self.item_pointer = len(self.game.world.player_inventory) - 1
568 self.switch_widgets(inventory_widget, map_widget)
570 self.item_pointer += 1
571 elif key == 'k' and self.item_pointer > 0:
572 self.item_pointer -= 1
573 elif key == 'd' and \
574 len(self.game.world.player_inventory) > 0:
575 id_ = self.game.world.player_inventory[self.item_pointer]
576 self.socket.send('TASK:DROP %s' % id_)
577 if self.item_pointer > 0:
578 self.item_pointer -= 1
581 self.to_update['inventory'] = True
584 if self.game.do_quit:
588 s = socket.create_connection(('127.0.0.1', 5000))
589 plom_socket = PlomSocket(s)
592 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
594 TUI(plom_socket, game, q)