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