6 from parser import ArgError, Parser
10 class MapSquare(game_common.Map):
12 def list_terrain_to_lines(self, terrain_as_list, center, size):
13 terrain = ''.join(terrain_as_list)
16 while start_cut < len(terrain):
17 limit = start_cut + self.size[1]
18 map_lines += [terrain[start_cut:limit]]
20 if len(map_lines) > size[0] and center[0] > size[0] / 2:
21 diff = len(map_lines) - size[0]
22 if center[0] > len(map_lines) - size[0] / 2:
23 map_lines = map_lines[diff:]
25 start = center[0] - int(size[0] / 2)
26 map_lines = map_lines[start:start + size[0]]
27 if self.size[1] > size[1] and center[1] > size[1] / 2:
28 if center[1] > self.size[1] - size[1] / 2:
29 cut_start = self.size[1] - size[1]
32 cut_start = center[1] - int(size[1] / 2)
33 cut_end = cut_start + size[1]
34 map_lines = [line[cut_start:cut_end] for line in map_lines]
38 class MapHex(game_common.Map):
40 def list_terrain_to_lines(self, terrain_as_list, center, size):
41 new_terrain_list = [' ']
44 for c in terrain_as_list:
45 new_terrain_list += [c, ' ']
48 new_terrain_list += ['\n']
52 new_terrain_list += [' ']
53 map_lines = ''.join(new_terrain_list).split('\n')
54 if len(map_lines) > size[0] and center[0] > size[0] / 2:
55 diff = len(map_lines) - size[0]
56 if center[0] > len(map_lines) - size[0] / 2:
57 map_lines = map_lines[diff:]
59 start = center[0] - int(size[0] / 2)
60 map_lines = map_lines[start:start + size[0]]
61 if self.size[1]*2 > size[1] and center[1]*4 > size[1]:
62 if center[1]*2 > self.size[1]*2 - size[1] / 2:
63 cut_start = self.size[1] * 2 - size[1]
66 cut_start = center[1]*2 - int(size[1] / 2)
67 cut_end = cut_start + size[1]
68 map_lines = [line[cut_start:cut_end] for line in map_lines]
72 map_manager = game_common.MapManager(globals())
75 class World(game_common.World):
77 def __init__(self, game, *args, **kwargs):
78 """Extend original with local classes and empty default map.
80 We need the empty default map because we draw the map widget
81 on any update, even before we actually receive map data.
83 super().__init__(*args, **kwargs)
85 self.map_ = self.game.map_manager.get_map_class('Hex')()
86 self.player_position = (0, 0)
89 class Game(game_common.CommonCommandsMixin):
91 def __init__(self, tui):
93 self.map_manager = map_manager
94 self.parser = Parser(self)
95 self.world = World(self)
99 """Prefix msg plus newline to self.log_text."""
100 self.log_text = msg + '\n' + self.log_text
102 def symbol_for_type(self, type_):
106 elif type_ == 'monster':
110 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
113 self.tui.log.do_update = True
114 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
116 def cmd_TURN_FINISHED(self, n):
117 """Do nothing. (This may be extended later.)"""
119 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
121 def cmd_NEW_TURN(self, n):
122 """Set self.turn to n, empty self.things."""
124 self.world.things = []
125 cmd_NEW_TURN.argtypes = 'int:nonneg'
127 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
128 self.world.map_.set_line(y, terrain_line)
129 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
131 def cmd_PLAYER_POS(self, yx):
132 self.world.player_position = yx
133 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
135 def cmd_GAME_STATE_COMPLETE(self):
136 self.tui.turn.do_update = True
137 self.tui.map_.do_update = True
140 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
141 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
144 def recv_loop(server_output):
145 for msg in plom_socket_io.recv(s):
146 while len(server_output) > 0:
148 server_output += [msg]
153 def __init__(self, tui, start, size):
156 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
157 self.size_def = size # store for re-calling .size on SIGWINCH
159 self.do_update = True
163 return self.win.getmaxyx()
166 def size(self, size):
167 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
168 n_lines, n_cols = size
170 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
172 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
173 self.win.resize(n_lines, n_cols)
176 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
178 def safe_write(self, foo):
180 def to_chars_with_attrs(part):
181 attr = curses.A_NORMAL
183 if not type(part) == str:
184 part_string = part[0]
186 if len(part_string) > 0:
187 return [(char, attr) for char in part_string]
188 elif len(part_string) == 1:
192 chars_with_attrs = []
193 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
194 chars_with_attrs += to_chars_with_attrs(foo)
197 chars_with_attrs += to_chars_with_attrs(part)
199 if len(chars_with_attrs) < len(self):
200 for char_with_attr in chars_with_attrs:
201 self.win.addstr(char_with_attr[0], char_with_attr[1])
202 else: # workaround to <https://stackoverflow.com/q/7063128>
203 cut = chars_with_attrs[:len(self) - 1]
204 last_char_with_attr = chars_with_attrs[len(self) - 1]
205 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
206 last_char_with_attr[0], last_char_with_attr[1])
207 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
209 for char_with_attr in cut:
210 self.win.addstr(char_with_attr[0], char_with_attr[1])
212 def draw_and_refresh(self):
218 class EditWidget(Widget):
221 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
224 class LogWidget(Widget):
227 line_width = self.size[1]
228 log_lines = self.tui.game.log_text.split('\n')
230 for line in log_lines:
231 to_pad = line_width - (len(line) % line_width)
232 if to_pad == line_width:
234 to_join += [line + ' '*to_pad]
235 self.safe_write((''.join(to_join), curses.color_pair(3)))
238 class MapWidget(Widget):
242 if len(self.tui.game.world.map_.terrain) > 0:
243 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
244 for t in self.tui.game.world.things:
245 pos_i = self.tui.game.world.map_.get_position_index(t.position)
246 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
247 center = self.tui.game.world.player_position
248 lines = self.tui.game.world.map_.list_terrain_to_lines(terrain_as_list, center, self.size)
249 line_width = self.size[1]
251 if line_width > len(line):
252 to_pad = line_width - (len(line) % line_width)
253 to_join += [line + '0' * to_pad]
255 to_join += [line[:line_width]]
256 if len(to_join) < self.size[0]:
257 to_pad = self.size[0] - len(to_join)
258 to_join += to_pad * ['0' * self.size[1]]
259 text = ''.join(to_join)
263 text_as_list += [(c, curses.color_pair(1))]
265 text_as_list += [(c, curses.color_pair(2))]
266 elif c in {'x', 'X', '#'}:
267 text_as_list += [(c, curses.color_pair(3))]
270 self.safe_write(text_as_list)
273 class TurnWidget(Widget):
276 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
281 def __init__(self, server_output):
282 self.server_output = server_output
283 self.game = Game(self)
284 self.parser = Parser(self.game)
285 self.do_update = True
286 curses.wrapper(self.loop)
288 def setup_screen(self, stdscr):
290 self.stdscr.refresh() # will be called by getkey else, clearing screen
291 self.stdscr.timeout(1)
292 self.stdscr.addstr(0, 0, 'SEND:')
293 self.stdscr.addstr(2, 0, 'TURN:')
295 def loop(self, stdscr):
296 self.setup_screen(stdscr)
297 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
298 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
299 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
300 curses.curs_set(False) # hide cursor
302 self.edit = EditWidget(self, (0, 6), (1, 14))
303 self.turn = TurnWidget(self, (2, 6), (1, 14))
304 self.log = LogWidget(self, (4, 0), (None, 20))
305 self.map_ = MapWidget(self, (0, 21), (None, None))
306 widgets = (self.edit, self.turn, self.log, self.map_)
313 key = self.stdscr.getkey()
314 if len(key) == 1 and key in ASCII_printable and \
315 len(self.to_send) < len(self.edit):
316 self.to_send += [key]
317 self.edit.do_update = True
318 elif key == 'KEY_BACKSPACE':
319 self.to_send[:] = self.to_send[:-1]
320 self.edit.do_update = True
322 plom_socket_io.send(s, ''.join(self.to_send))
324 self.edit.do_update = True
325 elif key == 'KEY_RESIZE':
327 self.setup_screen(curses.initscr())
333 if len(self.server_output) > 0:
334 do_quit = self.handle_input(self.server_output[0])
337 self.server_output[:] = []
338 self.do_update = True
340 def handle_input(self, msg):
344 command = self.parser.parse(msg)
346 self.game.log('UNHANDLED INPUT: ' + msg)
347 self.log.do_update = True
350 except ArgError as e:
351 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
352 self.log.do_update = True
357 s = socket.create_connection(('127.0.0.1', 5000))
358 t = threading.Thread(target=recv_loop, args=(server_output,))