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 = []
75 def new_map(self, yx):
80 return self.get_thing(self.player_id)
83 def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
86 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
88 def cmd_TURN_FINISHED(game, n):
89 """Do nothing. (This may be extended later.)"""
91 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
93 def cmd_TURN(game, n):
94 """Set game.turn to n, empty game.things."""
96 game.world.things = []
97 game.to_update['turn'] = False
98 game.to_update['map'] = False
99 cmd_TURN.argtypes = 'int:nonneg'
101 def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
102 game.world.map_.set_line(y, terrain_line)
103 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
105 def cmd_GAME_STATE_COMPLETE(game):
106 game.to_update['turn'] = True
107 game.to_update['map'] = True
109 def cmd_THING_TYPE(game, i, type_):
110 t = game.world.get_thing(i)
112 cmd_THING_TYPE.argtypes = 'int:nonneg string'
114 def cmd_PLAYER_INVENTORY(game, ids):
115 game.world.player_inventory = ids # TODO: test whether valid IDs
116 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
122 self.parser = Parser(self)
123 self.world = World(self)
124 self.thing_type = ThingBase
125 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
126 'TURN_FINISHED': cmd_TURN_FINISHED,
128 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
129 'PLAYER_ID': cmd_PLAYER_ID,
130 'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
131 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
133 'THING_TYPE': cmd_THING_TYPE,
134 'THING_POS': cmd_THING_POS}
143 def get_command(self, command_name):
144 from functools import partial
145 if command_name in self.commands:
146 f = partial(self.commands[command_name], self)
147 if hasattr(self.commands[command_name], 'argtypes'):
148 f.argtypes = self.commands[command_name].argtypes
152 def get_string_options(self, string_option_type):
155 def handle_input(self, msg):
160 command, args = self.parser.parse(msg)
162 self.log('UNHANDLED INPUT: ' + msg)
163 self.to_update['log'] = True
166 except ArgError as e:
167 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
168 self.to_update['log'] = True
171 """Prefix msg plus newline to self.log_text."""
172 self.log_text = msg + '\n' + self.log_text
173 self.to_update['log'] = True
175 def symbol_for_type(self, type_):
179 elif type_ == 'monster':
181 elif type_ == 'item':
186 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
187 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
190 def recv_loop(plom_socket, game):
191 for msg in plom_socket.recv():
192 game.handle_input(msg)
197 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
198 self.check_game = check_game
199 self.check_tui = check_tui
202 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
203 self.size_def = size # store for re-calling .size on SIGWINCH
205 self.do_update = True
210 return self.win.getmaxyx()
213 def size(self, size):
214 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
215 n_lines, n_cols = size
217 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
219 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
220 self.win.resize(n_lines, n_cols)
223 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
225 def safe_write(self, foo):
227 def to_chars_with_attrs(part):
228 attr = curses.A_NORMAL
230 if not type(part) == str:
231 part_string = part[0]
233 if len(part_string) > 0:
234 return [(char, attr) for char in part_string]
235 elif len(part_string) == 1:
239 chars_with_attrs = []
240 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
241 chars_with_attrs += to_chars_with_attrs(foo)
244 chars_with_attrs += to_chars_with_attrs(part)
246 if len(chars_with_attrs) < len(self):
247 for char_with_attr in chars_with_attrs:
248 self.win.addstr(char_with_attr[0], char_with_attr[1])
249 else: # workaround to <https://stackoverflow.com/q/7063128>
250 cut = chars_with_attrs[:len(self) - 1]
251 last_char_with_attr = chars_with_attrs[len(self) - 1]
252 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
253 last_char_with_attr[0], last_char_with_attr[1])
254 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
256 for char_with_attr in cut:
257 self.win.addstr(char_with_attr[0], char_with_attr[1])
259 def ensure_freshness(self, do_refresh=False):
263 for key in self.check_game:
264 if key in self.tui.game.to_update and self.tui.game.to_update[key]:
268 for key in self.check_tui:
269 if key in self.tui.to_update and self.tui.to_update[key]:
278 class EditWidget(Widget):
281 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
284 class LogWidget(Widget):
287 line_width = self.size[1]
288 log_lines = self.tui.game.log_text.split('\n')
290 for line in log_lines:
291 to_pad = line_width - (len(line) % line_width)
292 if to_pad == line_width:
294 to_join += [line + ' '*to_pad]
295 self.safe_write((''.join(to_join), curses.color_pair(3)))
298 class PopUpWidget(Widget):
301 self.safe_write(self.tui.popup_text)
303 def reconfigure(self):
305 size = (1, len(self.tui.popup_text))
308 offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
309 offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
310 self.start = (offset_y, offset_x)
311 self.win.mvwin(self.start[0], self.start[1])
312 self.ensure_freshness(True)
316 class MapWidget(Widget):
319 if self.tui.view == 'map':
321 elif self.tui.view == 'inventory':
322 self.draw_inventory()
324 def draw_inventory(self):
325 lines = ['INVENTORY:']
327 for id_ in self.tui.game.world.player_inventory:
328 pointer = '*' if counter == self.tui.inventory_pointer else ' '
329 t = self.tui.game.world.get_thing(id_)
330 lines += ['%s %s' % (pointer, t.type_)]
332 line_width = self.size[1]
335 to_pad = line_width - (len(line) % line_width)
336 if to_pad == line_width:
338 to_join += [line + ' '*to_pad]
339 self.safe_write((''.join(to_join), curses.color_pair(3)))
343 def terrain_with_objects():
344 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
345 for t in self.tui.game.world.things:
346 pos_i = self.tui.game.world.map_.get_position_index(t.position)
347 symbol = self.tui.game.symbol_for_type(t.type_)
348 if symbol in {'i'} and terrain_as_list[pos_i] in {'@', 'm'}:
350 terrain_as_list[pos_i] = symbol
351 return ''.join(terrain_as_list)
353 def pad_or_cut_x(lines):
354 line_width = self.size[1]
355 for y in range(len(lines)):
357 if line_width > len(line):
358 to_pad = line_width - (len(line) % line_width)
359 lines[y] = line + '0' * to_pad
361 lines[y] = line[:line_width]
364 if len(lines) < self.size[0]:
365 to_pad = self.size[0] - len(lines)
366 lines += to_pad * ['0' * self.size[1]]
368 def lines_to_colored_chars(lines):
369 chars_with_attrs = []
370 for c in ''.join(lines):
372 chars_with_attrs += [(c, curses.color_pair(1))]
374 chars_with_attrs += [(c, curses.color_pair(4))]
376 chars_with_attrs += [(c, curses.color_pair(2))]
377 elif c in {'x', 'X', '#'}:
378 chars_with_attrs += [(c, curses.color_pair(3))]
380 chars_with_attrs += [c]
381 return chars_with_attrs
383 if self.tui.game.world.map_.terrain == '':
386 self.safe_write(''.join(lines))
389 terrain_with_objects = terrain_with_objects()
390 center = self.tui.game.world.player.position
391 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
395 self.safe_write(lines_to_colored_chars(lines))
398 class TurnWidget(Widget):
401 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
406 def __init__(self, plom_socket, game):
407 self.socket = plom_socket
409 self.parser = Parser(self.game)
410 self.to_update = {'edit': False}
411 self.inventory_pointer = 0
412 curses.wrapper(self.loop)
414 def draw_screen(self):
415 self.stdscr.addstr(0, 0, 'SEND:')
416 self.stdscr.addstr(2, 0, 'TURN:')
418 def setup_screen(self, stdscr):
420 self.stdscr.refresh() # will be called by getkey else, clearing screen
421 self.stdscr.timeout(10)
424 def loop(self, stdscr):
425 self.setup_screen(stdscr)
426 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
427 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
428 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
429 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
430 curses.curs_set(False) # hide cursor
432 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
433 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
434 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
435 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
436 self.popup = PopUpWidget(self, (0, 0), (1, 1), ['popup'])
437 self.popup.visible = False
438 self.popup_text = 'Hi bob'
439 widgets = (self.edit, self.turn, self.log, self.map_, self.popup)
445 for key in self.game.to_update:
446 self.game.to_update[key] = False
447 for key in self.to_update:
448 self.to_update[key] = False
450 key = self.stdscr.getkey()
451 if key == 'KEY_RESIZE':
453 self.setup_screen(curses.initscr())
456 w.ensure_freshness(True)
457 elif key == '\t': # Tabulator key.
458 write_mode = False if write_mode else True
460 if len(key) == 1 and key in ASCII_printable and \
461 len(self.to_send) < len(self.edit):
462 self.to_send += [key]
463 self.to_update['edit'] = True
464 elif key == 'KEY_BACKSPACE':
465 self.to_send[:] = self.to_send[:-1]
466 self.to_update['edit'] = True
467 elif key == '\n': # Return key
468 self.socket.send(''.join(self.to_send))
470 self.to_update['edit'] = True
471 elif self.view == 'map':
473 self.socket.send('TASK:MOVE UPLEFT')
475 self.socket.send('TASK:MOVE UPRIGHT')
477 self.socket.send('TASK:MOVE LEFT')
479 self.socket.send('TASK:MOVE RIGHT')
481 self.socket.send('TASK:MOVE DOWNLEFT')
483 self.socket.send('TASK:MOVE DOWNRIGHT')
485 if not self.popup.visible:
486 self.to_update['popup'] = True
487 self.popup.visible = True
488 self.popup.reconfigure()
490 self.popup.visible = False
491 self.stdscr.erase() # we'll call refresh here so
492 self.stdscr.refresh() # getkey doesn't, erasing screen
495 w.ensure_freshness(True)
497 for t in self.game.world.things:
498 if t == self.game.world.player or \
499 t.id_ in self.game.world.player_inventory:
501 if t.position == self.game.world.player.position:
502 self.socket.send('TASK:PICKUP %s' % t.id_)
505 self.view = 'inventory'
506 self.game.to_update['map'] = True
507 elif self.view == 'inventory':
510 elif key == 'j' and \
511 len(self.game.world.player_inventory) > \
512 self.inventory_pointer + 1:
513 self.inventory_pointer += 1
514 elif key == 'k' and self.inventory_pointer > 0:
515 self.inventory_pointer -= 1
516 elif key == 'd' and \
517 len(self.game.world.player_inventory) > 0:
518 id_ = self.game.world.player_inventory[self.inventory_pointer]
519 self.socket.send('TASK:DROP %s' % id_)
520 if self.inventory_pointer > 0:
521 self.inventory_pointer -= 1
524 self.game.to_update['map'] = True
527 if self.game.do_quit:
531 s = socket.create_connection(('127.0.0.1', 5000))
532 plom_socket = PlomSocket(s)
534 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
536 TUI(plom_socket, game)