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):
92 self.map_manager = map_manager
93 self.parser = Parser(self)
94 self.world = World(self)
103 def handle_input(self, msg):
108 command = self.parser.parse(msg)
110 self.log('UNHANDLED INPUT: ' + msg)
111 self.to_update['log'] = True
114 except ArgError as e:
115 self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
116 self.to_update['log'] = True
119 """Prefix msg plus newline to self.log_text."""
120 self.log_text = msg + '\n' + self.log_text
122 def symbol_for_type(self, type_):
126 elif type_ == 'monster':
130 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
133 self.to_update['log'] = True
134 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
136 def cmd_TURN_FINISHED(self, n):
137 """Do nothing. (This may be extended later.)"""
139 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
141 def cmd_NEW_TURN(self, n):
142 """Set self.turn to n, empty self.things."""
144 self.world.things = []
145 cmd_NEW_TURN.argtypes = 'int:nonneg'
147 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
148 self.world.map_.set_line(y, terrain_line)
149 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
151 def cmd_PLAYER_POS(self, yx):
152 self.world.player_position = yx
153 cmd_PLAYER_POS.argtypes = 'yx_tuple:pos'
155 def cmd_GAME_STATE_COMPLETE(self):
156 self.to_update['turn'] = True
157 self.to_update['map'] = True
160 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
161 'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
164 def recv_loop(socket, game):
165 for msg in plom_socket_io.recv(s):
166 game.handle_input(msg)
171 def __init__(self, tui, start, size, check_game=[], check_tui=[]):
172 self.check_game = check_game
173 self.check_tui = check_tui
176 self.win = curses.newwin(1, 1, self.start[0], self.start[1])
177 self.size_def = size # store for re-calling .size on SIGWINCH
179 self.do_update = True
183 return self.win.getmaxyx()
186 def size(self, size):
187 """Set window size. Size be y,x tuple. If y or x None, use legal max."""
188 n_lines, n_cols = size
190 n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
192 n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
193 self.win.resize(n_lines, n_cols)
196 return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
198 def safe_write(self, foo):
200 def to_chars_with_attrs(part):
201 attr = curses.A_NORMAL
203 if not type(part) == str:
204 part_string = part[0]
206 if len(part_string) > 0:
207 return [(char, attr) for char in part_string]
208 elif len(part_string) == 1:
212 chars_with_attrs = []
213 if type(foo) == str or len(foo) == 2 and type(foo[1]) == int:
214 chars_with_attrs += to_chars_with_attrs(foo)
217 chars_with_attrs += to_chars_with_attrs(part)
219 if len(chars_with_attrs) < len(self):
220 for char_with_attr in chars_with_attrs:
221 self.win.addstr(char_with_attr[0], char_with_attr[1])
222 else: # workaround to <https://stackoverflow.com/q/7063128>
223 cut = chars_with_attrs[:len(self) - 1]
224 last_char_with_attr = chars_with_attrs[len(self) - 1]
225 self.win.addstr(self.size[0] - 1, self.size[1] - 2,
226 last_char_with_attr[0], last_char_with_attr[1])
227 self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
229 for char_with_attr in cut:
230 self.win.addstr(char_with_attr[0], char_with_attr[1])
232 def ensure_freshness(self, do_refresh=False):
234 for key in self.check_game:
235 if self.tui.game.to_update[key]:
239 for key in self.check_tui:
240 if self.tui.to_update[key]:
249 class EditWidget(Widget):
252 self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
255 class LogWidget(Widget):
258 line_width = self.size[1]
259 log_lines = self.tui.game.log_text.split('\n')
261 for line in log_lines:
262 to_pad = line_width - (len(line) % line_width)
263 if to_pad == line_width:
265 to_join += [line + ' '*to_pad]
266 self.safe_write((''.join(to_join), curses.color_pair(3)))
269 class MapWidget(Widget):
273 if len(self.tui.game.world.map_.terrain) > 0:
274 terrain_as_list = list(self.tui.game.world.map_.terrain[:])
275 for t in self.tui.game.world.things:
276 pos_i = self.tui.game.world.map_.get_position_index(t.position)
277 terrain_as_list[pos_i] = self.tui.game.symbol_for_type(t.type_)
278 center = self.tui.game.world.player_position
279 lines = self.tui.game.world.map_.list_terrain_to_lines(terrain_as_list, center, self.size)
280 line_width = self.size[1]
282 if line_width > len(line):
283 to_pad = line_width - (len(line) % line_width)
284 to_join += [line + '0' * to_pad]
286 to_join += [line[:line_width]]
287 if len(to_join) < self.size[0]:
288 to_pad = self.size[0] - len(to_join)
289 to_join += to_pad * ['0' * self.size[1]]
290 text = ''.join(to_join)
294 text_as_list += [(c, curses.color_pair(1))]
296 text_as_list += [(c, curses.color_pair(2))]
297 elif c in {'x', 'X', '#'}:
298 text_as_list += [(c, curses.color_pair(3))]
301 self.safe_write(text_as_list)
304 class TurnWidget(Widget):
307 self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
312 def __init__(self, socket, game):
315 self.parser = Parser(self.game)
316 self.to_update = {'edit': False}
317 curses.wrapper(self.loop)
319 def setup_screen(self, stdscr):
321 self.stdscr.refresh() # will be called by getkey else, clearing screen
322 self.stdscr.timeout(10)
323 self.stdscr.addstr(0, 0, 'SEND:')
324 self.stdscr.addstr(2, 0, 'TURN:')
326 def loop(self, stdscr):
327 self.setup_screen(stdscr)
328 curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
329 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
330 curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
331 curses.curs_set(False) # hide cursor
333 self.edit = EditWidget(self, (0, 6), (1, 14), check_tui = ['edit'])
334 self.turn = TurnWidget(self, (2, 6), (1, 14), ['turn'])
335 self.log = LogWidget(self, (4, 0), (None, 20), ['log'])
336 self.map_ = MapWidget(self, (0, 21), (None, None), ['map'])
337 widgets = (self.edit, self.turn, self.log, self.map_)
341 for key in self.game.to_update:
342 self.game.to_update[key] = False
343 for key in self.to_update:
344 self.to_update[key] = False
346 key = self.stdscr.getkey()
347 if len(key) == 1 and key in ASCII_printable and \
348 len(self.to_send) < len(self.edit):
349 self.to_send += [key]
350 self.to_update['edit'] = True
351 elif key == 'KEY_BACKSPACE':
352 self.to_send[:] = self.to_send[:-1]
353 self.to_update['edit'] = True
355 plom_socket_io.send(self.socket, ''.join(self.to_send))
357 self.to_update['edit'] = True
358 elif key == 'KEY_RESIZE':
360 self.setup_screen(curses.initscr())
363 w.ensure_freshness(True)
366 if self.game.do_quit:
370 s = socket.create_connection(('127.0.0.1', 5000))
372 t = threading.Thread(target=recv_loop, args=(s, game))