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 'FLATTEN_SURROUNDINGS' in self.game.tasks:
759 content += "[%s] – flatten surroundings\n" % self.keys['flatten']
761 elif self.mode.name == 'control_tile_draw':
762 content += "Available actions:\n"
763 content += "[%s] – toggle tile protection drawing\n" % self.keys['toggle_tile_draw']
765 elif self.mode.name == 'chat':
766 content += '/nick NAME – re-name yourself to NAME\n'
767 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
768 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
769 content += '/%s or /edit – switch to map edit mode\n' % self.keys['switch_to_edit']
770 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
771 content += self.mode.list_available_modes(self)
772 for i in range(self.size.y):
774 self.window_width * (not self.mode.has_input_prompt),
775 ' '*self.window_width)
777 for line in content.split('\n'):
778 lines += msg_into_lines_of_width(line, self.window_width)
779 for i in range(len(lines)):
783 self.window_width * (not self.mode.has_input_prompt),
788 if self.mode.has_input_prompt:
791 if self.mode.shows_info:
796 if not self.mode.is_intro:
802 curses.curs_set(False) # hide cursor
803 curses.use_default_colors();
806 self.explorer = YX(0, 0)
809 interval = datetime.timedelta(seconds=5)
810 last_ping = datetime.datetime.now() - interval
812 if self.disconnected and self.force_instant_connect:
813 self.force_instant_connect = False
815 now = datetime.datetime.now()
816 if now - last_ping > interval:
817 if self.disconnected:
827 self.do_refresh = False
830 msg = self.queue.get(block=False)
835 key = stdscr.getkey()
836 self.do_refresh = True
839 self.show_help = False
840 if key == 'KEY_RESIZE':
842 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
843 self.input_ = self.input_[:-1]
844 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
845 self.show_help = True
847 self.restore_input_values()
848 elif self.mode.has_input_prompt and key != '\n': # Return key
850 max_length = self.window_width * self.size.y - len(input_prompt) - 1
851 if len(self.input_) > max_length:
852 self.input_ = self.input_[:max_length]
853 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
854 self.show_help = True
855 elif self.mode.name == 'login' and key == '\n':
856 self.login_name = self.input_
857 self.send('LOGIN ' + quote(self.input_))
859 elif self.mode.name == 'control_pw_pw' and key == '\n':
860 if self.input_ == '':
861 self.log_msg('@ aborted')
863 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
864 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
865 self.switch_mode('admin')
866 elif self.mode.name == 'password' and key == '\n':
867 if self.input_ == '':
869 self.password = self.input_
870 self.switch_mode('edit')
871 elif self.mode.name == 'admin_enter' and key == '\n':
872 self.send('BECOME_ADMIN ' + quote(self.input_))
873 self.switch_mode('play')
874 elif self.mode.name == 'control_pw_type' and key == '\n':
875 if len(self.input_) != 1:
876 self.log_msg('@ entered non-single-char, therefore aborted')
877 self.switch_mode('admin')
879 self.tile_control_char = self.input_
880 self.switch_mode('control_pw_pw')
881 elif self.mode.name == 'control_tile_type' and key == '\n':
882 if len(self.input_) != 1:
883 self.log_msg('@ entered non-single-char, therefore aborted')
884 self.switch_mode('admin')
886 self.tile_control_char = self.input_
887 self.switch_mode('control_tile_draw')
888 elif self.mode.name == 'chat' and key == '\n':
889 if self.input_ == '':
891 if self.input_[0] == '/': # FIXME fails on empty input
892 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
893 self.switch_mode('play')
894 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
895 self.switch_mode('study')
896 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
897 self.switch_mode('edit')
898 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
899 self.switch_mode('admin_enter')
900 elif self.input_.startswith('/nick'):
901 tokens = self.input_.split(maxsplit=1)
903 self.send('NICK ' + quote(tokens[1]))
905 self.log_msg('? need login name')
907 self.log_msg('? unknown command')
909 self.send('ALL ' + quote(self.input_))
911 elif self.mode.name == 'annotate' and key == '\n':
912 if self.input_ == '':
914 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
915 quote(self.password)))
916 self.switch_mode('edit')
917 elif self.mode.name == 'portal' and key == '\n':
918 if self.input_ == '':
920 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
921 quote(self.password)))
922 self.switch_mode('edit')
923 elif self.mode.name == 'study':
924 if self.mode.mode_switch_on_key(self, key):
926 elif key == self.keys['toggle_map_mode']:
927 if self.map_mode == 'terrain only':
928 self.map_mode = 'terrain + annotations'
929 elif self.map_mode == 'terrain + annotations':
930 self.map_mode = 'terrain + things'
932 self.map_mode = 'terrain only'
933 elif key in self.movement_keys:
934 move_explorer(self.movement_keys[key])
935 elif self.mode.name == 'play':
936 if self.mode.mode_switch_on_key(self, key):
938 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
939 self.send('TASK:PICK_UP')
940 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
941 self.send('TASK:DROP')
942 elif key == self.keys['teleport']:
943 player = self.game.get_thing(self.game.player_id)
944 if player.position in self.game.portals:
945 self.host = self.game.portals[player.position]
949 self.log_msg('? not standing on portal')
950 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
951 self.send('TASK:MOVE ' + self.movement_keys[key])
952 elif self.mode.name == 'write':
953 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
954 self.switch_mode('edit')
955 elif self.mode.name == 'control_tile_draw':
956 if self.mode.mode_switch_on_key(self, key):
958 elif key in self.movement_keys:
959 move_explorer(self.movement_keys[key])
960 elif key == self.keys['toggle_tile_draw']:
961 self.tile_draw = False if self.tile_draw else True
962 elif self.mode.name == 'admin':
963 if self.mode.mode_switch_on_key(self, key):
965 elif self.mode.name == 'edit':
966 if self.mode.mode_switch_on_key(self, key):
968 if key == self.keys['flatten'] and\
969 'FLATTEN_SURROUNDINGS' in self.game.tasks:
970 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
971 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
972 self.send('TASK:MOVE ' + self.movement_keys[key])
974 if len(sys.argv) != 2:
975 raise ArgError('wrong number of arguments, need game host')