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 'long': 'This mode allows you access to actions limited to administrators.'
80 from ws4py.client import WebSocketBaseClient
81 class WebSocketClient(WebSocketBaseClient):
83 def __init__(self, recv_handler, *args, **kwargs):
84 super().__init__(*args, **kwargs)
85 self.recv_handler = recv_handler
88 def received_message(self, message):
90 message = str(message)
91 self.recv_handler(message)
94 def plom_closed(self):
95 return self.client_terminated
97 from plomrogue.io_tcp import PlomSocket
98 class PlomSocketClient(PlomSocket):
100 def __init__(self, recv_handler, url):
102 self.recv_handler = recv_handler
103 host, port = url.split(':')
104 super().__init__(socket.create_connection((host, port)))
112 for msg in self.recv():
113 if msg == 'NEED_SSL':
114 self.socket = ssl.wrap_socket(self.socket)
116 self.recv_handler(msg)
117 except BrokenSocketConnection:
118 pass # we assume socket will be known as dead by now
120 def cmd_TURN(game, n):
126 game.turn_complete = False
127 cmd_TURN.argtypes = 'int:nonneg'
129 def cmd_LOGIN_OK(game):
130 game.tui.switch_mode('post_login_wait')
131 game.tui.send('GET_GAMESTATE')
132 game.tui.log_msg('@ welcome')
133 cmd_LOGIN_OK.argtypes = ''
135 def cmd_ADMIN_OK(game):
136 game.tui.is_admin = True
137 game.tui.log_msg('@ you now have admin rights')
138 game.tui.switch_mode('admin')
139 game.tui.do_refresh = True
140 cmd_ADMIN_OK.argtypes = ''
142 def cmd_CHAT(game, msg):
143 game.tui.log_msg('# ' + msg)
144 game.tui.do_refresh = True
145 cmd_CHAT.argtypes = 'string'
147 def cmd_PLAYER_ID(game, player_id):
148 game.player_id = player_id
149 cmd_PLAYER_ID.argtypes = 'int:nonneg'
151 def cmd_THING(game, yx, thing_type, thing_id):
152 t = game.get_thing(thing_id)
154 t = ThingBase(game, thing_id)
158 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
160 def cmd_THING_NAME(game, thing_id, name):
161 t = game.get_thing(thing_id)
164 cmd_THING_NAME.argtypes = 'int:nonneg string'
166 def cmd_THING_CHAR(game, thing_id, c):
167 t = game.get_thing(thing_id)
170 cmd_THING_CHAR.argtypes = 'int:nonneg char'
172 def cmd_MAP(game, geometry, size, content):
173 map_geometry_class = globals()['MapGeometry' + geometry]
174 game.map_geometry = map_geometry_class(size)
175 game.map_content = content
176 if type(game.map_geometry) == MapGeometrySquare:
177 game.tui.movement_keys = {
178 game.tui.keys['square_move_up']: 'UP',
179 game.tui.keys['square_move_left']: 'LEFT',
180 game.tui.keys['square_move_down']: 'DOWN',
181 game.tui.keys['square_move_right']: 'RIGHT',
183 elif type(game.map_geometry) == MapGeometryHex:
184 game.tui.movement_keys = {
185 game.tui.keys['hex_move_upleft']: 'UPLEFT',
186 game.tui.keys['hex_move_upright']: 'UPRIGHT',
187 game.tui.keys['hex_move_right']: 'RIGHT',
188 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
189 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
190 game.tui.keys['hex_move_left']: 'LEFT',
192 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
194 def cmd_FOV(game, content):
196 cmd_FOV.argtypes = 'string'
198 def cmd_MAP_CONTROL(game, content):
199 game.map_control_content = content
200 cmd_MAP_CONTROL.argtypes = 'string'
202 def cmd_GAME_STATE_COMPLETE(game):
203 if game.tui.mode.name == 'post_login_wait':
204 game.tui.switch_mode('play')
205 if game.tui.mode.shows_info:
206 game.tui.query_info()
207 game.turn_complete = True
208 game.tui.do_refresh = True
209 cmd_GAME_STATE_COMPLETE.argtypes = ''
211 def cmd_PORTAL(game, position, msg):
212 game.portals[position] = msg
213 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
215 def cmd_PLAY_ERROR(game, msg):
216 game.tui.log_msg('? ' + msg)
217 game.tui.flash = True
218 game.tui.do_refresh = True
219 cmd_PLAY_ERROR.argtypes = 'string'
221 def cmd_GAME_ERROR(game, msg):
222 game.tui.log_msg('? game error: ' + msg)
223 game.tui.do_refresh = True
224 cmd_GAME_ERROR.argtypes = 'string'
226 def cmd_ARGUMENT_ERROR(game, msg):
227 game.tui.log_msg('? syntax error: ' + msg)
228 game.tui.do_refresh = True
229 cmd_ARGUMENT_ERROR.argtypes = 'string'
231 def cmd_ANNOTATION_HINT(game, position):
232 game.info_hints += [position]
233 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
235 def cmd_ANNOTATION(game, position, msg):
236 game.info_db[position] = msg
237 game.tui.restore_input_values()
238 if game.tui.mode.shows_info:
239 game.tui.do_refresh = True
240 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
242 def cmd_TASKS(game, tasks_comma_separated):
243 game.tasks = tasks_comma_separated.split(',')
244 game.tui.mode_edit.legal = 'WRITE' in game.tasks
245 cmd_TASKS.argtypes = 'string'
247 def cmd_THING_TYPE(game, thing_type, symbol_hint):
248 game.thing_types[thing_type] = symbol_hint
249 cmd_THING_TYPE.argtypes = 'string char'
251 def cmd_TERRAIN(game, terrain_char, terrain_desc):
252 game.terrains[terrain_char] = terrain_desc
253 cmd_TERRAIN.argtypes = 'char string'
257 cmd_PONG.argtypes = ''
259 class Game(GameBase):
260 turn_complete = False
264 def __init__(self, *args, **kwargs):
265 super().__init__(*args, **kwargs)
266 self.register_command(cmd_LOGIN_OK)
267 self.register_command(cmd_ADMIN_OK)
268 self.register_command(cmd_PONG)
269 self.register_command(cmd_CHAT)
270 self.register_command(cmd_PLAYER_ID)
271 self.register_command(cmd_TURN)
272 self.register_command(cmd_THING)
273 self.register_command(cmd_THING_TYPE)
274 self.register_command(cmd_THING_NAME)
275 self.register_command(cmd_THING_CHAR)
276 self.register_command(cmd_TERRAIN)
277 self.register_command(cmd_MAP)
278 self.register_command(cmd_MAP_CONTROL)
279 self.register_command(cmd_PORTAL)
280 self.register_command(cmd_ANNOTATION)
281 self.register_command(cmd_ANNOTATION_HINT)
282 self.register_command(cmd_GAME_STATE_COMPLETE)
283 self.register_command(cmd_ARGUMENT_ERROR)
284 self.register_command(cmd_GAME_ERROR)
285 self.register_command(cmd_PLAY_ERROR)
286 self.register_command(cmd_TASKS)
287 self.register_command(cmd_FOV)
288 self.map_content = ''
295 def get_string_options(self, string_option_type):
296 if string_option_type == 'map_geometry':
297 return ['Hex', 'Square']
298 elif string_option_type == 'thing_type':
299 return self.thing_types.keys()
302 def get_command(self, command_name):
303 from functools import partial
304 f = partial(self.commands[command_name], self)
305 f.argtypes = self.commands[command_name].argtypes
310 def __init__(self, name, has_input_prompt=False, shows_info=False,
311 is_intro=False, is_single_char_entry=False):
313 self.short_desc = mode_helps[name]['short']
314 self.available_modes = []
315 self.has_input_prompt = has_input_prompt
316 self.shows_info = shows_info
317 self.is_intro = is_intro
318 self.help_intro = mode_helps[name]['long']
319 self.is_single_char_entry = is_single_char_entry
322 def iter_available_modes(self, tui):
323 for mode_name in self.available_modes:
324 mode = getattr(tui, 'mode_' + mode_name)
327 key = tui.keys['switch_to_' + mode.name]
330 def list_available_modes(self, tui):
332 if len(self.available_modes) > 0:
333 msg = 'Other modes available from here:\n'
334 for mode, key in self.iter_available_modes(tui):
335 msg += '[%s] – %s\n' % (key, mode.short_desc)
338 def mode_switch_on_key(self, tui, key_pressed):
339 for mode, key in self.iter_available_modes(tui):
340 if key_pressed == key:
341 tui.switch_mode(mode.name)
346 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
347 mode_admin = Mode('admin')
348 mode_play = Mode('play')
349 mode_study = Mode('study', shows_info=True)
350 mode_edit = Mode('edit', is_single_char_entry=True)
351 mode_control_pw_type = Mode('control_pw_type', is_single_char_entry=True)
352 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
353 mode_control_tile_type = Mode('control_tile_type', is_single_char_entry=True)
354 mode_control_tile_draw = Mode('control_tile_draw')
355 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
356 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
357 mode_chat = Mode('chat', has_input_prompt=True)
358 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
359 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
360 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
361 mode_password = Mode('password', has_input_prompt=True)
364 def __init__(self, host):
367 self.mode_play.available_modes = ["chat", "study", "edit",
368 "annotate", "portal",
369 "password", "admin_enter"]
370 self.mode_study.available_modes = ["chat", "play", "admin_enter"]
371 self.mode_admin.available_modes = ["chat", "play", "study",
374 self.mode_control_tile_draw.available_modes = ["admin"]
378 self.parser = Parser(self.game)
380 self.do_refresh = True
381 self.queue = queue.Queue()
382 self.login_name = None
383 self.map_mode = 'terrain'
384 self.password = 'foo'
385 self.switch_mode('waiting_for_server')
387 'switch_to_chat': 't',
388 'switch_to_play': 'p',
389 'switch_to_password': 'P',
390 'switch_to_annotate': 'M',
391 'switch_to_portal': 'T',
392 'switch_to_study': '?',
393 'switch_to_edit': 'm',
394 'switch_to_admin_enter': 'A',
395 'switch_to_control_pw_type': 'C',
396 'switch_to_control_tile_type': 'Q',
402 'toggle_map_mode': 'M',
403 'hex_move_upleft': 'w',
404 'hex_move_upright': 'e',
405 'hex_move_right': 'd',
406 'hex_move_downright': 'x',
407 'hex_move_downleft': 'y',
408 'hex_move_left': 'a',
409 'square_move_up': 'w',
410 'square_move_left': 'a',
411 'square_move_down': 's',
412 'square_move_right': 'd',
414 if os.path.isfile('config.json'):
415 with open('config.json', 'r') as f:
416 keys_conf = json.loads(f.read())
418 self.keys[k] = keys_conf[k]
419 self.show_help = False
420 self.disconnected = True
421 self.force_instant_connect = True
422 self.input_lines = []
425 curses.wrapper(self.loop)
429 def handle_recv(msg):
435 self.log_msg('@ attempting connect')
436 socket_client_class = PlomSocketClient
437 if self.host.startswith('ws://') or self.host.startswith('wss://'):
438 socket_client_class = WebSocketClient
440 self.socket = socket_client_class(handle_recv, self.host)
441 self.socket_thread = threading.Thread(target=self.socket.run)
442 self.socket_thread.start()
443 self.disconnected = False
444 self.game.thing_types = {}
445 self.game.terrains = {}
446 self.socket.send('TASKS')
447 self.socket.send('TERRAINS')
448 self.socket.send('THING_TYPES')
449 self.switch_mode('login')
450 except ConnectionRefusedError:
451 self.log_msg('@ server connect failure')
452 self.disconnected = True
453 self.switch_mode('waiting_for_server')
454 self.do_refresh = True
457 self.log_msg('@ attempting reconnect')
459 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
460 # conditions with ws4py, find out what exactly
461 self.switch_mode('waiting_for_server')
466 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
467 raise BrokenSocketConnection
468 self.socket.send(msg)
469 except (BrokenPipeError, BrokenSocketConnection):
470 self.log_msg('@ server disconnected :(')
471 self.disconnected = True
472 self.force_instant_connect = True
473 self.do_refresh = True
475 def log_msg(self, msg):
477 if len(self.log) > 100:
478 self.log = self.log[-100:]
480 def query_info(self):
481 self.send('GET_ANNOTATION ' + str(self.explorer))
483 def restore_input_values(self):
484 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
485 info = self.game.info_db[self.explorer]
488 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
489 self.input_ = self.game.portals[self.explorer]
490 elif self.mode.name == 'password':
491 self.input_ = self.password
493 def send_tile_control_command(self):
494 self.send('SET_TILE_CONTROL %s %s' %
495 (self.explorer, quote(self.tile_control_char)))
497 def switch_mode(self, mode_name):
498 self.map_mode = 'terrain'
499 if mode_name == 'admin_enter' and self.is_admin:
501 self.mode = getattr(self, 'mode_' + mode_name)
502 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
503 player = self.game.get_thing(self.game.player_id)
504 self.explorer = YX(player.position.y, player.position.x)
505 if self.mode.shows_info:
507 elif self.mode.name == 'control_tile_draw':
508 self.send_tile_control_command()
509 self.map_mode = 'control'
510 if self.mode.is_single_char_entry:
511 self.show_help = True
512 if self.mode.name == 'waiting_for_server':
513 self.log_msg('@ waiting for server …')
514 elif self.mode.name == 'login':
516 self.send('LOGIN ' + quote(self.login_name))
518 self.log_msg('@ enter username')
519 elif self.mode.name == 'admin_enter':
520 self.log_msg('@ enter admin password:')
521 elif self.mode.name == 'control_pw_pw':
522 self.log_msg('@ enter tile control password for "%s":' % self.tile_control_char)
523 self.restore_input_values()
525 def loop(self, stdscr):
528 def safe_addstr(y, x, line):
529 if y < self.size.y - 1 or x + len(line) < self.size.x:
530 stdscr.addstr(y, x, line)
531 else: # workaround to <https://stackoverflow.com/q/7063128>
532 cut_i = self.size.x - x - 1
534 last_char = line[cut_i]
535 stdscr.addstr(y, self.size.x - 2, last_char)
536 stdscr.insstr(y, self.size.x - 2, ' ')
537 stdscr.addstr(y, x, cut)
539 def handle_input(msg):
540 command, args = self.parser.parse(msg)
543 def msg_into_lines_of_width(msg, width):
547 for i in range(len(msg)):
548 if x >= width or msg[i] == "\n":
560 def reset_screen_size():
561 self.size = YX(*stdscr.getmaxyx())
562 self.size = self.size - YX(self.size.y % 4, 0)
563 self.size = self.size - YX(0, self.size.x % 4)
564 self.window_width = int(self.size.x / 2)
566 def recalc_input_lines():
567 if not self.mode.has_input_prompt:
568 self.input_lines = []
570 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
573 def move_explorer(direction):
574 target = self.game.map_geometry.move_yx(self.explorer, direction)
576 self.explorer = target
577 if self.mode.shows_info:
579 elif self.mode.name == 'control_tile_draw':
580 self.send_tile_control_command()
586 for line in self.log:
587 lines += msg_into_lines_of_width(line, self.window_width)
590 max_y = self.size.y - len(self.input_lines)
591 for i in range(len(lines)):
592 if (i >= max_y - height_header):
594 safe_addstr(max_y - i - 1, self.window_width, lines[i])
597 if not self.game.turn_complete:
599 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
600 info = 'outside field of view'
601 if self.game.fov[pos_i] == '.':
602 terrain_char = self.game.map_content[pos_i]
604 if terrain_char in self.game.terrains:
605 terrain_desc = self.game.terrains[terrain_char]
606 info = 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
607 protection = self.game.map_control_content[pos_i]
608 if protection == '.':
609 protection = 'unprotected'
610 info = 'PROTECTION: %s\n' % protection
611 for t in self.game.things:
612 if t.position == self.explorer:
613 info += 'THING: %s / %s' % (t.type_,
614 self.game.thing_types[t.type_])
615 if hasattr(t, 'player_char'):
616 info += t.player_char
617 if hasattr(t, 'name'):
618 info += ' (%s)' % t.name
620 if self.explorer in self.game.portals:
621 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
623 info += 'PORTAL: (none)\n'
624 if self.explorer in self.game.info_db:
625 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
627 info += 'ANNOTATION: waiting …'
628 lines = msg_into_lines_of_width(info, self.window_width)
630 for i in range(len(lines)):
631 y = height_header + i
632 if y >= self.size.y - len(self.input_lines):
634 safe_addstr(y, self.window_width, lines[i])
637 y = self.size.y - len(self.input_lines)
638 for i in range(len(self.input_lines)):
639 safe_addstr(y, self.window_width, self.input_lines[i])
643 if not self.game.turn_complete:
645 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
648 help = "hit [%s] for help" % self.keys['help']
649 if self.mode.has_input_prompt:
650 help = "enter /help for help"
651 safe_addstr(1, self.window_width,
652 'MODE: %s – %s' % (self.mode.short_desc, help))
655 if not self.game.turn_complete:
658 map_content = self.game.map_content
659 if self.map_mode == 'control':
660 map_content = self.game.map_control_content
661 for y in range(self.game.map_geometry.size.y):
662 start = self.game.map_geometry.size.x * y
663 end = start + self.game.map_geometry.size.x
664 map_lines_split += [[c + ' ' for c in map_content[start:end]]]
665 if self.map_mode == 'annotations':
666 for p in self.game.info_hints:
667 map_lines_split[p.y][p.x] = 'A '
668 elif self.map_mode == 'terrain':
669 for p in self.game.portals.keys():
670 map_lines_split[p.y][p.x] = 'P '
672 for t in self.game.things:
673 symbol = self.game.thing_types[t.type_]
675 if hasattr(t, 'player_char'):
676 meta_char = t.player_char
677 if t.position in used_positions:
679 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
680 used_positions += [t.position]
681 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
682 map_lines_split[self.explorer.y][self.explorer.x] = '??'
684 if type(self.game.map_geometry) == MapGeometryHex:
686 for line in map_lines_split:
687 map_lines += [indent*' ' + ''.join(line)]
688 indent = 0 if indent else 1
690 for line in map_lines_split:
691 map_lines += [''.join(line)]
692 window_center = YX(int(self.size.y / 2),
693 int(self.window_width / 2))
694 player = self.game.get_thing(self.game.player_id)
695 center = player.position
696 if self.mode.shows_info:
697 center = self.explorer
698 center = YX(center.y, center.x * 2)
699 offset = center - window_center
700 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
702 term_y = max(0, -offset.y)
703 term_x = max(0, -offset.x)
704 map_y = max(0, offset.y)
705 map_x = max(0, offset.x)
706 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
707 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
708 safe_addstr(term_y, term_x, to_draw)
713 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
714 self.mode.help_intro)
715 if self.mode.name == 'play':
716 content += "Available actions:\n"
717 if 'MOVE' in self.game.tasks:
718 content += "[%s] – move player\n" % ','.join(self.movement_keys)
719 if 'PICK_UP' in self.game.tasks:
720 content += "[%s] – take thing under player\n" % self.keys['take_thing']
721 if 'DROP' in self.game.tasks:
722 content += "[%s] – drop carried thing\n" % self.keys['drop_thing']
723 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
724 content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
725 content += '[%s] – teleport to other space\n' % self.keys['teleport']
727 elif self.mode.name == 'study':
728 content += 'Available actions:\n'
729 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
730 content += '[%s] – toggle view between terrain, annotations, and password protection areas\n' % self.keys['toggle_map_mode']
732 elif self.mode.name == 'chat':
733 content += '/nick NAME – re-name yourself to NAME\n'
734 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
735 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
736 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
737 content += self.mode.list_available_modes(self)
738 for i in range(self.size.y):
740 self.window_width * (not self.mode.has_input_prompt),
741 ' '*self.window_width)
743 for line in content.split('\n'):
744 lines += msg_into_lines_of_width(line, self.window_width)
745 for i in range(len(lines)):
749 self.window_width * (not self.mode.has_input_prompt),
754 if self.mode.has_input_prompt:
757 if self.mode.shows_info:
762 if not self.mode.is_intro:
768 curses.curs_set(False) # hide cursor
769 curses.use_default_colors();
772 self.explorer = YX(0, 0)
775 interval = datetime.timedelta(seconds=5)
776 last_ping = datetime.datetime.now() - interval
778 if self.disconnected and self.force_instant_connect:
779 self.force_instant_connect = False
781 now = datetime.datetime.now()
782 if now - last_ping > interval:
783 if self.disconnected:
793 self.do_refresh = False
796 msg = self.queue.get(block=False)
801 key = stdscr.getkey()
802 self.do_refresh = True
805 self.show_help = False
806 if key == 'KEY_RESIZE':
808 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
809 self.input_ = self.input_[:-1]
810 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
811 self.show_help = True
813 self.restore_input_values()
814 elif self.mode.has_input_prompt and key != '\n': # Return key
816 max_length = self.window_width * self.size.y - len(input_prompt) - 1
817 if len(self.input_) > max_length:
818 self.input_ = self.input_[:max_length]
819 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
820 self.show_help = True
821 elif self.mode.name == 'login' and key == '\n':
822 self.login_name = self.input_
823 self.send('LOGIN ' + quote(self.input_))
825 elif self.mode.name == 'control_pw_pw' and key == '\n':
826 if self.input_ == '':
827 self.log_msg('@ aborted')
829 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
831 self.switch_mode('admin')
832 elif self.mode.name == 'password' and key == '\n':
833 if self.input_ == '':
835 self.password = self.input_
837 self.switch_mode('play')
838 elif self.mode.name == 'admin_enter' and key == '\n':
839 self.send('BECOME_ADMIN ' + quote(self.input_))
841 self.switch_mode('play')
842 elif self.mode.name == 'chat' and key == '\n':
843 if self.input_ == '':
845 if self.input_[0] == '/': # FIXME fails on empty input
846 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
847 self.switch_mode('play')
848 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
849 self.switch_mode('study')
850 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
851 self.switch_mode('admin_enter')
852 elif self.input_.startswith('/nick'):
853 tokens = self.input_.split(maxsplit=1)
855 self.send('NICK ' + quote(tokens[1]))
857 self.log_msg('? need login name')
859 self.log_msg('? unknown command')
861 self.send('ALL ' + quote(self.input_))
863 elif self.mode.name == 'annotate' and key == '\n':
864 if self.input_ == '':
866 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
867 quote(self.password)))
869 self.switch_mode('play')
870 elif self.mode.name == 'portal' and key == '\n':
871 if self.input_ == '':
873 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
874 quote(self.password)))
876 self.switch_mode('play')
877 elif self.mode.name == 'study':
878 if self.mode.mode_switch_on_key(self, key):
880 elif key == self.keys['toggle_map_mode']:
881 if self.map_mode == 'terrain':
882 self.map_mode = 'annotations'
883 elif self.map_mode == 'annotations':
884 self.map_mode = 'control'
886 self.map_mode = 'terrain'
887 elif key in self.movement_keys:
888 move_explorer(self.movement_keys[key])
889 elif self.mode.name == 'play':
890 if self.mode.mode_switch_on_key(self, key):
892 if key == self.keys['flatten'] and\
893 'FLATTEN_SURROUNDINGS' in self.game.tasks:
894 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
895 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
896 self.send('TASK:PICK_UP')
897 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
898 self.send('TASK:DROP')
899 elif key == self.keys['teleport']:
900 player = self.game.get_thing(self.game.player_id)
901 if player.position in self.game.portals:
902 self.host = self.game.portals[player.position]
906 self.log_msg('? not standing on portal')
907 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
908 self.send('TASK:MOVE ' + self.movement_keys[key])
909 elif self.mode.name == 'edit':
910 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
911 self.switch_mode('play')
912 elif self.mode.name == 'control_pw_type':
913 self.tile_control_char = key
914 self.switch_mode('control_pw_pw')
915 elif self.mode.name == 'control_tile_type':
916 self.tile_control_char = key
917 self.switch_mode('control_tile_draw')
918 elif self.mode.name == 'control_tile_draw':
919 if self.mode.mode_switch_on_key(self, key):
921 elif key in self.movement_keys:
922 move_explorer(self.movement_keys[key])
923 elif self.mode.name == 'admin':
924 if self.mode.mode_switch_on_key(self, key):
927 if len(sys.argv) != 2:
928 raise ArgError('wrong number of arguments, need game host')