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 = ['+']
60 map_view_chars += [c, ' ']
63 map_view_chars += ['\n']
67 map_view_chars += ['+']
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 cmd_NEW_TURN.argtypes = 'int:nonneg'
155 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
156 self.world.map_.set_line(y, terrain_line)
157 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
159 def cmd_PLAYER_POS(self, yx):
160 self.world.player_position = yx
161 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
163 def cmd_GAME_STATE_COMPLETE(self):
164 self.to_update['turn'] = True
165 self.to_update['map'] = True
168 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
169 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
172 def recv_loop(socket, game):
173 for msg in plom_socket_io.recv(s):
174 game.handle_input(msg)
179 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
180 self.check_game = check_game
181 self.check_tui = check_tui
184 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
185 self.size_def = size # store for re-calling .size on SIGWINCH
187 self.do_update = True
191 return self.win.getmaxyx()
194 def size(self, size):
195 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
196 n_lines, n_cols = size
198 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
200 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
201 self.win.resize(n_lines, n_cols)
204 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
206 def safe_write(self, foo):
208 def to_chars_with_attrs(part):
209 attr = curses.A_NORMAL
211 if not type(part) == str:
212 part_string = part[0]
214 if len(part_string) > 0:
215 return [(char, attr) for char in part_string]
216 elif len(part_string) == 1:
220 chars_with_attrs = []
221 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
222 chars_with_attrs += to_chars_with_attrs(foo)
225 chars_with_attrs += to_chars_with_attrs(part)
227 if len(chars_with_attrs) < len(self):
228 for char_with_attr in chars_with_attrs:
229 self.win.addstr(char_with_attr[0], char_with_attr[1])
230 else: # workaround to <https://stackoverflow.com/q/7063128>
231 cut = chars_with_attrs[:len(self) - 1]
232 last_char_with_attr = chars_with_attrs[len(self) - 1]
233 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
234 last_char_with_attr[0], last_char_with_attr[1])
235 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
237 for char_with_attr in cut:
238 self.win.addstr(char_with_attr[0], char_with_attr[1])
240 def ensure_freshness(self, do_refresh=False):
242 for key in self.check_game:
243 if self.tui.game.to_update[key]:
247 for key in self.check_tui:
248 if self.tui.to_update[key]:
257 class EditWidget(Widget):
260 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
263 class LogWidget(Widget):
266 line_width = self.size[1]
267 log_lines = self.tui.game.log_text.split('\n')
269 for line in log_lines:
270 to_pad = line_width - (len(line) % line_width)
271 if to_pad == line_width:
273 to_join += [line + ' '*to_pad]
274 self.safe_write((''.join(to_join), curses.color_pair(3)))
277 class MapWidget(Widget):
281 def terrain_with_objects():
282 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
283 for t in self.tui.game.world.things:
284 pos_i = self.tui.game.world.map_.get_position_index(t.position)
285 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
286 return ''.join(terrain_as_list)
288 def pad_or_cut_x(lines):
289 line_width = self.size[1]
290 for y in range(len(lines)):
292 if line_width > len(line):
293 to_pad = line_width - (len(line) % line_width)
294 lines[y] = line + '0' * to_pad
296 lines[y] = line[:line_width]
299 if len(lines) < self.size[0]:
300 to_pad = self.size[0] - len(lines)
301 lines += to_pad * ['0' * self.size[1]]
303 def lines_to_colored_chars(lines):
304 chars_with_attrs = []
305 for c in ''.join(lines):
307 chars_with_attrs += [(c, curses.color_pair(1))]
309 chars_with_attrs += [(c, curses.color_pair(2))]
310 elif c in {'x', 'X', '#'}:
311 chars_with_attrs += [(c, curses.color_pair(3))]
313 chars_with_attrs += [c]
314 return chars_with_attrs
316 if self.tui.game.world.map_.terrain == '':
319 self.safe_write(''.join(lines))
322 terrain_with_objects = terrain_with_objects()
323 center = self.tui.game.world.player_position
324 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
328 self.safe_write(lines_to_colored_chars(lines))
331 class TurnWidget(Widget):
334 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
339 def __init__(self, socket, game):
342 self.parser = Parser(self.game)
343 self.to_update = {'edit': False}
344 curses.wrapper(self.loop)
346 def setup_screen(self, stdscr):
348 self.stdscr.refresh() # will be called by getkey else, clearing screen
349 self.stdscr.timeout(10)
350 self.stdscr.addstr(0, 0, 'SEND:')
351 self.stdscr.addstr(2, 0, 'TURN:')
353 def loop(self, stdscr):
354 self.setup_screen(stdscr)
355 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
356 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
357 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
358 curses.curs_set(False) # hide cursor
360 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
361 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
362 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
363 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
364 widgets = (self.edit, self.turn, self.log, self.map_)
369 for key in self.game.to_update:
370 self.game.to_update[key] = False
371 for key in self.to_update:
372 self.to_update[key] = False
374 key = self.stdscr.getkey()
375 if key == 'KEY_RESIZE':
377 self.setup_screen(curses.initscr())
380 w.ensure_freshness(True)
381 elif key == '\t': # Tabulator key.
382 map_mode = False if map_mode else True
384 if type(self.game.world.map_) == MapSquare:
386 plom_socket_io.send(self.socket, 'MOVE LEFT')
388 plom_socket_io.send(self.socket, 'MOVE RIGHT')
390 plom_socket_io.send(self.socket, 'MOVE UP')
392 plom_socket_io.send(self.socket, 'MOVE DOWN')
393 elif type(self.game.world.map_) == MapHex:
395 plom_socket_io.send(self.socket, 'MOVE UPLEFT')
397 plom_socket_io.send(self.socket, 'MOVE UPRIGHT')
399 plom_socket_io.send(self.socket, 'MOVE LEFT')
401 plom_socket_io.send(self.socket, 'MOVE RIGHT')
403 plom_socket_io.send(self.socket, 'MOVE DOWNLEFT')
405 plom_socket_io.send(self.socket, '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 plom_socket_io.send(self.socket, ''.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))
426 t = threading.Thread(target=recv_loop, args=(s, game))