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':
178 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
179 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
182 def recv_loop(plom_socket, game):
183 for msg in plom_socket.recv():
184 game.handle_input(msg)
189 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
190 self.check_game = check_game
191 self.check_tui = check_tui
194 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
195 self.size_def = size # store for re-calling .size on SIGWINCH
197 self.do_update = True
201 return self.win.getmaxyx()
204 def size(self, size):
205 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
206 n_lines, n_cols = size
208 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
210 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
211 self.win.resize(n_lines, n_cols)
214 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
216 def safe_write(self, foo):
218 def to_chars_with_attrs(part):
219 attr = curses.A_NORMAL
221 if not type(part) == str:
222 part_string = part[0]
224 if len(part_string) > 0:
225 return [(char, attr) for char in part_string]
226 elif len(part_string) == 1:
230 chars_with_attrs = []
231 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
232 chars_with_attrs += to_chars_with_attrs(foo)
235 chars_with_attrs += to_chars_with_attrs(part)
237 if len(chars_with_attrs) < len(self):
238 for char_with_attr in chars_with_attrs:
239 self.win.addstr(char_with_attr[0], char_with_attr[1])
240 else: # workaround to <https://stackoverflow.com/q/7063128>
241 cut = chars_with_attrs[:len(self) - 1]
242 last_char_with_attr = chars_with_attrs[len(self) - 1]
243 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
244 last_char_with_attr[0], last_char_with_attr[1])
245 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
247 for char_with_attr in cut:
248 self.win.addstr(char_with_attr[0], char_with_attr[1])
250 def ensure_freshness(self, do_refresh=False):
252 for key in self.check_game:
253 if self.tui.game.to_update[key]:
257 for key in self.check_tui:
258 if self.tui.to_update[key]:
267 class EditWidget(Widget):
270 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
273 class LogWidget(Widget):
276 line_width = self.size[1]
277 log_lines = self.tui.game.log_text.split('\n')
279 for line in log_lines:
280 to_pad = line_width - (len(line) % line_width)
281 if to_pad == line_width:
283 to_join += [line + ' '*to_pad]
284 self.safe_write((''.join(to_join), curses.color_pair(3)))
287 class MapWidget(Widget):
291 def terrain_with_objects():
292 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
293 for t in self.tui.game.world.things:
294 pos_i = self.tui.game.world.map_.get_position_index(t.position)
295 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
296 return ''.join(terrain_as_list)
298 def pad_or_cut_x(lines):
299 line_width = self.size[1]
300 for y in range(len(lines)):
302 if line_width > len(line):
303 to_pad = line_width - (len(line) % line_width)
304 lines[y] = line + '0' * to_pad
306 lines[y] = line[:line_width]
309 if len(lines) < self.size[0]:
310 to_pad = self.size[0] - len(lines)
311 lines += to_pad * ['0' * self.size[1]]
313 def lines_to_colored_chars(lines):
314 chars_with_attrs = []
315 for c in ''.join(lines):
317 chars_with_attrs += [(c, curses.color_pair(1))]
319 chars_with_attrs += [(c, curses.color_pair(2))]
320 elif c in {'x', 'X', '#'}:
321 chars_with_attrs += [(c, curses.color_pair(3))]
323 chars_with_attrs += [c]
324 return chars_with_attrs
326 if self.tui.game.world.map_.terrain == '':
329 self.safe_write(''.join(lines))
332 terrain_with_objects = terrain_with_objects()
333 center = self.tui.game.world.player_position
334 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
338 self.safe_write(lines_to_colored_chars(lines))
341 class TurnWidget(Widget):
344 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
349 def __init__(self, plom_socket, game):
350 self.socket = plom_socket
352 self.parser = Parser(self.game)
353 self.to_update = {'edit': False}
354 curses.wrapper(self.loop)
356 def setup_screen(self, stdscr):
358 self.stdscr.refresh() # will be called by getkey else, clearing screen
359 self.stdscr.timeout(10)
360 self.stdscr.addstr(0, 0, 'SEND:')
361 self.stdscr.addstr(2, 0, 'TURN:')
363 def loop(self, stdscr):
364 self.setup_screen(stdscr)
365 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
366 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
367 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
368 curses.curs_set(False) # hide cursor
370 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
371 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
372 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
373 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
374 widgets = (self.edit, self.turn, self.log, self.map_)
379 for key in self.game.to_update:
380 self.game.to_update[key] = False
381 for key in self.to_update:
382 self.to_update[key] = False
384 key = self.stdscr.getkey()
385 if key == 'KEY_RESIZE':
387 self.setup_screen(curses.initscr())
390 w.ensure_freshness(True)
391 elif key == '\t': # Tabulator key.
392 map_mode = False if map_mode else True
395 self.socket.send('TASK:MOVE UPLEFT')
397 self.socket.send('TASK:MOVE UPRIGHT')
399 self.socket.send('TASK:MOVE LEFT')
401 self.socket.send('TASK:MOVE RIGHT')
403 self.socket.send('TASK:MOVE DOWNLEFT')
405 self.socket.send('TASK:MOVE DOWNRIGHT')
407 if len(key) == 1 and key in ASCII_printable and \
408 len(self.to_send) < len(self.edit):
409 self.to_send += [key]
410 self.to_update['edit'] = True
411 elif key == 'KEY_BACKSPACE':
412 self.to_send[:] = self.to_send[:-1]
413 self.to_update['edit'] = True
414 elif key == '\n': # Return key
415 self.socket.send(''.join(self.to_send))
417 self.to_update['edit'] = True
420 if self.game.do_quit:
424 s = socket.create_connection(('127.0.0.1', 5000))
425 plom_socket = PlomSocket(s)
427 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
429 TUI(plom_socket, game)