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