home · contact · privacy
Only allow renaming and protection changing on thing player carries.
[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 carried thing.'
33     },
34     'command_thing': {
35         'short': 'command',
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',
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',
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 carried thing.'
53     },
54     'enter_face': {
55         'short': 'edit 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': 'edit hat',
61         'intro': '@ enter hat line (enter nothing to abort):',
62         '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.  Eat cookies to extend the ASCII characters available for drawing.'
63     },
64     'write': {
65         'short': 'edit tile',
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_OTHER_WIPE(game):
182     game.portals_new = {}
183     game.annotations_new = {}
184     game.things_new = []
185 cmd_OTHER_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_temp(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_OTHER_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",
518                                           "password",
519                                           "chat", "study", "play", "admin_enter"]
520         self.mode_edit.available_actions = ["move", "flatten", "install",
521                                             "toggle_map_mode"]
522         self.mode = None
523         self.host = host
524         self.game = Game()
525         self.game.tui = self
526         self.parser = Parser(self.game)
527         self.log = []
528         self.do_refresh = True
529         self.queue = queue.Queue()
530         self.login_name = None
531         self.map_mode = 'terrain + things'
532         self.password = 'foo'
533         self.switch_mode('waiting_for_server')
534         self.keys = {
535             'switch_to_chat': 't',
536             'switch_to_play': 'p',
537             'switch_to_password': 'P',
538             'switch_to_annotate': 'M',
539             'switch_to_portal': 'T',
540             'switch_to_study': '?',
541             'switch_to_edit': 'E',
542             'switch_to_write': 'm',
543             'switch_to_name_thing': 'N',
544             'switch_to_command_thing': 'O',
545             'switch_to_admin_enter': 'A',
546             'switch_to_control_pw_type': 'C',
547             'switch_to_control_tile_type': 'Q',
548             'switch_to_admin_thing_protect': 'T',
549             'flatten': 'F',
550             'switch_to_enter_face': 'f',
551             'switch_to_enter_hat': 'H',
552             'switch_to_take_thing': 'z',
553             'switch_to_drop_thing': 'u',
554             'teleport': 'p',
555             'consume': 'C',
556             'door': 'D',
557             'install': 'I',
558             'wear': 'W',
559             'spin': 'S',
560             'help': 'h',
561             'toggle_map_mode': 'L',
562             'toggle_tile_draw': 'm',
563             'hex_move_upleft': 'w',
564             'hex_move_upright': 'e',
565             'hex_move_right': 'd',
566             'hex_move_downright': 'x',
567             'hex_move_downleft': 'y',
568             'hex_move_left': 'a',
569             'square_move_up': 'w',
570             'square_move_left': 'a',
571             'square_move_down': 's',
572             'square_move_right': 'd',
573         }
574         if os.path.isfile('config.json'):
575             with open('config.json', 'r') as f:
576                 keys_conf = json.loads(f.read())
577             for k in keys_conf:
578                 self.keys[k] = keys_conf[k]
579         self.show_help = False
580         self.disconnected = True
581         self.force_instant_connect = True
582         self.input_lines = []
583         self.fov = ''
584         self.flash = False
585         self.map_lines = []
586         self.ascii_draw_stage = 0
587         self.full_ascii_draw = ''
588         self.offset = YX(0,0)
589         curses.wrapper(self.loop)
590
591     def connect(self):
592
593         def handle_recv(msg):
594             if msg == 'BYE':
595                 self.socket.close()
596             else:
597                 self.queue.put(msg)
598
599         self.log_msg('@ attempting connect')
600         socket_client_class = PlomSocketClient
601         if self.host.startswith('ws://') or self.host.startswith('wss://'):
602             socket_client_class = WebSocketClient
603         try:
604             self.socket = socket_client_class(handle_recv, self.host)
605             self.socket_thread = threading.Thread(target=self.socket.run)
606             self.socket_thread.start()
607             self.disconnected = False
608             self.game.thing_types = {}
609             self.game.terrains = {}
610             time.sleep(0.1)  # give potential SSL negotation some time …
611             self.socket.send('TASKS')
612             self.socket.send('TERRAINS')
613             self.socket.send('THING_TYPES')
614             self.switch_mode('login')
615         except ConnectionRefusedError:
616             self.log_msg('@ server connect failure')
617             self.disconnected = True
618             self.switch_mode('waiting_for_server')
619         self.do_refresh = True
620
621     def reconnect(self):
622         self.log_msg('@ attempting reconnect')
623         self.send('QUIT')
624         # necessitated by some strange SSL race conditions with ws4py
625         time.sleep(0.1)  # FIXME find out why exactly necessary
626         self.switch_mode('waiting_for_server')
627         self.connect()
628
629     def send(self, msg):
630         try:
631             if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
632                 raise BrokenSocketConnection
633             self.socket.send(msg)
634         except (BrokenPipeError, BrokenSocketConnection):
635             self.log_msg('@ server disconnected :(')
636             self.disconnected = True
637             self.force_instant_connect = True
638             self.do_refresh = True
639
640     def log_msg(self, msg):
641         self.log += [msg]
642         if len(self.log) > 100:
643             self.log = self.log[-100:]
644
645     def restore_input_values(self):
646         if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
647             self.input_ = self.game.annotations[self.explorer]
648         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
649             self.input_ = self.game.portals[self.explorer]
650         elif self.mode.name == 'password':
651             self.input_ = self.password
652         elif self.mode.name == 'name_thing':
653             if hasattr(self.game.player.carrying, 'name'):
654                 self.input_ = self.game.player.carrying.name
655         elif self.mode.name == 'admin_thing_protect':
656             if hasattr(self.game.player.carrying, 'protection'):
657                 self.input_ = self.game.player.carrying.protection
658         elif self.mode.name in {'enter_face', 'enter_hat'}:
659             start = self.ascii_draw_stage * 6
660             end = (self.ascii_draw_stage + 1) * 6
661             if self.mode.name == 'enter_face':
662                 self.input_ = self.game.player.face[start:end]
663             elif self.mode.name == 'enter_hat':
664                 self.input_ = self.game.player.hat[start:end]
665
666     def send_tile_control_command(self):
667         self.send('SET_TILE_CONTROL %s %s' %
668                   (self.explorer, quote(self.tile_control_char)))
669
670     def toggle_map_mode(self):
671         if self.map_mode == 'terrain only':
672             self.map_mode = 'terrain + annotations'
673         elif self.map_mode == 'terrain + annotations':
674             self.map_mode = 'terrain + things'
675         elif self.map_mode == 'terrain + things':
676             self.map_mode = 'protections'
677         elif self.map_mode == 'protections':
678             self.map_mode = 'terrain only'
679
680     def switch_mode(self, mode_name):
681
682         def fail(msg, return_mode='play'):
683             self.log_msg('? ' + msg)
684             self.flash = True
685             self.switch_mode(return_mode)
686
687         if self.mode and self.mode.name == 'control_tile_draw':
688             self.log_msg('@ finished tile protection drawing.')
689         self.draw_face = False
690         self.tile_draw = False
691         if mode_name == 'command_thing' and\
692            (not self.game.player.carrying or
693             not self.game.player.carrying.commandable):
694             return fail('not carrying anything commandable')
695         if mode_name == 'name_thing' and not self.game.player.carrying:
696             return fail('not carrying anything to re-name')
697         if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
698             return fail('not carrying anything to protect')
699         if mode_name == 'take_thing' and self.game.player.carrying:
700             return fail('already carrying something')
701         if mode_name == 'drop_thing' and not self.game.player.carrying:
702             return fail('not carrying anything droppable')
703         if mode_name == 'enter_hat' and not hasattr(self.game.player, 'hat'):
704             return fail('not wearing hat to edit', 'edit')
705         if mode_name == 'admin_enter' and self.is_admin:
706             mode_name = 'admin'
707         self.mode = getattr(self, 'mode_' + mode_name)
708         if self.mode.name in {'control_tile_draw', 'control_tile_type',
709                               'control_pw_type'}:
710             self.map_mode = 'protections'
711         elif self.mode.name != 'edit':
712             self.map_mode = 'terrain + things'
713         if self.mode.shows_info or self.mode.name == 'control_tile_draw':
714             self.explorer = YX(self.game.player.position.y,
715                                self.game.player.position.x)
716         if self.mode.is_single_char_entry:
717             self.show_help = True
718         if len(self.mode.intro_msg) > 0:
719             self.log_msg(self.mode.intro_msg)
720         if self.mode.name == 'login':
721             if self.login_name:
722                 self.send('LOGIN ' + quote(self.login_name))
723             else:
724                 self.log_msg('@ enter username')
725         elif self.mode.name == 'take_thing':
726             self.log_msg('Portable things in reach for pick-up:')
727             select_range = [self.game.player.position,
728                             self.game.player.position + YX(0,-1),
729                             self.game.player.position + YX(0, 1),
730                             self.game.player.position + YX(-1, 0),
731                             self.game.player.position + YX(1, 0)]
732             if type(self.game.map_geometry) == MapGeometryHex:
733                 if self.game.player.position.y % 2:
734                     select_range += [self.game.player.position + YX(-1, 1),
735                                      self.game.player.position + YX(1, 1)]
736                 else:
737                     select_range += [self.game.player.position + YX(-1, -1),
738                                      self.game.player.position + YX(1, -1)]
739             self.selectables = [t.id_ for t in self.game.things
740                                 if t.portable and t.position in select_range]
741             if len(self.selectables) == 0:
742                 return fail('nothing to pick-up')
743             else:
744                 for i in range(len(self.selectables)):
745                     t = self.game.get_thing(self.selectables[i])
746                     self.log_msg(str(i) + ': ' + self.get_thing_info(t))
747         elif self.mode.name == 'drop_thing':
748             self.log_msg('Direction to drop thing to:')
749             self.selectables =\
750                 ['HERE'] + list(self.game.tui.movement_keys.values())
751             for i in range(len(self.selectables)):
752                 self.log_msg(str(i) + ': ' + self.selectables[i])
753         elif self.mode.name == 'enter_hat':
754             self.log_msg('legal characters: ' + self.game.players_hat_chars)
755         elif self.mode.name == 'command_thing':
756             self.send('TASK:COMMAND ' + quote('HELP'))
757         elif self.mode.name == 'control_pw_pw':
758             self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
759         elif self.mode.name == 'control_tile_draw':
760             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']))
761         self.input_ = ""
762         self.restore_input_values()
763
764     def set_default_colors(self):
765         curses.init_color(1, 1000, 1000, 1000)
766         curses.init_color(2, 0, 0, 0)
767         self.do_refresh = True
768
769     def set_random_colors(self):
770
771         def rand(offset):
772             import random
773             return int(offset + random.random()*375)
774
775         curses.init_color(1, rand(625), rand(625), rand(625))
776         curses.init_color(2, rand(0), rand(0), rand(0))
777         self.do_refresh = True
778
779     def get_info(self):
780         if self.info_cached:
781             return self.info_cached
782         pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
783         info_to_cache = ''
784         if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
785             info_to_cache += 'outside field of view'
786         else:
787             for t in self.game.things:
788                 if t.position == self.explorer:
789                     info_to_cache += 'THING: %s' % self.get_thing_info(t)
790                     protection = t.protection
791                     if protection == '.':
792                         protection = 'none'
793                     info_to_cache += ' / protection: %s\n' % protection
794                     if hasattr(t, 'hat'):
795                         info_to_cache += t.hat[0:6] + '\n'
796                         info_to_cache += t.hat[6:12] + '\n'
797                         info_to_cache += t.hat[12:18] + '\n'
798                     if hasattr(t, 'face'):
799                         info_to_cache += t.face[0:6] + '\n'
800                         info_to_cache += t.face[6:12] + '\n'
801                         info_to_cache += t.face[12:18] + '\n'
802             terrain_char = self.game.map_content[pos_i]
803             terrain_desc = '?'
804             if terrain_char in self.game.terrains:
805                 terrain_desc = self.game.terrains[terrain_char]
806             info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
807                                                        terrain_desc)
808             protection = self.game.map_control_content[pos_i]
809             if protection == '.':
810                 protection = 'unprotected'
811             info_to_cache += 'PROTECTION: %s\n' % protection
812             if self.explorer in self.game.portals:
813                 info_to_cache += 'PORTAL: ' +\
814                     self.game.portals[self.explorer] + '\n'
815             else:
816                 info_to_cache += 'PORTAL: (none)\n'
817             if self.explorer in self.game.annotations:
818                 info_to_cache += 'ANNOTATION: ' +\
819                     self.game.annotations[self.explorer]
820         self.info_cached = info_to_cache
821         return self.info_cached
822
823     def get_thing_info(self, t):
824         info = '%s / %s' %\
825             (t.type_, self.game.thing_types[t.type_])
826         if hasattr(t, 'thing_char'):
827             info += t.thing_char
828         if hasattr(t, 'name'):
829             info += ' (%s)' % t.name
830         if hasattr(t, 'installed'):
831             info += ' / installed'
832         return info
833
834     def loop(self, stdscr):
835         import datetime
836
837         def safe_addstr(y, x, line):
838             if y < self.size.y - 1 or x + len(line) < self.size.x:
839                 stdscr.addstr(y, x, line, curses.color_pair(1))
840             else:  # workaround to <https://stackoverflow.com/q/7063128>
841                 cut_i = self.size.x - x - 1
842                 cut = line[:cut_i]
843                 last_char = line[cut_i]
844                 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
845                 stdscr.insstr(y, self.size.x - 2, ' ')
846                 stdscr.addstr(y, x, cut, curses.color_pair(1))
847
848         def handle_input(msg):
849             command, args = self.parser.parse(msg)
850             command(*args)
851
852         def task_action_on(action):
853             return action_tasks[action] in self.game.tasks
854
855         def msg_into_lines_of_width(msg, width):
856             chunk = ''
857             lines = []
858             x = 0
859             for i in range(len(msg)):
860                 if x >= width or msg[i] == "\n":
861                     lines += [chunk]
862                     chunk = ''
863                     x = 0
864                     if msg[i] == "\n":
865                         x -= 1
866                 if msg[i] != "\n":
867                     chunk += msg[i]
868                 x += 1
869             lines += [chunk]
870             return lines
871
872         def reset_screen_size():
873             self.size = YX(*stdscr.getmaxyx())
874             self.size = self.size - YX(self.size.y % 4, 0)
875             self.size = self.size - YX(0, self.size.x % 4)
876             self.window_width = int(self.size.x / 2)
877
878         def recalc_input_lines():
879             if not self.mode.has_input_prompt:
880                 self.input_lines = []
881             else:
882                 self.input_lines = msg_into_lines_of_width(input_prompt
883                                                            + self.input_ + '█',
884                                                            self.window_width)
885
886         def move_explorer(direction):
887             target = self.game.map_geometry.move_yx(self.explorer, direction)
888             if target:
889                 self.info_cached = None
890                 self.explorer = target
891                 if self.tile_draw:
892                     self.send_tile_control_command()
893             else:
894                 self.flash = True
895
896         def draw_history():
897             lines = []
898             for line in self.log:
899                 lines += msg_into_lines_of_width(line, self.window_width)
900             lines.reverse()
901             height_header = 2
902             max_y = self.size.y - len(self.input_lines)
903             for i in range(len(lines)):
904                 if (i >= max_y - height_header):
905                     break
906                 safe_addstr(max_y - i - 1, self.window_width, lines[i])
907
908         def draw_info():
909             info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
910             lines = msg_into_lines_of_width(info, self.window_width)
911             height_header = 2
912             for i in range(len(lines)):
913                 y = height_header + i
914                 if y >= self.size.y - len(self.input_lines):
915                     break
916                 safe_addstr(y, self.window_width, lines[i])
917
918         def draw_input():
919             y = self.size.y - len(self.input_lines)
920             for i in range(len(self.input_lines)):
921                 safe_addstr(y, self.window_width, self.input_lines[i])
922                 y += 1
923
924         def draw_turn():
925             if not self.game.turn_complete:
926                 return
927             safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
928
929         def draw_mode():
930             help = "hit [%s] for help" % self.keys['help']
931             if self.mode.has_input_prompt:
932                 help = "enter /help for help"
933             safe_addstr(1, self.window_width,
934                         'MODE: %s – %s' % (self.mode.short_desc, help))
935
936         def draw_map():
937             if (not self.game.turn_complete) and len(self.map_lines) == 0:
938                 return
939             if self.game.turn_complete:
940                 map_lines_split = []
941                 for y in range(self.game.map_geometry.size.y):
942                     start = self.game.map_geometry.size.x * y
943                     end = start + self.game.map_geometry.size.x
944                     if self.map_mode == 'protections':
945                         map_lines_split += [[c + ' ' for c
946                                              in self.game.map_control_content[start:end]]]
947                     else:
948                         map_lines_split += [[c + ' ' for c
949                                              in self.game.map_content[start:end]]]
950                 if self.map_mode == 'terrain + annotations':
951                     for p in self.game.annotations:
952                         map_lines_split[p.y][p.x] = 'A '
953                 elif self.map_mode == 'terrain + things':
954                     for p in self.game.portals.keys():
955                         original = map_lines_split[p.y][p.x]
956                         map_lines_split[p.y][p.x] = original[0] + 'P'
957                     used_positions = []
958
959                     def draw_thing(t, used_positions):
960                         symbol = self.game.thing_types[t.type_]
961                         meta_char = ' '
962                         if hasattr(t, 'thing_char'):
963                             meta_char = t.thing_char
964                         if t.position in used_positions:
965                             meta_char = '+'
966                         if hasattr(t, 'carrying') and t.carrying:
967                             meta_char = '$'
968                         map_lines_split[t.position.y][t.position.x] = symbol + meta_char
969                         used_positions += [t.position]
970
971                     for t in [t for t in self.game.things if t.type_ != 'Player']:
972                         draw_thing(t, used_positions)
973                     for t in [t for t in self.game.things if t.type_ == 'Player']:
974                         draw_thing(t, used_positions)
975                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
976                     map_lines_split[self.explorer.y][self.explorer.x] = '??'
977                 elif self.map_mode != 'terrain + things':
978                     map_lines_split[self.game.player.position.y]\
979                         [self.game.player.position.x] = '??'
980                 self.map_lines = []
981                 if type(self.game.map_geometry) == MapGeometryHex:
982                     indent = 0
983                     for line in map_lines_split:
984                         self.map_lines += [indent * ' ' + ''.join(line)]
985                         indent = 0 if indent else 1
986                 else:
987                     for line in map_lines_split:
988                         self.map_lines += [''.join(line)]
989                 window_center = YX(int(self.size.y / 2),
990                                    int(self.window_width / 2))
991                 center = self.game.player.position
992                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
993                     center = self.explorer
994                 center = YX(center.y, center.x * 2)
995                 self.offset = center - window_center
996                 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
997                     self.offset += YX(0, 1)
998             term_y = max(0, -self.offset.y)
999             term_x = max(0, -self.offset.x)
1000             map_y = max(0, self.offset.y)
1001             map_x = max(0, self.offset.x)
1002             while term_y < self.size.y and map_y < len(self.map_lines):
1003                 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1004                 safe_addstr(term_y, term_x, to_draw)
1005                 term_y += 1
1006                 map_y += 1
1007
1008         def draw_face_popup():
1009             t = self.game.get_thing(self.draw_face)
1010             if not t or not hasattr(t, 'face'):
1011                 self.draw_face = False
1012                 return
1013
1014             start_x = self.window_width - 10
1015             t_char = ' '
1016             if hasattr(t, 'thing_char'):
1017                 t_char = t.thing_char
1018             def draw_body_part(body_part, end_y):
1019                 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1020                 safe_addstr(end_y - 3, start_x, '|        |')
1021                 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1022                 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1023                 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1024
1025             if hasattr(t, 'face'):
1026                 draw_body_part(t.face, self.size.y - 2)
1027             if hasattr(t, 'hat'):
1028                 draw_body_part(t.hat, self.size.y - 5)
1029             safe_addstr(self.size.y - 1, start_x, '|        |')
1030
1031         def draw_help():
1032             content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1033                                              self.mode.help_intro)
1034             if len(self.mode.available_actions) > 0:
1035                 content += "Available actions:\n"
1036                 for action in self.mode.available_actions:
1037                     if action in action_tasks:
1038                         if action_tasks[action] not in self.game.tasks:
1039                             continue
1040                     if action == 'move_explorer':
1041                         action = 'move'
1042                     if action == 'move':
1043                         key = ','.join(self.movement_keys)
1044                     else:
1045                         key = self.keys[action]
1046                     content += '[%s] – %s\n' % (key, action_descriptions[action])
1047                 content += '\n'
1048             content += self.mode.list_available_modes(self)
1049             for i in range(self.size.y):
1050                 safe_addstr(i,
1051                             self.window_width * (not self.mode.has_input_prompt),
1052                             ' ' * self.window_width)
1053             lines = []
1054             for line in content.split('\n'):
1055                 lines += msg_into_lines_of_width(line, self.window_width)
1056             for i in range(len(lines)):
1057                 if i >= self.size.y:
1058                     break
1059                 safe_addstr(i,
1060                             self.window_width * (not self.mode.has_input_prompt),
1061                             lines[i])
1062
1063         def draw_screen():
1064             stdscr.clear()
1065             stdscr.bkgd(' ', curses.color_pair(1))
1066             recalc_input_lines()
1067             if self.mode.has_input_prompt:
1068                 draw_input()
1069             if self.mode.shows_info:
1070                 draw_info()
1071             else:
1072                 draw_history()
1073             draw_mode()
1074             if not self.mode.is_intro:
1075                 draw_turn()
1076                 draw_map()
1077             if self.show_help:
1078                 draw_help()
1079             if self.draw_face and self.mode.name in {'chat', 'play'}:
1080                 draw_face_popup()
1081
1082         def pick_selectable(task_name):
1083             try:
1084                 i = int(self.input_)
1085                 if i < 0 or i >= len(self.selectables):
1086                     self.log_msg('? invalid index, aborted')
1087                 else:
1088                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1089             except ValueError:
1090                 self.log_msg('? invalid index, aborted')
1091             self.input_ = ''
1092             self.switch_mode('play')
1093
1094         def enter_ascii_art(command):
1095             if len(self.input_) != 6:
1096                 self.log_msg('? wrong input length, must be 6; try again')
1097                 return
1098             self.log_msg('  ' + self.input_)
1099             self.full_ascii_draw += self.input_
1100             self.ascii_draw_stage += 1
1101             if self.ascii_draw_stage < 3:
1102                 self.restore_input_values()
1103             else:
1104                 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1105                 self.full_ascii_draw = ""
1106                 self.ascii_draw_stage = 0
1107                 self.input_ = ""
1108                 self.switch_mode('edit')
1109
1110         action_descriptions = {
1111             'move': 'move',
1112             'flatten': 'flatten surroundings',
1113             'teleport': 'teleport',
1114             'take_thing': 'pick up thing',
1115             'drop_thing': 'drop thing',
1116             'toggle_map_mode': 'toggle map view',
1117             'toggle_tile_draw': 'toggle protection character drawing',
1118             'install': '(un-)install',
1119             'wear': '(un-)wear',
1120             'door': 'open/close',
1121             'consume': 'consume',
1122             'spin': 'spin',
1123         }
1124
1125         action_tasks = {
1126             'flatten': 'FLATTEN_SURROUNDINGS',
1127             'take_thing': 'PICK_UP',
1128             'drop_thing': 'DROP',
1129             'door': 'DOOR',
1130             'install': 'INSTALL',
1131             'wear': 'WEAR',
1132             'move': 'MOVE',
1133             'command': 'COMMAND',
1134             'consume': 'INTOXICATE',
1135             'spin': 'SPIN',
1136         }
1137
1138         curses.curs_set(False)  # hide cursor
1139         curses.start_color()
1140         self.set_default_colors()
1141         curses.init_pair(1, 1, 2)
1142         stdscr.timeout(10)
1143         reset_screen_size()
1144         self.explorer = YX(0, 0)
1145         self.input_ = ''
1146         input_prompt = '> '
1147         interval = datetime.timedelta(seconds=5)
1148         last_ping = datetime.datetime.now() - interval
1149         while True:
1150             if self.disconnected and self.force_instant_connect:
1151                 self.force_instant_connect = False
1152                 self.connect()
1153             now = datetime.datetime.now()
1154             if now - last_ping > interval:
1155                 if self.disconnected:
1156                     self.connect()
1157                 else:
1158                     self.send('PING')
1159                 last_ping = now
1160             if self.flash:
1161                 curses.flash()
1162                 self.flash = False
1163             if self.do_refresh:
1164                 draw_screen()
1165                 self.do_refresh = False
1166             while True:
1167                 try:
1168                     msg = self.queue.get(block=False)
1169                     handle_input(msg)
1170                 except queue.Empty:
1171                     break
1172             try:
1173                 key = stdscr.getkey()
1174                 self.do_refresh = True
1175             except curses.error:
1176                 continue
1177             keycode = None
1178             if len(key) == 1:
1179                 keycode = ord(key)
1180             self.show_help = False
1181             self.draw_face = False
1182             if key == 'KEY_RESIZE':
1183                 reset_screen_size()
1184             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1185                 self.input_ = self.input_[:-1]
1186             elif (((not self.mode.is_intro) and keycode == 27)  # Escape
1187                   or (self.mode.has_input_prompt and key == '\n'
1188                       and self.input_ == ''\
1189                       and self.mode.name in {'chat', 'command_thing',
1190                                              'take_thing', 'drop_thing',
1191                                              'admin_enter'})):
1192                 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1193                     self.log_msg('@ aborted')
1194                 self.switch_mode('play')
1195             elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1196                 self.show_help = True
1197                 self.input_ = ""
1198                 self.restore_input_values()
1199             elif self.mode.has_input_prompt and key != '\n':  # Return key
1200                 self.input_ += key
1201                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1202                 if len(self.input_) > max_length:
1203                     self.input_ = self.input_[:max_length]
1204             elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1205                 self.show_help = True
1206             elif self.mode.name == 'login' and key == '\n':
1207                 self.login_name = self.input_
1208                 self.send('LOGIN ' + quote(self.input_))
1209                 self.input_ = ""
1210             elif self.mode.name == 'enter_face' and key == '\n':
1211                 enter_ascii_art('PLAYER_FACE')
1212             elif self.mode.name == 'enter_hat' and key == '\n':
1213                 enter_ascii_art('PLAYER_HAT')
1214             elif self.mode.name == 'take_thing' and key == '\n':
1215                 pick_selectable('PICK_UP')
1216             elif self.mode.name == 'drop_thing' and key == '\n':
1217                 pick_selectable('DROP')
1218             elif self.mode.name == 'command_thing' and key == '\n':
1219                 self.send('TASK:COMMAND ' + quote(self.input_))
1220                 self.input_ = ""
1221             elif self.mode.name == 'control_pw_pw' and key == '\n':
1222                 if self.input_ == '':
1223                     self.log_msg('@ aborted')
1224                 else:
1225                     self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1226                     self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1227                 self.switch_mode('admin')
1228             elif self.mode.name == 'password' and key == '\n':
1229                 if self.input_ == '':
1230                     self.input_ = ' '
1231                 self.password = self.input_
1232                 self.switch_mode('edit')
1233             elif self.mode.name == 'admin_enter' and key == '\n':
1234                 self.send('BECOME_ADMIN ' + quote(self.input_))
1235                 self.switch_mode('play')
1236             elif self.mode.name == 'control_pw_type' and key == '\n':
1237                 if len(self.input_) != 1:
1238                     self.log_msg('@ entered non-single-char, therefore aborted')
1239                     self.switch_mode('admin')
1240                 else:
1241                     self.tile_control_char = self.input_
1242                     self.switch_mode('control_pw_pw')
1243             elif self.mode.name == 'admin_thing_protect' and key == '\n':
1244                 if len(self.input_) != 1:
1245                     self.log_msg('@ entered non-single-char, therefore aborted')
1246                 else:
1247                     self.send('THING_PROTECTION %s' % (quote(self.input_)))
1248                     self.log_msg('@ sent new protection character for thing')
1249                 self.switch_mode('admin')
1250             elif self.mode.name == 'control_tile_type' and key == '\n':
1251                 if len(self.input_) != 1:
1252                     self.log_msg('@ entered non-single-char, therefore aborted')
1253                     self.switch_mode('admin')
1254                 else:
1255                     self.tile_control_char = self.input_
1256                     self.switch_mode('control_tile_draw')
1257             elif self.mode.name == 'chat' and key == '\n':
1258                 if self.input_ == '':
1259                     continue
1260                 if self.input_[0] == '/':
1261                     if self.input_.startswith('/nick'):
1262                         tokens = self.input_.split(maxsplit=1)
1263                         if len(tokens) == 2:
1264                             self.send('NICK ' + quote(tokens[1]))
1265                         else:
1266                             self.log_msg('? need login name')
1267                     else:
1268                         self.log_msg('? unknown command')
1269                 else:
1270                     self.send('ALL ' + quote(self.input_))
1271                 self.input_ = ""
1272             elif self.mode.name == 'name_thing' and key == '\n':
1273                 if self.input_ == '':
1274                     self.input_ = ' '
1275                 self.send('THING_NAME %s %s' % (quote(self.input_),
1276                                                 quote(self.password)))
1277                 self.switch_mode('edit')
1278             elif self.mode.name == 'annotate' and key == '\n':
1279                 if self.input_ == '':
1280                     self.input_ = ' '
1281                 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1282                                                  quote(self.password)))
1283                 self.switch_mode('edit')
1284             elif self.mode.name == 'portal' and key == '\n':
1285                 if self.input_ == '':
1286                     self.input_ = ' '
1287                 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1288                                                quote(self.password)))
1289                 self.switch_mode('edit')
1290             elif self.mode.name == 'study':
1291                 if self.mode.mode_switch_on_key(self, key):
1292                     continue
1293                 elif key == self.keys['toggle_map_mode']:
1294                     self.toggle_map_mode()
1295                 elif key in self.movement_keys:
1296                     move_explorer(self.movement_keys[key])
1297             elif self.mode.name == 'play':
1298                 if self.mode.mode_switch_on_key(self, key):
1299                     continue
1300                 elif key == self.keys['door'] and task_action_on('door'):
1301                     self.send('TASK:DOOR')
1302                 elif key == self.keys['consume'] and task_action_on('consume'):
1303                     self.send('TASK:INTOXICATE')
1304                 elif key == self.keys['wear'] and task_action_on('wear'):
1305                     self.send('TASK:WEAR')
1306                 elif key == self.keys['spin'] and task_action_on('spin'):
1307                     self.send('TASK:SPIN')
1308                 elif key == self.keys['teleport']:
1309                     if self.game.player.position in self.game.portals:
1310                         self.host = self.game.portals[self.game.player.position]
1311                         self.reconnect()
1312                     else:
1313                         self.flash = True
1314                         self.log_msg('? not standing on portal')
1315                 elif key in self.movement_keys and task_action_on('move'):
1316                     self.send('TASK:MOVE ' + self.movement_keys[key])
1317             elif self.mode.name == 'write':
1318                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1319                 self.switch_mode('edit')
1320             elif self.mode.name == 'control_tile_draw':
1321                 if self.mode.mode_switch_on_key(self, key):
1322                     continue
1323                 elif key in self.movement_keys:
1324                     move_explorer(self.movement_keys[key])
1325                 elif key == self.keys['toggle_tile_draw']:
1326                     self.tile_draw = False if self.tile_draw else True
1327             elif self.mode.name == 'admin':
1328                 if self.mode.mode_switch_on_key(self, key):
1329                     continue
1330                 elif key in self.movement_keys and task_action_on('move'):
1331                     self.send('TASK:MOVE ' + self.movement_keys[key])
1332             elif self.mode.name == 'edit':
1333                 if self.mode.mode_switch_on_key(self, key):
1334                     continue
1335                 elif key == self.keys['flatten'] and task_action_on('flatten'):
1336                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1337                 elif key == self.keys['install'] and task_action_on('install'):
1338                     self.send('TASK:INSTALL %s' % quote(self.password))
1339                 elif key == self.keys['toggle_map_mode']:
1340                     self.toggle_map_mode()
1341                 elif key in self.movement_keys and task_action_on('move'):
1342                     self.send('TASK:MOVE ' + self.movement_keys[key])
1343
1344 if len(sys.argv) != 2:
1345     raise ArgError('wrong number of arguments, need game host')
1346 host = sys.argv[1]
1347 TUI(host)