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