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)
74 def new_map(self, yx):
78 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
81 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
83 def cmd_TURN_FINISHED(self, n):
84 """Do nothing. (This may be extended later.)"""
86 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
88 def cmd_TURN(self, n):
89 """Set self.turn to n, empty self.things."""
91 self.world.things = []
92 self.to_update['turn'] = False
93 self.to_update['map'] = False
94 cmd_TURN.argtypes = 'int:nonneg'
96 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
97 self.world.map_.set_line(y, terrain_line)
98 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
100 def cmd_PLAYER_POS(self, yx):
101 self.world.player_position = yx
102 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
104 def cmd_GAME_STATE_COMPLETE(self):
105 self.to_update['turn'] = True
106 self.to_update['map'] = True
108 def cmd_THING_TYPE(game, i, type_):
109 t = game.world.get_thing(i)
111 cmd_THING_TYPE.argtypes = 'int:nonneg string'
117 self.parser = Parser(self)
118 self.world = World(self)
119 self.thing_type = ThingBase
120 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
121 'TURN_FINISHED': cmd_TURN_FINISHED,
123 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
124 'PLAYER_POS': cmd_PLAYER_POS,
125 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
127 'THING_TYPE': cmd_THING_TYPE,
128 'THING_POS': cmd_THING_POS}
137 def get_command(self, command_name):
138 from functools import partial
139 if command_name in self.commands:
140 f = partial(self.commands[command_name], self)
141 if hasattr(self.commands[command_name], 'argtypes'):
142 f.argtypes = self.commands[command_name].argtypes
146 def get_string_options(self, string_option_type):
149 def handle_input(self, msg):
154 command, args = self.parser.parse(msg)
156 self.log('UNHANDLED INPUT: ' + msg)
157 self.to_update['log'] = True
160 except ArgError as e:
161 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
162 self.to_update['log'] = True
165 """Prefix msg plus newline to self.log_text."""
166 self.log_text = msg + '\n' + self.log_text
167 self.to_update['log'] = True
169 def symbol_for_type(self, type_):
173 elif type_ == 'monster':
175 elif type_ == 'item':
180 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
181 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
184 def recv_loop(plom_socket, game):
185 for msg in plom_socket.recv():
186 game.handle_input(msg)
191 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
192 self.check_game = check_game
193 self.check_tui = check_tui
196 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
197 self.size_def = size # store for re-calling .size on SIGWINCH
199 self.do_update = True
203 return self.win.getmaxyx()
206 def size(self, size):
207 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
208 n_lines, n_cols = size
210 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
212 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
213 self.win.resize(n_lines, n_cols)
216 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
218 def safe_write(self, foo):
220 def to_chars_with_attrs(part):
221 attr = curses.A_NORMAL
223 if not type(part) == str:
224 part_string = part[0]
226 if len(part_string) > 0:
227 return [(char, attr) for char in part_string]
228 elif len(part_string) == 1:
232 chars_with_attrs = []
233 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
234 chars_with_attrs += to_chars_with_attrs(foo)
237 chars_with_attrs += to_chars_with_attrs(part)
239 if len(chars_with_attrs) < len(self):
240 for char_with_attr in chars_with_attrs:
241 self.win.addstr(char_with_attr[0], char_with_attr[1])
242 else: # workaround to <https://stackoverflow.com/q/7063128>
243 cut = chars_with_attrs[:len(self) - 1]
244 last_char_with_attr = chars_with_attrs[len(self) - 1]
245 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
246 last_char_with_attr[0], last_char_with_attr[1])
247 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
249 for char_with_attr in cut:
250 self.win.addstr(char_with_attr[0], char_with_attr[1])
252 def ensure_freshness(self, do_refresh=False):
254 for key in self.check_game:
255 if self.tui.game.to_update[key]:
259 for key in self.check_tui:
260 if self.tui.to_update[key]:
269 class EditWidget(Widget):
272 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
275 class LogWidget(Widget):
278 line_width = self.size[1]
279 log_lines = self.tui.game.log_text.split('\n')
281 for line in log_lines:
282 to_pad = line_width - (len(line) % line_width)
283 if to_pad == line_width:
285 to_join += [line + ' '*to_pad]
286 self.safe_write((''.join(to_join), curses.color_pair(3)))
289 class MapWidget(Widget):
293 def terrain_with_objects():
294 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
295 for t in self.tui.game.world.things:
296 pos_i = self.tui.game.world.map_.get_position_index(t.position)
297 symbol = self.tui.game.symbol_for_type(t.type_)
298 if symbol in {'i'} and terrain_as_list[pos_i] in {'@', 'm'}:
300 terrain_as_list[pos_i] = symbol
301 return ''.join(terrain_as_list)
303 def pad_or_cut_x(lines):
304 line_width = self.size[1]
305 for y in range(len(lines)):
307 if line_width > len(line):
308 to_pad = line_width - (len(line) % line_width)
309 lines[y] = line + '0' * to_pad
311 lines[y] = line[:line_width]
314 if len(lines) < self.size[0]:
315 to_pad = self.size[0] - len(lines)
316 lines += to_pad * ['0' * self.size[1]]
318 def lines_to_colored_chars(lines):
319 chars_with_attrs = []
320 for c in ''.join(lines):
322 chars_with_attrs += [(c, curses.color_pair(1))]
324 chars_with_attrs += [(c, curses.color_pair(4))]
326 chars_with_attrs += [(c, curses.color_pair(2))]
327 elif c in {'x', 'X', '#'}:
328 chars_with_attrs += [(c, curses.color_pair(3))]
330 chars_with_attrs += [c]
331 return chars_with_attrs
333 if self.tui.game.world.map_.terrain == '':
336 self.safe_write(''.join(lines))
339 terrain_with_objects = terrain_with_objects()
340 center = self.tui.game.world.player_position
341 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
345 self.safe_write(lines_to_colored_chars(lines))
348 class TurnWidget(Widget):
351 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
356 def __init__(self, plom_socket, game):
357 self.socket = plom_socket
359 self.parser = Parser(self.game)
360 self.to_update = {'edit': False}
361 curses.wrapper(self.loop)
363 def setup_screen(self, stdscr):
365 self.stdscr.refresh() # will be called by getkey else, clearing screen
366 self.stdscr.timeout(10)
367 self.stdscr.addstr(0, 0, 'SEND:')
368 self.stdscr.addstr(2, 0, 'TURN:')
370 def loop(self, stdscr):
371 self.setup_screen(stdscr)
372 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
373 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
374 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
375 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
376 curses.curs_set(False) # hide cursor
378 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
379 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
380 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
381 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
382 widgets = (self.edit, self.turn, self.log, self.map_)
387 for key in self.game.to_update:
388 self.game.to_update[key] = False
389 for key in self.to_update:
390 self.to_update[key] = False
392 key = self.stdscr.getkey()
393 if key == 'KEY_RESIZE':
395 self.setup_screen(curses.initscr())
398 w.ensure_freshness(True)
399 elif key == '\t': # Tabulator key.
400 map_mode = False if map_mode else True
403 self.socket.send('TASK:MOVE UPLEFT')
405 self.socket.send('TASK:MOVE UPRIGHT')
407 self.socket.send('TASK:MOVE LEFT')
409 self.socket.send('TASK:MOVE RIGHT')
411 self.socket.send('TASK:MOVE DOWNLEFT')
413 self.socket.send('TASK:MOVE DOWNRIGHT')
415 if len(key) == 1 and key in ASCII_printable and \
416 len(self.to_send) < len(self.edit):
417 self.to_send += [key]
418 self.to_update['edit'] = True
419 elif key == 'KEY_BACKSPACE':
420 self.to_send[:] = self.to_send[:-1]
421 self.to_update['edit'] = True
422 elif key == '\n': # Return key
423 self.socket.send(''.join(self.to_send))
425 self.to_update['edit'] = True
428 if self.game.do_quit:
432 s = socket.create_connection(('127.0.0.1', 5000))
433 plom_socket = PlomSocket(s)
435 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
437 TUI(plom_socket, game)