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