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(globals())
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 handle_input(self, msg):
116 command = self.parser.parse(msg)
118 self.log('UNHANDLED INPUT: ' + msg)
119 self.to_update['log'] = True
122 except ArgError as e:
123 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
124 self.to_update['log'] = True
127 """Prefix msg plus newline to self.log_text."""
128 self.log_text = msg + '\n' + self.log_text
129 self.to_update['log'] = True
131 def symbol_for_type(self, type_):
135 elif type_ == 'monster':
139 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
142 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
144 def cmd_TURN_FINISHED(self, n):
145 """Do nothing. (This may be extended later.)"""
147 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
149 def cmd_NEW_TURN(self, n):
150 """Set self.turn to n, empty self.things."""
152 self.world.things = []
153 self.to_update['turn'] = False
154 self.to_update['map'] = False
155 cmd_NEW_TURN.argtypes = 'int:nonneg'
157 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
158 self.world.map_.set_line(y, terrain_line)
159 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
161 def cmd_PLAYER_POS(self, yx):
162 self.world.player_position = yx
163 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
165 def cmd_GAME_STATE_COMPLETE(self):
166 self.to_update['turn'] = True
167 self.to_update['map'] = True
170 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
171 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
174 def recv_loop(socket, game):
175 for msg in plom_socket_io.recv(s):
176 game.handle_input(msg)
181 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
182 self.check_game = check_game
183 self.check_tui = check_tui
186 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
187 self.size_def = size # store for re-calling .size on SIGWINCH
189 self.do_update = True
193 return self.win.getmaxyx()
196 def size(self, size):
197 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
198 n_lines, n_cols = size
200 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
202 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
203 self.win.resize(n_lines, n_cols)
206 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
208 def safe_write(self, foo):
210 def to_chars_with_attrs(part):
211 attr = curses.A_NORMAL
213 if not type(part) == str:
214 part_string = part[0]
216 if len(part_string) > 0:
217 return [(char, attr) for char in part_string]
218 elif len(part_string) == 1:
222 chars_with_attrs = []
223 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
224 chars_with_attrs += to_chars_with_attrs(foo)
227 chars_with_attrs += to_chars_with_attrs(part)
229 if len(chars_with_attrs) < len(self):
230 for char_with_attr in chars_with_attrs:
231 self.win.addstr(char_with_attr[0], char_with_attr[1])
232 else: # workaround to <https://stackoverflow.com/q/7063128>
233 cut = chars_with_attrs[:len(self) - 1]
234 last_char_with_attr = chars_with_attrs[len(self) - 1]
235 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
236 last_char_with_attr[0], last_char_with_attr[1])
237 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
239 for char_with_attr in cut:
240 self.win.addstr(char_with_attr[0], char_with_attr[1])
242 def ensure_freshness(self, do_refresh=False):
244 for key in self.check_game:
245 if self.tui.game.to_update[key]:
249 for key in self.check_tui:
250 if self.tui.to_update[key]:
259 class EditWidget(Widget):
262 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
265 class LogWidget(Widget):
268 line_width = self.size[1]
269 log_lines = self.tui.game.log_text.split('\n')
271 for line in log_lines:
272 to_pad = line_width - (len(line) % line_width)
273 if to_pad == line_width:
275 to_join += [line + ' '*to_pad]
276 self.safe_write((''.join(to_join), curses.color_pair(3)))
279 class MapWidget(Widget):
283 def terrain_with_objects():
284 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
285 for t in self.tui.game.world.things:
286 pos_i = self.tui.game.world.map_.get_position_index(t.position)
287 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
288 return ''.join(terrain_as_list)
290 def pad_or_cut_x(lines):
291 line_width = self.size[1]
292 for y in range(len(lines)):
294 if line_width > len(line):
295 to_pad = line_width - (len(line) % line_width)
296 lines[y] = line + '0' * to_pad
298 lines[y] = line[:line_width]
301 if len(lines) < self.size[0]:
302 to_pad = self.size[0] - len(lines)
303 lines += to_pad * ['0' * self.size[1]]
305 def lines_to_colored_chars(lines):
306 chars_with_attrs = []
307 for c in ''.join(lines):
309 chars_with_attrs += [(c, curses.color_pair(1))]
311 chars_with_attrs += [(c, curses.color_pair(2))]
312 elif c in {'x', 'X', '#'}:
313 chars_with_attrs += [(c, curses.color_pair(3))]
315 chars_with_attrs += [c]
316 return chars_with_attrs
318 if self.tui.game.world.map_.terrain == '':
321 self.safe_write(''.join(lines))
324 terrain_with_objects = terrain_with_objects()
325 center = self.tui.game.world.player_position
326 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
330 self.safe_write(lines_to_colored_chars(lines))
333 class TurnWidget(Widget):
336 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
341 def __init__(self, socket, game):
344 self.parser = Parser(self.game)
345 self.to_update = {'edit': False}
346 curses.wrapper(self.loop)
348 def setup_screen(self, stdscr):
350 self.stdscr.refresh() # will be called by getkey else, clearing screen
351 self.stdscr.timeout(10)
352 self.stdscr.addstr(0, 0, 'SEND:')
353 self.stdscr.addstr(2, 0, 'TURN:')
355 def loop(self, stdscr):
356 self.setup_screen(stdscr)
357 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
358 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
359 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
360 curses.curs_set(False) # hide cursor
362 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
363 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
364 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
365 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
366 widgets = (self.edit, self.turn, self.log, self.map_)
371 for key in self.game.to_update:
372 self.game.to_update[key] = False
373 for key in self.to_update:
374 self.to_update[key] = False
376 key = self.stdscr.getkey()
377 if key == 'KEY_RESIZE':
379 self.setup_screen(curses.initscr())
382 w.ensure_freshness(True)
383 elif key == '\t': # Tabulator key.
384 map_mode = False if map_mode else True
386 if type(self.game.world.map_) == MapSquare:
388 plom_socket_io.send(self.socket, 'MOVE LEFT')
390 plom_socket_io.send(self.socket, 'MOVE RIGHT')
392 plom_socket_io.send(self.socket, 'MOVE UP')
394 plom_socket_io.send(self.socket, 'MOVE DOWN')
395 elif type(self.game.world.map_) == MapHex:
397 plom_socket_io.send(self.socket, 'MOVE UPLEFT')
399 plom_socket_io.send(self.socket, 'MOVE UPRIGHT')
401 plom_socket_io.send(self.socket, 'MOVE LEFT')
403 plom_socket_io.send(self.socket, 'MOVE RIGHT')
405 plom_socket_io.send(self.socket, 'MOVE DOWNLEFT')
407 plom_socket_io.send(self.socket, 'MOVE DOWNRIGHT')
409 if len(key) == 1 and key in ASCII_printable and \
410 len(self.to_send) < len(self.edit):
411 self.to_send += [key]
412 self.to_update['edit'] = True
413 elif key == 'KEY_BACKSPACE':
414 self.to_send[:] = self.to_send[:-1]
415 self.to_update['edit'] = True
416 elif key == '\n': # Return key
417 plom_socket_io.send(self.socket, ''.join(self.to_send))
419 self.to_update['edit'] = True
422 if self.game.do_quit:
426 s = socket.create_connection(('127.0.0.1', 5000))
428 t = threading.Thread(target=recv_loop, args=(s, game))