home · contact · privacy
f10c79650020d615ff2480ff26af0b5a384347ba
[plomrogue2] / rogue_chat_curses.py
1 #!/usr/bin/env python3
2 import curses
3 import queue
4 import threading
5 import time
6 import sys
7 from plomrogue.game import GameBase
8 from plomrogue.parser import Parser
9 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
10 from plomrogue.things import ThingBase
11 from plomrogue.misc import quote
12 from plomrogue.errors import BrokenSocketConnection, ArgError
13
14 mode_helps = {
15     'play': {
16         'short': 'play',
17         'intro': '',
18         'long': 'This mode allows you to interact with the map in various ways.'
19     },
20     'study': {
21         'short': 'study',
22         'intro': '',
23         'long': 'This mode allows you to study the map and its tiles in detail.  Move the question mark over a tile, and the right half of the screen will show detailed information on it.  Toggle the map view to show or hide different information layers.'},
24     'edit': {
25         'short': 'world edit',
26         'intro': '',
27         'long': 'This mode allows you to change the game world in various ways.  Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view.  You can edit a tile if you set the world edit password that matches its protection character.  The character "." marks the absence of protection:  Such tiles can always be edited.'
28     },
29     'name_thing': {
30         'short': 'name thing',
31         'intro': '',
32         'long': 'Give name to/change name of thing here.'
33     },
34     'command_thing': {
35         'short': 'command thing',
36         'intro': '',
37         'long': 'Enter a command to the thing you carry.  Enter nothing to return to play mode.'
38     },
39     'take_thing': {
40         'short': 'take thing',
41         'intro': 'Pick up a thing in reach by entering its index number.  Enter nothing to abort.',
42         'long': 'You see a list of things which you could pick up.  Enter the target thing\'s index, or, to leave, nothing.'
43     },
44     'drop_thing': {
45         'short': 'drop thing',
46         'intro': 'Enter number of direction to which you want to drop thing.',
47         'long': 'Drop currently carried thing by entering the target direction index.  Enter nothing to return to play mode..'
48     },
49     'admin_thing_protect': {
50         'short': 'change thing protection',
51         'intro': '@ enter thing protection character:',
52         'long': 'Change protection character for thing here.'
53     },
54     'enter_face': {
55         'short': 'enter your face',
56         'intro': '@ enter face line (enter nothing to abort):',
57         'long': 'Draw your face as ASCII art.  The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
58     },
59     'write': {
60         'short': 'change terrain',
61         'intro': '',
62         'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so).  Just enter any printable ASCII character to imprint it on the ground below you.'
63     },
64     'control_pw_type': {
65         'short': 'change protection character password',
66         'intro': '@ enter protection character for which you want to change the password:',
67         'long': 'This mode is the first of two steps to change the password for a protection character.  First enter the protection character for which you want to change the password.'
68     },
69     'control_pw_pw': {
70         'short': 'change protection character password',
71         'intro': '',
72         'long': 'This mode is the second of two steps to change the password for a protection character.  Enter the new password for the protection character you chose.'
73     },
74     'control_tile_type': {
75         'short': 'change tiles protection',
76         'intro': '@ enter protection character which you want to draw:',
77         'long': 'This mode is the first of two steps to change tile protection areas on the map.  First enter the tile protection character you want to write.'
78     },
79     'control_tile_draw': {
80         'short': 'change tiles protection',
81         'intro': '',
82         'long': 'This mode is the second of two steps to change tile protection areas on the map.  Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
83     },
84     'annotate': {
85         'short': 'annotate tile',
86         'intro': '',
87         'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so).  Hit Return to leave.'
88     },
89     'portal': {
90         'short': 'edit portal',
91         'intro': '',
92         'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so).  Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target.  Hit Return to leave.'
93     },
94     'chat': {
95         'short': 'chat',
96         'intro': '',
97         'long': 'This mode allows you to engage in chit-chat with other users.  Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message.  Lines that start with a "/" are used for commands like:\n\n/nick NAME – re-name yourself to NAME'
98     },
99     'login': {
100         'short': 'login',
101         'intro': '',
102         'long': 'Enter your player name.'
103     },
104     'waiting_for_server': {
105         'short': 'waiting for server response',
106         'intro': '@ waiting for server …',
107         'long': 'Waiting for a server response.'
108     },
109     'post_login_wait': {
110         'short': 'waiting for server response',
111         'intro': '',
112         'long': 'Waiting for a server response.'
113     },
114     'password': {
115         'short': 'set world edit password',
116         'intro': '',
117         'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world.  Hit return to confirm and leave.'
118     },
119     'admin_enter': {
120         'short': 'become admin',
121         'intro': '@ enter admin password:',
122         'long': 'This mode allows you to become admin if you know an admin password.'
123     },
124     'admin': {
125         'short': 'admin',
126         'intro': '',
127         'long': 'This mode allows you access to actions limited to administrators.'
128     }
129 }
130
131 from ws4py.client import WebSocketBaseClient
132 class WebSocketClient(WebSocketBaseClient):
133
134     def __init__(self, recv_handler, *args, **kwargs):
135         super().__init__(*args, **kwargs)
136         self.recv_handler = recv_handler
137         self.connect()
138
139     def received_message(self, message):
140         if message.is_text:
141             message = str(message)
142             self.recv_handler(message)
143
144     @property
145     def plom_closed(self):
146         return self.client_terminated
147
148 from plomrogue.io_tcp import PlomSocket
149 class PlomSocketClient(PlomSocket):
150
151     def __init__(self, recv_handler, url):
152         import socket
153         self.recv_handler = recv_handler
154         host, port = url.split(':')
155         super().__init__(socket.create_connection((host, port)))
156
157     def close(self):
158         self.socket.close()
159
160     def run(self):
161         import ssl
162         try:
163             for msg in self.recv():
164                 if msg == 'NEED_SSL':
165                     self.socket = ssl.wrap_socket(self.socket)
166                     continue
167                 self.recv_handler(msg)
168         except BrokenSocketConnection:
169             pass  # we assume socket will be known as dead by now
170
171 def cmd_TURN(game, n):
172     game.annotations = {}
173     game.turn = n
174     game.things = []
175     game.portals = {}
176     game.turn_complete = False
177     game.fov = ''
178 cmd_TURN.argtypes = 'int:nonneg'
179
180 def cmd_LOGIN_OK(game):
181     game.tui.switch_mode('post_login_wait')
182     game.tui.send('GET_GAMESTATE')
183     game.tui.log_msg('@ welcome')
184 cmd_LOGIN_OK.argtypes = ''
185
186 def cmd_ADMIN_OK(game):
187     game.tui.is_admin = True
188     game.tui.log_msg('@ you now have admin rights')
189     game.tui.switch_mode('admin')
190     game.tui.do_refresh = True
191 cmd_ADMIN_OK.argtypes = ''
192
193 def cmd_REPLY(game, msg):
194     game.tui.log_msg('#MUSICPLAYER: ' + msg)
195     game.tui.do_refresh = True
196 cmd_REPLY.argtypes = 'string'
197
198 def cmd_CHAT(game, msg):
199     game.tui.log_msg('# ' + msg)
200     game.tui.do_refresh = True
201 cmd_CHAT.argtypes = 'string'
202
203 def cmd_PLAYER_ID(game, player_id):
204     game.player_id = player_id
205 cmd_PLAYER_ID.argtypes = 'int:nonneg'
206
207 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
208     t = game.get_thing(thing_id)
209     if not t:
210         t = ThingBase(game, thing_id)
211         game.things += [t]
212     t.position = yx
213     t.type_ = thing_type
214     t.protection = protection
215     t.portable = portable
216     t.commandable = commandable
217 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
218
219 def cmd_THING_NAME(game, thing_id, name):
220     t = game.get_thing(thing_id)
221     t.name = name
222 cmd_THING_NAME.argtypes = 'int:pos string'
223
224 def cmd_THING_FACE(game, thing_id, face):
225     t = game.get_thing(thing_id)
226     t.face = face
227 cmd_THING_FACE.argtypes = 'int:pos string'
228
229 def cmd_THING_HAT(game, thing_id, hat):
230     t = game.get_thing(thing_id)
231     t.hat = hat
232 cmd_THING_HAT.argtypes = 'int:pos string'
233
234 def cmd_THING_CHAR(game, thing_id, c):
235     t = game.get_thing(thing_id)
236     t.thing_char = c
237 cmd_THING_CHAR.argtypes = 'int:pos char'
238
239 def cmd_MAP(game, geometry, size, content):
240     map_geometry_class = globals()['MapGeometry' + geometry]
241     game.map_geometry = map_geometry_class(size)
242     game.map_content = content
243     if type(game.map_geometry) == MapGeometrySquare:
244         game.tui.movement_keys = {
245             game.tui.keys['square_move_up']: 'UP',
246             game.tui.keys['square_move_left']: 'LEFT',
247             game.tui.keys['square_move_down']: 'DOWN',
248             game.tui.keys['square_move_right']: 'RIGHT',
249         }
250     elif type(game.map_geometry) == MapGeometryHex:
251         game.tui.movement_keys = {
252             game.tui.keys['hex_move_upleft']: 'UPLEFT',
253             game.tui.keys['hex_move_upright']: 'UPRIGHT',
254             game.tui.keys['hex_move_right']: 'RIGHT',
255             game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
256             game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
257             game.tui.keys['hex_move_left']: 'LEFT',
258         }
259 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
260
261 def cmd_FOV(game, content):
262     game.fov = content
263 cmd_FOV.argtypes = 'string'
264
265 def cmd_MAP_CONTROL(game, content):
266     game.map_control_content = content
267 cmd_MAP_CONTROL.argtypes = 'string'
268
269 def cmd_GAME_STATE_COMPLETE(game):
270     game.turn_complete = True
271     game.tui.do_refresh = True
272     game.tui.info_cached = None
273     game.player = game.get_thing(game.player_id)
274     if game.tui.mode.name == 'post_login_wait':
275         game.tui.switch_mode('play')
276 cmd_GAME_STATE_COMPLETE.argtypes = ''
277
278 def cmd_PORTAL(game, position, msg):
279     game.portals[position] = msg
280 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
281
282 def cmd_PLAY_ERROR(game, msg):
283     game.tui.log_msg('? ' + msg)
284     game.tui.flash = True
285     game.tui.do_refresh = True
286 cmd_PLAY_ERROR.argtypes = 'string'
287
288 def cmd_GAME_ERROR(game, msg):
289     game.tui.log_msg('? game error: ' + msg)
290     game.tui.do_refresh = True
291 cmd_GAME_ERROR.argtypes = 'string'
292
293 def cmd_ARGUMENT_ERROR(game, msg):
294     game.tui.log_msg('? syntax error: ' + msg)
295     game.tui.do_refresh = True
296 cmd_ARGUMENT_ERROR.argtypes = 'string'
297
298 def cmd_ANNOTATION(game, position, msg):
299     game.annotations[position] = msg
300     if game.tui.mode.shows_info:
301         game.tui.do_refresh = True
302 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
303
304 def cmd_TASKS(game, tasks_comma_separated):
305     game.tasks = tasks_comma_separated.split(',')
306     game.tui.mode_write.legal = 'WRITE' in game.tasks
307     game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
308     game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
309     game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
310 cmd_TASKS.argtypes = 'string'
311
312 def cmd_THING_TYPE(game, thing_type, symbol_hint):
313     game.thing_types[thing_type] = symbol_hint
314 cmd_THING_TYPE.argtypes = 'string char'
315
316 def cmd_THING_INSTALLED(game, thing_id):
317     game.get_thing(thing_id).installed = True
318 cmd_THING_INSTALLED.argtypes = 'int:pos'
319
320 def cmd_THING_CARRYING(game, thing_id, carried_id):
321     game.get_thing(thing_id).carrying = game.get_thing(carried_id)
322 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
323
324 def cmd_TERRAIN(game, terrain_char, terrain_desc):
325     game.terrains[terrain_char] = terrain_desc
326 cmd_TERRAIN.argtypes = 'char string'
327
328 def cmd_PONG(game):
329     pass
330 cmd_PONG.argtypes = ''
331
332 def cmd_DEFAULT_COLORS(game):
333     game.tui.set_default_colors()
334 cmd_DEFAULT_COLORS.argtypes = ''
335
336 def cmd_RANDOM_COLORS(game):
337     game.tui.set_random_colors()
338 cmd_RANDOM_COLORS.argtypes = ''
339
340 class Game(GameBase):
341     turn_complete = False
342     tasks = {}
343     thing_types = {}
344
345     def __init__(self, *args, **kwargs):
346         super().__init__(*args, **kwargs)
347         self.register_command(cmd_LOGIN_OK)
348         self.register_command(cmd_ADMIN_OK)
349         self.register_command(cmd_PONG)
350         self.register_command(cmd_CHAT)
351         self.register_command(cmd_REPLY)
352         self.register_command(cmd_PLAYER_ID)
353         self.register_command(cmd_TURN)
354         self.register_command(cmd_THING)
355         self.register_command(cmd_THING_TYPE)
356         self.register_command(cmd_THING_NAME)
357         self.register_command(cmd_THING_CHAR)
358         self.register_command(cmd_THING_FACE)
359         self.register_command(cmd_THING_HAT)
360         self.register_command(cmd_THING_CARRYING)
361         self.register_command(cmd_THING_INSTALLED)
362         self.register_command(cmd_TERRAIN)
363         self.register_command(cmd_MAP)
364         self.register_command(cmd_MAP_CONTROL)
365         self.register_command(cmd_PORTAL)
366         self.register_command(cmd_ANNOTATION)
367         self.register_command(cmd_GAME_STATE_COMPLETE)
368         self.register_command(cmd_ARGUMENT_ERROR)
369         self.register_command(cmd_GAME_ERROR)
370         self.register_command(cmd_PLAY_ERROR)
371         self.register_command(cmd_TASKS)
372         self.register_command(cmd_FOV)
373         self.register_command(cmd_DEFAULT_COLORS)
374         self.register_command(cmd_RANDOM_COLORS)
375         self.map_content = ''
376         self.player_id = -1
377         self.annotations = {}
378         self.portals = {}
379         self.terrains = {}
380         self.player = None
381
382     def get_string_options(self, string_option_type):
383         if string_option_type == 'map_geometry':
384             return ['Hex', 'Square']
385         elif string_option_type == 'thing_type':
386             return self.thing_types.keys()
387         return None
388
389     def get_command(self, command_name):
390         from functools import partial
391         f = partial(self.commands[command_name], self)
392         f.argtypes = self.commands[command_name].argtypes
393         return f
394
395 class Mode:
396
397     def __init__(self, name, has_input_prompt=False, shows_info=False,
398                  is_intro=False, is_single_char_entry=False):
399         self.name = name
400         self.short_desc = mode_helps[name]['short']
401         self.available_modes = []
402         self.available_actions = []
403         self.has_input_prompt = has_input_prompt
404         self.shows_info = shows_info
405         self.is_intro = is_intro
406         self.help_intro = mode_helps[name]['long']
407         self.intro_msg = mode_helps[name]['intro']
408         self.is_single_char_entry = is_single_char_entry
409         self.legal = True
410
411     def iter_available_modes(self, tui):
412         for mode_name in self.available_modes:
413             mode = getattr(tui, 'mode_' + mode_name)
414             if not mode.legal:
415                 continue
416             key = tui.keys['switch_to_' + mode.name]
417             yield mode, key
418
419     def list_available_modes(self, tui):
420         msg = ''
421         if len(self.available_modes) > 0:
422             msg = 'Other modes available from here:\n'
423             for mode, key in self.iter_available_modes(tui):
424                 msg += '[%s] – %s\n' % (key, mode.short_desc)
425         return msg
426
427     def mode_switch_on_key(self, tui, key_pressed):
428         for mode, key in self.iter_available_modes(tui):
429             if key_pressed == key:
430                 tui.switch_mode(mode.name)
431                 return True
432         return False
433
434 class TUI:
435     mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
436     mode_admin = Mode('admin')
437     mode_play = Mode('play')
438     mode_study = Mode('study', shows_info=True)
439     mode_write = Mode('write', is_single_char_entry=True)
440     mode_edit = Mode('edit')
441     mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
442     mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
443     mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
444     mode_control_tile_draw = Mode('control_tile_draw')
445     mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
446     mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
447     mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
448     mode_chat = Mode('chat', has_input_prompt=True)
449     mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
450     mode_login = Mode('login', has_input_prompt=True, is_intro=True)
451     mode_post_login_wait = Mode('post_login_wait', is_intro=True)
452     mode_password = Mode('password', has_input_prompt=True)
453     mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
454     mode_command_thing = Mode('command_thing', has_input_prompt=True)
455     mode_take_thing = Mode('take_thing', has_input_prompt=True)
456     mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
457     mode_enter_face = Mode('enter_face', has_input_prompt=True)
458     is_admin = False
459     tile_draw = False
460
461     def __init__(self, host):
462         import os
463         import json
464         self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
465                                           "command_thing", "take_thing",
466                                           "drop_thing"]
467         self.mode_play.available_actions = ["move", "teleport", "door", "consume",
468                                             "install", "wear", "spin"]
469         self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
470         self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
471         self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
472                                            "control_tile_type", "chat",
473                                            "study", "play", "edit"]
474         self.mode_admin.available_actions = ["move"]
475         self.mode_control_tile_draw.available_modes = ["admin_enter"]
476         self.mode_control_tile_draw.available_actions = ["move_explorer",
477                                                          "toggle_tile_draw"]
478         self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
479                                           "password", "chat", "study", "play",
480                                           "admin_enter", "enter_face"]
481         self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
482         self.mode = None
483         self.host = host
484         self.game = Game()
485         self.game.tui = self
486         self.parser = Parser(self.game)
487         self.log = []
488         self.do_refresh = True
489         self.queue = queue.Queue()
490         self.login_name = None
491         self.map_mode = 'terrain + things'
492         self.password = 'foo'
493         self.switch_mode('waiting_for_server')
494         self.keys = {
495             'switch_to_chat': 't',
496             'switch_to_play': 'p',
497             'switch_to_password': 'P',
498             'switch_to_annotate': 'M',
499             'switch_to_portal': 'T',
500             'switch_to_study': '?',
501             'switch_to_edit': 'E',
502             'switch_to_write': 'm',
503             'switch_to_name_thing': 'N',
504             'switch_to_command_thing': 'O',
505             'switch_to_admin_enter': 'A',
506             'switch_to_control_pw_type': 'C',
507             'switch_to_control_tile_type': 'Q',
508             'switch_to_admin_thing_protect': 'T',
509             'flatten': 'F',
510             'switch_to_enter_face': 'f',
511             'switch_to_take_thing': 'z',
512             'switch_to_drop_thing': 'u',
513             'teleport': 'p',
514             'consume': 'C',
515             'door': 'D',
516             'install': 'I',
517             'wear': 'W',
518             'spin': 'S',
519             'help': 'h',
520             'toggle_map_mode': 'L',
521             'toggle_tile_draw': 'm',
522             'hex_move_upleft': 'w',
523             'hex_move_upright': 'e',
524             'hex_move_right': 'd',
525             'hex_move_downright': 'x',
526             'hex_move_downleft': 'y',
527             'hex_move_left': 'a',
528             'square_move_up': 'w',
529             'square_move_left': 'a',
530             'square_move_down': 's',
531             'square_move_right': 'd',
532         }
533         if os.path.isfile('config.json'):
534             with open('config.json', 'r') as f:
535                 keys_conf = json.loads(f.read())
536             for k in keys_conf:
537                 self.keys[k] = keys_conf[k]
538         self.show_help = False
539         self.disconnected = True
540         self.force_instant_connect = True
541         self.input_lines = []
542         self.fov = ''
543         self.flash = False
544         self.map_lines = []
545         self.offset = YX(0,0)
546         curses.wrapper(self.loop)
547
548     def connect(self):
549
550         def handle_recv(msg):
551             if msg == 'BYE':
552                 self.socket.close()
553             else:
554                 self.queue.put(msg)
555
556         self.log_msg('@ attempting connect')
557         socket_client_class = PlomSocketClient
558         if self.host.startswith('ws://') or self.host.startswith('wss://'):
559             socket_client_class = WebSocketClient
560         try:
561             self.socket = socket_client_class(handle_recv, self.host)
562             self.socket_thread = threading.Thread(target=self.socket.run)
563             self.socket_thread.start()
564             self.disconnected = False
565             self.game.thing_types = {}
566             self.game.terrains = {}
567             time.sleep(0.1)  # give potential SSL negotation some time …
568             self.socket.send('TASKS')
569             self.socket.send('TERRAINS')
570             self.socket.send('THING_TYPES')
571             self.switch_mode('login')
572         except ConnectionRefusedError:
573             self.log_msg('@ server connect failure')
574             self.disconnected = True
575             self.switch_mode('waiting_for_server')
576         self.do_refresh = True
577
578     def reconnect(self):
579         self.log_msg('@ attempting reconnect')
580         self.send('QUIT')
581         # necessitated by some strange SSL race conditions with ws4py
582         time.sleep(0.1)  # FIXME find out why exactly necessary
583         self.switch_mode('waiting_for_server')
584         self.connect()
585
586     def send(self, msg):
587         try:
588             if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
589                 raise BrokenSocketConnection
590             self.socket.send(msg)
591         except (BrokenPipeError, BrokenSocketConnection):
592             self.log_msg('@ server disconnected :(')
593             self.disconnected = True
594             self.force_instant_connect = True
595             self.do_refresh = True
596
597     def log_msg(self, msg):
598         self.log += [msg]
599         if len(self.log) > 100:
600             self.log = self.log[-100:]
601
602     def restore_input_values(self):
603         if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
604             self.input_ = self.game.annotations[self.explorer]
605         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
606             self.input_ = self.game.portals[self.explorer]
607         elif self.mode.name == 'password':
608             self.input_ = self.password
609         elif self.mode.name == 'name_thing':
610             if hasattr(self.thing_selected, 'name'):
611                 self.input_ = self.thing_selected.name
612         elif self.mode.name == 'admin_thing_protect':
613             if hasattr(self.thing_selected, 'protection'):
614                 self.input_ = self.thing_selected.protection
615
616     def send_tile_control_command(self):
617         self.send('SET_TILE_CONTROL %s %s' %
618                   (self.explorer, quote(self.tile_control_char)))
619
620     def toggle_map_mode(self):
621         if self.map_mode == 'terrain only':
622             self.map_mode = 'terrain + annotations'
623         elif self.map_mode == 'terrain + annotations':
624             self.map_mode = 'terrain + things'
625         elif self.map_mode == 'terrain + things':
626             self.map_mode = 'protections'
627         elif self.map_mode == 'protections':
628             self.map_mode = 'terrain only'
629
630     def switch_mode(self, mode_name):
631         if self.mode and self.mode.name == 'control_tile_draw':
632             self.log_msg('@ finished tile protection drawing.')
633         self.tile_draw = False
634         if mode_name == 'command_thing' and\
635            (not self.game.player.carrying or
636             not self.game.player.carrying.commandable):
637             self.log_msg('? not carrying anything commandable')
638             self.flash = True
639             self.switch_mode('play')
640             return
641         if mode_name == 'drop_thing' and not self.game.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
651                       if t.position == self.game.player.position
652                       and t.id_ != self.game.player.id_]:
653                 thing = t
654                 break
655             if not thing:
656                 self.flash = True
657                 self.log_msg('? not standing over thing')
658                 return
659             else:
660                 self.thing_selected = thing
661         self.mode = getattr(self, 'mode_' + mode_name)
662         if self.mode.name in {'control_tile_draw', 'control_tile_type',
663                               'control_pw_type'}:
664             self.map_mode = 'protections'
665         elif self.mode.name != 'edit':
666             self.map_mode = 'terrain + things'
667         if self.mode.shows_info or self.mode.name == 'control_tile_draw':
668             self.explorer = YX(self.game.player.position.y,
669                                self.game.player.position.x)
670         if self.mode.is_single_char_entry:
671             self.show_help = True
672         if len(self.mode.intro_msg) > 0:
673             self.log_msg(self.mode.intro_msg)
674         if self.mode.name == 'login':
675             if self.login_name:
676                 self.send('LOGIN ' + quote(self.login_name))
677             else:
678                 self.log_msg('@ enter username')
679         elif self.mode.name == 'take_thing':
680             self.log_msg('Portable things in reach for pick-up:')
681             select_range = [self.game.player.position,
682                             self.game.player.position + YX(0,-1),
683                             self.game.player.position + YX(0, 1),
684                             self.game.player.position + YX(-1, 0),
685                             self.game.player.position + YX(1, 0)]
686             if type(self.game.map_geometry) == MapGeometryHex:
687                 if self.game.player.position.y % 2:
688                     select_range += [self.game.player.position + YX(-1, 1),
689                                      self.game.player.position + YX(1, 1)]
690                 else:
691                     select_range += [self.game.player.position + YX(-1, -1),
692                                      self.game.player.position + YX(1, -1)]
693             self.selectables = [t.id_ for t in self.game.things
694                                 if t.portable and t.position in select_range]
695             if len(self.selectables) == 0:
696                 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                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
930                     map_lines_split[self.explorer.y][self.explorer.x] = '??'
931                 elif self.map_mode != 'terrain + things':
932                     map_lines_split[self.game.player.position.y]\
933                         [self.game.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 = self.game.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                     if self.game.player.position in self.game.portals:
1228                         self.host = self.game.portals[self.game.player.position]
1229                         self.reconnect()
1230                     else:
1231                         self.flash = True
1232                         self.log_msg('? not standing on portal')
1233                 elif key in self.movement_keys and task_action_on('move'):
1234                     self.send('TASK:MOVE ' + self.movement_keys[key])
1235             elif self.mode.name == 'write':
1236                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1237                 self.switch_mode('edit')
1238             elif self.mode.name == 'control_tile_draw':
1239                 if self.mode.mode_switch_on_key(self, key):
1240                     continue
1241                 elif key in self.movement_keys:
1242                     move_explorer(self.movement_keys[key])
1243                 elif key == self.keys['toggle_tile_draw']:
1244                     self.tile_draw = False if self.tile_draw else True
1245             elif self.mode.name == 'admin':
1246                 if self.mode.mode_switch_on_key(self, key):
1247                     continue
1248                 elif key in self.movement_keys and task_action_on('move'):
1249                     self.send('TASK:MOVE ' + self.movement_keys[key])
1250             elif self.mode.name == 'edit':
1251                 if self.mode.mode_switch_on_key(self, key):
1252                     continue
1253                 elif key == self.keys['flatten'] and task_action_on('flatten'):
1254                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1255                 elif key == self.keys['toggle_map_mode']:
1256                     self.toggle_map_mode()
1257                 elif key in self.movement_keys and task_action_on('move'):
1258                     self.send('TASK:MOVE ' + self.movement_keys[key])
1259
1260 if len(sys.argv) != 2:
1261     raise ArgError('wrong number of arguments, need game host')
1262 host = sys.argv[1]
1263 TUI(host)