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 in various ways.'
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. Toggle the map view to show or hide different information layers.'},
24 'long': 'This mode allows you to change the map in various ways. Individual map tiles are shown together with their "protection characters". You can edit a tile if you set the map edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
27 'short': 'change terrain',
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 protection character password',
32 'long': 'This mode is the first of two steps to change the password for a tile protection character. First enter the tile protection character for which you want to change the password.'
35 'short': 'change protection character password',
36 'long': 'This mode is the second of two steps to change the password for a tile protection character. Enter the new password for the tile protection character you chose.'
38 'control_tile_type': {
39 'short': 'change tiles protection',
40 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile tile protection character you want to write.'
42 'control_tile_draw': {
43 'short': 'change tiles protection',
44 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected tile protection 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': 'Enter 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': 'set 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",
386 self.parser = Parser(self.game)
388 self.do_refresh = True
389 self.queue = queue.Queue()
390 self.login_name = None
391 self.map_mode = 'terrain + things'
392 self.password = 'foo'
393 self.switch_mode('waiting_for_server')
395 'switch_to_chat': 't',
396 'switch_to_play': 'p',
397 'switch_to_password': 'P',
398 'switch_to_annotate': 'M',
399 'switch_to_portal': 'T',
400 'switch_to_study': '?',
401 'switch_to_edit': 'E',
402 'switch_to_write': 'm',
403 'switch_to_admin_enter': 'A',
404 'switch_to_control_pw_type': 'C',
405 'switch_to_control_tile_type': 'Q',
411 'toggle_map_mode': 'M',
412 'toggle_tile_draw': 'm',
413 'hex_move_upleft': 'w',
414 'hex_move_upright': 'e',
415 'hex_move_right': 'd',
416 'hex_move_downright': 'x',
417 'hex_move_downleft': 'y',
418 'hex_move_left': 'a',
419 'square_move_up': 'w',
420 'square_move_left': 'a',
421 'square_move_down': 's',
422 'square_move_right': 'd',
424 if os.path.isfile('config.json'):
425 with open('config.json', 'r') as f:
426 keys_conf = json.loads(f.read())
428 self.keys[k] = keys_conf[k]
429 self.show_help = False
430 self.disconnected = True
431 self.force_instant_connect = True
432 self.input_lines = []
435 curses.wrapper(self.loop)
439 def handle_recv(msg):
445 self.log_msg('@ attempting connect')
446 socket_client_class = PlomSocketClient
447 if self.host.startswith('ws://') or self.host.startswith('wss://'):
448 socket_client_class = WebSocketClient
450 self.socket = socket_client_class(handle_recv, self.host)
451 self.socket_thread = threading.Thread(target=self.socket.run)
452 self.socket_thread.start()
453 self.disconnected = False
454 self.game.thing_types = {}
455 self.game.terrains = {}
456 self.socket.send('TASKS')
457 self.socket.send('TERRAINS')
458 self.socket.send('THING_TYPES')
459 self.switch_mode('login')
460 except ConnectionRefusedError:
461 self.log_msg('@ server connect failure')
462 self.disconnected = True
463 self.switch_mode('waiting_for_server')
464 self.do_refresh = True
467 self.log_msg('@ attempting reconnect')
469 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
470 # conditions with ws4py, find out what exactly
471 self.switch_mode('waiting_for_server')
476 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
477 raise BrokenSocketConnection
478 self.socket.send(msg)
479 except (BrokenPipeError, BrokenSocketConnection):
480 self.log_msg('@ server disconnected :(')
481 self.disconnected = True
482 self.force_instant_connect = True
483 self.do_refresh = True
485 def log_msg(self, msg):
487 if len(self.log) > 100:
488 self.log = self.log[-100:]
490 def query_info(self):
491 self.send('GET_ANNOTATION ' + str(self.explorer))
493 def restore_input_values(self):
494 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
495 info = self.game.info_db[self.explorer]
498 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
499 self.input_ = self.game.portals[self.explorer]
500 elif self.mode.name == 'password':
501 self.input_ = self.password
503 def send_tile_control_command(self):
504 self.send('SET_TILE_CONTROL %s %s' %
505 (self.explorer, quote(self.tile_control_char)))
507 def switch_mode(self, mode_name):
508 if self.mode and self.mode.name == 'control_tile_draw':
509 self.log_msg('@ finished tile protection drawing.')
510 self.map_mode = 'terrain + things'
511 self.tile_draw = False
512 if mode_name == 'admin_enter' and self.is_admin:
514 self.mode = getattr(self, 'mode_' + mode_name)
515 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
516 player = self.game.get_thing(self.game.player_id)
517 self.explorer = YX(player.position.y, player.position.x)
518 if self.mode.shows_info:
520 if self.mode.is_single_char_entry:
521 self.show_help = True
522 if self.mode.name == 'waiting_for_server':
523 self.log_msg('@ waiting for server …')
524 elif self.mode.name == 'login':
526 self.send('LOGIN ' + quote(self.login_name))
528 self.log_msg('@ enter username')
529 elif self.mode.name == 'admin_enter':
530 self.log_msg('@ enter admin password:')
531 elif self.mode.name == 'control_pw_type':
532 self.log_msg('@ enter tile protection character for which you want to change the password:')
533 elif self.mode.name == 'control_tile_type':
534 self.log_msg('@ enter tile protection character which you want to draw:')
535 elif self.mode.name == 'control_pw_pw':
536 self.log_msg('@ enter tile protection password for "%s":' % self.tile_control_char)
537 elif self.mode.name == 'control_tile_draw':
538 self.log_msg('@ can draw tile protection 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']))
540 self.restore_input_values()
542 def loop(self, stdscr):
545 def safe_addstr(y, x, line):
546 if y < self.size.y - 1 or x + len(line) < self.size.x:
547 stdscr.addstr(y, x, line)
548 else: # workaround to <https://stackoverflow.com/q/7063128>
549 cut_i = self.size.x - x - 1
551 last_char = line[cut_i]
552 stdscr.addstr(y, self.size.x - 2, last_char)
553 stdscr.insstr(y, self.size.x - 2, ' ')
554 stdscr.addstr(y, x, cut)
556 def handle_input(msg):
557 command, args = self.parser.parse(msg)
560 def msg_into_lines_of_width(msg, width):
564 for i in range(len(msg)):
565 if x >= width or msg[i] == "\n":
577 def reset_screen_size():
578 self.size = YX(*stdscr.getmaxyx())
579 self.size = self.size - YX(self.size.y % 4, 0)
580 self.size = self.size - YX(0, self.size.x % 4)
581 self.window_width = int(self.size.x / 2)
583 def recalc_input_lines():
584 if not self.mode.has_input_prompt:
585 self.input_lines = []
587 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
590 def move_explorer(direction):
591 target = self.game.map_geometry.move_yx(self.explorer, direction)
593 self.explorer = target
594 if self.mode.shows_info:
597 self.send_tile_control_command()
603 for line in self.log:
604 lines += msg_into_lines_of_width(line, self.window_width)
607 max_y = self.size.y - len(self.input_lines)
608 for i in range(len(lines)):
609 if (i >= max_y - height_header):
611 safe_addstr(max_y - i - 1, self.window_width, lines[i])
614 if not self.game.turn_complete:
616 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
617 info = 'MAP VIEW: %s\n' % self.map_mode
618 if self.game.fov[pos_i] != '.':
619 info += 'outside field of view'
621 terrain_char = self.game.map_content[pos_i]
623 if terrain_char in self.game.terrains:
624 terrain_desc = self.game.terrains[terrain_char]
625 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
626 protection = self.game.map_control_content[pos_i]
627 if protection == '.':
628 protection = 'unprotected'
629 info += 'PROTECTION: %s\n' % protection
630 for t in self.game.things:
631 if t.position == self.explorer:
632 info += 'THING: %s / %s' % (t.type_,
633 self.game.thing_types[t.type_])
634 if hasattr(t, 'player_char'):
635 info += t.player_char
636 if hasattr(t, 'name'):
637 info += ' (%s)' % t.name
639 if self.explorer in self.game.portals:
640 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
642 info += 'PORTAL: (none)\n'
643 if self.explorer in self.game.info_db:
644 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
646 info += 'ANNOTATION: waiting …'
647 lines = msg_into_lines_of_width(info, self.window_width)
649 for i in range(len(lines)):
650 y = height_header + i
651 if y >= self.size.y - len(self.input_lines):
653 safe_addstr(y, self.window_width, lines[i])
656 y = self.size.y - len(self.input_lines)
657 for i in range(len(self.input_lines)):
658 safe_addstr(y, self.window_width, self.input_lines[i])
662 if not self.game.turn_complete:
664 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
667 help = "hit [%s] for help" % self.keys['help']
668 if self.mode.has_input_prompt:
669 help = "enter /help for help"
670 safe_addstr(1, self.window_width,
671 'MODE: %s – %s' % (self.mode.short_desc, help))
674 if not self.game.turn_complete:
677 for y in range(self.game.map_geometry.size.y):
678 start = self.game.map_geometry.size.x * y
679 end = start + self.game.map_geometry.size.x
680 if self.mode.name in {'edit', 'write', 'control_tile_draw',
681 'control_tile_type'}:
683 for i in range(start, end):
684 line += [self.game.map_content[i]
685 + self.game.map_control_content[i]]
686 map_lines_split += [line]
688 map_lines_split += [[c + ' ' for c
689 in self.game.map_content[start:end]]]
690 if self.map_mode == 'terrain + annotations':
691 for p in self.game.info_hints:
692 map_lines_split[p.y][p.x] = 'A '
693 elif self.map_mode == 'terrain + things':
694 for p in self.game.portals.keys():
695 original = map_lines_split[p.y][p.x]
696 map_lines_split[p.y][p.x] = original[0] + 'P'
698 for t in self.game.things:
699 symbol = self.game.thing_types[t.type_]
701 if hasattr(t, 'player_char'):
702 meta_char = t.player_char
703 if t.position in used_positions:
705 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
706 used_positions += [t.position]
707 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
708 map_lines_split[self.explorer.y][self.explorer.x] = '??'
710 if type(self.game.map_geometry) == MapGeometryHex:
712 for line in map_lines_split:
713 map_lines += [indent*' ' + ''.join(line)]
714 indent = 0 if indent else 1
716 for line in map_lines_split:
717 map_lines += [''.join(line)]
718 window_center = YX(int(self.size.y / 2),
719 int(self.window_width / 2))
720 player = self.game.get_thing(self.game.player_id)
721 center = player.position
722 if self.mode.shows_info:
723 center = self.explorer
724 center = YX(center.y, center.x * 2)
725 offset = center - window_center
726 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
728 term_y = max(0, -offset.y)
729 term_x = max(0, -offset.x)
730 map_y = max(0, offset.y)
731 map_x = max(0, offset.x)
732 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
733 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
734 safe_addstr(term_y, term_x, to_draw)
739 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
740 self.mode.help_intro)
741 if self.mode.name == 'play':
742 content += "Available actions:\n"
743 if 'MOVE' in self.game.tasks:
744 content += "[%s] – move player\n" % ','.join(self.movement_keys)
745 if 'PICK_UP' in self.game.tasks:
746 content += "[%s] – pick up thing\n" % self.keys['take_thing']
747 if 'DROP' in self.game.tasks:
748 content += "[%s] – drop thing\n" % self.keys['drop_thing']
749 content += '[%s] – teleport\n' % self.keys['teleport']
751 elif self.mode.name == 'study':
752 content += 'Available actions:\n'
753 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
754 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
756 elif self.mode.name == 'edit':
757 content += "Available actions:\n"
758 if 'MOVE' in self.game.tasks:
759 content += "[%s] – move player\n" % ','.join(self.movement_keys)
760 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
761 content += "[%s] – flatten surroundings\n" % self.keys['flatten']
763 elif self.mode.name == 'control_tile_draw':
764 content += "Available actions:\n"
765 content += "[%s] – toggle tile protection drawing\n" % self.keys['toggle_tile_draw']
767 elif self.mode.name == 'chat':
768 content += '/nick NAME – re-name yourself to NAME\n'
769 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
770 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
771 content += '/%s or /edit – switch to map edit mode\n' % self.keys['switch_to_edit']
772 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
773 content += self.mode.list_available_modes(self)
774 for i in range(self.size.y):
776 self.window_width * (not self.mode.has_input_prompt),
777 ' '*self.window_width)
779 for line in content.split('\n'):
780 lines += msg_into_lines_of_width(line, self.window_width)
781 for i in range(len(lines)):
785 self.window_width * (not self.mode.has_input_prompt),
790 if self.mode.has_input_prompt:
793 if self.mode.shows_info:
798 if not self.mode.is_intro:
804 curses.curs_set(False) # hide cursor
805 curses.use_default_colors();
808 self.explorer = YX(0, 0)
811 interval = datetime.timedelta(seconds=5)
812 last_ping = datetime.datetime.now() - interval
814 if self.disconnected and self.force_instant_connect:
815 self.force_instant_connect = False
817 now = datetime.datetime.now()
818 if now - last_ping > interval:
819 if self.disconnected:
829 self.do_refresh = False
832 msg = self.queue.get(block=False)
837 key = stdscr.getkey()
838 self.do_refresh = True
841 self.show_help = False
842 if key == 'KEY_RESIZE':
844 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
845 self.input_ = self.input_[:-1]
846 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
847 self.show_help = True
849 self.restore_input_values()
850 elif self.mode.has_input_prompt and key != '\n': # Return key
852 max_length = self.window_width * self.size.y - len(input_prompt) - 1
853 if len(self.input_) > max_length:
854 self.input_ = self.input_[:max_length]
855 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
856 self.show_help = True
857 elif self.mode.name == 'login' and key == '\n':
858 self.login_name = self.input_
859 self.send('LOGIN ' + quote(self.input_))
861 elif self.mode.name == 'control_pw_pw' and key == '\n':
862 if self.input_ == '':
863 self.log_msg('@ aborted')
865 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
866 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
867 self.switch_mode('admin')
868 elif self.mode.name == 'password' and key == '\n':
869 if self.input_ == '':
871 self.password = self.input_
872 self.switch_mode('edit')
873 elif self.mode.name == 'admin_enter' and key == '\n':
874 self.send('BECOME_ADMIN ' + quote(self.input_))
875 self.switch_mode('play')
876 elif self.mode.name == 'control_pw_type' and key == '\n':
877 if len(self.input_) != 1:
878 self.log_msg('@ entered non-single-char, therefore aborted')
879 self.switch_mode('admin')
881 self.tile_control_char = self.input_
882 self.switch_mode('control_pw_pw')
883 elif self.mode.name == 'control_tile_type' and key == '\n':
884 if len(self.input_) != 1:
885 self.log_msg('@ entered non-single-char, therefore aborted')
886 self.switch_mode('admin')
888 self.tile_control_char = self.input_
889 self.switch_mode('control_tile_draw')
890 elif self.mode.name == 'chat' and key == '\n':
891 if self.input_ == '':
893 if self.input_[0] == '/': # FIXME fails on empty input
894 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
895 self.switch_mode('play')
896 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
897 self.switch_mode('study')
898 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
899 self.switch_mode('edit')
900 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
901 self.switch_mode('admin_enter')
902 elif self.input_.startswith('/nick'):
903 tokens = self.input_.split(maxsplit=1)
905 self.send('NICK ' + quote(tokens[1]))
907 self.log_msg('? need login name')
909 self.log_msg('? unknown command')
911 self.send('ALL ' + quote(self.input_))
913 elif self.mode.name == 'annotate' and key == '\n':
914 if self.input_ == '':
916 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
917 quote(self.password)))
918 self.switch_mode('edit')
919 elif self.mode.name == 'portal' and key == '\n':
920 if self.input_ == '':
922 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
923 quote(self.password)))
924 self.switch_mode('edit')
925 elif self.mode.name == 'study':
926 if self.mode.mode_switch_on_key(self, key):
928 elif key == self.keys['toggle_map_mode']:
929 if self.map_mode == 'terrain only':
930 self.map_mode = 'terrain + annotations'
931 elif self.map_mode == 'terrain + annotations':
932 self.map_mode = 'terrain + things'
934 self.map_mode = 'terrain only'
935 elif key in self.movement_keys:
936 move_explorer(self.movement_keys[key])
937 elif self.mode.name == 'play':
938 if self.mode.mode_switch_on_key(self, key):
940 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
941 self.send('TASK:PICK_UP')
942 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
943 self.send('TASK:DROP')
944 elif key == self.keys['teleport']:
945 player = self.game.get_thing(self.game.player_id)
946 if player.position in self.game.portals:
947 self.host = self.game.portals[player.position]
951 self.log_msg('? not standing on portal')
952 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
953 self.send('TASK:MOVE ' + self.movement_keys[key])
954 elif self.mode.name == 'write':
955 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
956 self.switch_mode('edit')
957 elif self.mode.name == 'control_tile_draw':
958 if self.mode.mode_switch_on_key(self, key):
960 elif key in self.movement_keys:
961 move_explorer(self.movement_keys[key])
962 elif key == self.keys['toggle_tile_draw']:
963 self.tile_draw = False if self.tile_draw else True
964 elif self.mode.name == 'admin':
965 if self.mode.mode_switch_on_key(self, key):
967 elif self.mode.name == 'edit':
968 if self.mode.mode_switch_on_key(self, key):
970 if key == self.keys['flatten'] and\
971 'FLATTEN_SURROUNDINGS' in self.game.tasks:
972 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
973 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
974 self.send('TASK:MOVE ' + self.movement_keys[key])
976 if len(sys.argv) != 2:
977 raise ArgError('wrong number of arguments, need game host')