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