home · contact · privacy
Add player faces/hats popping up n their chat messages, if in FOV.
[plomrogue2] / rogue_chat_curses.py
1 #!/usr/bin/env python3
2 import curses
3 import queue
4 import threading
5 import time
6 import sys
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, ArgError
13
14 mode_helps = {
15     'play': {
16         'short': 'play',
17         'intro': '',
18         'long': 'This mode allows you to interact with the map in various ways.'
19     },
20     'study': {
21         'short': 'study',
22         'intro': '',
23         '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     'edit': {
25         'short': 'world edit',
26         'intro': '',
27         'long': 'This mode allows you to change the game world in various ways.  Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view.  You can edit a tile if you set the world edit password that matches its protection character.  The character "." marks the absence of protection:  Such tiles can always be edited.'
28     },
29     'name_thing': {
30         'short': 'name thing',
31         'intro': '',
32         'long': 'Give name to/change name of thing here.'
33     },
34     'command_thing': {
35         'short': 'command thing',
36         'intro': '',
37         'long': 'Enter a command to the thing you carry.  Enter nothing to return to play mode.'
38     },
39     'take_thing': {
40         'short': 'take thing',
41         'intro': 'Pick up a thing in reach by entering its index number.  Enter nothing to abort.',
42         'long': 'You see a list of things which you could pick up.  Enter the target thing\'s index, or, to leave, nothing.'
43     },
44     'drop_thing': {
45         'short': 'drop thing',
46         'intro': 'Enter number of direction to which you want to drop thing.',
47         'long': 'Drop currently carried thing by entering the target direction index.  Enter nothing to return to play mode..'
48     },
49     'admin_thing_protect': {
50         'short': 'change thing protection',
51         'intro': '@ enter thing protection character:',
52         'long': 'Change protection character for thing here.'
53     },
54     'enter_face': {
55         'short': 'enter your face',
56         'intro': '@ enter face line (enter nothing to abort):',
57         'long': 'Draw your face as ASCII art.  The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
58     },
59     'write': {
60         'short': 'change terrain',
61         'intro': '',
62         'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so).  Just enter any printable ASCII character to imprint it on the ground below you.'
63     },
64     'control_pw_type': {
65         'short': 'change protection character password',
66         'intro': '@ enter protection character for which you want to change the password:',
67         'long': 'This mode is the first of two steps to change the password for a protection character.  First enter the protection character for which you want to change the password.'
68     },
69     'control_pw_pw': {
70         'short': 'change protection character password',
71         'intro': '',
72         'long': 'This mode is the second of two steps to change the password for a protection character.  Enter the new password for the protection character you chose.'
73     },
74     'control_tile_type': {
75         'short': 'change tiles protection',
76         'intro': '@ enter protection character which you want to draw:',
77         'long': 'This mode is the first of two steps to change tile protection areas on the map.  First enter the tile protection character you want to write.'
78     },
79     'control_tile_draw': {
80         'short': 'change tiles protection',
81         'intro': '',
82         '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 protection character.'
83     },
84     'annotate': {
85         'short': 'annotate tile',
86         'intro': '',
87         'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so).  Hit Return to leave.'
88     },
89     'portal': {
90         'short': 'edit portal',
91         'intro': '',
92         'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world 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.'
93     },
94     'chat': {
95         'short': 'chat',
96         'intro': '',
97         '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:\n\n/nick NAME – re-name yourself to NAME'
98     },
99     'login': {
100         'short': 'login',
101         'intro': '',
102         'long': 'Enter your player name.'
103     },
104     'waiting_for_server': {
105         'short': 'waiting for server response',
106         'intro': '@ waiting for server …',
107         'long': 'Waiting for a server response.'
108     },
109     'post_login_wait': {
110         'short': 'waiting for server response',
111         'intro': '',
112         'long': 'Waiting for a server response.'
113     },
114     'password': {
115         'short': 'set world edit password',
116         'intro': '',
117         'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world.  Hit return to confirm and leave.'
118     },
119     'admin_enter': {
120         'short': 'become admin',
121         'intro': '@ enter admin password:',
122         'long': 'This mode allows you to become admin if you know an admin password.'
123     },
124     'admin': {
125         'short': 'admin',
126         'intro': '',
127         'long': 'This mode allows you access to actions limited to administrators.'
128     }
129 }
130
131 from ws4py.client import WebSocketBaseClient
132 class WebSocketClient(WebSocketBaseClient):
133
134     def __init__(self, recv_handler, *args, **kwargs):
135         super().__init__(*args, **kwargs)
136         self.recv_handler = recv_handler
137         self.connect()
138
139     def received_message(self, message):
140         if message.is_text:
141             message = str(message)
142             self.recv_handler(message)
143
144     @property
145     def plom_closed(self):
146         return self.client_terminated
147
148 from plomrogue.io_tcp import PlomSocket
149 class PlomSocketClient(PlomSocket):
150
151     def __init__(self, recv_handler, url):
152         import socket
153         self.recv_handler = recv_handler
154         host, port = url.split(':')
155         super().__init__(socket.create_connection((host, port)))
156
157     def close(self):
158         self.socket.close()
159
160     def run(self):
161         import ssl
162         try:
163             for msg in self.recv():
164                 if msg == 'NEED_SSL':
165                     self.socket = ssl.wrap_socket(self.socket)
166                     continue
167                 self.recv_handler(msg)
168         except BrokenSocketConnection:
169             pass  # we assume socket will be known as dead by now
170
171 def cmd_TURN(game, n):
172     game.annotations = {}
173     game.turn = n
174     game.things = []
175     game.portals = {}
176     game.turn_complete = False
177     game.fov = ''
178 cmd_TURN.argtypes = 'int:nonneg'
179
180 def cmd_LOGIN_OK(game):
181     game.tui.switch_mode('post_login_wait')
182     game.tui.send('GET_GAMESTATE')
183     game.tui.log_msg('@ welcome')
184 cmd_LOGIN_OK.argtypes = ''
185
186 def cmd_ADMIN_OK(game):
187     game.tui.is_admin = True
188     game.tui.log_msg('@ you now have admin rights')
189     game.tui.switch_mode('admin')
190     game.tui.do_refresh = True
191 cmd_ADMIN_OK.argtypes = ''
192
193 def cmd_REPLY(game, msg):
194     game.tui.log_msg('#MUSICPLAYER: ' + msg)
195     game.tui.do_refresh = True
196 cmd_REPLY.argtypes = 'string'
197
198 def cmd_CHAT(game, msg):
199     game.tui.log_msg('# ' + msg)
200     game.tui.do_refresh = True
201 cmd_CHAT.argtypes = 'string'
202
203 def cmd_CHATFACE(game, thing_id):
204     game.tui.draw_face = thing_id
205 cmd_CHATFACE.argtypes = 'int:pos'
206
207 def cmd_PLAYER_ID(game, player_id):
208     game.player_id = player_id
209 cmd_PLAYER_ID.argtypes = 'int:nonneg'
210
211 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
212     t = game.get_thing(thing_id)
213     if not t:
214         t = ThingBase(game, thing_id)
215         game.things += [t]
216     t.position = yx
217     t.type_ = thing_type
218     t.protection = protection
219     t.portable = portable
220     t.commandable = commandable
221 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
222
223 def cmd_THING_NAME(game, thing_id, name):
224     t = game.get_thing(thing_id)
225     t.name = name
226 cmd_THING_NAME.argtypes = 'int:pos string'
227
228 def cmd_THING_FACE(game, thing_id, face):
229     t = game.get_thing(thing_id)
230     t.face = face
231 cmd_THING_FACE.argtypes = 'int:pos string'
232
233 def cmd_THING_HAT(game, thing_id, hat):
234     t = game.get_thing(thing_id)
235     t.hat = hat
236 cmd_THING_HAT.argtypes = 'int:pos string'
237
238 def cmd_THING_CHAR(game, thing_id, c):
239     t = game.get_thing(thing_id)
240     t.thing_char = c
241 cmd_THING_CHAR.argtypes = 'int:pos char'
242
243 def cmd_MAP(game, geometry, size, content):
244     map_geometry_class = globals()['MapGeometry' + geometry]
245     game.map_geometry = map_geometry_class(size)
246     game.map_content = content
247     if type(game.map_geometry) == MapGeometrySquare:
248         game.tui.movement_keys = {
249             game.tui.keys['square_move_up']: 'UP',
250             game.tui.keys['square_move_left']: 'LEFT',
251             game.tui.keys['square_move_down']: 'DOWN',
252             game.tui.keys['square_move_right']: 'RIGHT',
253         }
254     elif type(game.map_geometry) == MapGeometryHex:
255         game.tui.movement_keys = {
256             game.tui.keys['hex_move_upleft']: 'UPLEFT',
257             game.tui.keys['hex_move_upright']: 'UPRIGHT',
258             game.tui.keys['hex_move_right']: 'RIGHT',
259             game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
260             game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
261             game.tui.keys['hex_move_left']: 'LEFT',
262         }
263 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
264
265 def cmd_FOV(game, content):
266     game.fov = content
267 cmd_FOV.argtypes = 'string'
268
269 def cmd_MAP_CONTROL(game, content):
270     game.map_control_content = content
271 cmd_MAP_CONTROL.argtypes = 'string'
272
273 def cmd_GAME_STATE_COMPLETE(game):
274     game.turn_complete = True
275     game.tui.do_refresh = True
276     game.tui.info_cached = None
277     game.player = game.get_thing(game.player_id)
278     if game.tui.mode.name == 'post_login_wait':
279         game.tui.switch_mode('play')
280 cmd_GAME_STATE_COMPLETE.argtypes = ''
281
282 def cmd_PORTAL(game, position, msg):
283     game.portals[position] = msg
284 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
285
286 def cmd_PLAY_ERROR(game, msg):
287     game.tui.log_msg('? ' + msg)
288     game.tui.flash = True
289     game.tui.do_refresh = True
290 cmd_PLAY_ERROR.argtypes = 'string'
291
292 def cmd_GAME_ERROR(game, msg):
293     game.tui.log_msg('? game error: ' + msg)
294     game.tui.do_refresh = True
295 cmd_GAME_ERROR.argtypes = 'string'
296
297 def cmd_ARGUMENT_ERROR(game, msg):
298     game.tui.log_msg('? syntax error: ' + msg)
299     game.tui.do_refresh = True
300 cmd_ARGUMENT_ERROR.argtypes = 'string'
301
302 def cmd_ANNOTATION(game, position, msg):
303     game.annotations[position] = msg
304     if game.tui.mode.shows_info:
305         game.tui.do_refresh = True
306 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
307
308 def cmd_TASKS(game, tasks_comma_separated):
309     game.tasks = tasks_comma_separated.split(',')
310     game.tui.mode_write.legal = 'WRITE' in game.tasks
311     game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
312     game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
313     game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
314 cmd_TASKS.argtypes = 'string'
315
316 def cmd_THING_TYPE(game, thing_type, symbol_hint):
317     game.thing_types[thing_type] = symbol_hint
318 cmd_THING_TYPE.argtypes = 'string char'
319
320 def cmd_THING_INSTALLED(game, thing_id):
321     game.get_thing(thing_id).installed = True
322 cmd_THING_INSTALLED.argtypes = 'int:pos'
323
324 def cmd_THING_CARRYING(game, thing_id, carried_id):
325     game.get_thing(thing_id).carrying = game.get_thing(carried_id)
326 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
327
328 def cmd_TERRAIN(game, terrain_char, terrain_desc):
329     game.terrains[terrain_char] = terrain_desc
330 cmd_TERRAIN.argtypes = 'char string'
331
332 def cmd_PONG(game):
333     pass
334 cmd_PONG.argtypes = ''
335
336 def cmd_DEFAULT_COLORS(game):
337     game.tui.set_default_colors()
338 cmd_DEFAULT_COLORS.argtypes = ''
339
340 def cmd_RANDOM_COLORS(game):
341     game.tui.set_random_colors()
342 cmd_RANDOM_COLORS.argtypes = ''
343
344 class Game(GameBase):
345     turn_complete = False
346     tasks = {}
347     thing_types = {}
348
349     def __init__(self, *args, **kwargs):
350         super().__init__(*args, **kwargs)
351         self.register_command(cmd_LOGIN_OK)
352         self.register_command(cmd_ADMIN_OK)
353         self.register_command(cmd_PONG)
354         self.register_command(cmd_CHAT)
355         self.register_command(cmd_CHATFACE)
356         self.register_command(cmd_REPLY)
357         self.register_command(cmd_PLAYER_ID)
358         self.register_command(cmd_TURN)
359         self.register_command(cmd_THING)
360         self.register_command(cmd_THING_TYPE)
361         self.register_command(cmd_THING_NAME)
362         self.register_command(cmd_THING_CHAR)
363         self.register_command(cmd_THING_FACE)
364         self.register_command(cmd_THING_HAT)
365         self.register_command(cmd_THING_CARRYING)
366         self.register_command(cmd_THING_INSTALLED)
367         self.register_command(cmd_TERRAIN)
368         self.register_command(cmd_MAP)
369         self.register_command(cmd_MAP_CONTROL)
370         self.register_command(cmd_PORTAL)
371         self.register_command(cmd_ANNOTATION)
372         self.register_command(cmd_GAME_STATE_COMPLETE)
373         self.register_command(cmd_ARGUMENT_ERROR)
374         self.register_command(cmd_GAME_ERROR)
375         self.register_command(cmd_PLAY_ERROR)
376         self.register_command(cmd_TASKS)
377         self.register_command(cmd_FOV)
378         self.register_command(cmd_DEFAULT_COLORS)
379         self.register_command(cmd_RANDOM_COLORS)
380         self.map_content = ''
381         self.player_id = -1
382         self.annotations = {}
383         self.portals = {}
384         self.terrains = {}
385         self.player = None
386
387     def get_string_options(self, string_option_type):
388         if string_option_type == 'map_geometry':
389             return ['Hex', 'Square']
390         elif string_option_type == 'thing_type':
391             return self.thing_types.keys()
392         return None
393
394     def get_command(self, command_name):
395         from functools import partial
396         f = partial(self.commands[command_name], self)
397         f.argtypes = self.commands[command_name].argtypes
398         return f
399
400 class Mode:
401
402     def __init__(self, name, has_input_prompt=False, shows_info=False,
403                  is_intro=False, is_single_char_entry=False):
404         self.name = name
405         self.short_desc = mode_helps[name]['short']
406         self.available_modes = []
407         self.available_actions = []
408         self.has_input_prompt = has_input_prompt
409         self.shows_info = shows_info
410         self.is_intro = is_intro
411         self.help_intro = mode_helps[name]['long']
412         self.intro_msg = mode_helps[name]['intro']
413         self.is_single_char_entry = is_single_char_entry
414         self.legal = True
415
416     def iter_available_modes(self, tui):
417         for mode_name in self.available_modes:
418             mode = getattr(tui, 'mode_' + mode_name)
419             if not mode.legal:
420                 continue
421             key = tui.keys['switch_to_' + mode.name]
422             yield mode, key
423
424     def list_available_modes(self, tui):
425         msg = ''
426         if len(self.available_modes) > 0:
427             msg = 'Other modes available from here:\n'
428             for mode, key in self.iter_available_modes(tui):
429                 msg += '[%s] – %s\n' % (key, mode.short_desc)
430         return msg
431
432     def mode_switch_on_key(self, tui, key_pressed):
433         for mode, key in self.iter_available_modes(tui):
434             if key_pressed == key:
435                 tui.switch_mode(mode.name)
436                 return True
437         return False
438
439 class TUI:
440     mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
441     mode_admin = Mode('admin')
442     mode_play = Mode('play')
443     mode_study = Mode('study', shows_info=True)
444     mode_write = Mode('write', is_single_char_entry=True)
445     mode_edit = Mode('edit')
446     mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
447     mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
448     mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
449     mode_control_tile_draw = Mode('control_tile_draw')
450     mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
451     mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
452     mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
453     mode_chat = Mode('chat', has_input_prompt=True)
454     mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
455     mode_login = Mode('login', has_input_prompt=True, is_intro=True)
456     mode_post_login_wait = Mode('post_login_wait', is_intro=True)
457     mode_password = Mode('password', has_input_prompt=True)
458     mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
459     mode_command_thing = Mode('command_thing', has_input_prompt=True)
460     mode_take_thing = Mode('take_thing', has_input_prompt=True)
461     mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
462     mode_enter_face = Mode('enter_face', has_input_prompt=True)
463     is_admin = False
464     tile_draw = False
465
466     def __init__(self, host):
467         import os
468         import json
469         self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
470                                           "command_thing", "take_thing",
471                                           "drop_thing"]
472         self.mode_play.available_actions = ["move", "teleport", "door", "consume",
473                                             "install", "wear", "spin"]
474         self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
475         self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
476         self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
477                                            "control_tile_type", "chat",
478                                            "study", "play", "edit"]
479         self.mode_admin.available_actions = ["move"]
480         self.mode_control_tile_draw.available_modes = ["admin_enter"]
481         self.mode_control_tile_draw.available_actions = ["move_explorer",
482                                                          "toggle_tile_draw"]
483         self.mode_edit.available_modes = ["write", "annotate", "portal",
484                                           "name_thing", "enter_face", "password",
485                                           "chat", "study", "play", "admin_enter"]
486         self.mode_edit.available_actions = ["move", "flatten", "install",
487                                             "toggle_map_mode"]
488         self.mode = None
489         self.host = host
490         self.game = Game()
491         self.game.tui = self
492         self.parser = Parser(self.game)
493         self.log = []
494         self.do_refresh = True
495         self.queue = queue.Queue()
496         self.login_name = None
497         self.map_mode = 'terrain + things'
498         self.password = 'foo'
499         self.switch_mode('waiting_for_server')
500         self.keys = {
501             'switch_to_chat': 't',
502             'switch_to_play': 'p',
503             'switch_to_password': 'P',
504             'switch_to_annotate': 'M',
505             'switch_to_portal': 'T',
506             'switch_to_study': '?',
507             'switch_to_edit': 'E',
508             'switch_to_write': 'm',
509             'switch_to_name_thing': 'N',
510             'switch_to_command_thing': 'O',
511             'switch_to_admin_enter': 'A',
512             'switch_to_control_pw_type': 'C',
513             'switch_to_control_tile_type': 'Q',
514             'switch_to_admin_thing_protect': 'T',
515             'flatten': 'F',
516             'switch_to_enter_face': 'f',
517             'switch_to_take_thing': 'z',
518             'switch_to_drop_thing': 'u',
519             'teleport': 'p',
520             'consume': 'C',
521             'door': 'D',
522             'install': 'I',
523             'wear': 'W',
524             'spin': 'S',
525             'help': 'h',
526             'toggle_map_mode': 'L',
527             'toggle_tile_draw': 'm',
528             'hex_move_upleft': 'w',
529             'hex_move_upright': 'e',
530             'hex_move_right': 'd',
531             'hex_move_downright': 'x',
532             'hex_move_downleft': 'y',
533             'hex_move_left': 'a',
534             'square_move_up': 'w',
535             'square_move_left': 'a',
536             'square_move_down': 's',
537             'square_move_right': 'd',
538         }
539         if os.path.isfile('config.json'):
540             with open('config.json', 'r') as f:
541                 keys_conf = json.loads(f.read())
542             for k in keys_conf:
543                 self.keys[k] = keys_conf[k]
544         self.show_help = False
545         self.disconnected = True
546         self.force_instant_connect = True
547         self.input_lines = []
548         self.fov = ''
549         self.flash = False
550         self.map_lines = []
551         self.draw_face = False
552         self.offset = YX(0,0)
553         curses.wrapper(self.loop)
554
555     def connect(self):
556
557         def handle_recv(msg):
558             if msg == 'BYE':
559                 self.socket.close()
560             else:
561                 self.queue.put(msg)
562
563         self.log_msg('@ attempting connect')
564         socket_client_class = PlomSocketClient
565         if self.host.startswith('ws://') or self.host.startswith('wss://'):
566             socket_client_class = WebSocketClient
567         try:
568             self.socket = socket_client_class(handle_recv, self.host)
569             self.socket_thread = threading.Thread(target=self.socket.run)
570             self.socket_thread.start()
571             self.disconnected = False
572             self.game.thing_types = {}
573             self.game.terrains = {}
574             time.sleep(0.1)  # give potential SSL negotation some time …
575             self.socket.send('TASKS')
576             self.socket.send('TERRAINS')
577             self.socket.send('THING_TYPES')
578             self.switch_mode('login')
579         except ConnectionRefusedError:
580             self.log_msg('@ server connect failure')
581             self.disconnected = True
582             self.switch_mode('waiting_for_server')
583         self.do_refresh = True
584
585     def reconnect(self):
586         self.log_msg('@ attempting reconnect')
587         self.send('QUIT')
588         # necessitated by some strange SSL race conditions with ws4py
589         time.sleep(0.1)  # FIXME find out why exactly necessary
590         self.switch_mode('waiting_for_server')
591         self.connect()
592
593     def send(self, msg):
594         try:
595             if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
596                 raise BrokenSocketConnection
597             self.socket.send(msg)
598         except (BrokenPipeError, BrokenSocketConnection):
599             self.log_msg('@ server disconnected :(')
600             self.disconnected = True
601             self.force_instant_connect = True
602             self.do_refresh = True
603
604     def log_msg(self, msg):
605         self.log += [msg]
606         if len(self.log) > 100:
607             self.log = self.log[-100:]
608
609     def restore_input_values(self):
610         if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
611             self.input_ = self.game.annotations[self.explorer]
612         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
613             self.input_ = self.game.portals[self.explorer]
614         elif self.mode.name == 'password':
615             self.input_ = self.password
616         elif self.mode.name == 'name_thing':
617             if hasattr(self.thing_selected, 'name'):
618                 self.input_ = self.thing_selected.name
619         elif self.mode.name == 'admin_thing_protect':
620             if hasattr(self.thing_selected, 'protection'):
621                 self.input_ = self.thing_selected.protection
622
623     def send_tile_control_command(self):
624         self.send('SET_TILE_CONTROL %s %s' %
625                   (self.explorer, quote(self.tile_control_char)))
626
627     def toggle_map_mode(self):
628         if self.map_mode == 'terrain only':
629             self.map_mode = 'terrain + annotations'
630         elif self.map_mode == 'terrain + annotations':
631             self.map_mode = 'terrain + things'
632         elif self.map_mode == 'terrain + things':
633             self.map_mode = 'protections'
634         elif self.map_mode == 'protections':
635             self.map_mode = 'terrain only'
636
637     def switch_mode(self, mode_name):
638
639         def fail(msg, return_mode='play'):
640             self.log_msg('? ' + msg)
641             self.flash = True
642             self.switch_mode(return_mode)
643
644         if self.mode and self.mode.name == 'control_tile_draw':
645             self.log_msg('@ finished tile protection drawing.')
646         self.tile_draw = False
647         if mode_name == 'command_thing' and\
648            (not self.game.player.carrying or
649             not self.game.player.carrying.commandable):
650             return fail('not carrying anything commandable')
651         if mode_name == 'take_thing' and self.game.player.carrying:
652             return fail('already carrying something')
653         if mode_name == 'drop_thing' and not self.game.player.carrying:
654             return fail('not carrying anything droppable')
655         if mode_name == 'admin_enter' and self.is_admin:
656             mode_name = 'admin'
657         elif mode_name in {'name_thing', 'admin_thing_protect'}:
658             thing = None
659             for t in [t for t in self.game.things
660                       if t.position == self.game.player.position
661                       and t.id_ != self.game.player.id_]:
662                 thing = t
663                 break
664             if not thing:
665                 return fail('not standing over thing', 'edit')
666             else:
667                 self.thing_selected = thing
668         self.mode = getattr(self, 'mode_' + mode_name)
669         if self.mode.name in {'control_tile_draw', 'control_tile_type',
670                               'control_pw_type'}:
671             self.map_mode = 'protections'
672         elif self.mode.name != 'edit':
673             self.map_mode = 'terrain + things'
674         if self.mode.shows_info or self.mode.name == 'control_tile_draw':
675             self.explorer = YX(self.game.player.position.y,
676                                self.game.player.position.x)
677         if self.mode.is_single_char_entry:
678             self.show_help = True
679         if len(self.mode.intro_msg) > 0:
680             self.log_msg(self.mode.intro_msg)
681         if self.mode.name == 'login':
682             if self.login_name:
683                 self.send('LOGIN ' + quote(self.login_name))
684             else:
685                 self.log_msg('@ enter username')
686         elif self.mode.name == 'take_thing':
687             self.log_msg('Portable things in reach for pick-up:')
688             select_range = [self.game.player.position,
689                             self.game.player.position + YX(0,-1),
690                             self.game.player.position + YX(0, 1),
691                             self.game.player.position + YX(-1, 0),
692                             self.game.player.position + YX(1, 0)]
693             if type(self.game.map_geometry) == MapGeometryHex:
694                 if self.game.player.position.y % 2:
695                     select_range += [self.game.player.position + YX(-1, 1),
696                                      self.game.player.position + YX(1, 1)]
697                 else:
698                     select_range += [self.game.player.position + YX(-1, -1),
699                                      self.game.player.position + YX(1, -1)]
700             self.selectables = [t.id_ for t in self.game.things
701                                 if t.portable and t.position in select_range]
702             if len(self.selectables) == 0:
703                 return fail('nothing to pick-up')
704             else:
705                 for i in range(len(self.selectables)):
706                     t = self.game.get_thing(self.selectables[i])
707                     self.log_msg(str(i) + ': ' + self.get_thing_info(t))
708         elif self.mode.name == 'drop_thing':
709             self.log_msg('Direction to drop thing to:')
710             self.selectables =\
711                 ['HERE'] + list(self.game.tui.movement_keys.values())
712             for i in range(len(self.selectables)):
713                 self.log_msg(str(i) + ': ' + self.selectables[i])
714         elif self.mode.name == 'command_thing':
715             self.send('TASK:COMMAND ' + quote('HELP'))
716         elif self.mode.name == 'control_pw_pw':
717             self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
718         elif self.mode.name == 'control_tile_draw':
719             self.log_msg('@ can draw 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']))
720         self.input_ = ""
721         self.restore_input_values()
722
723     def set_default_colors(self):
724         curses.init_color(1, 1000, 1000, 1000)
725         curses.init_color(2, 0, 0, 0)
726         self.do_refresh = True
727
728     def set_random_colors(self):
729
730         def rand(offset):
731             import random
732             return int(offset + random.random()*375)
733
734         curses.init_color(1, rand(625), rand(625), rand(625))
735         curses.init_color(2, rand(0), rand(0), rand(0))
736         self.do_refresh = True
737
738     def get_info(self):
739         if self.info_cached:
740             return self.info_cached
741         pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
742         info_to_cache = ''
743         if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
744             info_to_cache += 'outside field of view'
745         else:
746             for t in self.game.things:
747                 if t.position == self.explorer:
748                     info_to_cache += 'THING: %s' % self.get_thing_info(t)
749                     protection = t.protection
750                     if protection == '.':
751                         protection = 'none'
752                     info_to_cache += ' / protection: %s\n' % protection
753                     if hasattr(t, 'hat'):
754                         info_to_cache += t.hat[0:6] + '\n'
755                         info_to_cache += t.hat[6:12] + '\n'
756                         info_to_cache += t.hat[12:18] + '\n'
757                     if hasattr(t, 'face'):
758                         info_to_cache += t.face[0:6] + '\n'
759                         info_to_cache += t.face[6:12] + '\n'
760                         info_to_cache += t.face[12:18] + '\n'
761             terrain_char = self.game.map_content[pos_i]
762             terrain_desc = '?'
763             if terrain_char in self.game.terrains:
764                 terrain_desc = self.game.terrains[terrain_char]
765             info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
766                                                        terrain_desc)
767             protection = self.game.map_control_content[pos_i]
768             if protection == '.':
769                 protection = 'unprotected'
770             info_to_cache += 'PROTECTION: %s\n' % protection
771             if self.explorer in self.game.portals:
772                 info_to_cache += 'PORTAL: ' +\
773                     self.game.portals[self.explorer] + '\n'
774             else:
775                 info_to_cache += 'PORTAL: (none)\n'
776             if self.explorer in self.game.annotations:
777                 info_to_cache += 'ANNOTATION: ' +\
778                     self.game.annotations[self.explorer]
779         self.info_cached = info_to_cache
780         return self.info_cached
781
782     def get_thing_info(self, t):
783         info = '%s / %s' %\
784             (t.type_, self.game.thing_types[t.type_])
785         if hasattr(t, 'thing_char'):
786             info += t.thing_char
787         if hasattr(t, 'name'):
788             info += ' (%s)' % t.name
789         if hasattr(t, 'installed'):
790             info += ' / installed'
791         return info
792
793     def loop(self, stdscr):
794         import datetime
795
796         def safe_addstr(y, x, line):
797             if y < self.size.y - 1 or x + len(line) < self.size.x:
798                 stdscr.addstr(y, x, line, curses.color_pair(1))
799             else:  # workaround to <https://stackoverflow.com/q/7063128>
800                 cut_i = self.size.x - x - 1
801                 cut = line[:cut_i]
802                 last_char = line[cut_i]
803                 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
804                 stdscr.insstr(y, self.size.x - 2, ' ')
805                 stdscr.addstr(y, x, cut, curses.color_pair(1))
806
807         def handle_input(msg):
808             command, args = self.parser.parse(msg)
809             command(*args)
810
811         def task_action_on(action):
812             return action_tasks[action] in self.game.tasks
813
814         def msg_into_lines_of_width(msg, width):
815             chunk = ''
816             lines = []
817             x = 0
818             for i in range(len(msg)):
819                 if x >= width or msg[i] == "\n":
820                     lines += [chunk]
821                     chunk = ''
822                     x = 0
823                     if msg[i] == "\n":
824                         x -= 1
825                 if msg[i] != "\n":
826                     chunk += msg[i]
827                 x += 1
828             lines += [chunk]
829             return lines
830
831         def reset_screen_size():
832             self.size = YX(*stdscr.getmaxyx())
833             self.size = self.size - YX(self.size.y % 4, 0)
834             self.size = self.size - YX(0, self.size.x % 4)
835             self.window_width = int(self.size.x / 2)
836
837         def recalc_input_lines():
838             if not self.mode.has_input_prompt:
839                 self.input_lines = []
840             else:
841                 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
842                                                            self.window_width)
843
844         def move_explorer(direction):
845             target = self.game.map_geometry.move_yx(self.explorer, direction)
846             if target:
847                 self.info_cached = None
848                 self.explorer = target
849                 if self.tile_draw:
850                     self.send_tile_control_command()
851             else:
852                 self.flash = True
853
854         def draw_history():
855             lines = []
856             for line in self.log:
857                 lines += msg_into_lines_of_width(line, self.window_width)
858             lines.reverse()
859             height_header = 2
860             max_y = self.size.y - len(self.input_lines)
861             for i in range(len(lines)):
862                 if (i >= max_y - height_header):
863                     break
864                 safe_addstr(max_y - i - 1, self.window_width, lines[i])
865
866         def draw_info():
867             info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
868             lines = msg_into_lines_of_width(info, self.window_width)
869             height_header = 2
870             for i in range(len(lines)):
871                 y = height_header + i
872                 if y >= self.size.y - len(self.input_lines):
873                     break
874                 safe_addstr(y, self.window_width, lines[i])
875
876         def draw_input():
877             y = self.size.y - len(self.input_lines)
878             for i in range(len(self.input_lines)):
879                 safe_addstr(y, self.window_width, self.input_lines[i])
880                 y += 1
881
882         def draw_turn():
883             if not self.game.turn_complete:
884                 return
885             safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
886
887         def draw_mode():
888             help = "hit [%s] for help" % self.keys['help']
889             if self.mode.has_input_prompt:
890                 help = "enter /help for help"
891             safe_addstr(1, self.window_width,
892                         'MODE: %s – %s' % (self.mode.short_desc, help))
893
894         def draw_map():
895             if not self.game.turn_complete and len(self.map_lines) == 0:
896                 return
897             if self.game.turn_complete:
898                 map_lines_split = []
899                 for y in range(self.game.map_geometry.size.y):
900                     start = self.game.map_geometry.size.x * y
901                     end = start + self.game.map_geometry.size.x
902                     if self.map_mode == 'protections':
903                         map_lines_split += [[c + ' ' for c
904                                              in self.game.map_control_content[start:end]]]
905                     else:
906                         map_lines_split += [[c + ' ' for c
907                                              in self.game.map_content[start:end]]]
908                 if self.map_mode == 'terrain + annotations':
909                     for p in self.game.annotations:
910                         map_lines_split[p.y][p.x] = 'A '
911                 elif self.map_mode == 'terrain + things':
912                     for p in self.game.portals.keys():
913                         original = map_lines_split[p.y][p.x]
914                         map_lines_split[p.y][p.x] = original[0] + 'P'
915                     used_positions = []
916
917                     def draw_thing(t, used_positions):
918                         symbol = self.game.thing_types[t.type_]
919                         meta_char = ' '
920                         if hasattr(t, 'thing_char'):
921                             meta_char = t.thing_char
922                         if t.position in used_positions:
923                             meta_char = '+'
924                         if hasattr(t, 'carrying') and t.carrying:
925                             meta_char = '$'
926                         map_lines_split[t.position.y][t.position.x] = symbol + meta_char
927                         used_positions += [t.position]
928
929                     for t in [t for t in self.game.things if t.type_ != 'Player']:
930                         draw_thing(t, used_positions)
931                     for t in [t for t in self.game.things if t.type_ == 'Player']:
932                         draw_thing(t, used_positions)
933                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
934                     map_lines_split[self.explorer.y][self.explorer.x] = '??'
935                 elif self.map_mode != 'terrain + things':
936                     map_lines_split[self.game.player.position.y]\
937                         [self.game.player.position.x] = '??'
938                 self.map_lines = []
939                 if type(self.game.map_geometry) == MapGeometryHex:
940                     indent = 0
941                     for line in map_lines_split:
942                         self.map_lines += [indent * ' ' + ''.join(line)]
943                         indent = 0 if indent else 1
944                 else:
945                     for line in map_lines_split:
946                         self.map_lines += [''.join(line)]
947                 window_center = YX(int(self.size.y / 2),
948                                    int(self.window_width / 2))
949                 center = self.game.player.position
950                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
951                     center = self.explorer
952                 center = YX(center.y, center.x * 2)
953                 self.offset = center - window_center
954                 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
955                     self.offset += YX(0, 1)
956             term_y = max(0, -self.offset.y)
957             term_x = max(0, -self.offset.x)
958             map_y = max(0, self.offset.y)
959             map_x = max(0, self.offset.x)
960             while term_y < self.size.y and map_y < len(self.map_lines):
961                 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
962                 safe_addstr(term_y, term_x, to_draw)
963                 term_y += 1
964                 map_y += 1
965
966         def draw_face_popup():
967             t = self.game.get_thing(self.draw_face)
968             if not t:
969                 self.draw_face = False
970                 return
971
972             def draw_body_part(body_part, end_y):
973                 start_x = self.window_width - 10
974                 safe_addstr(end_y - 4, start_x, '+--------+')
975                 safe_addstr(end_y - 3, start_x, '|        |')
976                 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
977                 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
978                 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
979
980             if hasattr(t, 'face'):
981                 draw_body_part(t.face, self.size.y - 1)
982             if hasattr(t, 'hat'):
983                 draw_body_part(t.hat, self.size.y - 4)
984
985         def draw_help():
986             content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
987                                              self.mode.help_intro)
988             if len(self.mode.available_actions) > 0:
989                 content += "Available actions:\n"
990                 for action in self.mode.available_actions:
991                     if action in action_tasks:
992                         if action_tasks[action] not in self.game.tasks:
993                             continue
994                     if action == 'move_explorer':
995                         action = 'move'
996                     if action == 'move':
997                         key = ','.join(self.movement_keys)
998                     else:
999                         key = self.keys[action]
1000                     content += '[%s] – %s\n' % (key, action_descriptions[action])
1001                 content += '\n'
1002             content += self.mode.list_available_modes(self)
1003             for i in range(self.size.y):
1004                 safe_addstr(i,
1005                             self.window_width * (not self.mode.has_input_prompt),
1006                             ' ' * self.window_width)
1007             lines = []
1008             for line in content.split('\n'):
1009                 lines += msg_into_lines_of_width(line, self.window_width)
1010             for i in range(len(lines)):
1011                 if i >= self.size.y:
1012                     break
1013                 safe_addstr(i,
1014                             self.window_width * (not self.mode.has_input_prompt),
1015                             lines[i])
1016
1017         def draw_screen():
1018             stdscr.clear()
1019             stdscr.bkgd(' ', curses.color_pair(1))
1020             recalc_input_lines()
1021             if self.mode.has_input_prompt:
1022                 draw_input()
1023             if self.mode.shows_info:
1024                 draw_info()
1025             else:
1026                 draw_history()
1027             draw_mode()
1028             if not self.mode.is_intro:
1029                 draw_turn()
1030                 draw_map()
1031             if self.show_help:
1032                 draw_help()
1033             if self.draw_face and self.mode.name in {'chat', 'play'}:
1034                 draw_face_popup()
1035
1036         def pick_selectable(task_name):
1037             try:
1038                 i = int(self.input_)
1039                 if i < 0 or i >= len(self.selectables):
1040                     self.log_msg('? invalid index, aborted')
1041                 else:
1042                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1043             except ValueError:
1044                 self.log_msg('? invalid index, aborted')
1045             self.input_ = ''
1046             self.switch_mode('play')
1047
1048         action_descriptions = {
1049             'move': 'move',
1050             'flatten': 'flatten surroundings',
1051             'teleport': 'teleport',
1052             'take_thing': 'pick up thing',
1053             'drop_thing': 'drop thing',
1054             'toggle_map_mode': 'toggle map view',
1055             'toggle_tile_draw': 'toggle protection character drawing',
1056             'install': '(un-)install',
1057             'wear': '(un-)wear',
1058             'door': 'open/close',
1059             'consume': 'consume',
1060             'spin': 'spin',
1061         }
1062
1063         action_tasks = {
1064             'flatten': 'FLATTEN_SURROUNDINGS',
1065             'take_thing': 'PICK_UP',
1066             'drop_thing': 'DROP',
1067             'door': 'DOOR',
1068             'install': 'INSTALL',
1069             'wear': 'WEAR',
1070             'move': 'MOVE',
1071             'command': 'COMMAND',
1072             'consume': 'INTOXICATE',
1073             'spin': 'SPIN',
1074         }
1075
1076         curses.curs_set(False)  # hide cursor
1077         curses.start_color()
1078         self.set_default_colors()
1079         curses.init_pair(1, 1, 2)
1080         stdscr.timeout(10)
1081         reset_screen_size()
1082         self.explorer = YX(0, 0)
1083         self.input_ = ''
1084         input_prompt = '> '
1085         interval = datetime.timedelta(seconds=5)
1086         last_ping = datetime.datetime.now() - interval
1087         while True:
1088             if self.disconnected and self.force_instant_connect:
1089                 self.force_instant_connect = False
1090                 self.connect()
1091             now = datetime.datetime.now()
1092             if now - last_ping > interval:
1093                 if self.disconnected:
1094                     self.connect()
1095                 else:
1096                     self.send('PING')
1097                 last_ping = now
1098             if self.flash:
1099                 curses.flash()
1100                 self.flash = False
1101             if self.do_refresh:
1102                 draw_screen()
1103                 self.do_refresh = False
1104             while True:
1105                 try:
1106                     msg = self.queue.get(block=False)
1107                     handle_input(msg)
1108                 except queue.Empty:
1109                     break
1110             try:
1111                 key = stdscr.getkey()
1112                 self.do_refresh = True
1113             except curses.error:
1114                 continue
1115             keycode = None
1116             if len(key) == 1:
1117                 keycode = ord(key)
1118             self.show_help = False
1119             self.draw_face = False
1120             if key == 'KEY_RESIZE':
1121                 reset_screen_size()
1122             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1123                 self.input_ = self.input_[:-1]
1124             elif (((not self.mode.is_intro) and keycode == 27)  # Escape
1125                   or (self.mode.has_input_prompt and key == '\n'
1126                       and self.input_ == ''\
1127                       and self.mode.name in {'chat', 'command_thing',
1128                                              'take_thing', 'drop_thing',
1129                                              'admin_enter'})):
1130                 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1131                     self.log_msg('@ aborted')
1132                 self.switch_mode('play')
1133             elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1134                 self.show_help = True
1135                 self.input_ = ""
1136                 self.restore_input_values()
1137             elif self.mode.has_input_prompt and key != '\n':  # Return key
1138                 self.input_ += key
1139                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1140                 if len(self.input_) > max_length:
1141                     self.input_ = self.input_[:max_length]
1142             elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1143                 self.show_help = True
1144             elif self.mode.name == 'login' and key == '\n':
1145                 self.login_name = self.input_
1146                 self.send('LOGIN ' + quote(self.input_))
1147                 self.input_ = ""
1148             elif self.mode.name == 'enter_face' and key == '\n':
1149                 if len(self.input_) != 18:
1150                     self.log_msg('? wrong input length, aborting')
1151                 else:
1152                     self.send('PLAYER_FACE %s' % quote(self.input_))
1153                 self.input_ = ""
1154                 self.switch_mode('edit')
1155             elif self.mode.name == 'take_thing' and key == '\n':
1156                 pick_selectable('PICK_UP')
1157             elif self.mode.name == 'drop_thing' and key == '\n':
1158                 pick_selectable('DROP')
1159             elif self.mode.name == 'command_thing' and key == '\n':
1160                 self.send('TASK:COMMAND ' + quote(self.input_))
1161                 self.input_ = ""
1162             elif self.mode.name == 'control_pw_pw' and key == '\n':
1163                 if self.input_ == '':
1164                     self.log_msg('@ aborted')
1165                 else:
1166                     self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1167                     self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1168                 self.switch_mode('admin')
1169             elif self.mode.name == 'password' and key == '\n':
1170                 if self.input_ == '':
1171                     self.input_ = ' '
1172                 self.password = self.input_
1173                 self.switch_mode('edit')
1174             elif self.mode.name == 'admin_enter' and key == '\n':
1175                 self.send('BECOME_ADMIN ' + quote(self.input_))
1176                 self.switch_mode('play')
1177             elif self.mode.name == 'control_pw_type' and key == '\n':
1178                 if len(self.input_) != 1:
1179                     self.log_msg('@ entered non-single-char, therefore aborted')
1180                     self.switch_mode('admin')
1181                 else:
1182                     self.tile_control_char = self.input_
1183                     self.switch_mode('control_pw_pw')
1184             elif self.mode.name == 'admin_thing_protect' and key == '\n':
1185                 if len(self.input_) != 1:
1186                     self.log_msg('@ entered non-single-char, therefore aborted')
1187                 else:
1188                     self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1189                                                           quote(self.input_)))
1190                     self.log_msg('@ sent new protection character for thing')
1191                 self.switch_mode('admin')
1192             elif self.mode.name == 'control_tile_type' and key == '\n':
1193                 if len(self.input_) != 1:
1194                     self.log_msg('@ entered non-single-char, therefore aborted')
1195                     self.switch_mode('admin')
1196                 else:
1197                     self.tile_control_char = self.input_
1198                     self.switch_mode('control_tile_draw')
1199             elif self.mode.name == 'chat' and key == '\n':
1200                 if self.input_ == '':
1201                     continue
1202                 if self.input_[0] == '/':
1203                     if self.input_.startswith('/nick'):
1204                         tokens = self.input_.split(maxsplit=1)
1205                         if len(tokens) == 2:
1206                             self.send('NICK ' + quote(tokens[1]))
1207                         else:
1208                             self.log_msg('? need login name')
1209                     else:
1210                         self.log_msg('? unknown command')
1211                 else:
1212                     self.send('ALL ' + quote(self.input_))
1213                 self.input_ = ""
1214             elif self.mode.name == 'name_thing' and key == '\n':
1215                 if self.input_ == '':
1216                     self.input_ = ' '
1217                 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1218                                                    quote(self.input_),
1219                                                    quote(self.password)))
1220                 self.switch_mode('edit')
1221             elif self.mode.name == 'annotate' and key == '\n':
1222                 if self.input_ == '':
1223                     self.input_ = ' '
1224                 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1225                                                  quote(self.password)))
1226                 self.switch_mode('edit')
1227             elif self.mode.name == 'portal' and key == '\n':
1228                 if self.input_ == '':
1229                     self.input_ = ' '
1230                 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1231                                                quote(self.password)))
1232                 self.switch_mode('edit')
1233             elif self.mode.name == 'study':
1234                 if self.mode.mode_switch_on_key(self, key):
1235                     continue
1236                 elif key == self.keys['toggle_map_mode']:
1237                     self.toggle_map_mode()
1238                 elif key in self.movement_keys:
1239                     move_explorer(self.movement_keys[key])
1240             elif self.mode.name == 'play':
1241                 if self.mode.mode_switch_on_key(self, key):
1242                     continue
1243                 elif key == self.keys['door'] and task_action_on('door'):
1244                     self.send('TASK:DOOR')
1245                 elif key == self.keys['consume'] and task_action_on('consume'):
1246                     self.send('TASK:INTOXICATE')
1247                 elif key == self.keys['wear'] and task_action_on('wear'):
1248                     self.send('TASK:WEAR')
1249                 elif key == self.keys['spin'] and task_action_on('spin'):
1250                     self.send('TASK:SPIN')
1251                 elif key == self.keys['teleport']:
1252                     if self.game.player.position in self.game.portals:
1253                         self.host = self.game.portals[self.game.player.position]
1254                         self.reconnect()
1255                     else:
1256                         self.flash = True
1257                         self.log_msg('? not standing on portal')
1258                 elif key in self.movement_keys and task_action_on('move'):
1259                     self.send('TASK:MOVE ' + self.movement_keys[key])
1260             elif self.mode.name == 'write':
1261                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1262                 self.switch_mode('edit')
1263             elif self.mode.name == 'control_tile_draw':
1264                 if self.mode.mode_switch_on_key(self, key):
1265                     continue
1266                 elif key in self.movement_keys:
1267                     move_explorer(self.movement_keys[key])
1268                 elif key == self.keys['toggle_tile_draw']:
1269                     self.tile_draw = False if self.tile_draw else True
1270             elif self.mode.name == 'admin':
1271                 if self.mode.mode_switch_on_key(self, key):
1272                     continue
1273                 elif key in self.movement_keys and task_action_on('move'):
1274                     self.send('TASK:MOVE ' + self.movement_keys[key])
1275             elif self.mode.name == 'edit':
1276                 if self.mode.mode_switch_on_key(self, key):
1277                     continue
1278                 elif key == self.keys['flatten'] and task_action_on('flatten'):
1279                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1280                 elif key == self.keys['install'] and task_action_on('install'):
1281                     self.send('TASK:INSTALL %s' % quote(self.password))
1282                 elif key == self.keys['toggle_map_mode']:
1283                     self.toggle_map_mode()
1284                 elif key in self.movement_keys and task_action_on('move'):
1285                     self.send('TASK:MOVE ' + self.movement_keys[key])
1286
1287 if len(sys.argv) != 2:
1288     raise ArgError('wrong number of arguments, need game host')
1289 host = sys.argv[1]
1290 TUI(host)