5 from plomrogue.game import GameBase
6 from plomrogue.parser import Parser
7 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
8 from plomrogue.things import ThingBase
9 from plomrogue.misc import quote
10 from plomrogue.errors import BrokenSocketConnection
12 from ws4py.client import WebSocketBaseClient
13 class WebSocketClient(WebSocketBaseClient):
15 def __init__(self, recv_handler, *args, **kwargs):
16 super().__init__(*args, **kwargs)
17 self.recv_handler = recv_handler
20 def received_message(self, message):
22 message = str(message)
23 self.recv_handler(message)
26 def plom_closed(self):
27 return self.client_terminated
29 from plomrogue.io_tcp import PlomSocket
30 class PlomSocketClient(PlomSocket):
32 def __init__(self, recv_handler, url):
34 self.recv_handler = recv_handler
35 host, port = url.split(':')
36 super().__init__(socket.create_connection((host, port)))
44 for msg in self.recv():
46 self.socket = ssl.wrap_socket(self.socket)
48 self.recv_handler(msg)
49 except BrokenSocketConnection:
50 pass # we assume socket will be known as dead by now
52 def cmd_TURN(game, n):
56 game.turn_complete = False
57 cmd_TURN.argtypes = 'int:nonneg'
59 def cmd_LOGIN_OK(game):
60 game.tui.switch_mode('post_login_wait')
61 game.tui.send('GET_GAMESTATE')
62 game.tui.log_msg('@ welcome')
63 cmd_LOGIN_OK.argtypes = ''
65 def cmd_CHAT(game, msg):
66 game.tui.log_msg('# ' + msg)
67 game.tui.do_refresh = True
68 cmd_CHAT.argtypes = 'string'
70 def cmd_PLAYER_ID(game, player_id):
71 game.player_id = player_id
72 cmd_PLAYER_ID.argtypes = 'int:nonneg'
74 def cmd_THING_POS(game, thing_id, position):
75 t = game.get_thing(thing_id, True)
77 cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg'
79 def cmd_THING_NAME(game, thing_id, name):
80 t = game.get_thing(thing_id, True)
82 cmd_THING_NAME.argtypes = 'int:nonneg string'
84 def cmd_MAP(game, geometry, size, content):
85 map_geometry_class = globals()['MapGeometry' + geometry]
86 game.map_geometry = map_geometry_class(size)
87 game.map_content = content
88 if type(game.map_geometry) == MapGeometrySquare:
89 game.tui.movement_keys = {
90 game.tui.keys['square_move_up']: 'UP',
91 game.tui.keys['square_move_left']: 'LEFT',
92 game.tui.keys['square_move_down']: 'DOWN',
93 game.tui.keys['square_move_right']: 'RIGHT',
95 elif type(game.map_geometry) == MapGeometryHex:
96 game.tui.movement_keys = {
97 game.tui.keys['hex_move_upleft']: 'UPLEFT',
98 game.tui.keys['hex_move_upright']: 'UPRIGHT',
99 game.tui.keys['hex_move_right']: 'RIGHT',
100 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
101 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
102 game.tui.keys['hex_move_left']: 'LEFT',
104 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
106 def cmd_GAME_STATE_COMPLETE(game):
108 if game.tui.mode.name == 'post_login_wait':
109 game.tui.switch_mode('play')
111 if game.tui.mode.shows_info:
112 game.tui.query_info()
113 player = game.get_thing(game.player_id, False)
114 if player.position in game.portals:
115 game.tui.teleport_target_host = game.portals[player.position]
116 game.tui.switch_mode('teleport')
117 game.turn_complete = True
118 game.tui.do_refresh = True
119 cmd_GAME_STATE_COMPLETE.argtypes = ''
121 def cmd_PORTAL(game, position, msg):
122 game.portals[position] = msg
123 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
125 def cmd_PLAY_ERROR(game, msg):
127 game.tui.do_refresh = True
128 cmd_PLAY_ERROR.argtypes = 'string'
130 def cmd_GAME_ERROR(game, msg):
131 game.tui.log_msg('? game error: ' + msg)
132 game.tui.do_refresh = True
133 cmd_GAME_ERROR.argtypes = 'string'
135 def cmd_ARGUMENT_ERROR(game, msg):
136 game.tui.log_msg('? syntax error: ' + msg)
137 game.tui.do_refresh = True
138 cmd_ARGUMENT_ERROR.argtypes = 'string'
140 def cmd_ANNOTATION(game, position, msg):
141 game.info_db[position] = msg
142 if game.tui.mode.shows_info:
143 game.tui.do_refresh = True
144 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
146 def cmd_TASKS(game, tasks_comma_separated):
147 game.tasks = tasks_comma_separated.split(',')
148 cmd_TASKS.argtypes = 'string'
152 cmd_PONG.argtypes = ''
154 class Game(GameBase):
155 thing_type = ThingBase
156 turn_complete = False
159 def __init__(self, *args, **kwargs):
160 super().__init__(*args, **kwargs)
161 self.register_command(cmd_LOGIN_OK)
162 self.register_command(cmd_PONG)
163 self.register_command(cmd_CHAT)
164 self.register_command(cmd_PLAYER_ID)
165 self.register_command(cmd_TURN)
166 self.register_command(cmd_THING_POS)
167 self.register_command(cmd_THING_NAME)
168 self.register_command(cmd_MAP)
169 self.register_command(cmd_PORTAL)
170 self.register_command(cmd_ANNOTATION)
171 self.register_command(cmd_GAME_STATE_COMPLETE)
172 self.register_command(cmd_ARGUMENT_ERROR)
173 self.register_command(cmd_GAME_ERROR)
174 self.register_command(cmd_PLAY_ERROR)
175 self.register_command(cmd_TASKS)
176 self.map_content = ''
181 def get_string_options(self, string_option_type):
182 if string_option_type == 'map_geometry':
183 return ['Hex', 'Square']
186 def get_command(self, command_name):
187 from functools import partial
188 f = partial(self.commands[command_name], self)
189 f.argtypes = self.commands[command_name].argtypes
196 def __init__(self, name, has_input_prompt=False, shows_info=False,
199 self.has_input_prompt = has_input_prompt
200 self.shows_info = shows_info
201 self.is_intro = is_intro
203 def __init__(self, host):
207 self.mode_play = self.Mode('play')
208 self.mode_study = self.Mode('study', shows_info=True)
209 self.mode_edit = self.Mode('edit')
210 self.mode_annotate = self.Mode('annotate', has_input_prompt=True, shows_info=True)
211 self.mode_portal = self.Mode('portal', has_input_prompt=True, shows_info=True)
212 self.mode_chat = self.Mode('chat', has_input_prompt=True)
213 self.mode_waiting_for_server = self.Mode('waiting_for_server', is_intro=True)
214 self.mode_login = self.Mode('login', has_input_prompt=True, is_intro=True)
215 self.mode_post_login_wait = self.Mode('post_login_wait', is_intro=True)
216 self.mode_teleport = self.Mode('teleport', has_input_prompt=True)
219 self.parser = Parser(self.game)
221 self.do_refresh = True
222 self.queue = queue.Queue()
223 self.login_name = None
224 self.switch_mode('waiting_for_server')
226 'switch_to_chat': 't',
227 'switch_to_play': 'p',
228 'switch_to_annotate': 'm',
229 'switch_to_portal': 'P',
230 'switch_to_study': '?',
231 'switch_to_edit': 'm',
233 'hex_move_upleft': 'w',
234 'hex_move_upright': 'e',
235 'hex_move_right': 'd',
236 'hex_move_downright': 'x',
237 'hex_move_downleft': 'y',
238 'hex_move_left': 'a',
239 'square_move_up': 'w',
240 'square_move_left': 'a',
241 'square_move_down': 's',
242 'square_move_right': 'd',
244 if os.path.isfile('config.json'):
245 with open('config.json', 'r') as f:
246 keys_conf = json.loads(f.read())
248 self.keys[k] = keys_conf[k]
249 curses.wrapper(self.loop)
256 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
257 raise BrokenSocketConnection
258 self.socket.send(msg)
259 except (BrokenPipeError, BrokenSocketConnection):
260 self.log_msg('@ server disconnected :(')
261 self.do_refresh = True
263 def log_msg(self, msg):
265 if len(self.log) > 100:
266 self.log = self.log[-100:]
268 def query_info(self):
269 self.send('GET_ANNOTATION ' + str(self.explorer))
271 def switch_mode(self, mode_name, keep_position = False):
272 self.mode = getattr(self, 'mode_' + mode_name)
273 if self.mode.shows_info and not keep_position:
274 player = self.game.get_thing(self.game.player_id, False)
275 self.explorer = YX(player.position.y, player.position.x)
276 if self.mode.name == 'waiting_for_server':
277 self.log_msg('@ waiting for server …')
278 elif self.mode.name == 'login':
280 self.send('LOGIN ' + quote(self.login_name))
282 self.log_msg('@ enter username')
283 elif self.mode.name == 'teleport':
284 self.log_msg("@ May teleport to %s" % (self.teleport_target_host)),
285 self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
286 elif self.mode.name == 'annotate' and self.explorer in self.game.info_db:
287 info = self.game.info_db[self.explorer]
290 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
291 self.input_ = self.game.portals[self.explorer]
294 self.log_msg("HELP:");
295 self.log_msg("chat mode commands:");
296 self.log_msg(" /nick NAME - re-name yourself to NAME");
297 self.log_msg(" /msg USER TEXT - send TEXT to USER");
298 self.log_msg(" /help - show this help");
299 self.log_msg(" /%s or /play - switch to play mode" % self.keys['switch_to_play']);
300 self.log_msg(" /%s or /study - switch to study mode" % self.keys['switch_to_study']);
301 self.log_msg("commands common to study and play mode:");
302 if 'MOVE' in self.game.tasks:
303 self.log_msg(" %s - move" % ','.join(self.movement_keys));
304 self.log_msg(" %s - switch to chat mode" % self.keys['switch_to_chat']);
305 self.log_msg("commands specific to play mode:");
306 if 'WRITE' in self.game.tasks:
307 self.log_msg(" %s - write following ASCII character" % self.keys['switch_to_edit']);
308 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
309 self.log_msg(" %s - flatten surroundings" % self.keys['flatten']);
310 self.log_msg(" %s - switch to study mode" % self.keys['switch_to_study']);
311 self.log_msg("commands specific to study mode:");
312 if 'MOVE' not in self.game.tasks:
313 self.log_msg(" %s - move" % ','.join(self.movement_keys));
314 self.log_msg(" %s - annotate terrain" % self.keys['switch_to_annotate']);
315 self.log_msg(" %s - switch to play mode" % self.keys['switch_to_play']);
317 def loop(self, stdscr):
321 def safe_addstr(y, x, line):
322 if y < self.size.y - 1 or x + len(line) < self.size.x:
323 stdscr.addstr(y, x, line)
324 else: # workaround to <https://stackoverflow.com/q/7063128>
325 cut_i = self.size.x - x - 1
327 last_char = line[cut_i]
328 stdscr.addstr(y, self.size.x - 2, last_char)
329 stdscr.insstr(y, self.size.x - 2, ' ')
330 stdscr.addstr(y, x, cut)
334 def handle_recv(msg):
340 socket_client_class = PlomSocketClient
341 if self.host.startswith('ws://') or self.host.startswith('wss://'):
342 socket_client_class = WebSocketClient
345 self.socket = socket_client_class(handle_recv, self.host)
346 self.socket_thread = threading.Thread(target=self.socket.run)
347 self.socket_thread.start()
348 self.socket.send('TASKS')
349 self.switch_mode('login')
351 except ConnectionRefusedError:
352 self.log_msg('@ server connect failure, trying again …')
359 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
360 # conditions with ws4py, find out what exactly
361 self.switch_mode('waiting_for_server')
364 def handle_input(msg):
365 command, args = self.parser.parse(msg)
368 def msg_into_lines_of_width(msg, width):
372 for i in range(len(msg)):
373 if x >= width or msg[i] == "\n":
383 def reset_screen_size():
384 self.size = YX(*stdscr.getmaxyx())
385 self.size = self.size - YX(self.size.y % 4, 0)
386 self.size = self.size - YX(0, self.size.x % 4)
387 self.window_width = int(self.size.x / 2)
389 def recalc_input_lines():
390 if not self.mode.has_input_prompt:
391 self.input_lines = []
393 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
396 def move_explorer(direction):
397 target = self.game.map_geometry.move(self.explorer, direction)
399 self.explorer = target
406 for line in self.log:
407 lines += msg_into_lines_of_width(line, self.window_width)
410 max_y = self.size.y - len(self.input_lines)
411 for i in range(len(lines)):
412 if (i >= max_y - height_header):
414 safe_addstr(max_y - i - 1, self.window_width, lines[i])
417 if not self.game.turn_complete:
419 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
420 info = 'TERRAIN: %s\n' % self.game.map_content[pos_i]
421 for t in self.game.things:
422 if t.position == self.explorer:
423 info += 'PLAYER @: %s\n' % t.name
424 if self.explorer in self.game.portals:
425 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
427 info += 'PORTAL: (none)\n'
428 if self.explorer in self.game.info_db:
429 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
431 info += 'ANNOTATION: waiting …'
432 lines = msg_into_lines_of_width(info, self.window_width)
434 for i in range(len(lines)):
435 y = height_header + i
436 if y >= self.size.y - len(self.input_lines):
438 safe_addstr(y, self.window_width, lines[i])
441 y = self.size.y - len(self.input_lines)
442 for i in range(len(self.input_lines)):
443 safe_addstr(y, self.window_width, self.input_lines[i])
447 if not self.game.turn_complete:
449 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
452 safe_addstr(1, self.window_width, 'MODE: ' + self.mode.name)
455 if not self.game.turn_complete:
458 for y in range(self.game.map_geometry.size.y):
459 start = self.game.map_geometry.size.x * y
460 end = start + self.game.map_geometry.size.x
461 map_lines_split += [list(self.game.map_content[start:end])]
462 for t in self.game.things:
463 map_lines_split[t.position.y][t.position.x] = '@'
464 if self.mode.shows_info:
465 map_lines_split[self.explorer.y][self.explorer.x] = '?'
467 if type(self.game.map_geometry) == MapGeometryHex:
469 for line in map_lines_split:
470 map_lines += [indent*' ' + ' '.join(line)]
471 indent = 0 if indent else 1
473 for line in map_lines_split:
474 map_lines += [' '.join(line)]
475 window_center = YX(int(self.size.y / 2),
476 int(self.window_width / 2))
477 player = self.game.get_thing(self.game.player_id, False)
478 center = player.position
479 if self.mode.shows_info:
480 center = self.explorer
481 center = YX(center.y, center.x * 2)
482 offset = center - window_center
483 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
485 term_y = max(0, -offset.y)
486 term_x = max(0, -offset.x)
487 map_y = max(0, offset.y)
488 map_x = max(0, offset.x)
489 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
490 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
491 safe_addstr(term_y, term_x, to_draw)
498 if self.mode.has_input_prompt:
500 if self.mode.shows_info:
505 if not self.mode.is_intro:
509 curses.curs_set(False) # hide cursor
510 curses.use_default_colors();
513 self.explorer = YX(0, 0)
517 last_ping = datetime.datetime.now()
518 interval = datetime.timedelta(seconds=30)
520 now = datetime.datetime.now()
521 if now - last_ping > interval:
526 self.do_refresh = False
529 msg = self.queue.get(block=False)
534 key = stdscr.getkey()
535 self.do_refresh = True
538 if key == 'KEY_RESIZE':
540 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
541 self.input_ = self.input_[:-1]
542 elif self.mode.has_input_prompt and key != '\n': # Return key
544 max_length = self.window_width * self.size.y - len(input_prompt) - 1
545 if len(self.input_) > max_length:
546 self.input_ = self.input_[:max_length]
547 elif self.mode == self.mode_login and key == '\n':
548 self.login_name = self.input_
549 self.send('LOGIN ' + quote(self.input_))
551 elif self.mode == self.mode_chat and key == '\n':
552 if self.input_[0] == '/':
553 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
554 self.switch_mode('play')
555 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
556 self.switch_mode('study')
557 elif self.input_ == '/help':
559 elif self.input_ == '/reconnect':
561 elif self.input_.startswith('/nick'):
562 tokens = self.input_.split(maxsplit=1)
564 self.send('NICK ' + quote(tokens[1]))
566 self.log_msg('? need login name')
567 elif self.input_.startswith('/msg'):
568 tokens = self.input_.split(maxsplit=2)
570 self.send('QUERY %s %s' % (quote(tokens[1]),
573 self.log_msg('? need message target and message')
575 self.log_msg('? unknown command')
577 self.send('ALL ' + quote(self.input_))
579 elif self.mode == self.mode_annotate and key == '\n':
580 if self.input_ == '':
582 self.send('ANNOTATE %s %s' % (self.explorer, quote(self.input_)))
584 self.switch_mode('study', keep_position=True)
585 elif self.mode == self.mode_portal and key == '\n':
586 if self.input_ == '':
588 self.send('PORTAL %s %s' % (self.explorer, quote(self.input_)))
590 self.switch_mode('study', keep_position=True)
591 elif self.mode == self.mode_teleport and key == '\n':
592 if self.input_ == 'YES!':
593 self.host = self.teleport_target_host
596 self.log_msg('@ teleport aborted')
597 self.switch_mode('play')
599 elif self.mode == self.mode_study:
600 if key == self.keys['switch_to_chat']:
601 self.switch_mode('chat')
602 elif key == self.keys['switch_to_play']:
603 self.switch_mode('play')
604 elif key == self.keys['switch_to_annotate']:
605 self.switch_mode('annotate', keep_position=True)
606 elif key == self.keys['switch_to_portal']:
607 self.switch_mode('portal', keep_position=True)
608 elif key in self.movement_keys:
609 move_explorer(self.movement_keys[key])
610 elif self.mode == self.mode_play:
611 if key == self.keys['switch_to_chat']:
612 self.switch_mode('chat')
613 elif key == self.keys['switch_to_study']:
614 self.switch_mode('study')
615 if key == self.keys['switch_to_edit'] and\
616 'WRITE' in self.game.tasks:
617 self.switch_mode('edit')
618 elif key == self.keys['flatten'] and\
619 'FLATTEN_SURROUNDINGS' in self.game.tasks:
620 self.send('TASK:FLATTEN_SURROUNDINGS')
621 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
622 self.send('TASK:MOVE ' + self.movement_keys[key])
623 elif self.mode == self.mode_edit:
624 self.send('TASK:WRITE ' + key)
625 self.switch_mode('play')
627 TUI('localhost:5000')