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
110 def cmd_THING_TYPE(game, i, type_):
111 t = game.world.get_thing(i)
113 cmd_THING_TYPE.argtypes = 'int:nonneg string'
115 def cmd_PLAYER_INVENTORY(game, ids):
116 game.world.player_inventory = ids # TODO: test whether valid IDs
117 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
119 def cmd_PICKABLE_ITEMS(game, ids):
120 game.world.pickable_items = ids
121 game.tui.to_update['map'] = True
122 cmd_PICKABLE_ITEMS.argtypes = 'seq:int:nonneg'
128 self.parser = Parser(self)
129 self.world = World(self)
130 self.thing_type = ThingBase
131 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
132 'TURN_FINISHED': cmd_TURN_FINISHED,
134 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
135 'PLAYER_ID': cmd_PLAYER_ID,
136 'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
137 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
139 'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
140 'THING_TYPE': cmd_THING_TYPE,
141 'THING_POS': cmd_THING_POS}
146 def get_command(self, command_name):
147 from functools import partial
148 if command_name in self.commands:
149 f = partial(self.commands[command_name], self)
150 if hasattr(self.commands[command_name], 'argtypes'):
151 f.argtypes = self.commands[command_name].argtypes
155 def get_string_options(self, string_option_type):
158 def handle_input(self, msg):
164 command, args = self.parser.parse(msg)
166 self.log('UNHANDLED INPUT: ' + msg)
169 except ArgError as e:
170 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
173 """Prefix msg plus newline to self.log_text."""
174 self.log_text = msg + '\n' + self.log_text
175 self.tui.to_update['log'] = True
177 def symbol_for_type(self, type_):
181 elif type_ == 'monster':
183 elif type_ == 'item':
188 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
189 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
192 def recv_loop(plom_socket, game, q):
193 for msg in plom_socket.recv():
199 def __init__(self, tui, start, size, check_updates=[]):
200 self.check_updates = check_updates
203 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
204 self.size_def = size # store for re-calling .size on SIGWINCH
206 self.do_update = True
212 return self.win.getmaxyx()
215 def size(self, size):
216 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
217 n_lines, n_cols = size
219 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
221 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
222 self.win.resize(n_lines, n_cols)
225 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
227 def safe_write(self, foo):
229 def to_chars_with_attrs(part):
230 attr = curses.A_NORMAL
232 if not type(part) == str:
233 part_string = part[0]
235 if len(part_string) > 0:
236 return [(char, attr) for char in part_string]
237 elif len(part_string) == 1:
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 MapWidget(Widget):
318 if self.tui.view == 'map':
320 elif self.tui.view == 'inventory':
321 self.draw_item_selector('INVENTORY:',
322 self.tui.game.world.player_inventory)
323 elif self.tui.view == 'pickable_items':
324 self.draw_item_selector('PICKABLE:',
325 self.tui.game.world.pickable_items)
327 def draw_item_selector(self, title, selection):
330 for id_ in selection:
331 pointer = '*' if counter == self.tui.item_pointer else ' '
332 t = self.tui.game.world.get_thing(id_)
333 lines += ['%s %s' % (pointer, t.type_)]
335 line_width = self.size[1]
338 to_pad = line_width - (len(line) % line_width)
339 if to_pad == line_width:
341 to_join += [line + ' '*to_pad]
342 self.safe_write((''.join(to_join), curses.color_pair(3)))
346 def terrain_with_objects():
347 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
348 for t in self.tui.game.world.things:
349 pos_i = self.tui.game.world.map_.get_position_index(t.position)
350 symbol = self.tui.game.symbol_for_type(t.type_)
351 if symbol in {'i'} and terrain_as_list[pos_i] in {'@', 'm'}:
353 terrain_as_list[pos_i] = symbol
354 return ''.join(terrain_as_list)
356 def pad_or_cut_x(lines):
357 line_width = self.size[1]
358 for y in range(len(lines)):
360 if line_width > len(line):
361 to_pad = line_width - (len(line) % line_width)
362 lines[y] = line + '0' * to_pad
364 lines[y] = line[:line_width]
367 if len(lines) < self.size[0]:
368 to_pad = self.size[0] - len(lines)
369 lines += to_pad * ['0' * self.size[1]]
371 def lines_to_colored_chars(lines):
372 chars_with_attrs = []
373 for c in ''.join(lines):
375 chars_with_attrs += [(c, curses.color_pair(1))]
377 chars_with_attrs += [(c, curses.color_pair(4))]
379 chars_with_attrs += [(c, curses.color_pair(2))]
380 elif c in {'x', 'X', '#'}:
381 chars_with_attrs += [(c, curses.color_pair(3))]
383 chars_with_attrs += [c]
384 return chars_with_attrs
386 if self.tui.game.world.map_.terrain == '':
389 self.safe_write(''.join(lines))
392 terrain_with_objects = terrain_with_objects()
393 center = self.tui.game.world.player.position
394 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
398 self.safe_write(lines_to_colored_chars(lines))
401 class TurnWidget(Widget):
404 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
407 class TextLineWidget(Widget):
409 def __init__(self, text_line, *args, **kwargs):
410 self.text_line = text_line
411 super().__init__(*args, **kwargs)
414 self.safe_write(self.text_line)
419 def __init__(self, plom_socket, game, q):
420 self.socket = plom_socket
424 self.parser = Parser(self.game)
426 self.item_pointer = 0
427 curses.wrapper(self.loop)
429 def setup_screen(self, stdscr):
431 self.stdscr.refresh() # will be called by getkey else, clearing screen
432 self.stdscr.timeout(10)
434 def loop(self, stdscr):
435 self.setup_screen(stdscr)
436 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
437 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
438 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
439 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
440 curses.curs_set(False) # hide cursor
442 edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
443 edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
444 edit_widget.children += [edit_line_widget]
445 turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
446 turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
447 log_widget = LogWidget(self, (4, 0), (None, 20), ['log'])
448 map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
449 top_widgets = [edit_widget, turn_widget, log_widget, map_widget]
450 popup_widget = PopUpWidget(self, (0, 0), (1, 1))
451 popup_widget.visible = False
452 self.popup_text = 'Hi bob'
455 for w in top_widgets:
456 w.ensure_freshness(True)
457 draw_popup_if_visible = True
459 for w in top_widgets:
460 did_refresh = w.ensure_freshness()
461 draw_popup_if_visible = did_refresh | draw_popup_if_visible
462 if popup_widget.visible and draw_popup_if_visible:
463 popup_widget.ensure_freshness(True)
464 draw_popup_if_visible = False
465 for k in self.to_update.keys():
466 self.to_update[k] = False
469 command = self.queue.get(block=False)
472 self.game.handle_input(command)
474 key = self.stdscr.getkey()
475 if key == 'KEY_RESIZE':
477 self.setup_screen(curses.initscr())
478 for w in top_widgets:
480 w.ensure_freshness(True)
481 elif key == '\t': # Tabulator key.
482 write_mode = False if write_mode else True
484 if len(key) == 1 and key in ASCII_printable and \
485 len(self.to_send) < len(edit_line_widget):
486 self.to_send += [key]
487 self.to_update['edit'] = True
488 elif key == 'KEY_BACKSPACE':
489 self.to_send[:] = self.to_send[:-1]
490 self.to_update['edit'] = True
491 elif key == '\n': # Return key
492 self.socket.send(''.join(self.to_send))
494 self.to_update['edit'] = True
495 elif self.view == 'map':
497 self.socket.send('TASK:MOVE UPLEFT')
499 self.socket.send('TASK:MOVE UPRIGHT')
501 self.socket.send('TASK:MOVE LEFT')
503 self.socket.send('TASK:MOVE RIGHT')
505 self.socket.send('TASK:MOVE DOWNLEFT')
507 self.socket.send('TASK:MOVE DOWNRIGHT')
509 if not popup_widget.visible:
510 self.to_update['popup'] = True
511 popup_widget.visible = True
512 popup_widget.reconfigure()
513 draw_popup_if_visible = True
515 popup_widget.visible = False
516 for w in top_widgets:
517 w.ensure_freshness(True)
519 self.socket.send('GET_PICKABLE_ITEMS')
520 self.item_pointer = 0
521 self.view = 'pickable_items'
523 self.item_pointer = 0
524 self.view = 'inventory'
525 self.to_update['map'] = True
526 elif self.view == 'pickable_items':
527 if len(self.game.world.pickable_items) < self.item_pointer + 1\
528 and self.item_pointer > 0:
529 self.item_pointer = len(self.game.world.pickable_items) - 1
530 while len(self.game.world.pickable_items) <= self.item_pointer:
531 self.item_pointer -= 1
535 self.item_pointer += 1
536 elif key == 'k' and self.item_pointer > 0:
537 self.item_pointer -= 1
538 elif key == 'p' and \
539 len(self.game.world.pickable_items) > 0:
540 id_ = self.game.world.pickable_items[self.item_pointer]
541 self.socket.send('TASK:PICKUP %s' % id_)
542 self.socket.send('GET_PICKABLE_ITEMS')
543 if self.item_pointer > 0:
544 self.item_pointer -= 1
547 self.to_update['map'] = True
548 elif self.view == 'inventory':
549 if len(self.game.world.player_inventory) < self.item_pointer + 1\
550 and self.item_pointer > 0:
551 self.item_pointer = len(self.game.world.player_inventory) - 1
555 self.item_pointer += 1
556 elif key == 'k' and self.item_pointer > 0:
557 self.item_pointer -= 1
558 elif key == 'd' and \
559 len(self.game.world.player_inventory) > 0:
560 id_ = self.game.world.player_inventory[self.item_pointer]
561 self.socket.send('TASK:DROP %s' % id_)
562 if self.item_pointer > 0:
563 self.item_pointer -= 1
566 self.to_update['map'] = True
569 if self.game.do_quit:
573 s = socket.create_connection(('127.0.0.1', 5000))
574 plom_socket = PlomSocket(s)
577 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
579 TUI(plom_socket, game, q)