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
16 def y_cut(self, map_lines, center_y, view_height):
17 map_height = len(map_lines)
18 if map_height > view_height and center_y > view_height / 2:
19 if center_y > map_height - view_height / 2:
20 map_lines[:] = map_lines[map_height - view_height:]
22 start = center_y - int(view_height / 2) - 1
23 map_lines[:] = map_lines[start:start + view_height]
25 def x_cut(self, map_lines, center_x, view_width, map_width):
26 if map_width > view_width and center_x > view_width / 2:
27 if center_x > map_width - view_width / 2:
28 cut_start = map_width - view_width
31 cut_start = center_x - int(view_width / 2)
32 cut_end = cut_start + view_width
33 map_lines[:] = [line[cut_start:cut_end] for line in map_lines]
35 def format_to_view(self, map_string, center, size):
37 def map_string_to_lines(map_string):
38 map_view_chars = ['0']
42 map_view_chars += [c, ' ']
45 map_view_chars += ['\n']
49 map_view_chars += ['0']
51 map_view_chars = map_view_chars[:-1]
52 map_view_chars = map_view_chars[:-1]
53 return ''.join(map_view_chars).split('\n')
55 map_lines = map_string_to_lines(map_string)
56 self.y_cut(map_lines, center[0], size[0])
57 map_width = self.size[1] * 2 + 1
58 self.x_cut(map_lines, center[1] * 2, size[1], map_width)
62 class World(WorldBase):
64 def __init__(self, *args, **kwargs):
65 """Extend original with local classes and empty default map.
67 We need the empty default map because we draw the map widget
68 on any update, even before we actually receive map data.
70 super().__init__(*args, **kwargs)
72 self.player_inventory = []
74 self.pickable_items = []
76 def new_map(self, yx):
81 return self.get_thing(self.player_id)
84 def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
87 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
89 def cmd_TURN_FINISHED(game, n):
90 """Do nothing. (This may be extended later.)"""
92 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
94 def cmd_TURN(game, n):
95 """Set game.turn to n, empty game.things."""
97 game.world.things = []
98 game.world.pickable_items = []
99 game.to_update['turn'] = False
100 game.to_update['map'] = False
101 cmd_TURN.argtypes = 'int:nonneg'
103 def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
104 game.world.map_.set_line(y, terrain_line)
105 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
107 def cmd_GAME_STATE_COMPLETE(game):
108 game.to_update['turn'] = True
109 game.to_update['map'] = 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.to_update['map'] = 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}
150 self.to_update_lock = False
152 def get_command(self, command_name):
153 from functools import partial
154 if command_name in self.commands:
155 f = partial(self.commands[command_name], self)
156 if hasattr(self.commands[command_name], 'argtypes'):
157 f.argtypes = self.commands[command_name].argtypes
161 def get_string_options(self, string_option_type):
164 def handle_input(self, msg):
170 command, args = self.parser.parse(msg)
172 self.log('UNHANDLED INPUT: ' + msg)
173 self.to_update['log'] = True
176 except ArgError as e:
177 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
178 self.to_update['log'] = True
181 """Prefix msg plus newline to self.log_text."""
182 self.log_text = msg + '\n' + self.log_text
183 self.to_update['log'] = True
185 def symbol_for_type(self, type_):
189 elif type_ == 'monster':
191 elif type_ == 'item':
196 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
197 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
200 def recv_loop(plom_socket, game):
201 for msg in plom_socket.recv():
202 game.handle_input(msg)
207 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
208 self.check_game = check_game
209 self.check_tui = check_tui
212 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
213 self.size_def = size # store for re-calling .size on SIGWINCH
215 self.do_update = True
220 return self.win.getmaxyx()
223 def size(self, size):
224 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
225 n_lines, n_cols = size
227 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
229 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
230 self.win.resize(n_lines, n_cols)
233 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
235 def safe_write(self, foo):
237 def to_chars_with_attrs(part):
238 attr = curses.A_NORMAL
240 if not type(part) == str:
241 part_string = part[0]
243 if len(part_string) > 0:
244 return [(char, attr) for char in part_string]
245 elif len(part_string) == 1:
249 chars_with_attrs = []
250 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
251 chars_with_attrs += to_chars_with_attrs(foo)
254 chars_with_attrs += to_chars_with_attrs(part)
256 if len(chars_with_attrs) < len(self):
257 for char_with_attr in chars_with_attrs:
258 self.win.addstr(char_with_attr[0], char_with_attr[1])
259 else: # workaround to <https://stackoverflow.com/q/7063128>
260 cut = chars_with_attrs[:len(self) - 1]
261 last_char_with_attr = chars_with_attrs[len(self) - 1]
262 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
263 last_char_with_attr[0], last_char_with_attr[1])
264 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
266 for char_with_attr in cut:
267 self.win.addstr(char_with_attr[0], char_with_attr[1])
269 def ensure_freshness(self, do_refresh=False):
273 for key in self.check_game:
274 if key in self.tui.game.to_update and self.tui.game.to_update[key]:
278 for key in self.check_tui:
279 if key in self.tui.to_update and self.tui.to_update[key]:
288 class EditWidget(Widget):
291 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
294 class LogWidget(Widget):
297 line_width = self.size[1]
298 log_lines = self.tui.game.log_text.split('\n')
300 for line in log_lines:
301 to_pad = line_width - (len(line) % line_width)
302 if to_pad == line_width:
304 to_join += [line + ' '*to_pad]
305 self.safe_write((''.join(to_join), curses.color_pair(3)))
308 class PopUpWidget(Widget):
311 self.safe_write(self.tui.popup_text)
313 def reconfigure(self):
315 size = (1, len(self.tui.popup_text))
318 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
319 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
320 self.start = (offset_y, offset_x)
321 self.win.mvwin(self.start[0], self.start[1])
322 self.ensure_freshness(True)
326 class MapWidget(Widget):
329 if self.tui.view == 'map':
331 elif self.tui.view == 'inventory':
332 self.draw_item_selector('INVENTORY:',
333 self.tui.game.world.player_inventory)
334 elif self.tui.view == 'pickable_items':
335 self.draw_item_selector('PICKABLE:',
336 self.tui.game.world.pickable_items)
338 def draw_item_selector(self, title, selection):
341 for id_ in selection:
342 pointer = '*' if counter == self.tui.item_pointer else ' '
343 t = self.tui.game.world.get_thing(id_)
344 lines += ['%s %s' % (pointer, t.type_)]
346 line_width = self.size[1]
349 to_pad = line_width - (len(line) % line_width)
350 if to_pad == line_width:
352 to_join += [line + ' '*to_pad]
353 self.safe_write((''.join(to_join), curses.color_pair(3)))
357 def terrain_with_objects():
358 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
359 for t in self.tui.game.world.things:
360 pos_i = self.tui.game.world.map_.get_position_index(t.position)
361 symbol = self.tui.game.symbol_for_type(t.type_)
362 if symbol in {'i'} and terrain_as_list[pos_i] in {'@', 'm'}:
364 terrain_as_list[pos_i] = symbol
365 return ''.join(terrain_as_list)
367 def pad_or_cut_x(lines):
368 line_width = self.size[1]
369 for y in range(len(lines)):
371 if line_width > len(line):
372 to_pad = line_width - (len(line) % line_width)
373 lines[y] = line + '0' * to_pad
375 lines[y] = line[:line_width]
378 if len(lines) < self.size[0]:
379 to_pad = self.size[0] - len(lines)
380 lines += to_pad * ['0' * self.size[1]]
382 def lines_to_colored_chars(lines):
383 chars_with_attrs = []
384 for c in ''.join(lines):
386 chars_with_attrs += [(c, curses.color_pair(1))]
388 chars_with_attrs += [(c, curses.color_pair(4))]
390 chars_with_attrs += [(c, curses.color_pair(2))]
391 elif c in {'x', 'X', '#'}:
392 chars_with_attrs += [(c, curses.color_pair(3))]
394 chars_with_attrs += [c]
395 return chars_with_attrs
397 if self.tui.game.world.map_.terrain == '':
400 self.safe_write(''.join(lines))
403 terrain_with_objects = terrain_with_objects()
404 center = self.tui.game.world.player.position
405 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
409 self.safe_write(lines_to_colored_chars(lines))
412 class TurnWidget(Widget):
415 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
420 def __init__(self, plom_socket, game):
421 self.socket = plom_socket
423 self.parser = Parser(self.game)
424 self.to_update = {'edit': False}
425 self.item_pointer = 0
426 curses.wrapper(self.loop)
428 def draw_screen(self):
429 self.stdscr.addstr(0, 0, 'SEND:')
430 self.stdscr.addstr(2, 0, 'TURN:')
432 def setup_screen(self, stdscr):
434 self.stdscr.refresh() # will be called by getkey else, clearing screen
435 self.stdscr.timeout(10)
438 def loop(self, stdscr):
439 self.setup_screen(stdscr)
440 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
441 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
442 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
443 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
444 curses.curs_set(False) # hide cursor
446 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
447 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
448 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
449 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
450 self.popup = PopUpWidget(self, (0, 0), (1, 1), ['popup'])
451 self.popup.visible = False
452 self.popup_text = 'Hi bob'
453 widgets = (self.edit, self.turn, self.log, self.map_, self.popup)
459 for key in self.game.to_update:
460 self.game.to_update[key] = False
461 for key in self.to_update:
462 self.to_update[key] = False
464 key = self.stdscr.getkey()
465 if key == 'KEY_RESIZE':
467 self.setup_screen(curses.initscr())
470 w.ensure_freshness(True)
471 elif key == '\t': # Tabulator key.
472 write_mode = False if write_mode else True
474 if len(key) == 1 and key in ASCII_printable and \
475 len(self.to_send) < len(self.edit):
476 self.to_send += [key]
477 self.to_update['edit'] = True
478 elif key == 'KEY_BACKSPACE':
479 self.to_send[:] = self.to_send[:-1]
480 self.to_update['edit'] = True
481 elif key == '\n': # Return key
482 self.socket.send(''.join(self.to_send))
484 self.to_update['edit'] = True
485 elif self.view == 'map':
487 self.socket.send('TASK:MOVE UPLEFT')
489 self.socket.send('TASK:MOVE UPRIGHT')
491 self.socket.send('TASK:MOVE LEFT')
493 self.socket.send('TASK:MOVE RIGHT')
495 self.socket.send('TASK:MOVE DOWNLEFT')
497 self.socket.send('TASK:MOVE DOWNRIGHT')
499 if not self.popup.visible:
500 self.to_update['popup'] = True
501 self.popup.visible = True
502 self.popup.reconfigure()
504 self.popup.visible = False
505 self.stdscr.erase() # we'll call refresh here so
506 self.stdscr.refresh() # getkey doesn't, erasing screen
509 w.ensure_freshness(True)
511 self.socket.send('GET_PICKABLE_ITEMS')
512 self.item_pointer = 0
513 self.view = 'pickable_items'
515 self.item_pointer = 0
516 self.view = 'inventory'
517 self.game.to_update['map'] = True
518 elif self.view == 'pickable_items':
521 elif key == 'j' and \
522 len(self.game.world.pickable_items) > \
523 self.item_pointer + 1:
524 self.item_pointer += 1
525 elif key == 'k' and self.item_pointer > 0:
526 self.item_pointer -= 1
527 elif key == 'p' and \
528 len(self.game.world.pickable_items) > 0:
529 id_ = self.game.world.pickable_items[self.item_pointer]
530 self.socket.send('TASK:PICKUP %s' % id_)
534 self.game.to_update['map'] = True
535 elif self.view == 'inventory':
538 elif key == 'j' and \
539 len(self.game.world.player_inventory) > \
540 self.item_pointer + 1:
541 self.item_pointer += 1
542 elif key == 'k' and self.item_pointer > 0:
543 self.item_pointer -= 1
544 elif key == 'd' and \
545 len(self.game.world.player_inventory) > 0:
546 id_ = self.game.world.player_inventory[self.item_pointer]
547 self.socket.send('TASK:DROP %s' % id_)
548 if self.item_pointer > 0:
549 self.item_pointer -= 1
552 self.game.to_update['map'] = True
555 if self.game.do_quit:
559 s = socket.create_connection(('127.0.0.1', 5000))
560 plom_socket = PlomSocket(s)
562 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
564 TUI(plom_socket, game)