5 from plomrogue.parser import ArgError, Parser
6 from plomrogue.commands import cmd_MAP, cmd_THING_POS
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_position = (0, 0)
73 self.player_inventory = []
75 def new_map(self, yx):
79 def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
82 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
84 def cmd_TURN_FINISHED(game, n):
85 """Do nothing. (This may be extended later.)"""
87 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
89 def cmd_TURN(game, n):
90 """Set game.turn to n, empty game.things."""
92 game.world.things = []
93 game.to_update['turn'] = False
94 game.to_update['map'] = False
95 cmd_TURN.argtypes = 'int:nonneg'
97 def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
98 game.world.map_.set_line(y, terrain_line)
99 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
101 def cmd_PLAYER_POS(game, yx):
102 game.world.player_position = yx
103 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
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_POS': cmd_PLAYER_POS,
130 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
132 'THING_TYPE': cmd_THING_TYPE,
133 'THING_POS': cmd_THING_POS}
142 def get_command(self, command_name):
143 from functools import partial
144 if command_name in self.commands:
145 f = partial(self.commands[command_name], self)
146 if hasattr(self.commands[command_name], 'argtypes'):
147 f.argtypes = self.commands[command_name].argtypes
151 def get_string_options(self, string_option_type):
154 def handle_input(self, msg):
159 command, args = self.parser.parse(msg)
161 self.log('UNHANDLED INPUT: ' + msg)
162 self.to_update['log'] = True
165 except ArgError as e:
166 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
167 self.to_update['log'] = True
170 """Prefix msg plus newline to self.log_text."""
171 self.log_text = msg + '\n' + self.log_text
172 self.to_update['log'] = True
174 def symbol_for_type(self, type_):
178 elif type_ == 'monster':
180 elif type_ == 'item':
185 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
186 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
189 def recv_loop(plom_socket, game):
190 for msg in plom_socket.recv():
191 game.handle_input(msg)
196 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
197 self.check_game = check_game
198 self.check_tui = check_tui
201 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
202 self.size_def = size # store for re-calling .size on SIGWINCH
204 self.do_update = True
209 return self.win.getmaxyx()
212 def size(self, size):
213 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
214 n_lines, n_cols = size
216 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
218 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
219 self.win.resize(n_lines, n_cols)
222 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
224 def safe_write(self, foo):
226 def to_chars_with_attrs(part):
227 attr = curses.A_NORMAL
229 if not type(part) == str:
230 part_string = part[0]
232 if len(part_string) > 0:
233 return [(char, attr) for char in part_string]
234 elif len(part_string) == 1:
238 chars_with_attrs = []
239 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
240 chars_with_attrs += to_chars_with_attrs(foo)
243 chars_with_attrs += to_chars_with_attrs(part)
245 if len(chars_with_attrs) < len(self):
246 for char_with_attr in chars_with_attrs:
247 self.win.addstr(char_with_attr[0], char_with_attr[1])
248 else: # workaround to <https://stackoverflow.com/q/7063128>
249 cut = chars_with_attrs[:len(self) - 1]
250 last_char_with_attr = chars_with_attrs[len(self) - 1]
251 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
252 last_char_with_attr[0], last_char_with_attr[1])
253 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
255 for char_with_attr in cut:
256 self.win.addstr(char_with_attr[0], char_with_attr[1])
258 def ensure_freshness(self, do_refresh=False):
262 for key in self.check_game:
263 if key in self.tui.game.to_update and self.tui.game.to_update[key]:
267 for key in self.check_tui:
268 if key in self.tui.to_update and self.tui.to_update[key]:
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)
315 class MapWidget(Widget):
319 def terrain_with_objects():
320 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
321 for t in self.tui.game.world.things:
322 pos_i = self.tui.game.world.map_.get_position_index(t.position)
323 symbol = self.tui.game.symbol_for_type(t.type_)
324 if symbol in {'i'} and terrain_as_list[pos_i] in {'@', 'm'}:
326 terrain_as_list[pos_i] = symbol
327 return ''.join(terrain_as_list)
329 def pad_or_cut_x(lines):
330 line_width = self.size[1]
331 for y in range(len(lines)):
333 if line_width > len(line):
334 to_pad = line_width - (len(line) % line_width)
335 lines[y] = line + '0' * to_pad
337 lines[y] = line[:line_width]
340 if len(lines) < self.size[0]:
341 to_pad = self.size[0] - len(lines)
342 lines += to_pad * ['0' * self.size[1]]
344 def lines_to_colored_chars(lines):
345 chars_with_attrs = []
346 for c in ''.join(lines):
348 chars_with_attrs += [(c, curses.color_pair(1))]
350 chars_with_attrs += [(c, curses.color_pair(4))]
352 chars_with_attrs += [(c, curses.color_pair(2))]
353 elif c in {'x', 'X', '#'}:
354 chars_with_attrs += [(c, curses.color_pair(3))]
356 chars_with_attrs += [c]
357 return chars_with_attrs
359 if self.tui.game.world.map_.terrain == '':
362 self.safe_write(''.join(lines))
365 terrain_with_objects = terrain_with_objects()
366 center = self.tui.game.world.player_position
367 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
371 self.safe_write(lines_to_colored_chars(lines))
374 class TurnWidget(Widget):
377 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
382 def __init__(self, plom_socket, game):
383 self.socket = plom_socket
385 self.parser = Parser(self.game)
386 self.to_update = {'edit': False}
387 curses.wrapper(self.loop)
389 def draw_screen(self):
390 self.stdscr.addstr(0, 0, 'SEND:')
391 self.stdscr.addstr(2, 0, 'TURN:')
393 def setup_screen(self, stdscr):
395 self.stdscr.refresh() # will be called by getkey else, clearing screen
396 self.stdscr.timeout(10)
399 def loop(self, stdscr):
400 self.setup_screen(stdscr)
401 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
402 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
403 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
404 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
405 curses.curs_set(False) # hide cursor
407 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
408 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
409 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
410 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
411 self.popup = PopUpWidget(self, (0, 0), (1, 1), ['popup'])
412 self.popup.visible = False
413 self.popup_text = 'Hi bob'
414 widgets = (self.edit, self.turn, self.log, self.map_, self.popup)
419 for key in self.game.to_update:
420 self.game.to_update[key] = False
421 for key in self.to_update:
422 self.to_update[key] = False
424 key = self.stdscr.getkey()
425 if key == 'KEY_RESIZE':
427 self.setup_screen(curses.initscr())
430 w.ensure_freshness(True)
431 elif key == '\t': # Tabulator key.
432 map_mode = False if map_mode else True
435 self.socket.send('TASK:MOVE UPLEFT')
437 self.socket.send('TASK:MOVE UPRIGHT')
439 self.socket.send('TASK:MOVE LEFT')
441 self.socket.send('TASK:MOVE RIGHT')
443 self.socket.send('TASK:MOVE DOWNLEFT')
445 self.socket.send('TASK:MOVE DOWNRIGHT')
447 if not self.popup.visible:
448 self.to_update['popup'] = True
449 self.popup.visible = True
450 self.popup.reconfigure()
452 self.popup.visible = False
453 self.stdscr.erase() # we'll call refresh here so
454 self.stdscr.refresh() # getkey doesn't, erasing screen
457 w.ensure_freshness(True)
459 if len(key) == 1 and key in ASCII_printable and \
460 len(self.to_send) < len(self.edit):
461 self.to_send += [key]
462 self.to_update['edit'] = True
463 elif key == 'KEY_BACKSPACE':
464 self.to_send[:] = self.to_send[:-1]
465 self.to_update['edit'] = True
466 elif key == '\n': # Return key
467 self.socket.send(''.join(self.to_send))
469 self.to_update['edit'] = True
472 if self.game.do_quit:
476 s = socket.create_connection(('127.0.0.1', 5000))
477 plom_socket = PlomSocket(s)
479 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
481 TUI(plom_socket, game)