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
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
112 self.parser = Parser(self)
113 self.world = World(self)
114 self.thing_type = ThingBase
115 self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
116 'TURN_FINISHED': cmd_TURN_FINISHED,
118 'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
119 'PLAYER_POS': cmd_PLAYER_POS,
120 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
122 'THING_TYPE': cmd_THING_TYPE,
123 'THING_POS': cmd_THING_POS}
132 def get_command(self, command_name):
133 from functools import partial
134 if command_name in self.commands:
135 f = partial(self.commands[command_name], self)
136 if hasattr(self.commands[command_name], 'argtypes'):
137 f.argtypes = self.commands[command_name].argtypes
141 def get_string_options(self, string_option_type):
144 def handle_input(self, msg):
149 command, args = self.parser.parse(msg)
151 self.log('UNHANDLED INPUT: ' + msg)
152 self.to_update['log'] = True
155 except ArgError as e:
156 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
157 self.to_update['log'] = True
160 """Prefix msg plus newline to self.log_text."""
161 self.log_text = msg + '\n' + self.log_text
162 self.to_update['log'] = True
164 def symbol_for_type(self, type_):
168 elif type_ == 'monster':
173 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
174 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
177 def recv_loop(plom_socket, game):
178 for msg in plom_socket.recv():
179 game.handle_input(msg)
184 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
185 self.check_game = check_game
186 self.check_tui = check_tui
189 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
190 self.size_def = size # store for re-calling .size on SIGWINCH
192 self.do_update = True
196 return self.win.getmaxyx()
199 def size(self, size):
200 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
201 n_lines, n_cols = size
203 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
205 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
206 self.win.resize(n_lines, n_cols)
209 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
211 def safe_write(self, foo):
213 def to_chars_with_attrs(part):
214 attr = curses.A_NORMAL
216 if not type(part) == str:
217 part_string = part[0]
219 if len(part_string) > 0:
220 return [(char, attr) for char in part_string]
221 elif len(part_string) == 1:
225 chars_with_attrs = []
226 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
227 chars_with_attrs += to_chars_with_attrs(foo)
230 chars_with_attrs += to_chars_with_attrs(part)
232 if len(chars_with_attrs) < len(self):
233 for char_with_attr in chars_with_attrs:
234 self.win.addstr(char_with_attr[0], char_with_attr[1])
235 else: # workaround to <https://stackoverflow.com/q/7063128>
236 cut = chars_with_attrs[:len(self) - 1]
237 last_char_with_attr = chars_with_attrs[len(self) - 1]
238 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
239 last_char_with_attr[0], last_char_with_attr[1])
240 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
242 for char_with_attr in cut:
243 self.win.addstr(char_with_attr[0], char_with_attr[1])
245 def ensure_freshness(self, do_refresh=False):
247 for key in self.check_game:
248 if self.tui.game.to_update[key]:
252 for key in self.check_tui:
253 if self.tui.to_update[key]:
262 class EditWidget(Widget):
265 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
268 class LogWidget(Widget):
271 line_width = self.size[1]
272 log_lines = self.tui.game.log_text.split('\n')
274 for line in log_lines:
275 to_pad = line_width - (len(line) % line_width)
276 if to_pad == line_width:
278 to_join += [line + ' '*to_pad]
279 self.safe_write((''.join(to_join), curses.color_pair(3)))
282 class MapWidget(Widget):
286 def terrain_with_objects():
287 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
288 for t in self.tui.game.world.things:
289 pos_i = self.tui.game.world.map_.get_position_index(t.position)
290 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
291 return ''.join(terrain_as_list)
293 def pad_or_cut_x(lines):
294 line_width = self.size[1]
295 for y in range(len(lines)):
297 if line_width > len(line):
298 to_pad = line_width - (len(line) % line_width)
299 lines[y] = line + '0' * to_pad
301 lines[y] = line[:line_width]
304 if len(lines) < self.size[0]:
305 to_pad = self.size[0] - len(lines)
306 lines += to_pad * ['0' * self.size[1]]
308 def lines_to_colored_chars(lines):
309 chars_with_attrs = []
310 for c in ''.join(lines):
312 chars_with_attrs += [(c, curses.color_pair(1))]
314 chars_with_attrs += [(c, curses.color_pair(2))]
315 elif c in {'x', 'X', '#'}:
316 chars_with_attrs += [(c, curses.color_pair(3))]
318 chars_with_attrs += [c]
319 return chars_with_attrs
321 if self.tui.game.world.map_.terrain == '':
324 self.safe_write(''.join(lines))
327 terrain_with_objects = terrain_with_objects()
328 center = self.tui.game.world.player_position
329 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
333 self.safe_write(lines_to_colored_chars(lines))
336 class TurnWidget(Widget):
339 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
344 def __init__(self, plom_socket, game):
345 self.socket = plom_socket
347 self.parser = Parser(self.game)
348 self.to_update = {'edit': False}
349 curses.wrapper(self.loop)
351 def setup_screen(self, stdscr):
353 self.stdscr.refresh() # will be called by getkey else, clearing screen
354 self.stdscr.timeout(10)
355 self.stdscr.addstr(0, 0, 'SEND:')
356 self.stdscr.addstr(2, 0, 'TURN:')
358 def loop(self, stdscr):
359 self.setup_screen(stdscr)
360 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
361 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
362 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
363 curses.curs_set(False) # hide cursor
365 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
366 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
367 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
368 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
369 widgets = (self.edit, self.turn, self.log, self.map_)
374 for key in self.game.to_update:
375 self.game.to_update[key] = False
376 for key in self.to_update:
377 self.to_update[key] = False
379 key = self.stdscr.getkey()
380 if key == 'KEY_RESIZE':
382 self.setup_screen(curses.initscr())
385 w.ensure_freshness(True)
386 elif key == '\t': # Tabulator key.
387 map_mode = False if map_mode else True
390 self.socket.send('TASK:MOVE UPLEFT')
392 self.socket.send('TASK:MOVE UPRIGHT')
394 self.socket.send('TASK:MOVE LEFT')
396 self.socket.send('TASK:MOVE RIGHT')
398 self.socket.send('TASK:MOVE DOWNLEFT')
400 self.socket.send('TASK:MOVE DOWNRIGHT')
402 if len(key) == 1 and key in ASCII_printable and \
403 len(self.to_send) < len(self.edit):
404 self.to_send += [key]
405 self.to_update['edit'] = True
406 elif key == 'KEY_BACKSPACE':
407 self.to_send[:] = self.to_send[:-1]
408 self.to_update['edit'] = True
409 elif key == '\n': # Return key
410 self.socket.send(''.join(self.to_send))
412 self.to_update['edit'] = True
415 if self.game.do_quit:
419 s = socket.create_connection(('127.0.0.1', 5000))
420 plom_socket = PlomSocket(s)
422 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
424 TUI(plom_socket, game)