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. Toggle tile control drawing on, then 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', has_input_prompt=True)
357 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
358 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=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)
370 def __init__(self, host):
373 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
374 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
375 self.mode_admin.available_modes = ["control_pw_type",
376 "control_tile_type", "chat",
377 "study", "play", "edit"]
378 self.mode_control_tile_draw.available_modes = ["admin_enter"]
379 self.mode_edit.available_modes = ["write", "annotate", "portal",
380 "password", "chat", "study", "play",
385 self.parser = Parser(self.game)
387 self.do_refresh = True
388 self.queue = queue.Queue()
389 self.login_name = None
390 self.map_mode = 'terrain + things'
391 self.password = 'foo'
392 self.switch_mode('waiting_for_server')
394 'switch_to_chat': 't',
395 'switch_to_play': 'p',
396 'switch_to_password': 'P',
397 'switch_to_annotate': 'M',
398 'switch_to_portal': 'T',
399 'switch_to_study': '?',
400 'switch_to_edit': 'E',
401 'switch_to_write': 'm',
402 'switch_to_admin_enter': 'A',
403 'switch_to_control_pw_type': 'C',
404 'switch_to_control_tile_type': 'Q',
410 'toggle_map_mode': 'M',
411 'toggle_tile_draw': 'm',
412 'hex_move_upleft': 'w',
413 'hex_move_upright': 'e',
414 'hex_move_right': 'd',
415 'hex_move_downright': 'x',
416 'hex_move_downleft': 'y',
417 'hex_move_left': 'a',
418 'square_move_up': 'w',
419 'square_move_left': 'a',
420 'square_move_down': 's',
421 'square_move_right': 'd',
423 if os.path.isfile('config.json'):
424 with open('config.json', 'r') as f:
425 keys_conf = json.loads(f.read())
427 self.keys[k] = keys_conf[k]
428 self.show_help = False
429 self.disconnected = True
430 self.force_instant_connect = True
431 self.input_lines = []
434 curses.wrapper(self.loop)
438 def handle_recv(msg):
444 self.log_msg('@ attempting connect')
445 socket_client_class = PlomSocketClient
446 if self.host.startswith('ws://') or self.host.startswith('wss://'):
447 socket_client_class = WebSocketClient
449 self.socket = socket_client_class(handle_recv, self.host)
450 self.socket_thread = threading.Thread(target=self.socket.run)
451 self.socket_thread.start()
452 self.disconnected = False
453 self.game.thing_types = {}
454 self.game.terrains = {}
455 self.socket.send('TASKS')
456 self.socket.send('TERRAINS')
457 self.socket.send('THING_TYPES')
458 self.switch_mode('login')
459 except ConnectionRefusedError:
460 self.log_msg('@ server connect failure')
461 self.disconnected = True
462 self.switch_mode('waiting_for_server')
463 self.do_refresh = True
466 self.log_msg('@ attempting reconnect')
468 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
469 # conditions with ws4py, find out what exactly
470 self.switch_mode('waiting_for_server')
475 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
476 raise BrokenSocketConnection
477 self.socket.send(msg)
478 except (BrokenPipeError, BrokenSocketConnection):
479 self.log_msg('@ server disconnected :(')
480 self.disconnected = True
481 self.force_instant_connect = True
482 self.do_refresh = True
484 def log_msg(self, msg):
486 if len(self.log) > 100:
487 self.log = self.log[-100:]
489 def query_info(self):
490 self.send('GET_ANNOTATION ' + str(self.explorer))
492 def restore_input_values(self):
493 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
494 info = self.game.info_db[self.explorer]
497 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
498 self.input_ = self.game.portals[self.explorer]
499 elif self.mode.name == 'password':
500 self.input_ = self.password
502 def send_tile_control_command(self):
503 self.send('SET_TILE_CONTROL %s %s' %
504 (self.explorer, quote(self.tile_control_char)))
506 def switch_mode(self, mode_name):
507 self.map_mode = 'terrain + things'
508 self.tile_draw = False
509 if mode_name == 'admin_enter' and self.is_admin:
511 self.mode = getattr(self, 'mode_' + mode_name)
512 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
513 player = self.game.get_thing(self.game.player_id)
514 self.explorer = YX(player.position.y, player.position.x)
515 if self.mode.shows_info:
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_type':
529 self.log_msg('@ enter tile control character for which you want to change the password:')
530 elif self.mode.name == 'control_tile_type':
531 self.log_msg('@ enter tile control character which you want to draw:')
532 elif self.mode.name == 'control_pw_pw':
533 self.log_msg('@ enter tile control password for "%s":' % self.tile_control_char)
534 elif self.mode.name == 'control_tile_draw':
535 self.log_msg('@ can draw tile control character "%s", turn drawing on/off with [%s], finish with [%s].' % (self.tile_control_char, self.keys['toggle_tile_draw'], self.keys['switch_to_admin_enter']))
537 self.restore_input_values()
539 def loop(self, stdscr):
542 def safe_addstr(y, x, line):
543 if y < self.size.y - 1 or x + len(line) < self.size.x:
544 stdscr.addstr(y, x, line)
545 else: # workaround to <https://stackoverflow.com/q/7063128>
546 cut_i = self.size.x - x - 1
548 last_char = line[cut_i]
549 stdscr.addstr(y, self.size.x - 2, last_char)
550 stdscr.insstr(y, self.size.x - 2, ' ')
551 stdscr.addstr(y, x, cut)
553 def handle_input(msg):
554 command, args = self.parser.parse(msg)
557 def msg_into_lines_of_width(msg, width):
561 for i in range(len(msg)):
562 if x >= width or msg[i] == "\n":
574 def reset_screen_size():
575 self.size = YX(*stdscr.getmaxyx())
576 self.size = self.size - YX(self.size.y % 4, 0)
577 self.size = self.size - YX(0, self.size.x % 4)
578 self.window_width = int(self.size.x / 2)
580 def recalc_input_lines():
581 if not self.mode.has_input_prompt:
582 self.input_lines = []
584 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
587 def move_explorer(direction):
588 target = self.game.map_geometry.move_yx(self.explorer, direction)
590 self.explorer = target
591 if self.mode.shows_info:
594 self.send_tile_control_command()
600 for line in self.log:
601 lines += msg_into_lines_of_width(line, self.window_width)
604 max_y = self.size.y - len(self.input_lines)
605 for i in range(len(lines)):
606 if (i >= max_y - height_header):
608 safe_addstr(max_y - i - 1, self.window_width, lines[i])
611 if not self.game.turn_complete:
613 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
614 info = 'MAP VIEW: %s\n' % self.map_mode
615 if self.game.fov[pos_i] != '.':
616 info += 'outside field of view'
618 terrain_char = self.game.map_content[pos_i]
620 if terrain_char in self.game.terrains:
621 terrain_desc = self.game.terrains[terrain_char]
622 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
623 protection = self.game.map_control_content[pos_i]
624 if protection == '.':
625 protection = 'unprotected'
626 info += 'PROTECTION: %s\n' % protection
627 for t in self.game.things:
628 if t.position == self.explorer:
629 info += 'THING: %s / %s' % (t.type_,
630 self.game.thing_types[t.type_])
631 if hasattr(t, 'player_char'):
632 info += t.player_char
633 if hasattr(t, 'name'):
634 info += ' (%s)' % t.name
636 if self.explorer in self.game.portals:
637 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
639 info += 'PORTAL: (none)\n'
640 if self.explorer in self.game.info_db:
641 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
643 info += 'ANNOTATION: waiting …'
644 lines = msg_into_lines_of_width(info, self.window_width)
646 for i in range(len(lines)):
647 y = height_header + i
648 if y >= self.size.y - len(self.input_lines):
650 safe_addstr(y, self.window_width, lines[i])
653 y = self.size.y - len(self.input_lines)
654 for i in range(len(self.input_lines)):
655 safe_addstr(y, self.window_width, self.input_lines[i])
659 if not self.game.turn_complete:
661 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
664 help = "hit [%s] for help" % self.keys['help']
665 if self.mode.has_input_prompt:
666 help = "enter /help for help"
667 safe_addstr(1, self.window_width,
668 'MODE: %s – %s' % (self.mode.short_desc, help))
671 if not self.game.turn_complete:
674 for y in range(self.game.map_geometry.size.y):
675 start = self.game.map_geometry.size.x * y
676 end = start + self.game.map_geometry.size.x
677 if self.mode.name in {'edit', 'write', 'control_tile_draw',
678 'control_tile_type'}:
680 for i in range(start, end):
681 line += [self.game.map_content[i]
682 + self.game.map_control_content[i]]
683 map_lines_split += [line]
685 map_lines_split += [[c + ' ' for c
686 in self.game.map_content[start:end]]]
687 if self.map_mode == 'terrain + annotations':
688 for p in self.game.info_hints:
689 map_lines_split[p.y][p.x] = 'A '
690 elif self.map_mode == 'terrain + things':
691 for p in self.game.portals.keys():
692 original = map_lines_split[p.y][p.x]
693 map_lines_split[p.y][p.x] = original[0] + 'P'
695 for t in self.game.things:
696 symbol = self.game.thing_types[t.type_]
698 if hasattr(t, 'player_char'):
699 meta_char = t.player_char
700 if t.position in used_positions:
702 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
703 used_positions += [t.position]
704 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
705 map_lines_split[self.explorer.y][self.explorer.x] = '??'
707 if type(self.game.map_geometry) == MapGeometryHex:
709 for line in map_lines_split:
710 map_lines += [indent*' ' + ''.join(line)]
711 indent = 0 if indent else 1
713 for line in map_lines_split:
714 map_lines += [''.join(line)]
715 window_center = YX(int(self.size.y / 2),
716 int(self.window_width / 2))
717 player = self.game.get_thing(self.game.player_id)
718 center = player.position
719 if self.mode.shows_info:
720 center = self.explorer
721 center = YX(center.y, center.x * 2)
722 offset = center - window_center
723 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
725 term_y = max(0, -offset.y)
726 term_x = max(0, -offset.x)
727 map_y = max(0, offset.y)
728 map_x = max(0, offset.x)
729 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
730 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
731 safe_addstr(term_y, term_x, to_draw)
736 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
737 self.mode.help_intro)
738 if self.mode.name == 'play':
739 content += "Available actions:\n"
740 if 'MOVE' in self.game.tasks:
741 content += "[%s] – move player\n" % ','.join(self.movement_keys)
742 if 'PICK_UP' in self.game.tasks:
743 content += "[%s] – pick up thing\n" % self.keys['take_thing']
744 if 'DROP' in self.game.tasks:
745 content += "[%s] – drop picked-up thing\n" % self.keys['drop_thing']
746 content += '[%s] – teleport to other space\n' % self.keys['teleport']
748 elif self.mode.name == 'study':
749 content += 'Available actions:\n'
750 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
751 content += '[%s] – toggle view between anything, terrain, and annotations\n' % self.keys['toggle_map_mode']
753 elif self.mode.name == 'edit':
754 content += "Available actions:\n"
755 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
756 content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
758 elif self.mode.name == 'control_tile_draw':
759 content += "Available actions:\n"
760 content += "[%s] – toggle tile control drawing\n" % self.keys['toggle_tile_draw']
762 elif self.mode.name == 'chat':
763 content += '/nick NAME – re-name yourself to NAME\n'
764 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
765 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
766 content += '/%s or /edit – switch to map edit mode\n' % self.keys['switch_to_edit']
767 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
768 content += self.mode.list_available_modes(self)
769 for i in range(self.size.y):
771 self.window_width * (not self.mode.has_input_prompt),
772 ' '*self.window_width)
774 for line in content.split('\n'):
775 lines += msg_into_lines_of_width(line, self.window_width)
776 for i in range(len(lines)):
780 self.window_width * (not self.mode.has_input_prompt),
785 if self.mode.has_input_prompt:
788 if self.mode.shows_info:
793 if not self.mode.is_intro:
799 curses.curs_set(False) # hide cursor
800 curses.use_default_colors();
803 self.explorer = YX(0, 0)
806 interval = datetime.timedelta(seconds=5)
807 last_ping = datetime.datetime.now() - interval
809 if self.disconnected and self.force_instant_connect:
810 self.force_instant_connect = False
812 now = datetime.datetime.now()
813 if now - last_ping > interval:
814 if self.disconnected:
824 self.do_refresh = False
827 msg = self.queue.get(block=False)
832 key = stdscr.getkey()
833 self.do_refresh = True
836 self.show_help = False
837 if key == 'KEY_RESIZE':
839 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
840 self.input_ = self.input_[:-1]
841 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
842 self.show_help = True
844 self.restore_input_values()
845 elif self.mode.has_input_prompt and key != '\n': # Return key
847 max_length = self.window_width * self.size.y - len(input_prompt) - 1
848 if len(self.input_) > max_length:
849 self.input_ = self.input_[:max_length]
850 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
851 self.show_help = True
852 elif self.mode.name == 'login' and key == '\n':
853 self.login_name = self.input_
854 self.send('LOGIN ' + quote(self.input_))
856 elif self.mode.name == 'control_pw_pw' and key == '\n':
857 if self.input_ == '':
858 self.log_msg('@ aborted')
860 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
861 self.switch_mode('admin')
862 elif self.mode.name == 'password' and key == '\n':
863 if self.input_ == '':
865 self.password = self.input_
866 self.switch_mode('edit')
867 elif self.mode.name == 'admin_enter' and key == '\n':
868 self.send('BECOME_ADMIN ' + quote(self.input_))
869 self.switch_mode('play')
870 elif self.mode.name == 'control_pw_type' and key == '\n':
871 if len(self.input_) != 1:
872 self.log_msg('@ entered non-single-char, therefore aborted')
873 self.switch_mode('admin')
875 self.tile_control_char = self.input_
876 self.switch_mode('control_pw_pw')
877 elif self.mode.name == 'control_tile_type' and key == '\n':
878 if len(self.input_) != 1:
879 self.log_msg('@ entered non-single-char, therefore aborted')
880 self.switch_mode('admin')
882 self.tile_control_char = self.input_
883 self.switch_mode('control_tile_draw')
884 elif self.mode.name == 'chat' and key == '\n':
885 if self.input_ == '':
887 if self.input_[0] == '/': # FIXME fails on empty input
888 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
889 self.switch_mode('play')
890 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
891 self.switch_mode('study')
892 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
893 self.switch_mode('edit')
894 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
895 self.switch_mode('admin_enter')
896 elif self.input_.startswith('/nick'):
897 tokens = self.input_.split(maxsplit=1)
899 self.send('NICK ' + quote(tokens[1]))
901 self.log_msg('? need login name')
903 self.log_msg('? unknown command')
905 self.send('ALL ' + quote(self.input_))
907 elif self.mode.name == 'annotate' and key == '\n':
908 if self.input_ == '':
910 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
911 quote(self.password)))
912 self.switch_mode('edit')
913 elif self.mode.name == 'portal' and key == '\n':
914 if self.input_ == '':
916 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
917 quote(self.password)))
918 self.switch_mode('edit')
919 elif self.mode.name == 'study':
920 if self.mode.mode_switch_on_key(self, key):
922 elif key == self.keys['toggle_map_mode']:
923 if self.map_mode == 'terrain only':
924 self.map_mode = 'terrain + annotations'
925 elif self.map_mode == 'terrain + annotations':
926 self.map_mode = 'terrain + things'
928 self.map_mode = 'terrain only'
929 elif key in self.movement_keys:
930 move_explorer(self.movement_keys[key])
931 elif self.mode.name == 'play':
932 if self.mode.mode_switch_on_key(self, key):
934 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
935 self.send('TASK:PICK_UP')
936 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
937 self.send('TASK:DROP')
938 elif key == self.keys['teleport']:
939 player = self.game.get_thing(self.game.player_id)
940 if player.position in self.game.portals:
941 self.host = self.game.portals[player.position]
945 self.log_msg('? not standing on portal')
946 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
947 self.send('TASK:MOVE ' + self.movement_keys[key])
948 elif self.mode.name == 'write':
949 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
950 self.switch_mode('edit')
951 elif self.mode.name == 'control_tile_draw':
952 if self.mode.mode_switch_on_key(self, key):
954 elif key in self.movement_keys:
955 move_explorer(self.movement_keys[key])
956 elif key == self.keys['toggle_tile_draw']:
957 self.tile_draw = False if self.tile_draw else True
958 elif self.mode.name == 'admin':
959 if self.mode.mode_switch_on_key(self, key):
961 elif self.mode.name == 'edit':
962 if self.mode.mode_switch_on_key(self, key):
964 if key == self.keys['flatten'] and\
965 'FLATTEN_SURROUNDINGS' in self.game.tasks:
966 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
967 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
968 self.send('TASK:MOVE ' + self.movement_keys[key])
970 if len(sys.argv) != 2:
971 raise ArgError('wrong number of arguments, need game host')