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.'},
24 'long': 'This mode allows you to change the map in various ways.'
27 'short': 'terrain write',
28 '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.'
31 'short': 'change tiles control password',
32 '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!'
35 'short': 'change tiles control password',
36 '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.'
38 'control_tile_type': {
39 'short': 'change tiles control',
40 '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.'
42 'control_tile_draw': {
43 'short': 'change tiles control',
44 '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'
47 'short': 'annotate tile',
48 '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.'
51 'short': 'edit portal',
52 '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.'
56 '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:'
60 'long': 'Pick your player name.'
62 'waiting_for_server': {
63 'short': 'waiting for server response',
64 'long': 'Waiting for a server response.'
67 'short': 'waiting for server response',
68 'long': 'Waiting for a server response.'
71 'short': 'map edit password',
72 '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.'
75 'short': 'become admin',
76 'long': 'This mode allows you to become admin if you know an admin password.'
80 'long': 'This mode allows you access to actions limited to administrators.'
84 from ws4py.client import WebSocketBaseClient
85 class WebSocketClient(WebSocketBaseClient):
87 def __init__(self, recv_handler, *args, **kwargs):
88 super().__init__(*args, **kwargs)
89 self.recv_handler = recv_handler
92 def received_message(self, message):
94 message = str(message)
95 self.recv_handler(message)
98 def plom_closed(self):
99 return self.client_terminated
101 from plomrogue.io_tcp import PlomSocket
102 class PlomSocketClient(PlomSocket):
104 def __init__(self, recv_handler, url):
106 self.recv_handler = recv_handler
107 host, port = url.split(':')
108 super().__init__(socket.create_connection((host, port)))
116 for msg in self.recv():
117 if msg == 'NEED_SSL':
118 self.socket = ssl.wrap_socket(self.socket)
120 self.recv_handler(msg)
121 except BrokenSocketConnection:
122 pass # we assume socket will be known as dead by now
124 def cmd_TURN(game, n):
130 game.turn_complete = False
131 cmd_TURN.argtypes = 'int:nonneg'
133 def cmd_LOGIN_OK(game):
134 game.tui.switch_mode('post_login_wait')
135 game.tui.send('GET_GAMESTATE')
136 game.tui.log_msg('@ welcome')
137 cmd_LOGIN_OK.argtypes = ''
139 def cmd_ADMIN_OK(game):
140 game.tui.is_admin = True
141 game.tui.log_msg('@ you now have admin rights')
142 game.tui.switch_mode('admin')
143 game.tui.do_refresh = True
144 cmd_ADMIN_OK.argtypes = ''
146 def cmd_CHAT(game, msg):
147 game.tui.log_msg('# ' + msg)
148 game.tui.do_refresh = True
149 cmd_CHAT.argtypes = 'string'
151 def cmd_PLAYER_ID(game, player_id):
152 game.player_id = player_id
153 cmd_PLAYER_ID.argtypes = 'int:nonneg'
155 def cmd_THING(game, yx, thing_type, thing_id):
156 t = game.get_thing(thing_id)
158 t = ThingBase(game, thing_id)
162 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
164 def cmd_THING_NAME(game, thing_id, name):
165 t = game.get_thing(thing_id)
168 cmd_THING_NAME.argtypes = 'int:nonneg string'
170 def cmd_THING_CHAR(game, thing_id, c):
171 t = game.get_thing(thing_id)
174 cmd_THING_CHAR.argtypes = 'int:nonneg char'
176 def cmd_MAP(game, geometry, size, content):
177 map_geometry_class = globals()['MapGeometry' + geometry]
178 game.map_geometry = map_geometry_class(size)
179 game.map_content = content
180 if type(game.map_geometry) == MapGeometrySquare:
181 game.tui.movement_keys = {
182 game.tui.keys['square_move_up']: 'UP',
183 game.tui.keys['square_move_left']: 'LEFT',
184 game.tui.keys['square_move_down']: 'DOWN',
185 game.tui.keys['square_move_right']: 'RIGHT',
187 elif type(game.map_geometry) == MapGeometryHex:
188 game.tui.movement_keys = {
189 game.tui.keys['hex_move_upleft']: 'UPLEFT',
190 game.tui.keys['hex_move_upright']: 'UPRIGHT',
191 game.tui.keys['hex_move_right']: 'RIGHT',
192 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
193 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
194 game.tui.keys['hex_move_left']: 'LEFT',
196 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
198 def cmd_FOV(game, content):
200 cmd_FOV.argtypes = 'string'
202 def cmd_MAP_CONTROL(game, content):
203 game.map_control_content = content
204 cmd_MAP_CONTROL.argtypes = 'string'
206 def cmd_GAME_STATE_COMPLETE(game):
207 if game.tui.mode.name == 'post_login_wait':
208 game.tui.switch_mode('play')
209 if game.tui.mode.shows_info:
210 game.tui.query_info()
211 game.turn_complete = True
212 game.tui.do_refresh = True
213 cmd_GAME_STATE_COMPLETE.argtypes = ''
215 def cmd_PORTAL(game, position, msg):
216 game.portals[position] = msg
217 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
219 def cmd_PLAY_ERROR(game, msg):
220 game.tui.log_msg('? ' + msg)
221 game.tui.flash = True
222 game.tui.do_refresh = True
223 cmd_PLAY_ERROR.argtypes = 'string'
225 def cmd_GAME_ERROR(game, msg):
226 game.tui.log_msg('? game error: ' + msg)
227 game.tui.do_refresh = True
228 cmd_GAME_ERROR.argtypes = 'string'
230 def cmd_ARGUMENT_ERROR(game, msg):
231 game.tui.log_msg('? syntax error: ' + msg)
232 game.tui.do_refresh = True
233 cmd_ARGUMENT_ERROR.argtypes = 'string'
235 def cmd_ANNOTATION_HINT(game, position):
236 game.info_hints += [position]
237 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
239 def cmd_ANNOTATION(game, position, msg):
240 game.info_db[position] = msg
241 game.tui.restore_input_values()
242 if game.tui.mode.shows_info:
243 game.tui.do_refresh = True
244 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
246 def cmd_TASKS(game, tasks_comma_separated):
247 game.tasks = tasks_comma_separated.split(',')
248 game.tui.mode_write.legal = 'WRITE' in game.tasks
249 cmd_TASKS.argtypes = 'string'
251 def cmd_THING_TYPE(game, thing_type, symbol_hint):
252 game.thing_types[thing_type] = symbol_hint
253 cmd_THING_TYPE.argtypes = 'string char'
255 def cmd_TERRAIN(game, terrain_char, terrain_desc):
256 game.terrains[terrain_char] = terrain_desc
257 cmd_TERRAIN.argtypes = 'char string'
261 cmd_PONG.argtypes = ''
263 class Game(GameBase):
264 turn_complete = False
268 def __init__(self, *args, **kwargs):
269 super().__init__(*args, **kwargs)
270 self.register_command(cmd_LOGIN_OK)
271 self.register_command(cmd_ADMIN_OK)
272 self.register_command(cmd_PONG)
273 self.register_command(cmd_CHAT)
274 self.register_command(cmd_PLAYER_ID)
275 self.register_command(cmd_TURN)
276 self.register_command(cmd_THING)
277 self.register_command(cmd_THING_TYPE)
278 self.register_command(cmd_THING_NAME)
279 self.register_command(cmd_THING_CHAR)
280 self.register_command(cmd_TERRAIN)
281 self.register_command(cmd_MAP)
282 self.register_command(cmd_MAP_CONTROL)
283 self.register_command(cmd_PORTAL)
284 self.register_command(cmd_ANNOTATION)
285 self.register_command(cmd_ANNOTATION_HINT)
286 self.register_command(cmd_GAME_STATE_COMPLETE)
287 self.register_command(cmd_ARGUMENT_ERROR)
288 self.register_command(cmd_GAME_ERROR)
289 self.register_command(cmd_PLAY_ERROR)
290 self.register_command(cmd_TASKS)
291 self.register_command(cmd_FOV)
292 self.map_content = ''
299 def get_string_options(self, string_option_type):
300 if string_option_type == 'map_geometry':
301 return ['Hex', 'Square']
302 elif string_option_type == 'thing_type':
303 return self.thing_types.keys()
306 def get_command(self, command_name):
307 from functools import partial
308 f = partial(self.commands[command_name], self)
309 f.argtypes = self.commands[command_name].argtypes
314 def __init__(self, name, has_input_prompt=False, shows_info=False,
315 is_intro=False, is_single_char_entry=False):
317 self.short_desc = mode_helps[name]['short']
318 self.available_modes = []
319 self.has_input_prompt = has_input_prompt
320 self.shows_info = shows_info
321 self.is_intro = is_intro
322 self.help_intro = mode_helps[name]['long']
323 self.is_single_char_entry = is_single_char_entry
326 def iter_available_modes(self, tui):
327 for mode_name in self.available_modes:
328 mode = getattr(tui, 'mode_' + mode_name)
331 key = tui.keys['switch_to_' + mode.name]
334 def list_available_modes(self, tui):
336 if len(self.available_modes) > 0:
337 msg = 'Other modes available from here:\n'
338 for mode, key in self.iter_available_modes(tui):
339 msg += '[%s] – %s\n' % (key, mode.short_desc)
342 def mode_switch_on_key(self, tui, key_pressed):
343 for mode, key in self.iter_available_modes(tui):
344 if key_pressed == key:
345 tui.switch_mode(mode.name)
350 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
351 mode_admin = Mode('admin')
352 mode_play = Mode('play')
353 mode_study = Mode('study', shows_info=True)
354 mode_write = Mode('write', is_single_char_entry=True)
355 mode_edit = Mode('edit')
356 mode_control_pw_type = Mode('control_pw_type', is_single_char_entry=True)
357 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
358 mode_control_tile_type = Mode('control_tile_type', is_single_char_entry=True)
359 mode_control_tile_draw = Mode('control_tile_draw')
360 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
361 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
362 mode_chat = Mode('chat', has_input_prompt=True)
363 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
364 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
365 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
366 mode_password = Mode('password', has_input_prompt=True)
369 def __init__(self, host):
372 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
373 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
374 self.mode_admin.available_modes = ["control_pw_type",
375 "control_tile_type", "chat",
376 "study", "play", "edit"]
377 self.mode_control_tile_draw.available_modes = ["admin_enter"]
378 self.mode_edit.available_modes = ["write", "annotate", "portal",
379 "password", "chat", "study", "play",
384 self.parser = Parser(self.game)
386 self.do_refresh = True
387 self.queue = queue.Queue()
388 self.login_name = None
389 self.map_mode = 'all'
390 self.password = 'foo'
391 self.switch_mode('waiting_for_server')
393 'switch_to_chat': 't',
394 'switch_to_play': 'p',
395 'switch_to_password': 'P',
396 'switch_to_annotate': 'M',
397 'switch_to_portal': 'T',
398 'switch_to_study': '?',
399 'switch_to_edit': 'E',
400 'switch_to_write': 'm',
401 'switch_to_admin_enter': 'A',
402 'switch_to_control_pw_type': 'C',
403 'switch_to_control_tile_type': 'Q',
409 'toggle_map_mode': 'M',
410 'hex_move_upleft': 'w',
411 'hex_move_upright': 'e',
412 'hex_move_right': 'd',
413 'hex_move_downright': 'x',
414 'hex_move_downleft': 'y',
415 'hex_move_left': 'a',
416 'square_move_up': 'w',
417 'square_move_left': 'a',
418 'square_move_down': 's',
419 'square_move_right': 'd',
421 if os.path.isfile('config.json'):
422 with open('config.json', 'r') as f:
423 keys_conf = json.loads(f.read())
425 self.keys[k] = keys_conf[k]
426 self.show_help = False
427 self.disconnected = True
428 self.force_instant_connect = True
429 self.input_lines = []
432 curses.wrapper(self.loop)
436 def handle_recv(msg):
442 self.log_msg('@ attempting connect')
443 socket_client_class = PlomSocketClient
444 if self.host.startswith('ws://') or self.host.startswith('wss://'):
445 socket_client_class = WebSocketClient
447 self.socket = socket_client_class(handle_recv, self.host)
448 self.socket_thread = threading.Thread(target=self.socket.run)
449 self.socket_thread.start()
450 self.disconnected = False
451 self.game.thing_types = {}
452 self.game.terrains = {}
453 self.socket.send('TASKS')
454 self.socket.send('TERRAINS')
455 self.socket.send('THING_TYPES')
456 self.switch_mode('login')
457 except ConnectionRefusedError:
458 self.log_msg('@ server connect failure')
459 self.disconnected = True
460 self.switch_mode('waiting_for_server')
461 self.do_refresh = True
464 self.log_msg('@ attempting reconnect')
466 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
467 # conditions with ws4py, find out what exactly
468 self.switch_mode('waiting_for_server')
473 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
474 raise BrokenSocketConnection
475 self.socket.send(msg)
476 except (BrokenPipeError, BrokenSocketConnection):
477 self.log_msg('@ server disconnected :(')
478 self.disconnected = True
479 self.force_instant_connect = True
480 self.do_refresh = True
482 def log_msg(self, msg):
484 if len(self.log) > 100:
485 self.log = self.log[-100:]
487 def query_info(self):
488 self.send('GET_ANNOTATION ' + str(self.explorer))
490 def restore_input_values(self):
491 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
492 info = self.game.info_db[self.explorer]
495 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
496 self.input_ = self.game.portals[self.explorer]
497 elif self.mode.name == 'password':
498 self.input_ = self.password
500 def send_tile_control_command(self):
501 self.send('SET_TILE_CONTROL %s %s' %
502 (self.explorer, quote(self.tile_control_char)))
504 def switch_mode(self, mode_name):
505 self.map_mode = 'all'
506 if mode_name == 'admin_enter' and self.is_admin:
508 self.mode = getattr(self, 'mode_' + mode_name)
509 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
510 player = self.game.get_thing(self.game.player_id)
511 self.explorer = YX(player.position.y, player.position.x)
512 if self.mode.shows_info:
514 elif self.mode.name == 'control_tile_draw':
515 self.send_tile_control_command()
516 self.map_mode = 'control'
517 if self.mode.is_single_char_entry:
518 self.show_help = True
519 if self.mode.name == 'waiting_for_server':
520 self.log_msg('@ waiting for server …')
521 elif self.mode.name == 'login':
523 self.send('LOGIN ' + quote(self.login_name))
525 self.log_msg('@ enter username')
526 elif self.mode.name == 'admin_enter':
527 self.log_msg('@ enter admin password:')
528 elif self.mode.name == 'control_pw_pw':
529 self.log_msg('@ enter tile control password for "%s":' % self.tile_control_char)
530 self.restore_input_values()
532 def loop(self, stdscr):
535 def safe_addstr(y, x, line):
536 if y < self.size.y - 1 or x + len(line) < self.size.x:
537 stdscr.addstr(y, x, line)
538 else: # workaround to <https://stackoverflow.com/q/7063128>
539 cut_i = self.size.x - x - 1
541 last_char = line[cut_i]
542 stdscr.addstr(y, self.size.x - 2, last_char)
543 stdscr.insstr(y, self.size.x - 2, ' ')
544 stdscr.addstr(y, x, cut)
546 def handle_input(msg):
547 command, args = self.parser.parse(msg)
550 def msg_into_lines_of_width(msg, width):
554 for i in range(len(msg)):
555 if x >= width or msg[i] == "\n":
567 def reset_screen_size():
568 self.size = YX(*stdscr.getmaxyx())
569 self.size = self.size - YX(self.size.y % 4, 0)
570 self.size = self.size - YX(0, self.size.x % 4)
571 self.window_width = int(self.size.x / 2)
573 def recalc_input_lines():
574 if not self.mode.has_input_prompt:
575 self.input_lines = []
577 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
580 def move_explorer(direction):
581 target = self.game.map_geometry.move_yx(self.explorer, direction)
583 self.explorer = target
584 if self.mode.shows_info:
586 elif self.mode.name == 'control_tile_draw':
587 self.send_tile_control_command()
593 for line in self.log:
594 lines += msg_into_lines_of_width(line, self.window_width)
597 max_y = self.size.y - len(self.input_lines)
598 for i in range(len(lines)):
599 if (i >= max_y - height_header):
601 safe_addstr(max_y - i - 1, self.window_width, lines[i])
604 if not self.game.turn_complete:
606 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
607 info = 'outside field of view'
608 if self.game.fov[pos_i] == '.':
609 terrain_char = self.game.map_content[pos_i]
611 if terrain_char in self.game.terrains:
612 terrain_desc = self.game.terrains[terrain_char]
613 info = 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
614 protection = self.game.map_control_content[pos_i]
615 if protection == '.':
616 protection = 'unprotected'
617 info = 'PROTECTION: %s\n' % protection
618 for t in self.game.things:
619 if t.position == self.explorer:
620 info += 'THING: %s / %s' % (t.type_,
621 self.game.thing_types[t.type_])
622 if hasattr(t, 'player_char'):
623 info += t.player_char
624 if hasattr(t, 'name'):
625 info += ' (%s)' % t.name
627 if self.explorer in self.game.portals:
628 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
630 info += 'PORTAL: (none)\n'
631 if self.explorer in self.game.info_db:
632 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
634 info += 'ANNOTATION: waiting …'
635 lines = msg_into_lines_of_width(info, self.window_width)
637 for i in range(len(lines)):
638 y = height_header + i
639 if y >= self.size.y - len(self.input_lines):
641 safe_addstr(y, self.window_width, lines[i])
644 y = self.size.y - len(self.input_lines)
645 for i in range(len(self.input_lines)):
646 safe_addstr(y, self.window_width, self.input_lines[i])
650 if not self.game.turn_complete:
652 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
655 help = "hit [%s] for help" % self.keys['help']
656 if self.mode.has_input_prompt:
657 help = "enter /help for help"
658 safe_addstr(1, self.window_width,
659 'MODE: %s – %s' % (self.mode.short_desc, help))
662 if not self.game.turn_complete:
665 for y in range(self.game.map_geometry.size.y):
666 start = self.game.map_geometry.size.x * y
667 end = start + self.game.map_geometry.size.x
668 if self.mode.name in {'edit', 'write', 'control_tile_draw',
669 'control_tile_type'}:
671 for i in range(start, end):
672 line += [self.game.map_content[i]
673 + self.game.map_control_content[i]]
674 map_lines_split += [line]
676 map_lines_split += [[c + ' ' for c
677 in self.game.map_content[start:end]]]
678 if self.map_mode == 'annotations':
679 for p in self.game.info_hints:
680 map_lines_split[p.y][p.x] = 'A '
681 elif self.map_mode == 'all':
682 for p in self.game.portals.keys():
683 original = map_lines_split[p.y][p.x]
684 map_lines_split[p.y][p.x] = original[0] + 'P'
686 for t in self.game.things:
687 symbol = self.game.thing_types[t.type_]
689 if hasattr(t, 'player_char'):
690 meta_char = t.player_char
691 if t.position in used_positions:
693 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
694 used_positions += [t.position]
695 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
696 map_lines_split[self.explorer.y][self.explorer.x] = '??'
698 if type(self.game.map_geometry) == MapGeometryHex:
700 for line in map_lines_split:
701 map_lines += [indent*' ' + ''.join(line)]
702 indent = 0 if indent else 1
704 for line in map_lines_split:
705 map_lines += [''.join(line)]
706 window_center = YX(int(self.size.y / 2),
707 int(self.window_width / 2))
708 player = self.game.get_thing(self.game.player_id)
709 center = player.position
710 if self.mode.shows_info:
711 center = self.explorer
712 center = YX(center.y, center.x * 2)
713 offset = center - window_center
714 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
716 term_y = max(0, -offset.y)
717 term_x = max(0, -offset.x)
718 map_y = max(0, offset.y)
719 map_x = max(0, offset.x)
720 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
721 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
722 safe_addstr(term_y, term_x, to_draw)
727 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
728 self.mode.help_intro)
729 if self.mode.name == 'play':
730 content += "Available actions:\n"
731 if 'MOVE' in self.game.tasks:
732 content += "[%s] – move player\n" % ','.join(self.movement_keys)
733 if 'PICK_UP' in self.game.tasks:
734 content += "[%s] – pick up thing\n" % self.keys['take_thing']
735 if 'DROP' in self.game.tasks:
736 content += "[%s] – drop picked up thing\n" % self.keys['drop_thing']
737 content += '[%s] – teleport to other space\n' % self.keys['teleport']
739 elif self.mode.name == 'study':
740 content += 'Available actions:\n'
741 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
742 content += '[%s] – toggle view between anything, terrain, and annotations\n' % self.keys['toggle_map_mode']
744 elif self.mode.name == 'edit':
745 content += "Available actions:\n"
746 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
747 content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
749 elif self.mode.name == 'chat':
750 content += '/nick NAME – re-name yourself to NAME\n'
751 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
752 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
753 content += '/%s or /edit – switch to map edit mode\n' % self.keys['switch_to_edit']
754 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
755 content += self.mode.list_available_modes(self)
756 for i in range(self.size.y):
758 self.window_width * (not self.mode.has_input_prompt),
759 ' '*self.window_width)
761 for line in content.split('\n'):
762 lines += msg_into_lines_of_width(line, self.window_width)
763 for i in range(len(lines)):
767 self.window_width * (not self.mode.has_input_prompt),
772 if self.mode.has_input_prompt:
775 if self.mode.shows_info:
780 if not self.mode.is_intro:
786 curses.curs_set(False) # hide cursor
787 curses.use_default_colors();
790 self.explorer = YX(0, 0)
793 interval = datetime.timedelta(seconds=5)
794 last_ping = datetime.datetime.now() - interval
796 if self.disconnected and self.force_instant_connect:
797 self.force_instant_connect = False
799 now = datetime.datetime.now()
800 if now - last_ping > interval:
801 if self.disconnected:
811 self.do_refresh = False
814 msg = self.queue.get(block=False)
819 key = stdscr.getkey()
820 self.do_refresh = True
823 self.show_help = False
824 if key == 'KEY_RESIZE':
826 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
827 self.input_ = self.input_[:-1]
828 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
829 self.show_help = True
831 self.restore_input_values()
832 elif self.mode.has_input_prompt and key != '\n': # Return key
834 max_length = self.window_width * self.size.y - len(input_prompt) - 1
835 if len(self.input_) > max_length:
836 self.input_ = self.input_[:max_length]
837 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
838 self.show_help = True
839 elif self.mode.name == 'login' and key == '\n':
840 self.login_name = self.input_
841 self.send('LOGIN ' + quote(self.input_))
843 elif self.mode.name == 'control_pw_pw' and key == '\n':
844 if self.input_ == '':
845 self.log_msg('@ aborted')
847 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
849 self.switch_mode('admin')
850 elif self.mode.name == 'password' and key == '\n':
851 if self.input_ == '':
853 self.password = self.input_
855 self.switch_mode('edit')
856 elif self.mode.name == 'admin_enter' and key == '\n':
857 self.send('BECOME_ADMIN ' + quote(self.input_))
859 self.switch_mode('play')
860 elif self.mode.name == 'chat' and key == '\n':
861 if self.input_ == '':
863 if self.input_[0] == '/': # FIXME fails on empty input
864 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
865 self.switch_mode('play')
866 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
867 self.switch_mode('study')
868 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
869 self.switch_mode('edit')
870 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
871 self.switch_mode('admin_enter')
872 elif self.input_.startswith('/nick'):
873 tokens = self.input_.split(maxsplit=1)
875 self.send('NICK ' + quote(tokens[1]))
877 self.log_msg('? need login name')
879 self.log_msg('? unknown command')
881 self.send('ALL ' + quote(self.input_))
883 elif self.mode.name == 'annotate' and key == '\n':
884 if self.input_ == '':
886 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
887 quote(self.password)))
889 self.switch_mode('edit')
890 elif self.mode.name == 'portal' and key == '\n':
891 if self.input_ == '':
893 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
894 quote(self.password)))
896 self.switch_mode('edit')
897 elif self.mode.name == 'study':
898 if self.mode.mode_switch_on_key(self, key):
900 elif key == self.keys['toggle_map_mode']:
901 if self.map_mode == 'terrain':
902 self.map_mode = 'annotations'
903 elif self.map_mode == 'annotations':
904 self.map_mode = 'all'
906 self.map_mode = 'terrain'
907 elif key in self.movement_keys:
908 move_explorer(self.movement_keys[key])
909 elif self.mode.name == 'play':
910 if self.mode.mode_switch_on_key(self, key):
912 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
913 self.send('TASK:PICK_UP')
914 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
915 self.send('TASK:DROP')
916 elif key == self.keys['teleport']:
917 player = self.game.get_thing(self.game.player_id)
918 if player.position in self.game.portals:
919 self.host = self.game.portals[player.position]
923 self.log_msg('? not standing on portal')
924 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
925 self.send('TASK:MOVE ' + self.movement_keys[key])
926 elif self.mode.name == 'write':
927 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
928 self.switch_mode('edit')
929 elif self.mode.name == 'control_pw_type':
930 self.tile_control_char = key
931 self.switch_mode('control_pw_pw')
932 elif self.mode.name == 'control_tile_type':
933 self.tile_control_char = key
934 self.switch_mode('control_tile_draw')
935 elif self.mode.name == 'control_tile_draw':
936 if self.mode.mode_switch_on_key(self, key):
938 elif key in self.movement_keys:
939 move_explorer(self.movement_keys[key])
940 elif self.mode.name == 'admin':
941 if self.mode.mode_switch_on_key(self, key):
943 elif self.mode.name == 'edit':
944 if self.mode.mode_switch_on_key(self, key):
946 if key == self.keys['flatten'] and\
947 'FLATTEN_SURROUNDINGS' in self.game.tasks:
948 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
949 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
950 self.send('TASK:MOVE ' + self.movement_keys[key])
952 if len(sys.argv) != 2:
953 raise ArgError('wrong number of arguments, need game host')