6 from parser import ArgError, Parser
7 from plomrogue2 import (MapBase, WorldBase, ThingBase, cmd_MAP, cmd_THING_TYPE,
14 def y_cut(self, map_lines, center_y, view_height):
15 map_height = len(map_lines)
16 if map_height > view_height and center_y > view_height / 2:
17 if center_y > map_height - view_height / 2:
18 map_lines[:] = map_lines[map_height - view_height:]
20 start = center_y - int(view_height / 2) - 1
21 map_lines[:] = map_lines[start:start + view_height]
23 def x_cut(self, map_lines, center_x, view_width, map_width):
24 if map_width > view_width and center_x > view_width / 2:
25 if center_x > map_width - view_width / 2:
26 cut_start = map_width - view_width
29 cut_start = center_x - int(view_width / 2)
30 cut_end = cut_start + view_width
31 map_lines[:] = [line[cut_start:cut_end] for line in map_lines]
33 def format_to_view(self, map_string, center, size):
35 def map_string_to_lines(map_string):
36 map_view_chars = ['0']
40 map_view_chars += [c, ' ']
43 map_view_chars += ['\n']
47 map_view_chars += ['0']
49 map_view_chars = map_view_chars[:-1]
50 map_view_chars = map_view_chars[:-1]
51 return ''.join(map_view_chars).split('\n')
53 map_lines = map_string_to_lines(map_string)
54 self.y_cut(map_lines, center[0], size[0])
55 map_width = self.size[1] * 2 + 1
56 self.x_cut(map_lines, center[1] * 2, size[1], map_width)
60 class World(WorldBase):
62 def __init__(self, *args, **kwargs):
63 """Extend original with local classes and empty default map.
65 We need the empty default map because we draw the map widget
66 on any update, even before we actually receive map data.
68 super().__init__(*args, **kwargs)
70 self.player_position = (0, 0)
72 def new_map(self, yx):
76 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
79 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
81 def cmd_TURN_FINISHED(self, n):
82 """Do nothing. (This may be extended later.)"""
84 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
86 def cmd_TURN(self, n):
87 """Set self.turn to n, empty self.things."""
89 self.world.things = []
90 self.to_update['turn'] = False
91 self.to_update['map'] = False
92 cmd_TURN.argtypes = 'int:nonneg'
94 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
95 self.world.map_.set_line(y, terrain_line)
96 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
98 def cmd_PLAYER_POS(self, yx):
99 self.world.player_position = yx
100 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
102 def cmd_GAME_STATE_COMPLETE(self):
103 self.to_update['turn'] = True
104 self.to_update['map'] = True
110 self.parser = Parser(self)
111 self.world = World(self)
112 self.thing_type = ThingBase
113 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
114 'TURN_FINISHED': cmd_TURN_FINISHED,
116 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
117 'PLAYER_POS': cmd_PLAYER_POS,
118 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
120 'THING_TYPE': cmd_THING_TYPE,
121 'THING_POS': cmd_THING_POS}
130 def get_command(self, command_name):
131 from functools import partial
132 if command_name in self.commands:
133 f = partial(self.commands[command_name], self)
134 if hasattr(self.commands[command_name], 'argtypes'):
135 f.argtypes = self.commands[command_name].argtypes
139 def get_string_options(self, string_option_type):
142 def handle_input(self, msg):
147 command, args = self.parser.parse(msg)
149 self.log('UNHANDLED INPUT: ' + msg)
150 self.to_update['log'] = True
153 except ArgError as e:
154 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
155 self.to_update['log'] = True
158 """Prefix msg plus newline to self.log_text."""
159 self.log_text = msg + '\n' + self.log_text
160 self.to_update['log'] = True
162 def symbol_for_type(self, type_):
166 elif type_ == 'monster':
171 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
172 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
175 def recv_loop(plom_socket, game):
176 for msg in plom_socket.recv():
177 game.handle_input(msg)
182 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
183 self.check_game = check_game
184 self.check_tui = check_tui
187 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
188 self.size_def = size # store for re-calling .size on SIGWINCH
190 self.do_update = True
194 return self.win.getmaxyx()
197 def size(self, size):
198 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
199 n_lines, n_cols = size
201 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
203 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
204 self.win.resize(n_lines, n_cols)
207 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
209 def safe_write(self, foo):
211 def to_chars_with_attrs(part):
212 attr = curses.A_NORMAL
214 if not type(part) == str:
215 part_string = part[0]
217 if len(part_string) > 0:
218 return [(char, attr) for char in part_string]
219 elif len(part_string) == 1:
223 chars_with_attrs = []
224 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
225 chars_with_attrs += to_chars_with_attrs(foo)
228 chars_with_attrs += to_chars_with_attrs(part)
230 if len(chars_with_attrs) < len(self):
231 for char_with_attr in chars_with_attrs:
232 self.win.addstr(char_with_attr[0], char_with_attr[1])
233 else: # workaround to <https://stackoverflow.com/q/7063128>
234 cut = chars_with_attrs[:len(self) - 1]
235 last_char_with_attr = chars_with_attrs[len(self) - 1]
236 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
237 last_char_with_attr[0], last_char_with_attr[1])
238 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
240 for char_with_attr in cut:
241 self.win.addstr(char_with_attr[0], char_with_attr[1])
243 def ensure_freshness(self, do_refresh=False):
245 for key in self.check_game:
246 if self.tui.game.to_update[key]:
250 for key in self.check_tui:
251 if self.tui.to_update[key]:
260 class EditWidget(Widget):
263 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
266 class LogWidget(Widget):
269 line_width = self.size[1]
270 log_lines = self.tui.game.log_text.split('\n')
272 for line in log_lines:
273 to_pad = line_width - (len(line) % line_width)
274 if to_pad == line_width:
276 to_join += [line + ' '*to_pad]
277 self.safe_write((''.join(to_join), curses.color_pair(3)))
280 class MapWidget(Widget):
284 def terrain_with_objects():
285 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
286 for t in self.tui.game.world.things:
287 pos_i = self.tui.game.world.map_.get_position_index(t.position)
288 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
289 return ''.join(terrain_as_list)
291 def pad_or_cut_x(lines):
292 line_width = self.size[1]
293 for y in range(len(lines)):
295 if line_width > len(line):
296 to_pad = line_width - (len(line) % line_width)
297 lines[y] = line + '0' * to_pad
299 lines[y] = line[:line_width]
302 if len(lines) < self.size[0]:
303 to_pad = self.size[0] - len(lines)
304 lines += to_pad * ['0' * self.size[1]]
306 def lines_to_colored_chars(lines):
307 chars_with_attrs = []
308 for c in ''.join(lines):
310 chars_with_attrs += [(c, curses.color_pair(1))]
312 chars_with_attrs += [(c, curses.color_pair(2))]
313 elif c in {'x', 'X', '#'}:
314 chars_with_attrs += [(c, curses.color_pair(3))]
316 chars_with_attrs += [c]
317 return chars_with_attrs
319 if self.tui.game.world.map_.terrain == '':
322 self.safe_write(''.join(lines))
325 terrain_with_objects = terrain_with_objects()
326 center = self.tui.game.world.player_position
327 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
331 self.safe_write(lines_to_colored_chars(lines))
334 class TurnWidget(Widget):
337 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
342 def __init__(self, plom_socket, game):
343 self.socket = plom_socket
345 self.parser = Parser(self.game)
346 self.to_update = {'edit': False}
347 curses.wrapper(self.loop)
349 def setup_screen(self, stdscr):
351 self.stdscr.refresh() # will be called by getkey else, clearing screen
352 self.stdscr.timeout(10)
353 self.stdscr.addstr(0, 0, 'SEND:')
354 self.stdscr.addstr(2, 0, 'TURN:')
356 def loop(self, stdscr):
357 self.setup_screen(stdscr)
358 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
359 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
360 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
361 curses.curs_set(False) # hide cursor
363 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
364 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
365 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
366 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
367 widgets = (self.edit, self.turn, self.log, self.map_)
372 for key in self.game.to_update:
373 self.game.to_update[key] = False
374 for key in self.to_update:
375 self.to_update[key] = False
377 key = self.stdscr.getkey()
378 if key == 'KEY_RESIZE':
380 self.setup_screen(curses.initscr())
383 w.ensure_freshness(True)
384 elif key == '\t': # Tabulator key.
385 map_mode = False if map_mode else True
388 self.socket.send('TASK:MOVE UPLEFT')
390 self.socket.send('TASK:MOVE UPRIGHT')
392 self.socket.send('TASK:MOVE LEFT')
394 self.socket.send('TASK:MOVE RIGHT')
396 self.socket.send('TASK:MOVE DOWNLEFT')
398 self.socket.send('TASK:MOVE DOWNRIGHT')
400 if len(key) == 1 and key in ASCII_printable and \
401 len(self.to_send) < len(self.edit):
402 self.to_send += [key]
403 self.to_update['edit'] = True
404 elif key == 'KEY_BACKSPACE':
405 self.to_send[:] = self.to_send[:-1]
406 self.to_update['edit'] = True
407 elif key == '\n': # Return key
408 self.socket.send(''.join(self.to_send))
410 self.to_update['edit'] = True
413 if self.game.do_quit:
417 s = socket.create_connection(('127.0.0.1', 5000))
418 plom_socket = plom_socket.PlomSocket(s)
420 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
422 TUI(plom_socket, game)