6 from parser import ArgError, Parser
10 class Map(game_common.Map):
12 def y_cut(self, map_lines, center_y, view_height):
13 map_height = len(map_lines)
14 if map_height > view_height and center_y > view_height / 2:
15 if center_y > map_height - view_height / 2:
16 map_lines[:] = map_lines[map_height - view_height:]
18 start = center_y - int(view_height / 2) - 1
19 map_lines[:] = map_lines[start:start + view_height]
21 def x_cut(self, map_lines, center_x, view_width, map_width):
22 if map_width > view_width and center_x > view_width / 2:
23 if center_x > map_width - view_width / 2:
24 cut_start = map_width - view_width
27 cut_start = center_x - int(view_width / 2)
28 cut_end = cut_start + view_width
29 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):
39 while start_cut < len(map_string):
40 limit = start_cut + self.size[1]
41 map_lines += [map_string[start_cut:limit]]
45 map_lines = map_string_to_lines(map_string)
46 self.y_cut(map_lines, center[0], size[0])
47 self.x_cut(map_lines, center[1], size[1], self.size[1])
53 def format_to_view(self, map_string, center, size):
55 def map_string_to_lines(map_string):
56 map_view_chars = ['0']
60 map_view_chars += [c, ' ']
63 map_view_chars += ['\n']
67 map_view_chars += ['0']
69 map_view_chars = map_view_chars[:-1]
70 map_view_chars = map_view_chars[:-1]
71 return ''.join(map_view_chars).split('\n')
73 map_lines = map_string_to_lines(map_string)
74 self.y_cut(map_lines, center[0], size[0])
75 map_width = self.size[1] * 2 + 1
76 self.x_cut(map_lines, center[1] * 2, size[1], map_width)
80 map_manager = game_common.MapManager((MapHex, MapSquare))
83 class World(game_common.World):
85 def __init__(self, game, *args, **kwargs):
86 """Extend original with local classes and empty default map.
88 We need the empty default map because we draw the map widget
89 on any update, even before we actually receive map data.
91 super().__init__(*args, **kwargs)
93 self.map_ = self.game.map_manager.get_map_class('Hex')()
94 self.player_position = (0, 0)
97 class Game(game_common.CommonCommandsMixin):
100 self.map_manager = map_manager
101 self.parser = Parser(self)
102 self.world = World(self)
111 def get_command_signature(self, command_name):
112 method_candidate = 'cmd_' + command_name
115 if hasattr(self, method_candidate):
116 method = getattr(self, method_candidate)
117 if hasattr(method, 'argtypes'):
118 argtypes = method.argtypes
119 return method, argtypes
121 def get_string_options(self, string_option_type):
122 if string_option_type == 'geometry':
123 return self.map_manager.get_map_geometries()
126 def handle_input(self, msg):
131 command, args = self.parser.parse(msg)
133 self.log('UNHANDLED INPUT: ' + msg)
134 self.to_update['log'] = True
137 except ArgError as e:
138 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
139 self.to_update['log'] = True
142 """Prefix msg plus newline to self.log_text."""
143 self.log_text = msg + '\n' + self.log_text
144 self.to_update['log'] = True
146 def symbol_for_type(self, type_):
150 elif type_ == 'monster':
154 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
157 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
159 def cmd_TURN_FINISHED(self, n):
160 """Do nothing. (This may be extended later.)"""
162 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
164 def cmd_TURN(self, n):
165 """Set self.turn to n, empty self.things."""
167 self.world.things = []
168 self.to_update['turn'] = False
169 self.to_update['map'] = False
170 cmd_TURN.argtypes = 'int:nonneg'
172 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
173 self.world.map_.set_line(y, terrain_line)
174 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
176 def cmd_PLAYER_POS(self, yx):
177 self.world.player_position = yx
178 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
180 def cmd_GAME_STATE_COMPLETE(self):
181 self.to_update['turn'] = True
182 self.to_update['map'] = True
185 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
186 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
189 def recv_loop(plom_socket, game):
190 for msg in plom_socket.recv():
191 game.handle_input(msg)
196 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
197 self.check_game = check_game
198 self.check_tui = check_tui
201 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
202 self.size_def = size # store for re-calling .size on SIGWINCH
204 self.do_update = True
208 return self.win.getmaxyx()
211 def size(self, size):
212 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
213 n_lines, n_cols = size
215 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
217 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
218 self.win.resize(n_lines, n_cols)
221 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
223 def safe_write(self, foo):
225 def to_chars_with_attrs(part):
226 attr = curses.A_NORMAL
228 if not type(part) == str:
229 part_string = part[0]
231 if len(part_string) > 0:
232 return [(char, attr) for char in part_string]
233 elif len(part_string) == 1:
237 chars_with_attrs = []
238 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
239 chars_with_attrs += to_chars_with_attrs(foo)
242 chars_with_attrs += to_chars_with_attrs(part)
244 if len(chars_with_attrs) < len(self):
245 for char_with_attr in chars_with_attrs:
246 self.win.addstr(char_with_attr[0], char_with_attr[1])
247 else: # workaround to <https://stackoverflow.com/q/7063128>
248 cut = chars_with_attrs[:len(self) - 1]
249 last_char_with_attr = chars_with_attrs[len(self) - 1]
250 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
251 last_char_with_attr[0], last_char_with_attr[1])
252 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
254 for char_with_attr in cut:
255 self.win.addstr(char_with_attr[0], char_with_attr[1])
257 def ensure_freshness(self, do_refresh=False):
259 for key in self.check_game:
260 if self.tui.game.to_update[key]:
264 for key in self.check_tui:
265 if self.tui.to_update[key]:
274 class EditWidget(Widget):
277 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
280 class LogWidget(Widget):
283 line_width = self.size[1]
284 log_lines = self.tui.game.log_text.split('\n')
286 for line in log_lines:
287 to_pad = line_width - (len(line) % line_width)
288 if to_pad == line_width:
290 to_join += [line + ' '*to_pad]
291 self.safe_write((''.join(to_join), curses.color_pair(3)))
294 class MapWidget(Widget):
298 def terrain_with_objects():
299 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
300 for t in self.tui.game.world.things:
301 pos_i = self.tui.game.world.map_.get_position_index(t.position)
302 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
303 return ''.join(terrain_as_list)
305 def pad_or_cut_x(lines):
306 line_width = self.size[1]
307 for y in range(len(lines)):
309 if line_width > len(line):
310 to_pad = line_width - (len(line) % line_width)
311 lines[y] = line + '0' * to_pad
313 lines[y] = line[:line_width]
316 if len(lines) < self.size[0]:
317 to_pad = self.size[0] - len(lines)
318 lines += to_pad * ['0' * self.size[1]]
320 def lines_to_colored_chars(lines):
321 chars_with_attrs = []
322 for c in ''.join(lines):
324 chars_with_attrs += [(c, curses.color_pair(1))]
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.curs_set(False) # hide cursor
377 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
378 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
379 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
380 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
381 widgets = (self.edit, self.turn, self.log, self.map_)
386 for key in self.game.to_update:
387 self.game.to_update[key] = False
388 for key in self.to_update:
389 self.to_update[key] = False
391 key = self.stdscr.getkey()
392 if key == 'KEY_RESIZE':
394 self.setup_screen(curses.initscr())
397 w.ensure_freshness(True)
398 elif key == '\t': # Tabulator key.
399 map_mode = False if map_mode else True
401 if type(self.game.world.map_) == MapSquare:
403 self.socket.send('TASK:MOVE LEFT')
405 self.socket.send('TASK:MOVE RIGHT')
407 self.socket.send('TASK:MOVE UP')
409 self.socket.send('TASK:MOVE DOWN')
410 elif type(self.game.world.map_) == MapHex:
412 self.socket.send('TASK:MOVE UPLEFT')
414 self.socket.send('TASK:MOVE UPRIGHT')
416 self.socket.send('TASK:MOVE LEFT')
418 self.socket.send('TASK:MOVE RIGHT')
420 self.socket.send('TASK:MOVE DOWNLEFT')
422 self.socket.send('TASK:MOVE DOWNRIGHT')
424 if len(key) == 1 and key in ASCII_printable and \
425 len(self.to_send) < len(self.edit):
426 self.to_send += [key]
427 self.to_update['edit'] = True
428 elif key == 'KEY_BACKSPACE':
429 self.to_send[:] = self.to_send[:-1]
430 self.to_update['edit'] = True
431 elif key == '\n': # Return key
432 self.socket.send(''.join(self.to_send))
434 self.to_update['edit'] = True
437 if self.game.do_quit:
441 s = socket.create_connection(('127.0.0.1', 5000))
442 plom_socket = plom_socket.PlomSocket(s)
444 t = threading.Thread(target=recv_loop, args=(plom_socket, game))
446 TUI(plom_socket, game)