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