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)
19 map_lines[:] = map_lines[start:start + view_height]
21 def x_cut(self, map_lines, center_x, view_width):
22 map_width = len(map_lines[0])
23 if map_width > view_width and center_x > view_width / 2:
24 if center_x > map_width - view_width / 2:
25 cut_start = map_width - view_width
28 cut_start = center_x - int(view_width / 2)
29 cut_end = cut_start + view_width
30 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):
40 while start_cut < len(map_string):
41 limit = start_cut + self.size[1]
42 map_lines += [map_string[start_cut:limit]]
46 map_lines = map_string_to_lines(map_string)
47 self.y_cut(map_lines, center[0], size[0])
48 self.x_cut(map_lines, center[1], size[1])
54 def format_to_view(self, map_string, center, size):
56 def map_string_to_lines(map_string):
57 map_view_chars = [' ']
61 map_view_chars += [c, ' ']
64 map_view_chars += ['\n']
68 map_view_chars += [' ']
69 return ''.join(map_view_chars).split('\n')
71 map_lines = map_string_to_lines(map_string)
72 self.y_cut(map_lines, center[0], size[0])
73 self.x_cut(map_lines, center[1] * 2, size[1])
77 map_manager = game_common.MapManager(globals())
80 class World(game_common.World):
82 def __init__(self, game, *args, **kwargs):
83 """Extend original with local classes and empty default map.
85 We need the empty default map because we draw the map widget
86 on any update, even before we actually receive map data.
88 super().__init__(*args, **kwargs)
90 self.map_ = self.game.map_manager.get_map_class('Hex')()
91 self.player_position = (0, 0)
94 class Game(game_common.CommonCommandsMixin):
97 self.map_manager = map_manager
98 self.parser = Parser(self)
99 self.world = World(self)
108 def handle_input(self, msg):
113 command = self.parser.parse(msg)
115 self.log('UNHANDLED INPUT: ' + msg)
116 self.to_update['log'] = True
119 except ArgError as e:
120 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
121 self.to_update['log'] = True
124 """Prefix msg plus newline to self.log_text."""
125 self.log_text = msg + '\n' + self.log_text
127 def symbol_for_type(self, type_):
131 elif type_ == 'monster':
135 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
138 self.to_update['log'] = True
139 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
141 def cmd_TURN_FINISHED(self, n):
142 """Do nothing. (This may be extended later.)"""
144 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
146 def cmd_NEW_TURN(self, n):
147 """Set self.turn to n, empty self.things."""
149 self.world.things = []
150 cmd_NEW_TURN.argtypes = 'int:nonneg'
152 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
153 self.world.map_.set_line(y, terrain_line)
154 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
156 def cmd_PLAYER_POS(self, yx):
157 self.world.player_position = yx
158 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
160 def cmd_GAME_STATE_COMPLETE(self):
161 self.to_update['turn'] = True
162 self.to_update['map'] = True
165 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
166 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
169 def recv_loop(socket, game):
170 for msg in plom_socket_io.recv(s):
171 game.handle_input(msg)
176 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
177 self.check_game = check_game
178 self.check_tui = check_tui
181 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
182 self.size_def = size # store for re-calling .size on SIGWINCH
184 self.do_update = True
188 return self.win.getmaxyx()
191 def size(self, size):
192 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
193 n_lines, n_cols = size
195 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
197 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
198 self.win.resize(n_lines, n_cols)
201 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
203 def safe_write(self, foo):
205 def to_chars_with_attrs(part):
206 attr = curses.A_NORMAL
208 if not type(part) == str:
209 part_string = part[0]
211 if len(part_string) > 0:
212 return [(char, attr) for char in part_string]
213 elif len(part_string) == 1:
217 chars_with_attrs = []
218 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
219 chars_with_attrs += to_chars_with_attrs(foo)
222 chars_with_attrs += to_chars_with_attrs(part)
224 if len(chars_with_attrs) < len(self):
225 for char_with_attr in chars_with_attrs:
226 self.win.addstr(char_with_attr[0], char_with_attr[1])
227 else: # workaround to <https://stackoverflow.com/q/7063128>
228 cut = chars_with_attrs[:len(self) - 1]
229 last_char_with_attr = chars_with_attrs[len(self) - 1]
230 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
231 last_char_with_attr[0], last_char_with_attr[1])
232 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
234 for char_with_attr in cut:
235 self.win.addstr(char_with_attr[0], char_with_attr[1])
237 def ensure_freshness(self, do_refresh=False):
239 for key in self.check_game:
240 if self.tui.game.to_update[key]:
244 for key in self.check_tui:
245 if self.tui.to_update[key]:
254 class EditWidget(Widget):
257 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
260 class LogWidget(Widget):
263 line_width = self.size[1]
264 log_lines = self.tui.game.log_text.split('\n')
266 for line in log_lines:
267 to_pad = line_width - (len(line) % line_width)
268 if to_pad == line_width:
270 to_join += [line + ' '*to_pad]
271 self.safe_write((''.join(to_join), curses.color_pair(3)))
274 class MapWidget(Widget):
278 def terrain_with_objects():
279 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
280 for t in self.tui.game.world.things:
281 pos_i = self.tui.game.world.map_.get_position_index(t.position)
282 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
283 return ''.join(terrain_as_list)
285 def pad_or_cut_x(lines):
286 line_width = self.size[1]
287 for y in range(len(lines)):
289 if line_width > len(line):
290 to_pad = line_width - (len(line) % line_width)
291 lines[y] = line + '0' * to_pad
293 lines[y] = line[:line_width]
296 if len(lines) < self.size[0]:
297 to_pad = self.size[0] - len(lines)
298 lines += to_pad * ['0' * self.size[1]]
300 def lines_to_colored_chars(lines):
301 chars_with_attrs = []
302 for c in ''.join(lines):
304 chars_with_attrs += [(c, curses.color_pair(1))]
306 chars_with_attrs += [(c, curses.color_pair(2))]
307 elif c in {'x', 'X', '#'}:
308 chars_with_attrs += [(c, curses.color_pair(3))]
310 chars_with_attrs += [c]
311 return chars_with_attrs
313 if self.tui.game.world.map_.terrain == '':
316 self.safe_write(''.join(lines))
319 terrain_with_objects = terrain_with_objects()
320 center = self.tui.game.world.player_position
321 lines = self.tui.game.world.map_.format_to_view(terrain_with_objects,
325 self.safe_write(lines_to_colored_chars(lines))
328 class TurnWidget(Widget):
331 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
336 def __init__(self, socket, game):
339 self.parser = Parser(self.game)
340 self.to_update = {'edit': False}
341 curses.wrapper(self.loop)
343 def setup_screen(self, stdscr):
345 self.stdscr.refresh() # will be called by getkey else, clearing screen
346 self.stdscr.timeout(10)
347 self.stdscr.addstr(0, 0, 'SEND:')
348 self.stdscr.addstr(2, 0, 'TURN:')
350 def loop(self, stdscr):
351 self.setup_screen(stdscr)
352 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
353 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
354 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
355 curses.curs_set(False) # hide cursor
357 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
358 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
359 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
360 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
361 widgets = (self.edit, self.turn, self.log, self.map_)
365 for key in self.game.to_update:
366 self.game.to_update[key] = False
367 for key in self.to_update:
368 self.to_update[key] = False
370 key = self.stdscr.getkey()
371 if len(key) == 1 and key in ASCII_printable and \
372 len(self.to_send) < len(self.edit):
373 self.to_send += [key]
374 self.to_update['edit'] = True
375 elif key == 'KEY_BACKSPACE':
376 self.to_send[:] = self.to_send[:-1]
377 self.to_update['edit'] = True
379 plom_socket_io.send(self.socket, ''.join(self.to_send))
381 self.to_update['edit'] = True
382 elif key == 'KEY_RESIZE':
384 self.setup_screen(curses.initscr())
387 w.ensure_freshness(True)
390 if self.game.do_quit:
394 s = socket.create_connection(('127.0.0.1', 5000))
396 t = threading.Thread(target=recv_loop, args=(s, game))