7 from plomrogue.game import GameBase
8 from plomrogue.parser import Parser
9 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
10 from plomrogue.things import ThingBase
11 from plomrogue.misc import quote
12 from plomrogue.errors import BrokenSocketConnection
17 'long': 'This mode allows you to interact with the map.'
21 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it.'},
23 'short': 'terrain edit',
24 'long': 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
27 'short': 'change tiles control password',
28 'long': 'This mode is the first of two steps to change the password for a tile control character. First enter the tile control character for which you want to change the password!'
31 'short': 'change tiles control password',
32 'long': 'This mode is the second of two steps to change the password for a tile control character. Enter the new password for the tile control character you chose.'
34 'control_tile_type': {
35 'short': 'change tiles control',
36 'long': 'This mode is the first of two steps to change tile control areas on the map. First enter the tile control character you want to write.'
38 'control_tile_draw': {
39 'short': 'change tiles control',
40 'long': 'This mode is the second of two steps to change tile control areas on the map. Move cursor around the map to draw selected tile control character'
43 'short': 'annotate tile',
44 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.'
47 'short': 'edit portal',
48 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
52 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:'
56 'long': 'Pick your player name.'
58 'waiting_for_server': {
59 'short': 'waiting for server response',
60 'long': 'Waiting for a server response.'
63 'short': 'waiting for server response',
64 'long': 'Waiting for a server response.'
67 'short': 'map edit password',
68 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.'
71 'short': 'become admin',
72 'long': 'This mode allows you to become admin if you know an admin password.'
76 from ws4py.client import WebSocketBaseClient
77 class WebSocketClient(WebSocketBaseClient):
79 def __init__(self, recv_handler, *args, **kwargs):
80 super().__init__(*args, **kwargs)
81 self.recv_handler = recv_handler
84 def received_message(self, message):
86 message = str(message)
87 self.recv_handler(message)
90 def plom_closed(self):
91 return self.client_terminated
93 from plomrogue.io_tcp import PlomSocket
94 class PlomSocketClient(PlomSocket):
96 def __init__(self, recv_handler, url):
98 self.recv_handler = recv_handler
99 host, port = url.split(':')
100 super().__init__(socket.create_connection((host, port)))
108 for msg in self.recv():
109 if msg == 'NEED_SSL':
110 self.socket = ssl.wrap_socket(self.socket)
112 self.recv_handler(msg)
113 except BrokenSocketConnection:
114 pass # we assume socket will be known as dead by now
116 def cmd_TURN(game, n):
122 game.turn_complete = False
123 cmd_TURN.argtypes = 'int:nonneg'
125 def cmd_LOGIN_OK(game):
126 game.tui.switch_mode('post_login_wait')
127 game.tui.send('GET_GAMESTATE')
128 game.tui.log_msg('@ welcome')
129 cmd_LOGIN_OK.argtypes = ''
131 def cmd_CHAT(game, msg):
132 game.tui.log_msg('# ' + msg)
133 game.tui.do_refresh = True
134 cmd_CHAT.argtypes = 'string'
136 def cmd_PLAYER_ID(game, player_id):
137 game.player_id = player_id
138 cmd_PLAYER_ID.argtypes = 'int:nonneg'
140 def cmd_THING(game, yx, thing_type, thing_id):
141 t = game.get_thing(thing_id)
143 t = ThingBase(game, thing_id)
147 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
149 def cmd_THING_NAME(game, thing_id, name):
150 t = game.get_thing(thing_id)
153 cmd_THING_NAME.argtypes = 'int:nonneg string'
155 def cmd_THING_CHAR(game, thing_id, c):
156 t = game.get_thing(thing_id)
159 cmd_THING_CHAR.argtypes = 'int:nonneg char'
161 def cmd_MAP(game, geometry, size, content):
162 map_geometry_class = globals()['MapGeometry' + geometry]
163 game.map_geometry = map_geometry_class(size)
164 game.map_content = content
165 if type(game.map_geometry) == MapGeometrySquare:
166 game.tui.movement_keys = {
167 game.tui.keys['square_move_up']: 'UP',
168 game.tui.keys['square_move_left']: 'LEFT',
169 game.tui.keys['square_move_down']: 'DOWN',
170 game.tui.keys['square_move_right']: 'RIGHT',
172 elif type(game.map_geometry) == MapGeometryHex:
173 game.tui.movement_keys = {
174 game.tui.keys['hex_move_upleft']: 'UPLEFT',
175 game.tui.keys['hex_move_upright']: 'UPRIGHT',
176 game.tui.keys['hex_move_right']: 'RIGHT',
177 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
178 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
179 game.tui.keys['hex_move_left']: 'LEFT',
181 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
183 def cmd_FOV(game, content):
185 cmd_FOV.argtypes = 'string'
187 def cmd_MAP_CONTROL(game, content):
188 game.map_control_content = content
189 cmd_MAP_CONTROL.argtypes = 'string'
191 def cmd_GAME_STATE_COMPLETE(game):
192 if game.tui.mode.name == 'post_login_wait':
193 game.tui.switch_mode('play')
194 if game.tui.mode.shows_info:
195 game.tui.query_info()
196 game.turn_complete = True
197 game.tui.do_refresh = True
198 cmd_GAME_STATE_COMPLETE.argtypes = ''
200 def cmd_PORTAL(game, position, msg):
201 game.portals[position] = msg
202 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
204 def cmd_PLAY_ERROR(game, msg):
205 game.tui.log_msg('? ' + msg)
206 game.tui.flash = True
207 game.tui.do_refresh = True
208 cmd_PLAY_ERROR.argtypes = 'string'
210 def cmd_GAME_ERROR(game, msg):
211 game.tui.log_msg('? game error: ' + msg)
212 game.tui.do_refresh = True
213 cmd_GAME_ERROR.argtypes = 'string'
215 def cmd_ARGUMENT_ERROR(game, msg):
216 game.tui.log_msg('? syntax error: ' + msg)
217 game.tui.do_refresh = True
218 cmd_ARGUMENT_ERROR.argtypes = 'string'
220 def cmd_ANNOTATION_HINT(game, position):
221 game.info_hints += [position]
222 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
224 def cmd_ANNOTATION(game, position, msg):
225 game.info_db[position] = msg
226 game.tui.restore_input_values()
227 if game.tui.mode.shows_info:
228 game.tui.do_refresh = True
229 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
231 def cmd_TASKS(game, tasks_comma_separated):
232 game.tasks = tasks_comma_separated.split(',')
233 game.tui.mode_edit.legal = 'WRITE' in game.tasks
234 cmd_TASKS.argtypes = 'string'
236 def cmd_THING_TYPE(game, thing_type, symbol_hint):
237 game.thing_types[thing_type] = symbol_hint
238 cmd_THING_TYPE.argtypes = 'string char'
240 def cmd_TERRAIN(game, terrain_char, terrain_desc):
241 game.terrains[terrain_char] = terrain_desc
242 cmd_TERRAIN.argtypes = 'char string'
246 cmd_PONG.argtypes = ''
248 class Game(GameBase):
249 turn_complete = False
253 def __init__(self, *args, **kwargs):
254 super().__init__(*args, **kwargs)
255 self.register_command(cmd_LOGIN_OK)
256 self.register_command(cmd_PONG)
257 self.register_command(cmd_CHAT)
258 self.register_command(cmd_PLAYER_ID)
259 self.register_command(cmd_TURN)
260 self.register_command(cmd_THING)
261 self.register_command(cmd_THING_TYPE)
262 self.register_command(cmd_THING_NAME)
263 self.register_command(cmd_THING_CHAR)
264 self.register_command(cmd_TERRAIN)
265 self.register_command(cmd_MAP)
266 self.register_command(cmd_MAP_CONTROL)
267 self.register_command(cmd_PORTAL)
268 self.register_command(cmd_ANNOTATION)
269 self.register_command(cmd_ANNOTATION_HINT)
270 self.register_command(cmd_GAME_STATE_COMPLETE)
271 self.register_command(cmd_ARGUMENT_ERROR)
272 self.register_command(cmd_GAME_ERROR)
273 self.register_command(cmd_PLAY_ERROR)
274 self.register_command(cmd_TASKS)
275 self.register_command(cmd_FOV)
276 self.map_content = ''
283 def get_string_options(self, string_option_type):
284 if string_option_type == 'map_geometry':
285 return ['Hex', 'Square']
286 elif string_option_type == 'thing_type':
287 return self.thing_types.keys()
290 def get_command(self, command_name):
291 from functools import partial
292 f = partial(self.commands[command_name], self)
293 f.argtypes = self.commands[command_name].argtypes
298 def __init__(self, name, has_input_prompt=False, shows_info=False,
299 is_intro=False, is_single_char_entry=False):
301 self.short_desc = mode_helps[name]['short']
302 self.available_modes = []
303 self.has_input_prompt = has_input_prompt
304 self.shows_info = shows_info
305 self.is_intro = is_intro
306 self.help_intro = mode_helps[name]['long']
307 self.is_single_char_entry = is_single_char_entry
310 def iter_available_modes(self, tui):
311 for mode_name in self.available_modes:
312 mode = getattr(tui, 'mode_' + mode_name)
315 key = tui.keys['switch_to_' + mode.name]
318 def list_available_modes(self, tui):
320 if len(self.available_modes) > 0:
321 msg = 'Other modes available from here:\n'
322 for mode, key in self.iter_available_modes(tui):
323 msg += '[%s] – %s\n' % (key, mode.short_desc)
326 def mode_switch_on_key(self, tui, key_pressed):
327 for mode, key in self.iter_available_modes(tui):
328 if key_pressed == key:
329 tui.switch_mode(mode.name)
334 mode_admin = Mode('admin', has_input_prompt=True)
335 mode_play = Mode('play')
336 mode_study = Mode('study', shows_info=True)
337 mode_edit = Mode('edit', is_single_char_entry=True)
338 mode_control_pw_type = Mode('control_pw_type', is_single_char_entry=True)
339 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
340 mode_control_tile_type = Mode('control_tile_type', is_single_char_entry=True)
341 mode_control_tile_draw = Mode('control_tile_draw')
342 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
343 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
344 mode_chat = Mode('chat', has_input_prompt=True)
345 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
346 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
347 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
348 mode_password = Mode('password', has_input_prompt=True)
350 def __init__(self, host):
353 self.mode_play.available_modes = ["chat", "study", "edit",
354 "annotate", "portal",
358 self.mode_study.available_modes = ["chat", "play"]
359 self.mode_control_tile_draw.available_modes = ["play"]
363 self.parser = Parser(self.game)
365 self.do_refresh = True
366 self.queue = queue.Queue()
367 self.login_name = None
368 self.map_mode = 'terrain'
369 self.password = 'foo'
370 self.switch_mode('waiting_for_server')
372 'switch_to_chat': 't',
373 'switch_to_play': 'p',
374 'switch_to_password': 'P',
375 'switch_to_annotate': 'M',
376 'switch_to_portal': 'T',
377 'switch_to_study': '?',
378 'switch_to_edit': 'm',
379 'switch_to_admin': 'A',
380 'switch_to_control_pw_type': 'C',
381 'switch_to_control_tile_type': 'Q',
387 'toggle_map_mode': 'M',
388 'hex_move_upleft': 'w',
389 'hex_move_upright': 'e',
390 'hex_move_right': 'd',
391 'hex_move_downright': 'x',
392 'hex_move_downleft': 'y',
393 'hex_move_left': 'a',
394 'square_move_up': 'w',
395 'square_move_left': 'a',
396 'square_move_down': 's',
397 'square_move_right': 'd',
399 if os.path.isfile('config.json'):
400 with open('config.json', 'r') as f:
401 keys_conf = json.loads(f.read())
403 self.keys[k] = keys_conf[k]
404 self.show_help = False
405 self.disconnected = True
406 self.force_instant_connect = True
407 self.input_lines = []
410 curses.wrapper(self.loop)
414 def handle_recv(msg):
420 self.log_msg('@ attempting connect')
421 socket_client_class = PlomSocketClient
422 if self.host.startswith('ws://') or self.host.startswith('wss://'):
423 socket_client_class = WebSocketClient
425 self.socket = socket_client_class(handle_recv, self.host)
426 self.socket_thread = threading.Thread(target=self.socket.run)
427 self.socket_thread.start()
428 self.disconnected = False
429 self.game.thing_types = {}
430 self.game.terrains = {}
431 self.socket.send('TASKS')
432 self.socket.send('TERRAINS')
433 self.socket.send('THING_TYPES')
434 self.switch_mode('login')
435 except ConnectionRefusedError:
436 self.log_msg('@ server connect failure')
437 self.disconnected = True
438 self.switch_mode('waiting_for_server')
439 self.do_refresh = True
442 self.log_msg('@ attempting reconnect')
444 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
445 # conditions with ws4py, find out what exactly
446 self.switch_mode('waiting_for_server')
451 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
452 raise BrokenSocketConnection
453 self.socket.send(msg)
454 except (BrokenPipeError, BrokenSocketConnection):
455 self.log_msg('@ server disconnected :(')
456 self.disconnected = True
457 self.force_instant_connect = True
458 self.do_refresh = True
460 def log_msg(self, msg):
462 if len(self.log) > 100:
463 self.log = self.log[-100:]
465 def query_info(self):
466 self.send('GET_ANNOTATION ' + str(self.explorer))
468 def restore_input_values(self):
469 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
470 info = self.game.info_db[self.explorer]
473 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
474 self.input_ = self.game.portals[self.explorer]
475 elif self.mode.name == 'password':
476 self.input_ = self.password
478 def send_tile_control_command(self):
479 self.send('SET_TILE_CONTROL %s %s' %
480 (self.explorer, quote(self.tile_control_char)))
482 def switch_mode(self, mode_name):
483 self.map_mode = 'terrain'
484 self.mode = getattr(self, 'mode_' + mode_name)
485 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
486 player = self.game.get_thing(self.game.player_id)
487 self.explorer = YX(player.position.y, player.position.x)
488 if self.mode.shows_info:
490 elif self.mode.name == 'control_tile_draw':
491 self.send_tile_control_command()
492 self.map_mode = 'control'
493 if self.mode.is_single_char_entry:
494 self.show_help = True
495 if self.mode.name == 'waiting_for_server':
496 self.log_msg('@ waiting for server …')
497 elif self.mode.name == 'login':
499 self.send('LOGIN ' + quote(self.login_name))
501 self.log_msg('@ enter username')
502 elif self.mode.name == 'admin':
503 self.log_msg('@ enter admin password:')
504 elif self.mode.name == 'control_pw_pw':
505 self.log_msg('@ enter tile control password for "%s":' % self.tile_control_char)
506 self.restore_input_values()
508 def loop(self, stdscr):
511 def safe_addstr(y, x, line):
512 if y < self.size.y - 1 or x + len(line) < self.size.x:
513 stdscr.addstr(y, x, line)
514 else: # workaround to <https://stackoverflow.com/q/7063128>
515 cut_i = self.size.x - x - 1
517 last_char = line[cut_i]
518 stdscr.addstr(y, self.size.x - 2, last_char)
519 stdscr.insstr(y, self.size.x - 2, ' ')
520 stdscr.addstr(y, x, cut)
522 def handle_input(msg):
523 command, args = self.parser.parse(msg)
526 def msg_into_lines_of_width(msg, width):
530 for i in range(len(msg)):
531 if x >= width or msg[i] == "\n":
543 def reset_screen_size():
544 self.size = YX(*stdscr.getmaxyx())
545 self.size = self.size - YX(self.size.y % 4, 0)
546 self.size = self.size - YX(0, self.size.x % 4)
547 self.window_width = int(self.size.x / 2)
549 def recalc_input_lines():
550 if not self.mode.has_input_prompt:
551 self.input_lines = []
553 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
556 def move_explorer(direction):
557 target = self.game.map_geometry.move_yx(self.explorer, direction)
559 self.explorer = target
560 if self.mode.shows_info:
562 elif self.mode.name == 'control_tile_draw':
563 self.send_tile_control_command()
569 for line in self.log:
570 lines += msg_into_lines_of_width(line, self.window_width)
573 max_y = self.size.y - len(self.input_lines)
574 for i in range(len(lines)):
575 if (i >= max_y - height_header):
577 safe_addstr(max_y - i - 1, self.window_width, lines[i])
580 if not self.game.turn_complete:
582 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
583 info = 'outside field of view'
584 if self.game.fov[pos_i] == '.':
585 terrain_char = self.game.map_content[pos_i]
587 if terrain_char in self.game.terrains:
588 terrain_desc = self.game.terrains[terrain_char]
589 info = 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
590 protection = self.game.map_control_content[pos_i]
591 if protection == '.':
592 protection = 'unprotected'
593 info = 'PROTECTION: %s\n' % protection
594 for t in self.game.things:
595 if t.position == self.explorer:
596 info += 'THING: %s / %s' % (t.type_,
597 self.game.thing_types[t.type_])
598 if hasattr(t, 'player_char'):
599 info += t.player_char
600 if hasattr(t, 'name'):
601 info += ' (%s)' % t.name
603 if self.explorer in self.game.portals:
604 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
606 info += 'PORTAL: (none)\n'
607 if self.explorer in self.game.info_db:
608 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
610 info += 'ANNOTATION: waiting …'
611 lines = msg_into_lines_of_width(info, self.window_width)
613 for i in range(len(lines)):
614 y = height_header + i
615 if y >= self.size.y - len(self.input_lines):
617 safe_addstr(y, self.window_width, lines[i])
620 y = self.size.y - len(self.input_lines)
621 for i in range(len(self.input_lines)):
622 safe_addstr(y, self.window_width, self.input_lines[i])
626 if not self.game.turn_complete:
628 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
631 help = "hit [%s] for help" % self.keys['help']
632 if self.mode.has_input_prompt:
633 help = "enter /help for help"
634 safe_addstr(1, self.window_width,
635 'MODE: %s – %s' % (self.mode.short_desc, help))
638 if not self.game.turn_complete:
641 map_content = self.game.map_content
642 if self.map_mode == 'control':
643 map_content = self.game.map_control_content
644 for y in range(self.game.map_geometry.size.y):
645 start = self.game.map_geometry.size.x * y
646 end = start + self.game.map_geometry.size.x
647 map_lines_split += [[c + ' ' for c in map_content[start:end]]]
648 if self.map_mode == 'annotations':
649 for p in self.game.info_hints:
650 map_lines_split[p.y][p.x] = 'A '
651 elif self.map_mode == 'terrain':
652 for p in self.game.portals.keys():
653 map_lines_split[p.y][p.x] = 'P '
655 for t in self.game.things:
656 symbol = self.game.thing_types[t.type_]
658 if hasattr(t, 'player_char'):
659 meta_char = t.player_char
660 if t.position in used_positions:
662 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
663 used_positions += [t.position]
664 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
665 map_lines_split[self.explorer.y][self.explorer.x] = '??'
667 if type(self.game.map_geometry) == MapGeometryHex:
669 for line in map_lines_split:
670 map_lines += [indent*' ' + ''.join(line)]
671 indent = 0 if indent else 1
673 for line in map_lines_split:
674 map_lines += [''.join(line)]
675 window_center = YX(int(self.size.y / 2),
676 int(self.window_width / 2))
677 player = self.game.get_thing(self.game.player_id)
678 center = player.position
679 if self.mode.shows_info:
680 center = self.explorer
681 center = YX(center.y, center.x * 2)
682 offset = center - window_center
683 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
685 term_y = max(0, -offset.y)
686 term_x = max(0, -offset.x)
687 map_y = max(0, offset.y)
688 map_x = max(0, offset.x)
689 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
690 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
691 safe_addstr(term_y, term_x, to_draw)
696 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
697 self.mode.help_intro)
698 if self.mode.name == 'play':
699 content += "Available actions:\n"
700 if 'MOVE' in self.game.tasks:
701 content += "[%s] – move player\n" % ','.join(self.movement_keys)
702 if 'PICK_UP' in self.game.tasks:
703 content += "[%s] – take thing under player\n" % self.keys['take_thing']
704 if 'DROP' in self.game.tasks:
705 content += "[%s] – drop carried thing\n" % self.keys['drop_thing']
706 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
707 content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
708 content += '[%s] – teleport to other space\n' % self.keys['teleport']
710 elif self.mode.name == 'study':
711 content += 'Available actions:\n'
712 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
713 content += '[%s] – toggle view between terrain, annotations, and password protection areas\n' % self.keys['toggle_map_mode']
715 elif self.mode.name == 'chat':
716 content += '/nick NAME – re-name yourself to NAME\n'
717 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
718 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
719 content += self.mode.list_available_modes(self)
720 for i in range(self.size.y):
722 self.window_width * (not self.mode.has_input_prompt),
723 ' '*self.window_width)
725 for line in content.split('\n'):
726 lines += msg_into_lines_of_width(line, self.window_width)
727 for i in range(len(lines)):
731 self.window_width * (not self.mode.has_input_prompt),
736 if self.mode.has_input_prompt:
739 if self.mode.shows_info:
744 if not self.mode.is_intro:
750 curses.curs_set(False) # hide cursor
751 curses.use_default_colors();
754 self.explorer = YX(0, 0)
757 interval = datetime.timedelta(seconds=5)
758 last_ping = datetime.datetime.now() - interval
760 if self.disconnected and self.force_instant_connect:
761 self.force_instant_connect = False
763 now = datetime.datetime.now()
764 if now - last_ping > interval:
765 if self.disconnected:
775 self.do_refresh = False
778 msg = self.queue.get(block=False)
783 key = stdscr.getkey()
784 self.do_refresh = True
787 self.show_help = False
788 if key == 'KEY_RESIZE':
790 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
791 self.input_ = self.input_[:-1]
792 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
793 self.show_help = True
795 self.restore_input_values()
796 elif self.mode.has_input_prompt and key != '\n': # Return key
798 max_length = self.window_width * self.size.y - len(input_prompt) - 1
799 if len(self.input_) > max_length:
800 self.input_ = self.input_[:max_length]
801 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
802 self.show_help = True
803 elif self.mode.name == 'login' and key == '\n':
804 self.login_name = self.input_
805 self.send('LOGIN ' + quote(self.input_))
807 elif self.mode.name == 'control_pw_pw' and key == '\n':
808 if self.input_ == '':
809 self.log_msg('@ aborted')
811 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
813 self.switch_mode('play')
814 elif self.mode.name == 'password' and key == '\n':
815 if self.input_ == '':
817 self.password = self.input_
819 self.switch_mode('play')
820 elif self.mode.name == 'admin' and key == '\n':
821 self.send('BECOME_ADMIN ' + quote(self.input_))
823 self.switch_mode('play')
824 elif self.mode.name == 'chat' and key == '\n':
825 if self.input_ == '':
827 if self.input_[0] == '/': # FIXME fails on empty input
828 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
829 self.switch_mode('play')
830 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
831 self.switch_mode('study')
832 elif self.input_.startswith('/nick'):
833 tokens = self.input_.split(maxsplit=1)
835 self.send('NICK ' + quote(tokens[1]))
837 self.log_msg('? need login name')
839 self.log_msg('? unknown command')
841 self.send('ALL ' + quote(self.input_))
843 elif self.mode.name == 'annotate' and key == '\n':
844 if self.input_ == '':
846 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
847 quote(self.password)))
849 self.switch_mode('play')
850 elif self.mode.name == 'portal' and key == '\n':
851 if self.input_ == '':
853 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
854 quote(self.password)))
856 self.switch_mode('play')
857 elif self.mode.name == 'study':
858 if self.mode.mode_switch_on_key(self, key):
860 elif key == self.keys['toggle_map_mode']:
861 if self.map_mode == 'terrain':
862 self.map_mode = 'annotations'
863 elif self.map_mode == 'annotations':
864 self.map_mode = 'control'
866 self.map_mode = 'terrain'
867 elif key in self.movement_keys:
868 move_explorer(self.movement_keys[key])
869 elif self.mode.name == 'play':
870 if self.mode.mode_switch_on_key(self, key):
872 if key == self.keys['flatten'] and\
873 'FLATTEN_SURROUNDINGS' in self.game.tasks:
874 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
875 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
876 self.send('TASK:PICK_UP')
877 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
878 self.send('TASK:DROP')
879 elif key == self.keys['teleport']:
880 player = self.game.get_thing(self.game.player_id)
881 if player.position in self.game.portals:
882 self.host = self.game.portals[player.position]
886 self.log_msg('? not standing on portal')
887 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
888 self.send('TASK:MOVE ' + self.movement_keys[key])
889 elif self.mode.name == 'edit':
890 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
891 self.switch_mode('play')
892 elif self.mode.name == 'control_pw_type':
893 self.tile_control_char = key
894 self.switch_mode('control_pw_pw')
895 elif self.mode.name == 'control_tile_type':
896 self.tile_control_char = key
897 self.switch_mode('control_tile_draw')
898 elif self.mode.name == 'control_tile_draw':
899 if self.mode.mode_switch_on_key(self, key):
901 elif key in self.movement_keys:
902 move_explorer(self.movement_keys[key])
904 if len(sys.argv) != 2:
905 raise ArgError('wrong number of arguments, need game host')