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_MAP_CONTROL(game, content):
107 game.map_control_content = content
108 cmd_MAP_CONTROL.argtypes = 'string'
110 def cmd_GAME_STATE_COMPLETE(game):
112 if game.tui.mode.name == 'post_login_wait':
113 game.tui.switch_mode('play')
115 if game.tui.mode.shows_info:
116 game.tui.query_info()
117 player = game.get_thing(game.player_id, False)
118 if player.position in game.portals:
119 game.tui.teleport_target_host = game.portals[player.position]
120 game.tui.switch_mode('teleport')
121 game.turn_complete = True
122 game.tui.do_refresh = True
123 cmd_GAME_STATE_COMPLETE.argtypes = ''
125 def cmd_PORTAL(game, position, msg):
126 game.portals[position] = msg
127 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
129 def cmd_PLAY_ERROR(game, msg):
131 game.tui.do_refresh = True
132 cmd_PLAY_ERROR.argtypes = 'string'
134 def cmd_GAME_ERROR(game, msg):
135 game.tui.log_msg('? game error: ' + msg)
136 game.tui.do_refresh = True
137 cmd_GAME_ERROR.argtypes = 'string'
139 def cmd_ARGUMENT_ERROR(game, msg):
140 game.tui.log_msg('? syntax error: ' + msg)
141 game.tui.do_refresh = True
142 cmd_ARGUMENT_ERROR.argtypes = 'string'
144 def cmd_ANNOTATION(game, position, msg):
145 game.info_db[position] = msg
146 if game.tui.mode.shows_info:
147 game.tui.do_refresh = True
148 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
150 def cmd_TASKS(game, tasks_comma_separated):
151 game.tasks = tasks_comma_separated.split(',')
152 cmd_TASKS.argtypes = 'string'
156 cmd_PONG.argtypes = ''
158 class Game(GameBase):
159 thing_type = ThingBase
160 turn_complete = False
163 def __init__(self, *args, **kwargs):
164 super().__init__(*args, **kwargs)
165 self.register_command(cmd_LOGIN_OK)
166 self.register_command(cmd_PONG)
167 self.register_command(cmd_CHAT)
168 self.register_command(cmd_PLAYER_ID)
169 self.register_command(cmd_TURN)
170 self.register_command(cmd_THING_POS)
171 self.register_command(cmd_THING_NAME)
172 self.register_command(cmd_MAP)
173 self.register_command(cmd_MAP_CONTROL)
174 self.register_command(cmd_PORTAL)
175 self.register_command(cmd_ANNOTATION)
176 self.register_command(cmd_GAME_STATE_COMPLETE)
177 self.register_command(cmd_ARGUMENT_ERROR)
178 self.register_command(cmd_GAME_ERROR)
179 self.register_command(cmd_PLAY_ERROR)
180 self.register_command(cmd_TASKS)
181 self.map_content = ''
186 def get_string_options(self, string_option_type):
187 if string_option_type == 'map_geometry':
188 return ['Hex', 'Square']
191 def get_command(self, command_name):
192 from functools import partial
193 f = partial(self.commands[command_name], self)
194 f.argtypes = self.commands[command_name].argtypes
201 def __init__(self, name, has_input_prompt=False, shows_info=False,
204 self.has_input_prompt = has_input_prompt
205 self.shows_info = shows_info
206 self.is_intro = is_intro
208 def __init__(self, host):
212 self.mode_play = self.Mode('play')
213 self.mode_study = self.Mode('study', shows_info=True)
214 self.mode_edit = self.Mode('edit')
215 self.mode_annotate = self.Mode('annotate', has_input_prompt=True, shows_info=True)
216 self.mode_portal = self.Mode('portal', has_input_prompt=True, shows_info=True)
217 self.mode_chat = self.Mode('chat', has_input_prompt=True)
218 self.mode_waiting_for_server = self.Mode('waiting_for_server', is_intro=True)
219 self.mode_login = self.Mode('login', has_input_prompt=True, is_intro=True)
220 self.mode_post_login_wait = self.Mode('post_login_wait', is_intro=True)
221 self.mode_teleport = self.Mode('teleport', has_input_prompt=True)
222 self.mode_password = self.Mode('password', has_input_prompt=True)
225 self.parser = Parser(self.game)
227 self.do_refresh = True
228 self.queue = queue.Queue()
229 self.login_name = None
230 self.map_mode = 'terrain'
231 self.password = 'foo'
232 self.switch_mode('waiting_for_server')
234 'switch_to_chat': 't',
235 'switch_to_play': 'p',
236 'switch_to_password': 'p',
237 'switch_to_annotate': 'm',
238 'switch_to_portal': 'P',
239 'switch_to_study': '?',
240 'switch_to_edit': 'm',
242 'toggle_map_mode': 'M',
243 'hex_move_upleft': 'w',
244 'hex_move_upright': 'e',
245 'hex_move_right': 'd',
246 'hex_move_downright': 'x',
247 'hex_move_downleft': 'y',
248 'hex_move_left': 'a',
249 'square_move_up': 'w',
250 'square_move_left': 'a',
251 'square_move_down': 's',
252 'square_move_right': 'd',
254 if os.path.isfile('config.json'):
255 with open('config.json', 'r') as f:
256 keys_conf = json.loads(f.read())
258 self.keys[k] = keys_conf[k]
259 curses.wrapper(self.loop)
266 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
267 raise BrokenSocketConnection
268 self.socket.send(msg)
269 except (BrokenPipeError, BrokenSocketConnection):
270 self.log_msg('@ server disconnected :(')
271 self.do_refresh = True
273 def log_msg(self, msg):
275 if len(self.log) > 100:
276 self.log = self.log[-100:]
278 def query_info(self):
279 self.send('GET_ANNOTATION ' + str(self.explorer))
281 def switch_mode(self, mode_name, keep_position = False):
282 self.map_mode = 'terrain'
283 self.mode = getattr(self, 'mode_' + mode_name)
284 if self.mode.shows_info and not keep_position:
285 player = self.game.get_thing(self.game.player_id, False)
286 self.explorer = YX(player.position.y, player.position.x)
287 if self.mode.name == 'waiting_for_server':
288 self.log_msg('@ waiting for server …')
289 elif self.mode.name == 'login':
291 self.send('LOGIN ' + quote(self.login_name))
293 self.log_msg('@ enter username')
294 elif self.mode.name == 'teleport':
295 self.log_msg("@ May teleport to %s" % (self.teleport_target_host)),
296 self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
297 elif self.mode.name == 'annotate' and self.explorer in self.game.info_db:
298 info = self.game.info_db[self.explorer]
301 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
302 self.input_ = self.game.portals[self.explorer]
303 elif self.mode.name == 'password':
304 self.input_ = self.password
307 self.log_msg("HELP:");
308 self.log_msg("chat mode commands:");
309 self.log_msg(" /nick NAME - re-name yourself to NAME");
310 self.log_msg(" /msg USER TEXT - send TEXT to USER");
311 self.log_msg(" /help - show this help");
312 self.log_msg(" /%s or /play - switch to play mode" % self.keys['switch_to_play']);
313 self.log_msg(" /%s or /study - switch to study mode" % self.keys['switch_to_study']);
314 self.log_msg("commands common to study and play mode:");
315 if 'MOVE' in self.game.tasks:
316 self.log_msg(" %s - move" % ','.join(self.movement_keys));
317 self.log_msg(" %s - switch to chat mode" % self.keys['switch_to_chat']);
318 self.log_msg("commands specific to play mode:");
319 self.log_msg(" %s - enter terrain password" % self.keys['switch_to_password']);
320 if 'WRITE' in self.game.tasks:
321 self.log_msg(" %s - write following ASCII character" % self.keys['switch_to_edit']);
322 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
323 self.log_msg(" %s - flatten surroundings" % self.keys['flatten']);
324 self.log_msg(" %s - switch to study mode" % self.keys['switch_to_study']);
325 self.log_msg("commands specific to study mode:");
326 self.log_msg(" %s - switch to play mode" % self.keys['switch_to_play']);
327 if 'MOVE' not in self.game.tasks:
328 self.log_msg(" %s - move" % ','.join(self.movement_keys));
329 self.log_msg(" %s - annotate terrain" % self.keys['switch_to_annotate']);
330 self.log_msg(" %s - toggle terrain/control view" % self.keys['toggle_map_mode']);
332 def loop(self, stdscr):
336 def safe_addstr(y, x, line):
337 if y < self.size.y - 1 or x + len(line) < self.size.x:
338 stdscr.addstr(y, x, line)
339 else: # workaround to <https://stackoverflow.com/q/7063128>
340 cut_i = self.size.x - x - 1
342 last_char = line[cut_i]
343 stdscr.addstr(y, self.size.x - 2, last_char)
344 stdscr.insstr(y, self.size.x - 2, ' ')
345 stdscr.addstr(y, x, cut)
349 def handle_recv(msg):
355 socket_client_class = PlomSocketClient
356 if self.host.startswith('ws://') or self.host.startswith('wss://'):
357 socket_client_class = WebSocketClient
360 self.socket = socket_client_class(handle_recv, self.host)
361 self.socket_thread = threading.Thread(target=self.socket.run)
362 self.socket_thread.start()
363 self.socket.send('TASKS')
364 self.switch_mode('login')
366 except ConnectionRefusedError:
367 self.log_msg('@ server connect failure, trying again …')
374 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
375 # conditions with ws4py, find out what exactly
376 self.switch_mode('waiting_for_server')
379 def handle_input(msg):
380 command, args = self.parser.parse(msg)
383 def msg_into_lines_of_width(msg, width):
387 for i in range(len(msg)):
388 if x >= width or msg[i] == "\n":
398 def reset_screen_size():
399 self.size = YX(*stdscr.getmaxyx())
400 self.size = self.size - YX(self.size.y % 4, 0)
401 self.size = self.size - YX(0, self.size.x % 4)
402 self.window_width = int(self.size.x / 2)
404 def recalc_input_lines():
405 if not self.mode.has_input_prompt:
406 self.input_lines = []
408 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
411 def move_explorer(direction):
412 target = self.game.map_geometry.move(self.explorer, direction)
414 self.explorer = target
421 for line in self.log:
422 lines += msg_into_lines_of_width(line, self.window_width)
425 max_y = self.size.y - len(self.input_lines)
426 for i in range(len(lines)):
427 if (i >= max_y - height_header):
429 safe_addstr(max_y - i - 1, self.window_width, lines[i])
432 if not self.game.turn_complete:
434 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
435 info = 'TERRAIN: %s\n' % self.game.map_content[pos_i]
436 for t in self.game.things:
437 if t.position == self.explorer:
438 info += 'PLAYER @: %s\n' % t.name
439 if self.explorer in self.game.portals:
440 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
442 info += 'PORTAL: (none)\n'
443 if self.explorer in self.game.info_db:
444 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
446 info += 'ANNOTATION: waiting …'
447 lines = msg_into_lines_of_width(info, self.window_width)
449 for i in range(len(lines)):
450 y = height_header + i
451 if y >= self.size.y - len(self.input_lines):
453 safe_addstr(y, self.window_width, lines[i])
456 y = self.size.y - len(self.input_lines)
457 for i in range(len(self.input_lines)):
458 safe_addstr(y, self.window_width, self.input_lines[i])
462 if not self.game.turn_complete:
464 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
467 safe_addstr(1, self.window_width, 'MODE: ' + self.mode.name)
470 if not self.game.turn_complete:
473 map_content = self.game.map_content
474 if self.map_mode == 'control':
475 map_content = self.game.map_control_content
476 for y in range(self.game.map_geometry.size.y):
477 start = self.game.map_geometry.size.x * y
478 end = start + self.game.map_geometry.size.x
479 map_lines_split += [list(map_content[start:end])]
480 if self.map_mode == 'terrain':
481 for t in self.game.things:
482 map_lines_split[t.position.y][t.position.x] = '@'
483 if self.mode.shows_info:
484 map_lines_split[self.explorer.y][self.explorer.x] = '?'
486 if type(self.game.map_geometry) == MapGeometryHex:
488 for line in map_lines_split:
489 map_lines += [indent*' ' + ' '.join(line)]
490 indent = 0 if indent else 1
492 for line in map_lines_split:
493 map_lines += [' '.join(line)]
494 window_center = YX(int(self.size.y / 2),
495 int(self.window_width / 2))
496 player = self.game.get_thing(self.game.player_id, False)
497 center = player.position
498 if self.mode.shows_info:
499 center = self.explorer
500 center = YX(center.y, center.x * 2)
501 offset = center - window_center
502 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
504 term_y = max(0, -offset.y)
505 term_x = max(0, -offset.x)
506 map_y = max(0, offset.y)
507 map_x = max(0, offset.x)
508 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
509 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
510 safe_addstr(term_y, term_x, to_draw)
517 if self.mode.has_input_prompt:
519 if self.mode.shows_info:
524 if not self.mode.is_intro:
528 curses.curs_set(False) # hide cursor
529 curses.use_default_colors();
532 self.explorer = YX(0, 0)
536 last_ping = datetime.datetime.now()
537 interval = datetime.timedelta(seconds=30)
539 now = datetime.datetime.now()
540 if now - last_ping > interval:
545 self.do_refresh = False
548 msg = self.queue.get(block=False)
553 key = stdscr.getkey()
554 self.do_refresh = True
557 if key == 'KEY_RESIZE':
559 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
560 self.input_ = self.input_[:-1]
561 elif self.mode.has_input_prompt and key != '\n': # Return key
563 max_length = self.window_width * self.size.y - len(input_prompt) - 1
564 if len(self.input_) > max_length:
565 self.input_ = self.input_[:max_length]
566 elif self.mode == self.mode_login and key == '\n':
567 self.login_name = self.input_
568 self.send('LOGIN ' + quote(self.input_))
570 elif self.mode == self.mode_password and key == '\n':
571 if self.input_ == '':
573 self.password = self.input_
575 self.switch_mode('play')
576 elif self.mode == self.mode_chat and key == '\n':
577 if self.input_[0] == '/':
578 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
579 self.switch_mode('play')
580 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
581 self.switch_mode('study')
582 elif self.input_ == '/help':
584 elif self.input_ == '/reconnect':
586 elif self.input_.startswith('/nick'):
587 tokens = self.input_.split(maxsplit=1)
589 self.send('NICK ' + quote(tokens[1]))
591 self.log_msg('? need login name')
592 elif self.input_.startswith('/msg'):
593 tokens = self.input_.split(maxsplit=2)
595 self.send('QUERY %s %s' % (quote(tokens[1]),
598 self.log_msg('? need message target and message')
600 self.log_msg('? unknown command')
602 self.send('ALL ' + quote(self.input_))
604 elif self.mode == self.mode_annotate and key == '\n':
605 if self.input_ == '':
607 self.send('ANNOTATE %s %s' % (self.explorer, quote(self.input_)))
609 self.switch_mode('study', keep_position=True)
610 elif self.mode == self.mode_portal and key == '\n':
611 if self.input_ == '':
613 self.send('PORTAL %s %s' % (self.explorer, quote(self.input_)))
615 self.switch_mode('study', keep_position=True)
616 elif self.mode == self.mode_teleport and key == '\n':
617 if self.input_ == 'YES!':
618 self.host = self.teleport_target_host
621 self.log_msg('@ teleport aborted')
622 self.switch_mode('play')
624 elif self.mode == self.mode_study:
625 if key == self.keys['switch_to_chat']:
626 self.switch_mode('chat')
627 elif key == self.keys['switch_to_play']:
628 self.switch_mode('play')
629 elif key == self.keys['switch_to_annotate']:
630 self.switch_mode('annotate', keep_position=True)
631 elif key == self.keys['switch_to_portal']:
632 self.switch_mode('portal', keep_position=True)
633 elif key == self.keys['toggle_map_mode']:
634 if self.map_mode == 'terrain':
635 self.map_mode = 'control'
637 self.map_mode = 'terrain'
638 elif key in self.movement_keys:
639 move_explorer(self.movement_keys[key])
640 elif self.mode == self.mode_play:
641 if key == self.keys['switch_to_chat']:
642 self.switch_mode('chat')
643 elif key == self.keys['switch_to_study']:
644 self.switch_mode('study')
645 elif key == self.keys['switch_to_password']:
646 self.switch_mode('password')
647 if key == self.keys['switch_to_edit'] and\
648 'WRITE' in self.game.tasks:
649 self.switch_mode('edit')
650 elif key == self.keys['flatten'] and\
651 'FLATTEN_SURROUNDINGS' in self.game.tasks:
652 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
653 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
654 self.send('TASK:MOVE ' + self.movement_keys[key])
655 elif self.mode == self.mode_edit:
656 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
657 self.switch_mode('play')
659 TUI('localhost:5000')