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]:
273 for child in self.children:
274 child.ensure_freshness(do_refresh)
277 class EditWidget(Widget):
280 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
283 class LogWidget(Widget):
286 line_width = self.size[1]
287 log_lines = self.tui.game.log_text.split('\n')
289 for line in log_lines:
290 to_pad = line_width - (len(line) % line_width)
291 if to_pad == line_width:
293 to_join += [line + ' '*to_pad]
294 self.safe_write((''.join(to_join), curses.color_pair(3)))
297 class PopUpWidget(Widget):
300 self.safe_write(self.tui.popup_text)
302 def reconfigure(self):
304 size = (1, len(self.tui.popup_text))
307 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
308 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
309 self.start = (offset_y, offset_x)
310 self.win.mvwin(self.start[0], self.start[1])
311 self.ensure_freshness(True)
314 class MapWidget(Widget):
317 if self.tui.view == 'map':
319 elif self.tui.view == 'inventory':
320 self.draw_item_selector('INVENTORY:',
321 self.tui.game.world.player_inventory)
322 elif self.tui.view == 'pickable_items':
323 self.draw_item_selector('PICKABLE:',
324 self.tui.game.world.pickable_items)
326 def draw_item_selector(self, title, selection):
329 for id_ in selection:
330 pointer = '*' if counter == self.tui.item_pointer else ' '
331 t = self.tui.game.world.get_thing(id_)
332 lines += ['%s %s' % (pointer, t.type_)]
334 line_width = self.size[1]
337 to_pad = line_width - (len(line) % line_width)
338 if to_pad == line_width:
340 to_join += [line + ' '*to_pad]
341 self.safe_write((''.join(to_join), curses.color_pair(3)))
345 def terrain_with_objects():
346 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
347 for t in self.tui.game.world.things:
348 pos_i = self.tui.game.world.map_.get_position_index(t.position)
349 symbol = self.tui.game.symbol_for_type(t.type_)
350 if symbol in {'i'} and terrain_as_list[pos_i] in {'@', 'm'}:
352 terrain_as_list[pos_i] = symbol
353 return ''.join(terrain_as_list)
355 def pad_or_cut_x(lines):
356 line_width = self.size[1]
357 for y in range(len(lines)):
359 if line_width > len(line):
360 to_pad = line_width - (len(line) % line_width)
361 lines[y] = line + '0' * to_pad
363 lines[y] = line[:line_width]
366 if len(lines) < self.size[0]:
367 to_pad = self.size[0] - len(lines)
368 lines += to_pad * ['0' * self.size[1]]
370 def lines_to_colored_chars(lines):
371 chars_with_attrs = []
372 for c in ''.join(lines):
374 chars_with_attrs += [(c, curses.color_pair(1))]
376 chars_with_attrs += [(c, curses.color_pair(4))]
378 chars_with_attrs += [(c, curses.color_pair(2))]
379 elif c in {'x', 'X', '#'}:
380 chars_with_attrs += [(c, curses.color_pair(3))]
382 chars_with_attrs += [c]
383 return chars_with_attrs
385 if self.tui.game.world.map_.terrain == '':
388 self.safe_write(''.join(lines))
391 terrain_with_objects = terrain_with_objects()
392 center = self.tui.game.world.player.position
393 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
397 self.safe_write(lines_to_colored_chars(lines))
400 class TurnWidget(Widget):
403 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
406 class TextLineWidget(Widget):
408 def __init__(self, text_line, *args, **kwargs):
409 self.text_line = text_line
410 super().__init__(*args, **kwargs)
413 self.safe_write(self.text_line)
418 def __init__(self, plom_socket, game, q):
419 self.socket = plom_socket
423 self.parser = Parser(self.game)
425 self.item_pointer = 0
426 self.top_widgets = []
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 popup_widget = PopUpWidget(self, (0, 0), (1, 1), ['popup'])
450 popup_widget.visible = False
451 self.popup_text = 'Hi bob'
452 self.top_widgets = [edit_widget, turn_widget, log_widget, map_widget,
456 for w in self.top_widgets:
457 w.ensure_freshness(True)
459 for w in self.top_widgets:
461 for k in self.to_update.keys():
462 self.to_update[k] = False
465 command = self.queue.get(block=False)
468 self.game.handle_input(command)
470 key = self.stdscr.getkey()
471 if key == 'KEY_RESIZE':
473 self.setup_screen(curses.initscr())
474 for w in self.top_widgets:
476 w.ensure_freshness(True)
477 elif key == '\t': # Tabulator key.
478 write_mode = False if write_mode else True
480 if len(key) == 1 and key in ASCII_printable and \
481 len(self.to_send) < len(edit_line_widget):
482 self.to_send += [key]
483 self.to_update['edit'] = True
484 elif key == 'KEY_BACKSPACE':
485 self.to_send[:] = self.to_send[:-1]
486 self.to_update['edit'] = True
487 elif key == '\n': # Return key
488 self.socket.send(''.join(self.to_send))
490 self.to_update['edit'] = True
491 elif self.view == 'map':
493 self.socket.send('TASK:MOVE UPLEFT')
495 self.socket.send('TASK:MOVE UPRIGHT')
497 self.socket.send('TASK:MOVE LEFT')
499 self.socket.send('TASK:MOVE RIGHT')
501 self.socket.send('TASK:MOVE DOWNLEFT')
503 self.socket.send('TASK:MOVE DOWNRIGHT')
505 if not popup_widget.visible:
506 self.to_update['popup'] = True
507 popup_widget.visible = True
508 popup_widget.reconfigure()
510 popup_widget.visible = False
511 for w in self.top_widgets:
512 w.ensure_freshness(True)
514 self.socket.send('GET_PICKABLE_ITEMS')
515 self.item_pointer = 0
516 self.view = 'pickable_items'
518 self.item_pointer = 0
519 self.view = 'inventory'
520 self.to_update['map'] = True
521 elif self.view == 'pickable_items':
524 elif key == 'j' and \
525 len(self.game.world.pickable_items) > \
526 self.item_pointer + 1:
527 self.item_pointer += 1
528 elif key == 'k' and self.item_pointer > 0:
529 self.item_pointer -= 1
530 elif key == 'p' and \
531 len(self.game.world.pickable_items) > 0:
532 id_ = self.game.world.pickable_items[self.item_pointer]
533 self.socket.send('TASK:PICKUP %s' % id_)
534 self.socket.send('GET_PICKABLE_ITEMS')
535 if self.item_pointer > 0:
536 self.item_pointer -= 1
539 self.to_update['map'] = True
540 elif self.view == 'inventory':
543 elif key == 'j' and \
544 len(self.game.world.player_inventory) > \
545 self.item_pointer + 1:
546 self.item_pointer += 1
547 elif key == 'k' and self.item_pointer > 0:
548 self.item_pointer -= 1
549 elif key == 'd' and \
550 len(self.game.world.player_inventory) > 0:
551 id_ = self.game.world.player_inventory[self.item_pointer]
552 self.socket.send('TASK:DROP %s' % id_)
553 if self.item_pointer > 0:
554 self.item_pointer -= 1
557 self.to_update['map'] = True
560 if self.game.do_quit:
564 s = socket.create_connection(('127.0.0.1', 5000))
565 plom_socket = PlomSocket(s)
568 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
570 TUI(plom_socket, game, q)