5 from plomrogue.parser import ArgError, Parser
6 from plomrogue.commands import cmd_MAP, cmd_THING_TYPE, cmd_THING_POS
7 from plomrogue.game import Game, WorldBase, ThingBase
8 from plomrogue.mapping import MapBase
9 from plomrogue.io import PlomSocket
15 def y_cut(self, map_lines, center_y, view_height):
16 map_height = len(map_lines)
17 if map_height > view_height and center_y > view_height / 2:
18 if center_y > map_height - view_height / 2:
19 map_lines[:] = map_lines[map_height - view_height:]
21 start = center_y - int(view_height / 2) - 1
22 map_lines[:] = map_lines[start:start + view_height]
24 def x_cut(self, map_lines, center_x, view_width, map_width):
25 if map_width > view_width and center_x > view_width / 2:
26 if center_x > map_width - view_width / 2:
27 cut_start = map_width - view_width
30 cut_start = center_x - int(view_width / 2)
31 cut_end = cut_start + view_width
32 map_lines[:] = [line[cut_start:cut_end] for line in map_lines]
34 def format_to_view(self, map_string, center, size):
36 def map_string_to_lines(map_string):
37 map_view_chars = ['0']
41 map_view_chars += [c, ' ']
44 map_view_chars += ['\n']
48 map_view_chars += ['0']
50 map_view_chars = map_view_chars[:-1]
51 map_view_chars = map_view_chars[:-1]
52 return ''.join(map_view_chars).split('\n')
54 map_lines = map_string_to_lines(map_string)
55 self.y_cut(map_lines, center[0], size[0])
56 map_width = self.size[1] * 2 + 1
57 self.x_cut(map_lines, center[1] * 2, size[1], map_width)
61 class World(WorldBase):
63 def __init__(self, *args, **kwargs):
64 """Extend original with local classes and empty default map.
66 We need the empty default map because we draw the map widget
67 on any update, even before we actually receive map data.
69 super().__init__(*args, **kwargs)
71 self.player_position = (0, 0)
73 def new_map(self, yx):
77 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
80 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
82 def cmd_TURN_FINISHED(self, n):
83 """Do nothing. (This may be extended later.)"""
85 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
87 def cmd_TURN(self, n):
88 """Set self.turn to n, empty self.things."""
90 self.world.things = []
91 self.to_update['turn'] = False
92 self.to_update['map'] = False
93 cmd_TURN.argtypes = 'int:nonneg'
95 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
96 self.world.map_.set_line(y, terrain_line)
97 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
99 def cmd_PLAYER_POS(self, yx):
100 self.world.player_position = yx
101 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
103 def cmd_GAME_STATE_COMPLETE(self):
104 self.to_update['turn'] = True
105 self.to_update['map'] = True
111 self.parser = Parser(self)
112 self.world = World(self)
113 self.thing_type = ThingBase
114 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
115 'TURN_FINISHED': cmd_TURN_FINISHED,
117 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
118 'PLAYER_POS': cmd_PLAYER_POS,
119 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
121 'THING_TYPE': cmd_THING_TYPE,
122 'THING_POS': cmd_THING_POS}
131 def get_command(self, command_name):
132 from functools import partial
133 if command_name in self.commands:
134 f = partial(self.commands[command_name], self)
135 if hasattr(self.commands[command_name], 'argtypes'):
136 f.argtypes = self.commands[command_name].argtypes
140 def get_string_options(self, string_option_type):
143 def handle_input(self, msg):
148 command, args = self.parser.parse(msg)
150 self.log('UNHANDLED INPUT: ' + msg)
151 self.to_update['log'] = True
154 except ArgError as e:
155 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
156 self.to_update['log'] = True
159 """Prefix msg plus newline to self.log_text."""
160 self.log_text = msg + '\n' + self.log_text
161 self.to_update['log'] = True
163 def symbol_for_type(self, type_):
167 elif type_ == 'monster':
172 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
173 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
176 def recv_loop(plom_socket, game):
177 for msg in plom_socket.recv():
178 game.handle_input(msg)
183 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
184 self.check_game = check_game
185 self.check_tui = check_tui
188 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
189 self.size_def = size # store for re-calling .size on SIGWINCH
191 self.do_update = True
195 return self.win.getmaxyx()
198 def size(self, size):
199 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
200 n_lines, n_cols = size
202 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
204 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
205 self.win.resize(n_lines, n_cols)
208 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
210 def safe_write(self, foo):
212 def to_chars_with_attrs(part):
213 attr = curses.A_NORMAL
215 if not type(part) == str:
216 part_string = part[0]
218 if len(part_string) > 0:
219 return [(char, attr) for char in part_string]
220 elif len(part_string) == 1:
224 chars_with_attrs = []
225 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
226 chars_with_attrs += to_chars_with_attrs(foo)
229 chars_with_attrs += to_chars_with_attrs(part)
231 if len(chars_with_attrs) < len(self):
232 for char_with_attr in chars_with_attrs:
233 self.win.addstr(char_with_attr[0], char_with_attr[1])
234 else: # workaround to <https://stackoverflow.com/q/7063128>
235 cut = chars_with_attrs[:len(self) - 1]
236 last_char_with_attr = chars_with_attrs[len(self) - 1]
237 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
238 last_char_with_attr[0], last_char_with_attr[1])
239 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
241 for char_with_attr in cut:
242 self.win.addstr(char_with_attr[0], char_with_attr[1])
244 def ensure_freshness(self, do_refresh=False):
246 for key in self.check_game:
247 if self.tui.game.to_update[key]:
251 for key in self.check_tui:
252 if self.tui.to_update[key]:
261 class EditWidget(Widget):
264 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
267 class LogWidget(Widget):
270 line_width = self.size[1]
271 log_lines = self.tui.game.log_text.split('\n')
273 for line in log_lines:
274 to_pad = line_width - (len(line) % line_width)
275 if to_pad == line_width:
277 to_join += [line + ' '*to_pad]
278 self.safe_write((''.join(to_join), curses.color_pair(3)))
281 class MapWidget(Widget):
285 def terrain_with_objects():
286 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
287 for t in self.tui.game.world.things:
288 pos_i = self.tui.game.world.map_.get_position_index(t.position)
289 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
290 return ''.join(terrain_as_list)
292 def pad_or_cut_x(lines):
293 line_width = self.size[1]
294 for y in range(len(lines)):
296 if line_width > len(line):
297 to_pad = line_width - (len(line) % line_width)
298 lines[y] = line + '0' * to_pad
300 lines[y] = line[:line_width]
303 if len(lines) < self.size[0]:
304 to_pad = self.size[0] - len(lines)
305 lines += to_pad * ['0' * self.size[1]]
307 def lines_to_colored_chars(lines):
308 chars_with_attrs = []
309 for c in ''.join(lines):
311 chars_with_attrs += [(c, curses.color_pair(1))]
313 chars_with_attrs += [(c, curses.color_pair(2))]
314 elif c in {'x', 'X', '#'}:
315 chars_with_attrs += [(c, curses.color_pair(3))]
317 chars_with_attrs += [c]
318 return chars_with_attrs
320 if self.tui.game.world.map_.terrain == '':
323 self.safe_write(''.join(lines))
326 terrain_with_objects = terrain_with_objects()
327 center = self.tui.game.world.player_position
328 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
332 self.safe_write(lines_to_colored_chars(lines))
335 class TurnWidget(Widget):
338 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
343 def __init__(self, plom_socket, game):
344 self.socket = plom_socket
346 self.parser = Parser(self.game)
347 self.to_update = {'edit': False}
348 curses.wrapper(self.loop)
350 def setup_screen(self, stdscr):
352 self.stdscr.refresh() # will be called by getkey else, clearing screen
353 self.stdscr.timeout(10)
354 self.stdscr.addstr(0, 0, 'SEND:')
355 self.stdscr.addstr(2, 0, 'TURN:')
357 def loop(self, stdscr):
358 self.setup_screen(stdscr)
359 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
360 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
361 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
362 curses.curs_set(False) # hide cursor
364 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
365 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
366 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
367 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
368 widgets = (self.edit, self.turn, self.log, self.map_)
373 for key in self.game.to_update:
374 self.game.to_update[key] = False
375 for key in self.to_update:
376 self.to_update[key] = False
378 key = self.stdscr.getkey()
379 if key == 'KEY_RESIZE':
381 self.setup_screen(curses.initscr())
384 w.ensure_freshness(True)
385 elif key == '\t': # Tabulator key.
386 map_mode = False if map_mode else True
389 self.socket.send('TASK:MOVE UPLEFT')
391 self.socket.send('TASK:MOVE UPRIGHT')
393 self.socket.send('TASK:MOVE LEFT')
395 self.socket.send('TASK:MOVE RIGHT')
397 self.socket.send('TASK:MOVE DOWNLEFT')
399 self.socket.send('TASK:MOVE DOWNRIGHT')
401 if len(key) == 1 and key in ASCII_printable and \
402 len(self.to_send) < len(self.edit):
403 self.to_send += [key]
404 self.to_update['edit'] = True
405 elif key == 'KEY_BACKSPACE':
406 self.to_send[:] = self.to_send[:-1]
407 self.to_update['edit'] = True
408 elif key == '\n': # Return key
409 self.socket.send(''.join(self.to_send))
411 self.to_update['edit'] = True
414 if self.game.do_quit:
418 s = socket.create_connection(('127.0.0.1', 5000))
419 plom_socket = PlomSocket(s)
421 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
423 TUI(plom_socket, game)