home · contact · privacy
Show player symbol in face pop-up.
[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     game.tui.do_refresh = True
206 cmd_CHATFACE.argtypes = 'int:pos'
207
208 def cmd_PLAYER_ID(game, player_id):
209     game.player_id = player_id
210 cmd_PLAYER_ID.argtypes = 'int:nonneg'
211
212 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
213     t = game.get_thing(thing_id)
214     if not t:
215         t = ThingBase(game, thing_id)
216         game.things += [t]
217     t.position = yx
218     t.type_ = thing_type
219     t.protection = protection
220     t.portable = portable
221     t.commandable = commandable
222 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
223
224 def cmd_THING_NAME(game, thing_id, name):
225     t = game.get_thing(thing_id)
226     t.name = name
227 cmd_THING_NAME.argtypes = 'int:pos string'
228
229 def cmd_THING_FACE(game, thing_id, face):
230     t = game.get_thing(thing_id)
231     t.face = face
232 cmd_THING_FACE.argtypes = 'int:pos string'
233
234 def cmd_THING_HAT(game, thing_id, hat):
235     t = game.get_thing(thing_id)
236     t.hat = hat
237 cmd_THING_HAT.argtypes = 'int:pos string'
238
239 def cmd_THING_CHAR(game, thing_id, c):
240     t = game.get_thing(thing_id)
241     t.thing_char = c
242 cmd_THING_CHAR.argtypes = 'int:pos char'
243
244 def cmd_MAP(game, geometry, size, content):
245     map_geometry_class = globals()['MapGeometry' + geometry]
246     game.map_geometry = map_geometry_class(size)
247     game.map_content = content
248     if type(game.map_geometry) == MapGeometrySquare:
249         game.tui.movement_keys = {
250             game.tui.keys['square_move_up']: 'UP',
251             game.tui.keys['square_move_left']: 'LEFT',
252             game.tui.keys['square_move_down']: 'DOWN',
253             game.tui.keys['square_move_right']: 'RIGHT',
254         }
255     elif type(game.map_geometry) == MapGeometryHex:
256         game.tui.movement_keys = {
257             game.tui.keys['hex_move_upleft']: 'UPLEFT',
258             game.tui.keys['hex_move_upright']: 'UPRIGHT',
259             game.tui.keys['hex_move_right']: 'RIGHT',
260             game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
261             game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
262             game.tui.keys['hex_move_left']: 'LEFT',
263         }
264 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
265
266 def cmd_FOV(game, content):
267     game.fov = content
268 cmd_FOV.argtypes = 'string'
269
270 def cmd_MAP_CONTROL(game, content):
271     game.map_control_content = content
272 cmd_MAP_CONTROL.argtypes = 'string'
273
274 def cmd_GAME_STATE_COMPLETE(game):
275     game.turn_complete = True
276     game.tui.do_refresh = True
277     game.tui.info_cached = None
278     game.player = game.get_thing(game.player_id)
279     if game.tui.mode.name == 'post_login_wait':
280         game.tui.switch_mode('play')
281 cmd_GAME_STATE_COMPLETE.argtypes = ''
282
283 def cmd_PORTAL(game, position, msg):
284     game.portals[position] = msg
285 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
286
287 def cmd_PLAY_ERROR(game, msg):
288     game.tui.log_msg('? ' + msg)
289     game.tui.flash = True
290     game.tui.do_refresh = True
291 cmd_PLAY_ERROR.argtypes = 'string'
292
293 def cmd_GAME_ERROR(game, msg):
294     game.tui.log_msg('? game error: ' + msg)
295     game.tui.do_refresh = True
296 cmd_GAME_ERROR.argtypes = 'string'
297
298 def cmd_ARGUMENT_ERROR(game, msg):
299     game.tui.log_msg('? syntax error: ' + msg)
300     game.tui.do_refresh = True
301 cmd_ARGUMENT_ERROR.argtypes = 'string'
302
303 def cmd_ANNOTATION(game, position, msg):
304     game.annotations[position] = msg
305     if game.tui.mode.shows_info:
306         game.tui.do_refresh = True
307 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
308
309 def cmd_TASKS(game, tasks_comma_separated):
310     game.tasks = tasks_comma_separated.split(',')
311     game.tui.mode_write.legal = 'WRITE' in game.tasks
312     game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
313     game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
314     game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
315 cmd_TASKS.argtypes = 'string'
316
317 def cmd_THING_TYPE(game, thing_type, symbol_hint):
318     game.thing_types[thing_type] = symbol_hint
319 cmd_THING_TYPE.argtypes = 'string char'
320
321 def cmd_THING_INSTALLED(game, thing_id):
322     game.get_thing(thing_id).installed = True
323 cmd_THING_INSTALLED.argtypes = 'int:pos'
324
325 def cmd_THING_CARRYING(game, thing_id, carried_id):
326     game.get_thing(thing_id).carrying = game.get_thing(carried_id)
327 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
328
329 def cmd_TERRAIN(game, terrain_char, terrain_desc):
330     game.terrains[terrain_char] = terrain_desc
331 cmd_TERRAIN.argtypes = 'char string'
332
333 def cmd_PONG(game):
334     pass
335 cmd_PONG.argtypes = ''
336
337 def cmd_DEFAULT_COLORS(game):
338     game.tui.set_default_colors()
339 cmd_DEFAULT_COLORS.argtypes = ''
340
341 def cmd_RANDOM_COLORS(game):
342     game.tui.set_random_colors()
343 cmd_RANDOM_COLORS.argtypes = ''
344
345 class Game(GameBase):
346     turn_complete = False
347     tasks = {}
348     thing_types = {}
349
350     def __init__(self, *args, **kwargs):
351         super().__init__(*args, **kwargs)
352         self.register_command(cmd_LOGIN_OK)
353         self.register_command(cmd_ADMIN_OK)
354         self.register_command(cmd_PONG)
355         self.register_command(cmd_CHAT)
356         self.register_command(cmd_CHATFACE)
357         self.register_command(cmd_REPLY)
358         self.register_command(cmd_PLAYER_ID)
359         self.register_command(cmd_TURN)
360         self.register_command(cmd_THING)
361         self.register_command(cmd_THING_TYPE)
362         self.register_command(cmd_THING_NAME)
363         self.register_command(cmd_THING_CHAR)
364         self.register_command(cmd_THING_FACE)
365         self.register_command(cmd_THING_HAT)
366         self.register_command(cmd_THING_CARRYING)
367         self.register_command(cmd_THING_INSTALLED)
368         self.register_command(cmd_TERRAIN)
369         self.register_command(cmd_MAP)
370         self.register_command(cmd_MAP_CONTROL)
371         self.register_command(cmd_PORTAL)
372         self.register_command(cmd_ANNOTATION)
373         self.register_command(cmd_GAME_STATE_COMPLETE)
374         self.register_command(cmd_ARGUMENT_ERROR)
375         self.register_command(cmd_GAME_ERROR)
376         self.register_command(cmd_PLAY_ERROR)
377         self.register_command(cmd_TASKS)
378         self.register_command(cmd_FOV)
379         self.register_command(cmd_DEFAULT_COLORS)
380         self.register_command(cmd_RANDOM_COLORS)
381         self.map_content = ''
382         self.player_id = -1
383         self.annotations = {}
384         self.portals = {}
385         self.terrains = {}
386         self.player = None
387
388     def get_string_options(self, string_option_type):
389         if string_option_type == 'map_geometry':
390             return ['Hex', 'Square']
391         elif string_option_type == 'thing_type':
392             return self.thing_types.keys()
393         return None
394
395     def get_command(self, command_name):
396         from functools import partial
397         f = partial(self.commands[command_name], self)
398         f.argtypes = self.commands[command_name].argtypes
399         return f
400
401 class Mode:
402
403     def __init__(self, name, has_input_prompt=False, shows_info=False,
404                  is_intro=False, is_single_char_entry=False):
405         self.name = name
406         self.short_desc = mode_helps[name]['short']
407         self.available_modes = []
408         self.available_actions = []
409         self.has_input_prompt = has_input_prompt
410         self.shows_info = shows_info
411         self.is_intro = is_intro
412         self.help_intro = mode_helps[name]['long']
413         self.intro_msg = mode_helps[name]['intro']
414         self.is_single_char_entry = is_single_char_entry
415         self.legal = True
416
417     def iter_available_modes(self, tui):
418         for mode_name in self.available_modes:
419             mode = getattr(tui, 'mode_' + mode_name)
420             if not mode.legal:
421                 continue
422             key = tui.keys['switch_to_' + mode.name]
423             yield mode, key
424
425     def list_available_modes(self, tui):
426         msg = ''
427         if len(self.available_modes) > 0:
428             msg = 'Other modes available from here:\n'
429             for mode, key in self.iter_available_modes(tui):
430                 msg += '[%s] – %s\n' % (key, mode.short_desc)
431         return msg
432
433     def mode_switch_on_key(self, tui, key_pressed):
434         for mode, key in self.iter_available_modes(tui):
435             if key_pressed == key:
436                 tui.switch_mode(mode.name)
437                 return True
438         return False
439
440 class TUI:
441     mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
442     mode_admin = Mode('admin')
443     mode_play = Mode('play')
444     mode_study = Mode('study', shows_info=True)
445     mode_write = Mode('write', is_single_char_entry=True)
446     mode_edit = Mode('edit')
447     mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
448     mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
449     mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
450     mode_control_tile_draw = Mode('control_tile_draw')
451     mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
452     mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
453     mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
454     mode_chat = Mode('chat', has_input_prompt=True)
455     mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
456     mode_login = Mode('login', has_input_prompt=True, is_intro=True)
457     mode_post_login_wait = Mode('post_login_wait', is_intro=True)
458     mode_password = Mode('password', has_input_prompt=True)
459     mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
460     mode_command_thing = Mode('command_thing', has_input_prompt=True)
461     mode_take_thing = Mode('take_thing', has_input_prompt=True)
462     mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
463     mode_enter_face = Mode('enter_face', has_input_prompt=True)
464     is_admin = False
465     tile_draw = False
466
467     def __init__(self, host):
468         import os
469         import json
470         self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
471                                           "command_thing", "take_thing",
472                                           "drop_thing"]
473         self.mode_play.available_actions = ["move", "teleport", "door", "consume",
474                                             "install", "wear", "spin"]
475         self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
476         self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
477         self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
478                                            "control_tile_type", "chat",
479                                            "study", "play", "edit"]
480         self.mode_admin.available_actions = ["move"]
481         self.mode_control_tile_draw.available_modes = ["admin_enter"]
482         self.mode_control_tile_draw.available_actions = ["move_explorer",
483                                                          "toggle_tile_draw"]
484         self.mode_edit.available_modes = ["write", "annotate", "portal",
485                                           "name_thing", "enter_face", "password",
486                                           "chat", "study", "play", "admin_enter"]
487         self.mode_edit.available_actions = ["move", "flatten", "install",
488                                             "toggle_map_mode"]
489         self.mode = None
490         self.host = host
491         self.game = Game()
492         self.game.tui = self
493         self.parser = Parser(self.game)
494         self.log = []
495         self.do_refresh = True
496         self.queue = queue.Queue()
497         self.login_name = None
498         self.map_mode = 'terrain + things'
499         self.password = 'foo'
500         self.switch_mode('waiting_for_server')
501         self.keys = {
502             'switch_to_chat': 't',
503             'switch_to_play': 'p',
504             'switch_to_password': 'P',
505             'switch_to_annotate': 'M',
506             'switch_to_portal': 'T',
507             'switch_to_study': '?',
508             'switch_to_edit': 'E',
509             'switch_to_write': 'm',
510             'switch_to_name_thing': 'N',
511             'switch_to_command_thing': 'O',
512             'switch_to_admin_enter': 'A',
513             'switch_to_control_pw_type': 'C',
514             'switch_to_control_tile_type': 'Q',
515             'switch_to_admin_thing_protect': 'T',
516             'flatten': 'F',
517             'switch_to_enter_face': 'f',
518             'switch_to_take_thing': 'z',
519             'switch_to_drop_thing': 'u',
520             'teleport': 'p',
521             'consume': 'C',
522             'door': 'D',
523             'install': 'I',
524             'wear': 'W',
525             'spin': 'S',
526             'help': 'h',
527             'toggle_map_mode': 'L',
528             'toggle_tile_draw': 'm',
529             'hex_move_upleft': 'w',
530             'hex_move_upright': 'e',
531             'hex_move_right': 'd',
532             'hex_move_downright': 'x',
533             'hex_move_downleft': 'y',
534             'hex_move_left': 'a',
535             'square_move_up': 'w',
536             'square_move_left': 'a',
537             'square_move_down': 's',
538             'square_move_right': 'd',
539         }
540         if os.path.isfile('config.json'):
541             with open('config.json', 'r') as f:
542                 keys_conf = json.loads(f.read())
543             for k in keys_conf:
544                 self.keys[k] = keys_conf[k]
545         self.show_help = False
546         self.disconnected = True
547         self.force_instant_connect = True
548         self.input_lines = []
549         self.fov = ''
550         self.flash = False
551         self.map_lines = []
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.draw_face = False
647         self.tile_draw = False
648         if mode_name == 'command_thing' and\
649            (not self.game.player.carrying or
650             not self.game.player.carrying.commandable):
651             return fail('not carrying anything commandable')
652         if mode_name == 'take_thing' and self.game.player.carrying:
653             return fail('already carrying something')
654         if mode_name == 'drop_thing' and not self.game.player.carrying:
655             return fail('not carrying anything droppable')
656         if mode_name == 'admin_enter' and self.is_admin:
657             mode_name = 'admin'
658         elif mode_name in {'name_thing', 'admin_thing_protect'}:
659             thing = None
660             for t in [t for t in self.game.things
661                       if t.position == self.game.player.position
662                       and t.id_ != self.game.player.id_]:
663                 thing = t
664                 break
665             if not thing:
666                 return fail('not standing over thing', 'edit')
667             else:
668                 self.thing_selected = thing
669         self.mode = getattr(self, 'mode_' + mode_name)
670         if self.mode.name in {'control_tile_draw', 'control_tile_type',
671                               'control_pw_type'}:
672             self.map_mode = 'protections'
673         elif self.mode.name != 'edit':
674             self.map_mode = 'terrain + things'
675         if self.mode.shows_info or self.mode.name == 'control_tile_draw':
676             self.explorer = YX(self.game.player.position.y,
677                                self.game.player.position.x)
678         if self.mode.is_single_char_entry:
679             self.show_help = True
680         if len(self.mode.intro_msg) > 0:
681             self.log_msg(self.mode.intro_msg)
682         if self.mode.name == 'login':
683             if self.login_name:
684                 self.send('LOGIN ' + quote(self.login_name))
685             else:
686                 self.log_msg('@ enter username')
687         elif self.mode.name == 'take_thing':
688             self.log_msg('Portable things in reach for pick-up:')
689             select_range = [self.game.player.position,
690                             self.game.player.position + YX(0,-1),
691                             self.game.player.position + YX(0, 1),
692                             self.game.player.position + YX(-1, 0),
693                             self.game.player.position + YX(1, 0)]
694             if type(self.game.map_geometry) == MapGeometryHex:
695                 if self.game.player.position.y % 2:
696                     select_range += [self.game.player.position + YX(-1, 1),
697                                      self.game.player.position + YX(1, 1)]
698                 else:
699                     select_range += [self.game.player.position + YX(-1, -1),
700                                      self.game.player.position + YX(1, -1)]
701             self.selectables = [t.id_ for t in self.game.things
702                                 if t.portable and t.position in select_range]
703             if len(self.selectables) == 0:
704                 return fail('nothing to pick-up')
705             else:
706                 for i in range(len(self.selectables)):
707                     t = self.game.get_thing(self.selectables[i])
708                     self.log_msg(str(i) + ': ' + self.get_thing_info(t))
709         elif self.mode.name == 'drop_thing':
710             self.log_msg('Direction to drop thing to:')
711             self.selectables =\
712                 ['HERE'] + list(self.game.tui.movement_keys.values())
713             for i in range(len(self.selectables)):
714                 self.log_msg(str(i) + ': ' + self.selectables[i])
715         elif self.mode.name == 'command_thing':
716             self.send('TASK:COMMAND ' + quote('HELP'))
717         elif self.mode.name == 'control_pw_pw':
718             self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
719         elif self.mode.name == 'control_tile_draw':
720             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']))
721         self.input_ = ""
722         self.restore_input_values()
723
724     def set_default_colors(self):
725         curses.init_color(1, 1000, 1000, 1000)
726         curses.init_color(2, 0, 0, 0)
727         self.do_refresh = True
728
729     def set_random_colors(self):
730
731         def rand(offset):
732             import random
733             return int(offset + random.random()*375)
734
735         curses.init_color(1, rand(625), rand(625), rand(625))
736         curses.init_color(2, rand(0), rand(0), rand(0))
737         self.do_refresh = True
738
739     def get_info(self):
740         if self.info_cached:
741             return self.info_cached
742         pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
743         info_to_cache = ''
744         if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
745             info_to_cache += 'outside field of view'
746         else:
747             for t in self.game.things:
748                 if t.position == self.explorer:
749                     info_to_cache += 'THING: %s' % self.get_thing_info(t)
750                     protection = t.protection
751                     if protection == '.':
752                         protection = 'none'
753                     info_to_cache += ' / protection: %s\n' % protection
754                     if hasattr(t, 'hat'):
755                         info_to_cache += t.hat[0:6] + '\n'
756                         info_to_cache += t.hat[6:12] + '\n'
757                         info_to_cache += t.hat[12:18] + '\n'
758                     if hasattr(t, 'face'):
759                         info_to_cache += t.face[0:6] + '\n'
760                         info_to_cache += t.face[6:12] + '\n'
761                         info_to_cache += t.face[12:18] + '\n'
762             terrain_char = self.game.map_content[pos_i]
763             terrain_desc = '?'
764             if terrain_char in self.game.terrains:
765                 terrain_desc = self.game.terrains[terrain_char]
766             info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
767                                                        terrain_desc)
768             protection = self.game.map_control_content[pos_i]
769             if protection == '.':
770                 protection = 'unprotected'
771             info_to_cache += 'PROTECTION: %s\n' % protection
772             if self.explorer in self.game.portals:
773                 info_to_cache += 'PORTAL: ' +\
774                     self.game.portals[self.explorer] + '\n'
775             else:
776                 info_to_cache += 'PORTAL: (none)\n'
777             if self.explorer in self.game.annotations:
778                 info_to_cache += 'ANNOTATION: ' +\
779                     self.game.annotations[self.explorer]
780         self.info_cached = info_to_cache
781         return self.info_cached
782
783     def get_thing_info(self, t):
784         info = '%s / %s' %\
785             (t.type_, self.game.thing_types[t.type_])
786         if hasattr(t, 'thing_char'):
787             info += t.thing_char
788         if hasattr(t, 'name'):
789             info += ' (%s)' % t.name
790         if hasattr(t, 'installed'):
791             info += ' / installed'
792         return info
793
794     def loop(self, stdscr):
795         import datetime
796
797         def safe_addstr(y, x, line):
798             if y < self.size.y - 1 or x + len(line) < self.size.x:
799                 stdscr.addstr(y, x, line, curses.color_pair(1))
800             else:  # workaround to <https://stackoverflow.com/q/7063128>
801                 cut_i = self.size.x - x - 1
802                 cut = line[:cut_i]
803                 last_char = line[cut_i]
804                 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
805                 stdscr.insstr(y, self.size.x - 2, ' ')
806                 stdscr.addstr(y, x, cut, curses.color_pair(1))
807
808         def handle_input(msg):
809             command, args = self.parser.parse(msg)
810             command(*args)
811
812         def task_action_on(action):
813             return action_tasks[action] in self.game.tasks
814
815         def msg_into_lines_of_width(msg, width):
816             chunk = ''
817             lines = []
818             x = 0
819             for i in range(len(msg)):
820                 if x >= width or msg[i] == "\n":
821                     lines += [chunk]
822                     chunk = ''
823                     x = 0
824                     if msg[i] == "\n":
825                         x -= 1
826                 if msg[i] != "\n":
827                     chunk += msg[i]
828                 x += 1
829             lines += [chunk]
830             return lines
831
832         def reset_screen_size():
833             self.size = YX(*stdscr.getmaxyx())
834             self.size = self.size - YX(self.size.y % 4, 0)
835             self.size = self.size - YX(0, self.size.x % 4)
836             self.window_width = int(self.size.x / 2)
837
838         def recalc_input_lines():
839             if not self.mode.has_input_prompt:
840                 self.input_lines = []
841             else:
842                 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
843                                                            self.window_width)
844
845         def move_explorer(direction):
846             target = self.game.map_geometry.move_yx(self.explorer, direction)
847             if target:
848                 self.info_cached = None
849                 self.explorer = target
850                 if self.tile_draw:
851                     self.send_tile_control_command()
852             else:
853                 self.flash = True
854
855         def draw_history():
856             lines = []
857             for line in self.log:
858                 lines += msg_into_lines_of_width(line, self.window_width)
859             lines.reverse()
860             height_header = 2
861             max_y = self.size.y - len(self.input_lines)
862             for i in range(len(lines)):
863                 if (i >= max_y - height_header):
864                     break
865                 safe_addstr(max_y - i - 1, self.window_width, lines[i])
866
867         def draw_info():
868             info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
869             lines = msg_into_lines_of_width(info, self.window_width)
870             height_header = 2
871             for i in range(len(lines)):
872                 y = height_header + i
873                 if y >= self.size.y - len(self.input_lines):
874                     break
875                 safe_addstr(y, self.window_width, lines[i])
876
877         def draw_input():
878             y = self.size.y - len(self.input_lines)
879             for i in range(len(self.input_lines)):
880                 safe_addstr(y, self.window_width, self.input_lines[i])
881                 y += 1
882
883         def draw_turn():
884             if not self.game.turn_complete:
885                 return
886             safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
887
888         def draw_mode():
889             help = "hit [%s] for help" % self.keys['help']
890             if self.mode.has_input_prompt:
891                 help = "enter /help for help"
892             safe_addstr(1, self.window_width,
893                         'MODE: %s – %s' % (self.mode.short_desc, help))
894
895         def draw_map():
896             if not self.game.turn_complete and len(self.map_lines) == 0:
897                 return
898             if self.game.turn_complete:
899                 map_lines_split = []
900                 for y in range(self.game.map_geometry.size.y):
901                     start = self.game.map_geometry.size.x * y
902                     end = start + self.game.map_geometry.size.x
903                     if self.map_mode == 'protections':
904                         map_lines_split += [[c + ' ' for c
905                                              in self.game.map_control_content[start:end]]]
906                     else:
907                         map_lines_split += [[c + ' ' for c
908                                              in self.game.map_content[start:end]]]
909                 if self.map_mode == 'terrain + annotations':
910                     for p in self.game.annotations:
911                         map_lines_split[p.y][p.x] = 'A '
912                 elif self.map_mode == 'terrain + things':
913                     for p in self.game.portals.keys():
914                         original = map_lines_split[p.y][p.x]
915                         map_lines_split[p.y][p.x] = original[0] + 'P'
916                     used_positions = []
917
918                     def draw_thing(t, used_positions):
919                         symbol = self.game.thing_types[t.type_]
920                         meta_char = ' '
921                         if hasattr(t, 'thing_char'):
922                             meta_char = t.thing_char
923                         if t.position in used_positions:
924                             meta_char = '+'
925                         if hasattr(t, 'carrying') and t.carrying:
926                             meta_char = '$'
927                         map_lines_split[t.position.y][t.position.x] = symbol + meta_char
928                         used_positions += [t.position]
929
930                     for t in [t for t in self.game.things if t.type_ != 'Player']:
931                         draw_thing(t, used_positions)
932                     for t in [t for t in self.game.things if t.type_ == 'Player']:
933                         draw_thing(t, used_positions)
934                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
935                     map_lines_split[self.explorer.y][self.explorer.x] = '??'
936                 elif self.map_mode != 'terrain + things':
937                     map_lines_split[self.game.player.position.y]\
938                         [self.game.player.position.x] = '??'
939                 self.map_lines = []
940                 if type(self.game.map_geometry) == MapGeometryHex:
941                     indent = 0
942                     for line in map_lines_split:
943                         self.map_lines += [indent * ' ' + ''.join(line)]
944                         indent = 0 if indent else 1
945                 else:
946                     for line in map_lines_split:
947                         self.map_lines += [''.join(line)]
948                 window_center = YX(int(self.size.y / 2),
949                                    int(self.window_width / 2))
950                 center = self.game.player.position
951                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
952                     center = self.explorer
953                 center = YX(center.y, center.x * 2)
954                 self.offset = center - window_center
955                 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
956                     self.offset += YX(0, 1)
957             term_y = max(0, -self.offset.y)
958             term_x = max(0, -self.offset.x)
959             map_y = max(0, self.offset.y)
960             map_x = max(0, self.offset.x)
961             while term_y < self.size.y and map_y < len(self.map_lines):
962                 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
963                 safe_addstr(term_y, term_x, to_draw)
964                 term_y += 1
965                 map_y += 1
966
967         def draw_face_popup():
968             t = self.game.get_thing(self.draw_face)
969             if not t or not hasattr(t, 'face'):
970                 self.draw_face = False
971                 return
972
973             start_x = self.window_width - 10
974             t_char = ' '
975             if hasattr(t, 'thing_char'):
976                 t_char = t.thing_char
977             def draw_body_part(body_part, end_y):
978                 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
979                 safe_addstr(end_y - 3, start_x, '|        |')
980                 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
981                 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
982                 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
983
984             if hasattr(t, 'face'):
985                 draw_body_part(t.face, self.size.y - 2)
986             if hasattr(t, 'hat'):
987                 draw_body_part(t.hat, self.size.y - 5)
988             safe_addstr(self.size.y - 1, start_x, '|        |')
989
990         def draw_help():
991             content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
992                                              self.mode.help_intro)
993             if len(self.mode.available_actions) > 0:
994                 content += "Available actions:\n"
995                 for action in self.mode.available_actions:
996                     if action in action_tasks:
997                         if action_tasks[action] not in self.game.tasks:
998                             continue
999                     if action == 'move_explorer':
1000                         action = 'move'
1001                     if action == 'move':
1002                         key = ','.join(self.movement_keys)
1003                     else:
1004                         key = self.keys[action]
1005                     content += '[%s] – %s\n' % (key, action_descriptions[action])
1006                 content += '\n'
1007             content += self.mode.list_available_modes(self)
1008             for i in range(self.size.y):
1009                 safe_addstr(i,
1010                             self.window_width * (not self.mode.has_input_prompt),
1011                             ' ' * self.window_width)
1012             lines = []
1013             for line in content.split('\n'):
1014                 lines += msg_into_lines_of_width(line, self.window_width)
1015             for i in range(len(lines)):
1016                 if i >= self.size.y:
1017                     break
1018                 safe_addstr(i,
1019                             self.window_width * (not self.mode.has_input_prompt),
1020                             lines[i])
1021
1022         def draw_screen():
1023             stdscr.clear()
1024             stdscr.bkgd(' ', curses.color_pair(1))
1025             recalc_input_lines()
1026             if self.mode.has_input_prompt:
1027                 draw_input()
1028             if self.mode.shows_info:
1029                 draw_info()
1030             else:
1031                 draw_history()
1032             draw_mode()
1033             if not self.mode.is_intro:
1034                 draw_turn()
1035                 draw_map()
1036             if self.show_help:
1037                 draw_help()
1038             if self.draw_face and self.mode.name in {'chat', 'play'}:
1039                 draw_face_popup()
1040
1041         def pick_selectable(task_name):
1042             try:
1043                 i = int(self.input_)
1044                 if i < 0 or i >= len(self.selectables):
1045                     self.log_msg('? invalid index, aborted')
1046                 else:
1047                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1048             except ValueError:
1049                 self.log_msg('? invalid index, aborted')
1050             self.input_ = ''
1051             self.switch_mode('play')
1052
1053         action_descriptions = {
1054             'move': 'move',
1055             'flatten': 'flatten surroundings',
1056             'teleport': 'teleport',
1057             'take_thing': 'pick up thing',
1058             'drop_thing': 'drop thing',
1059             'toggle_map_mode': 'toggle map view',
1060             'toggle_tile_draw': 'toggle protection character drawing',
1061             'install': '(un-)install',
1062             'wear': '(un-)wear',
1063             'door': 'open/close',
1064             'consume': 'consume',
1065             'spin': 'spin',
1066         }
1067
1068         action_tasks = {
1069             'flatten': 'FLATTEN_SURROUNDINGS',
1070             'take_thing': 'PICK_UP',
1071             'drop_thing': 'DROP',
1072             'door': 'DOOR',
1073             'install': 'INSTALL',
1074             'wear': 'WEAR',
1075             'move': 'MOVE',
1076             'command': 'COMMAND',
1077             'consume': 'INTOXICATE',
1078             'spin': 'SPIN',
1079         }
1080
1081         curses.curs_set(False)  # hide cursor
1082         curses.start_color()
1083         self.set_default_colors()
1084         curses.init_pair(1, 1, 2)
1085         stdscr.timeout(10)
1086         reset_screen_size()
1087         self.explorer = YX(0, 0)
1088         self.input_ = ''
1089         input_prompt = '> '
1090         interval = datetime.timedelta(seconds=5)
1091         last_ping = datetime.datetime.now() - interval
1092         while True:
1093             if self.disconnected and self.force_instant_connect:
1094                 self.force_instant_connect = False
1095                 self.connect()
1096             now = datetime.datetime.now()
1097             if now - last_ping > interval:
1098                 if self.disconnected:
1099                     self.connect()
1100                 else:
1101                     self.send('PING')
1102                 last_ping = now
1103             if self.flash:
1104                 curses.flash()
1105                 self.flash = False
1106             if self.do_refresh:
1107                 draw_screen()
1108                 self.do_refresh = False
1109             while True:
1110                 try:
1111                     msg = self.queue.get(block=False)
1112                     handle_input(msg)
1113                 except queue.Empty:
1114                     break
1115             try:
1116                 key = stdscr.getkey()
1117                 self.do_refresh = True
1118             except curses.error:
1119                 continue
1120             keycode = None
1121             if len(key) == 1:
1122                 keycode = ord(key)
1123             self.show_help = False
1124             self.draw_face = False
1125             if key == 'KEY_RESIZE':
1126                 reset_screen_size()
1127             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1128                 self.input_ = self.input_[:-1]
1129             elif (((not self.mode.is_intro) and keycode == 27)  # Escape
1130                   or (self.mode.has_input_prompt and key == '\n'
1131                       and self.input_ == ''\
1132                       and self.mode.name in {'chat', 'command_thing',
1133                                              'take_thing', 'drop_thing',
1134                                              'admin_enter'})):
1135                 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1136                     self.log_msg('@ aborted')
1137                 self.switch_mode('play')
1138             elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1139                 self.show_help = True
1140                 self.input_ = ""
1141                 self.restore_input_values()
1142             elif self.mode.has_input_prompt and key != '\n':  # Return key
1143                 self.input_ += key
1144                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1145                 if len(self.input_) > max_length:
1146                     self.input_ = self.input_[:max_length]
1147             elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1148                 self.show_help = True
1149             elif self.mode.name == 'login' and key == '\n':
1150                 self.login_name = self.input_
1151                 self.send('LOGIN ' + quote(self.input_))
1152                 self.input_ = ""
1153             elif self.mode.name == 'enter_face' and key == '\n':
1154                 if len(self.input_) != 18:
1155                     self.log_msg('? wrong input length, aborting')
1156                 else:
1157                     self.send('PLAYER_FACE %s' % quote(self.input_))
1158                 self.input_ = ""
1159                 self.switch_mode('edit')
1160             elif self.mode.name == 'take_thing' and key == '\n':
1161                 pick_selectable('PICK_UP')
1162             elif self.mode.name == 'drop_thing' and key == '\n':
1163                 pick_selectable('DROP')
1164             elif self.mode.name == 'command_thing' and key == '\n':
1165                 self.send('TASK:COMMAND ' + quote(self.input_))
1166                 self.input_ = ""
1167             elif self.mode.name == 'control_pw_pw' and key == '\n':
1168                 if self.input_ == '':
1169                     self.log_msg('@ aborted')
1170                 else:
1171                     self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1172                     self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1173                 self.switch_mode('admin')
1174             elif self.mode.name == 'password' and key == '\n':
1175                 if self.input_ == '':
1176                     self.input_ = ' '
1177                 self.password = self.input_
1178                 self.switch_mode('edit')
1179             elif self.mode.name == 'admin_enter' and key == '\n':
1180                 self.send('BECOME_ADMIN ' + quote(self.input_))
1181                 self.switch_mode('play')
1182             elif self.mode.name == 'control_pw_type' and key == '\n':
1183                 if len(self.input_) != 1:
1184                     self.log_msg('@ entered non-single-char, therefore aborted')
1185                     self.switch_mode('admin')
1186                 else:
1187                     self.tile_control_char = self.input_
1188                     self.switch_mode('control_pw_pw')
1189             elif self.mode.name == 'admin_thing_protect' and key == '\n':
1190                 if len(self.input_) != 1:
1191                     self.log_msg('@ entered non-single-char, therefore aborted')
1192                 else:
1193                     self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1194                                                           quote(self.input_)))
1195                     self.log_msg('@ sent new protection character for thing')
1196                 self.switch_mode('admin')
1197             elif self.mode.name == 'control_tile_type' and key == '\n':
1198                 if len(self.input_) != 1:
1199                     self.log_msg('@ entered non-single-char, therefore aborted')
1200                     self.switch_mode('admin')
1201                 else:
1202                     self.tile_control_char = self.input_
1203                     self.switch_mode('control_tile_draw')
1204             elif self.mode.name == 'chat' and key == '\n':
1205                 if self.input_ == '':
1206                     continue
1207                 if self.input_[0] == '/':
1208                     if self.input_.startswith('/nick'):
1209                         tokens = self.input_.split(maxsplit=1)
1210                         if len(tokens) == 2:
1211                             self.send('NICK ' + quote(tokens[1]))
1212                         else:
1213                             self.log_msg('? need login name')
1214                     else:
1215                         self.log_msg('? unknown command')
1216                 else:
1217                     self.send('ALL ' + quote(self.input_))
1218                 self.input_ = ""
1219             elif self.mode.name == 'name_thing' and key == '\n':
1220                 if self.input_ == '':
1221                     self.input_ = ' '
1222                 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1223                                                    quote(self.input_),
1224                                                    quote(self.password)))
1225                 self.switch_mode('edit')
1226             elif self.mode.name == 'annotate' and key == '\n':
1227                 if self.input_ == '':
1228                     self.input_ = ' '
1229                 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1230                                                  quote(self.password)))
1231                 self.switch_mode('edit')
1232             elif self.mode.name == 'portal' and key == '\n':
1233                 if self.input_ == '':
1234                     self.input_ = ' '
1235                 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1236                                                quote(self.password)))
1237                 self.switch_mode('edit')
1238             elif self.mode.name == 'study':
1239                 if self.mode.mode_switch_on_key(self, key):
1240                     continue
1241                 elif key == self.keys['toggle_map_mode']:
1242                     self.toggle_map_mode()
1243                 elif key in self.movement_keys:
1244                     move_explorer(self.movement_keys[key])
1245             elif self.mode.name == 'play':
1246                 if self.mode.mode_switch_on_key(self, key):
1247                     continue
1248                 elif key == self.keys['door'] and task_action_on('door'):
1249                     self.send('TASK:DOOR')
1250                 elif key == self.keys['consume'] and task_action_on('consume'):
1251                     self.send('TASK:INTOXICATE')
1252                 elif key == self.keys['wear'] and task_action_on('wear'):
1253                     self.send('TASK:WEAR')
1254                 elif key == self.keys['spin'] and task_action_on('spin'):
1255                     self.send('TASK:SPIN')
1256                 elif key == self.keys['teleport']:
1257                     if self.game.player.position in self.game.portals:
1258                         self.host = self.game.portals[self.game.player.position]
1259                         self.reconnect()
1260                     else:
1261                         self.flash = True
1262                         self.log_msg('? not standing on portal')
1263                 elif key in self.movement_keys and task_action_on('move'):
1264                     self.send('TASK:MOVE ' + self.movement_keys[key])
1265             elif self.mode.name == 'write':
1266                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1267                 self.switch_mode('edit')
1268             elif self.mode.name == 'control_tile_draw':
1269                 if self.mode.mode_switch_on_key(self, key):
1270                     continue
1271                 elif key in self.movement_keys:
1272                     move_explorer(self.movement_keys[key])
1273                 elif key == self.keys['toggle_tile_draw']:
1274                     self.tile_draw = False if self.tile_draw else True
1275             elif self.mode.name == 'admin':
1276                 if self.mode.mode_switch_on_key(self, key):
1277                     continue
1278                 elif key in self.movement_keys and task_action_on('move'):
1279                     self.send('TASK:MOVE ' + self.movement_keys[key])
1280             elif self.mode.name == 'edit':
1281                 if self.mode.mode_switch_on_key(self, key):
1282                     continue
1283                 elif key == self.keys['flatten'] and task_action_on('flatten'):
1284                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1285                 elif key == self.keys['install'] and task_action_on('install'):
1286                     self.send('TASK:INSTALL %s' % quote(self.password))
1287                 elif key == self.keys['toggle_map_mode']:
1288                     self.toggle_map_mode()
1289                 elif key in self.movement_keys and task_action_on('move'):
1290                     self.send('TASK:MOVE ' + self.movement_keys[key])
1291
1292 if len(sys.argv) != 2:
1293     raise ArgError('wrong number of arguments, need game host')
1294 host = sys.argv[1]
1295 TUI(host)