home · contact · privacy
65d5385ee2d20d17f5fb117a4ac82ea987163445
[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             directed_moves = {
728                 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
729             }
730             if type(self.game.map_geometry) == MapGeometrySquare:
731                 directed_moves['UP'] = YX(-1, 0)
732                 directed_moves['DOWN'] = YX(1, 0)
733             elif type(self.game.map_geometry) == MapGeometryHex:
734                 if self.game.player.position.y % 2:
735                     directed_moves['UPLEFT'] = YX(-1, 0)
736                     directed_moves['UPRIGHT'] = YX(-1, 1)
737                     directed_moves['DOWNLEFT'] = YX(1, 0)
738                     directed_moves['DOWNRIGHT'] = YX(1, 1)
739                 else:
740                     directed_moves['UPLEFT'] = YX(-1, -1)
741                     directed_moves['UPRIGHT'] = YX(-1, 0)
742                     directed_moves['DOWNLEFT'] = YX(1, -1)
743                     directed_moves['DOWNRIGHT'] = YX(1, 0)
744             select_range = {}
745             for direction in directed_moves:
746                 move = directed_moves[direction]
747                 select_range[direction] = self.game.player.position + move
748             self.selectables = []
749             directions = []
750             for direction in select_range:
751                 for t in [t for t in self.game.things
752                           if t.portable and t.position == select_range[direction]]:
753                     self.selectables += [t.id_]
754                     directions += [direction]
755             if len(self.selectables) == 0:
756                 return fail('nothing to pick-up')
757             else:
758                 for i in range(len(self.selectables)):
759                     t = self.game.get_thing(self.selectables[i])
760                     self.log_msg('%s %s: %s' % (i, directions[i],
761                                                 self.get_thing_info(t)))
762         elif self.mode.name == 'drop_thing':
763             self.log_msg('Direction to drop thing to:')
764             self.selectables =\
765                 ['HERE'] + list(self.game.tui.movement_keys.values())
766             for i in range(len(self.selectables)):
767                 self.log_msg(str(i) + ': ' + self.selectables[i])
768         elif self.mode.name == 'enter_hat':
769             self.log_msg('legal characters: ' + self.game.players_hat_chars)
770         elif self.mode.name == 'command_thing':
771             self.send('TASK:COMMAND ' + quote('HELP'))
772         elif self.mode.name == 'control_pw_pw':
773             self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
774         elif self.mode.name == 'control_tile_draw':
775             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']))
776         self.input_ = ""
777         self.restore_input_values()
778
779     def set_default_colors(self):
780         curses.init_color(1, 1000, 1000, 1000)
781         curses.init_color(2, 0, 0, 0)
782         self.do_refresh = True
783
784     def set_random_colors(self):
785
786         def rand(offset):
787             import random
788             return int(offset + random.random()*375)
789
790         curses.init_color(1, rand(625), rand(625), rand(625))
791         curses.init_color(2, rand(0), rand(0), rand(0))
792         self.do_refresh = True
793
794     def get_info(self):
795         if self.info_cached:
796             return self.info_cached
797         pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
798         info_to_cache = ''
799         if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
800             info_to_cache += 'outside field of view'
801         else:
802             for t in self.game.things:
803                 if t.position == self.explorer:
804                     info_to_cache += 'THING: %s' % self.get_thing_info(t)
805                     protection = t.protection
806                     if protection == '.':
807                         protection = 'none'
808                     info_to_cache += ' / protection: %s\n' % protection
809                     if hasattr(t, 'hat'):
810                         info_to_cache += t.hat[0:6] + '\n'
811                         info_to_cache += t.hat[6:12] + '\n'
812                         info_to_cache += t.hat[12:18] + '\n'
813                     if hasattr(t, 'face'):
814                         info_to_cache += t.face[0:6] + '\n'
815                         info_to_cache += t.face[6:12] + '\n'
816                         info_to_cache += t.face[12:18] + '\n'
817             terrain_char = self.game.map_content[pos_i]
818             terrain_desc = '?'
819             if terrain_char in self.game.terrains:
820                 terrain_desc = self.game.terrains[terrain_char]
821             info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
822                                                        terrain_desc)
823             protection = self.game.map_control_content[pos_i]
824             if protection == '.':
825                 protection = 'unprotected'
826             info_to_cache += 'PROTECTION: %s\n' % protection
827             if self.explorer in self.game.portals:
828                 info_to_cache += 'PORTAL: ' +\
829                     self.game.portals[self.explorer] + '\n'
830             else:
831                 info_to_cache += 'PORTAL: (none)\n'
832             if self.explorer in self.game.annotations:
833                 info_to_cache += 'ANNOTATION: ' +\
834                     self.game.annotations[self.explorer]
835         self.info_cached = info_to_cache
836         return self.info_cached
837
838     def get_thing_info(self, t):
839         info = '%s / %s' %\
840             (t.type_, self.game.thing_types[t.type_])
841         if hasattr(t, 'thing_char'):
842             info += t.thing_char
843         if hasattr(t, 'name'):
844             info += ' (%s)' % t.name
845         if hasattr(t, 'installed'):
846             info += ' / installed'
847         return info
848
849     def loop(self, stdscr):
850         import datetime
851
852         def safe_addstr(y, x, line):
853             if y < self.size.y - 1 or x + len(line) < self.size.x:
854                 stdscr.addstr(y, x, line, curses.color_pair(1))
855             else:  # workaround to <https://stackoverflow.com/q/7063128>
856                 cut_i = self.size.x - x - 1
857                 cut = line[:cut_i]
858                 last_char = line[cut_i]
859                 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
860                 stdscr.insstr(y, self.size.x - 2, ' ')
861                 stdscr.addstr(y, x, cut, curses.color_pair(1))
862
863         def handle_input(msg):
864             command, args = self.parser.parse(msg)
865             command(*args)
866
867         def task_action_on(action):
868             return action_tasks[action] in self.game.tasks
869
870         def msg_into_lines_of_width(msg, width):
871             chunk = ''
872             lines = []
873             x = 0
874             for i in range(len(msg)):
875                 if x >= width or msg[i] == "\n":
876                     lines += [chunk]
877                     chunk = ''
878                     x = 0
879                     if msg[i] == "\n":
880                         x -= 1
881                 if msg[i] != "\n":
882                     chunk += msg[i]
883                 x += 1
884             lines += [chunk]
885             return lines
886
887         def reset_screen_size():
888             self.size = YX(*stdscr.getmaxyx())
889             self.size = self.size - YX(self.size.y % 4, 0)
890             self.size = self.size - YX(0, self.size.x % 4)
891             self.window_width = int(self.size.x / 2)
892
893         def recalc_input_lines():
894             if not self.mode.has_input_prompt:
895                 self.input_lines = []
896             else:
897                 self.input_lines = msg_into_lines_of_width(input_prompt
898                                                            + self.input_ + '█',
899                                                            self.window_width)
900
901         def move_explorer(direction):
902             target = self.game.map_geometry.move_yx(self.explorer, direction)
903             if target:
904                 self.info_cached = None
905                 self.explorer = target
906                 if self.tile_draw:
907                     self.send_tile_control_command()
908             else:
909                 self.flash = True
910
911         def draw_history():
912             lines = []
913             for line in self.log:
914                 lines += msg_into_lines_of_width(line, self.window_width)
915             lines.reverse()
916             height_header = 2
917             max_y = self.size.y - len(self.input_lines)
918             for i in range(len(lines)):
919                 if (i >= max_y - height_header):
920                     break
921                 safe_addstr(max_y - i - 1, self.window_width, lines[i])
922
923         def draw_info():
924             info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
925             lines = msg_into_lines_of_width(info, self.window_width)
926             height_header = 2
927             for i in range(len(lines)):
928                 y = height_header + i
929                 if y >= self.size.y - len(self.input_lines):
930                     break
931                 safe_addstr(y, self.window_width, lines[i])
932
933         def draw_input():
934             y = self.size.y - len(self.input_lines)
935             for i in range(len(self.input_lines)):
936                 safe_addstr(y, self.window_width, self.input_lines[i])
937                 y += 1
938
939         def draw_turn():
940             if not self.game.turn_complete:
941                 return
942             safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
943
944         def draw_mode():
945             help = "hit [%s] for help" % self.keys['help']
946             if self.mode.has_input_prompt:
947                 help = "enter /help for help"
948             safe_addstr(1, self.window_width,
949                         'MODE: %s – %s' % (self.mode.short_desc, help))
950
951         def draw_map():
952             if (not self.game.turn_complete) and len(self.map_lines) == 0:
953                 return
954             if self.game.turn_complete:
955                 map_lines_split = []
956                 for y in range(self.game.map_geometry.size.y):
957                     start = self.game.map_geometry.size.x * y
958                     end = start + self.game.map_geometry.size.x
959                     if self.map_mode == 'protections':
960                         map_lines_split += [[c + ' ' for c
961                                              in self.game.map_control_content[start:end]]]
962                     else:
963                         map_lines_split += [[c + ' ' for c
964                                              in self.game.map_content[start:end]]]
965                 if self.map_mode == 'terrain + annotations':
966                     for p in self.game.annotations:
967                         map_lines_split[p.y][p.x] = 'A '
968                 elif self.map_mode == 'terrain + things':
969                     for p in self.game.portals.keys():
970                         original = map_lines_split[p.y][p.x]
971                         map_lines_split[p.y][p.x] = original[0] + 'P'
972                     used_positions = []
973
974                     def draw_thing(t, used_positions):
975                         symbol = self.game.thing_types[t.type_]
976                         meta_char = ' '
977                         if hasattr(t, 'thing_char'):
978                             meta_char = t.thing_char
979                         if t.position in used_positions:
980                             meta_char = '+'
981                         if hasattr(t, 'carrying') and t.carrying:
982                             meta_char = '$'
983                         map_lines_split[t.position.y][t.position.x] = symbol + meta_char
984                         used_positions += [t.position]
985
986                     for t in [t for t in self.game.things if t.type_ != 'Player']:
987                         draw_thing(t, used_positions)
988                     for t in [t for t in self.game.things if t.type_ == 'Player']:
989                         draw_thing(t, used_positions)
990                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
991                     map_lines_split[self.explorer.y][self.explorer.x] = '??'
992                 elif self.map_mode != 'terrain + things':
993                     map_lines_split[self.game.player.position.y]\
994                         [self.game.player.position.x] = '??'
995                 self.map_lines = []
996                 if type(self.game.map_geometry) == MapGeometryHex:
997                     indent = 0
998                     for line in map_lines_split:
999                         self.map_lines += [indent * ' ' + ''.join(line)]
1000                         indent = 0 if indent else 1
1001                 else:
1002                     for line in map_lines_split:
1003                         self.map_lines += [''.join(line)]
1004                 window_center = YX(int(self.size.y / 2),
1005                                    int(self.window_width / 2))
1006                 center = self.game.player.position
1007                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1008                     center = self.explorer
1009                 center = YX(center.y, center.x * 2)
1010                 self.offset = center - window_center
1011                 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1012                     self.offset += YX(0, 1)
1013             term_y = max(0, -self.offset.y)
1014             term_x = max(0, -self.offset.x)
1015             map_y = max(0, self.offset.y)
1016             map_x = max(0, self.offset.x)
1017             while term_y < self.size.y and map_y < len(self.map_lines):
1018                 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1019                 safe_addstr(term_y, term_x, to_draw)
1020                 term_y += 1
1021                 map_y += 1
1022
1023         def draw_face_popup():
1024             t = self.game.get_thing(self.draw_face)
1025             if not t or not hasattr(t, 'face'):
1026                 self.draw_face = False
1027                 return
1028
1029             start_x = self.window_width - 10
1030             t_char = ' '
1031             if hasattr(t, 'thing_char'):
1032                 t_char = t.thing_char
1033             def draw_body_part(body_part, end_y):
1034                 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1035                 safe_addstr(end_y - 3, start_x, '|        |')
1036                 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1037                 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1038                 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1039
1040             if hasattr(t, 'face'):
1041                 draw_body_part(t.face, self.size.y - 2)
1042             if hasattr(t, 'hat'):
1043                 draw_body_part(t.hat, self.size.y - 5)
1044             safe_addstr(self.size.y - 1, start_x, '|        |')
1045
1046         def draw_help():
1047             content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1048                                              self.mode.help_intro)
1049             if len(self.mode.available_actions) > 0:
1050                 content += "Available actions:\n"
1051                 for action in self.mode.available_actions:
1052                     if action in action_tasks:
1053                         if action_tasks[action] not in self.game.tasks:
1054                             continue
1055                     if action == 'move_explorer':
1056                         action = 'move'
1057                     if action == 'move':
1058                         key = ','.join(self.movement_keys)
1059                     else:
1060                         key = self.keys[action]
1061                     content += '[%s] – %s\n' % (key, action_descriptions[action])
1062                 content += '\n'
1063             content += self.mode.list_available_modes(self)
1064             for i in range(self.size.y):
1065                 safe_addstr(i,
1066                             self.window_width * (not self.mode.has_input_prompt),
1067                             ' ' * self.window_width)
1068             lines = []
1069             for line in content.split('\n'):
1070                 lines += msg_into_lines_of_width(line, self.window_width)
1071             for i in range(len(lines)):
1072                 if i >= self.size.y:
1073                     break
1074                 safe_addstr(i,
1075                             self.window_width * (not self.mode.has_input_prompt),
1076                             lines[i])
1077
1078         def draw_screen():
1079             stdscr.clear()
1080             stdscr.bkgd(' ', curses.color_pair(1))
1081             recalc_input_lines()
1082             if self.mode.has_input_prompt:
1083                 draw_input()
1084             if self.mode.shows_info:
1085                 draw_info()
1086             else:
1087                 draw_history()
1088             draw_mode()
1089             if not self.mode.is_intro:
1090                 draw_turn()
1091                 draw_map()
1092             if self.show_help:
1093                 draw_help()
1094             if self.draw_face and self.mode.name in {'chat', 'play'}:
1095                 draw_face_popup()
1096
1097         def pick_selectable(task_name):
1098             try:
1099                 i = int(self.input_)
1100                 if i < 0 or i >= len(self.selectables):
1101                     self.log_msg('? invalid index, aborted')
1102                 else:
1103                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1104             except ValueError:
1105                 self.log_msg('? invalid index, aborted')
1106             self.input_ = ''
1107             self.switch_mode('play')
1108
1109         def enter_ascii_art(command):
1110             if len(self.input_) != 6:
1111                 self.log_msg('? wrong input length, must be 6; try again')
1112                 return
1113             self.log_msg('  ' + self.input_)
1114             self.full_ascii_draw += self.input_
1115             self.ascii_draw_stage += 1
1116             if self.ascii_draw_stage < 3:
1117                 self.restore_input_values()
1118             else:
1119                 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1120                 self.full_ascii_draw = ""
1121                 self.ascii_draw_stage = 0
1122                 self.input_ = ""
1123                 self.switch_mode('edit')
1124
1125         action_descriptions = {
1126             'move': 'move',
1127             'flatten': 'flatten surroundings',
1128             'teleport': 'teleport',
1129             'take_thing': 'pick up thing',
1130             'drop_thing': 'drop thing',
1131             'toggle_map_mode': 'toggle map view',
1132             'toggle_tile_draw': 'toggle protection character drawing',
1133             'install': '(un-)install',
1134             'wear': '(un-)wear',
1135             'door': 'open/close',
1136             'consume': 'consume',
1137             'spin': 'spin',
1138         }
1139
1140         action_tasks = {
1141             'flatten': 'FLATTEN_SURROUNDINGS',
1142             'take_thing': 'PICK_UP',
1143             'drop_thing': 'DROP',
1144             'door': 'DOOR',
1145             'install': 'INSTALL',
1146             'wear': 'WEAR',
1147             'move': 'MOVE',
1148             'command': 'COMMAND',
1149             'consume': 'INTOXICATE',
1150             'spin': 'SPIN',
1151         }
1152
1153         curses.curs_set(False)  # hide cursor
1154         curses.start_color()
1155         self.set_default_colors()
1156         curses.init_pair(1, 1, 2)
1157         stdscr.timeout(10)
1158         reset_screen_size()
1159         self.explorer = YX(0, 0)
1160         self.input_ = ''
1161         input_prompt = '> '
1162         interval = datetime.timedelta(seconds=5)
1163         last_ping = datetime.datetime.now() - interval
1164         while True:
1165             if self.disconnected and self.force_instant_connect:
1166                 self.force_instant_connect = False
1167                 self.connect()
1168             now = datetime.datetime.now()
1169             if now - last_ping > interval:
1170                 if self.disconnected:
1171                     self.connect()
1172                 else:
1173                     self.send('PING')
1174                 last_ping = now
1175             if self.flash:
1176                 curses.flash()
1177                 self.flash = False
1178             if self.do_refresh:
1179                 draw_screen()
1180                 self.do_refresh = False
1181             while True:
1182                 try:
1183                     msg = self.queue.get(block=False)
1184                     handle_input(msg)
1185                 except queue.Empty:
1186                     break
1187             try:
1188                 key = stdscr.getkey()
1189                 self.do_refresh = True
1190             except curses.error:
1191                 continue
1192             keycode = None
1193             if len(key) == 1:
1194                 keycode = ord(key)
1195             self.show_help = False
1196             self.draw_face = False
1197             if key == 'KEY_RESIZE':
1198                 reset_screen_size()
1199             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1200                 self.input_ = self.input_[:-1]
1201             elif (((not self.mode.is_intro) and keycode == 27)  # Escape
1202                   or (self.mode.has_input_prompt and key == '\n'
1203                       and self.input_ == ''\
1204                       and self.mode.name in {'chat', 'command_thing',
1205                                              'take_thing', 'drop_thing',
1206                                              'admin_enter'})):
1207                 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1208                     self.log_msg('@ aborted')
1209                 self.switch_mode('play')
1210             elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1211                 self.show_help = True
1212                 self.input_ = ""
1213                 self.restore_input_values()
1214             elif self.mode.has_input_prompt and key != '\n':  # Return key
1215                 self.input_ += key
1216                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1217                 if len(self.input_) > max_length:
1218                     self.input_ = self.input_[:max_length]
1219             elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1220                 self.show_help = True
1221             elif self.mode.name == 'login' and key == '\n':
1222                 self.login_name = self.input_
1223                 self.send('LOGIN ' + quote(self.input_))
1224                 self.input_ = ""
1225             elif self.mode.name == 'enter_face' and key == '\n':
1226                 enter_ascii_art('PLAYER_FACE')
1227             elif self.mode.name == 'enter_hat' and key == '\n':
1228                 enter_ascii_art('PLAYER_HAT')
1229             elif self.mode.name == 'take_thing' and key == '\n':
1230                 pick_selectable('PICK_UP')
1231             elif self.mode.name == 'drop_thing' and key == '\n':
1232                 pick_selectable('DROP')
1233             elif self.mode.name == 'command_thing' and key == '\n':
1234                 self.send('TASK:COMMAND ' + quote(self.input_))
1235                 self.input_ = ""
1236             elif self.mode.name == 'control_pw_pw' and key == '\n':
1237                 if self.input_ == '':
1238                     self.log_msg('@ aborted')
1239                 else:
1240                     self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1241                     self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1242                 self.switch_mode('admin')
1243             elif self.mode.name == 'password' and key == '\n':
1244                 if self.input_ == '':
1245                     self.input_ = ' '
1246                 self.password = self.input_
1247                 self.switch_mode('edit')
1248             elif self.mode.name == 'admin_enter' and key == '\n':
1249                 self.send('BECOME_ADMIN ' + quote(self.input_))
1250                 self.switch_mode('play')
1251             elif self.mode.name == 'control_pw_type' and key == '\n':
1252                 if len(self.input_) != 1:
1253                     self.log_msg('@ entered non-single-char, therefore aborted')
1254                     self.switch_mode('admin')
1255                 else:
1256                     self.tile_control_char = self.input_
1257                     self.switch_mode('control_pw_pw')
1258             elif self.mode.name == 'admin_thing_protect' and key == '\n':
1259                 if len(self.input_) != 1:
1260                     self.log_msg('@ entered non-single-char, therefore aborted')
1261                 else:
1262                     self.send('THING_PROTECTION %s' % (quote(self.input_)))
1263                     self.log_msg('@ sent new protection character for thing')
1264                 self.switch_mode('admin')
1265             elif self.mode.name == 'control_tile_type' and key == '\n':
1266                 if len(self.input_) != 1:
1267                     self.log_msg('@ entered non-single-char, therefore aborted')
1268                     self.switch_mode('admin')
1269                 else:
1270                     self.tile_control_char = self.input_
1271                     self.switch_mode('control_tile_draw')
1272             elif self.mode.name == 'chat' and key == '\n':
1273                 if self.input_ == '':
1274                     continue
1275                 if self.input_[0] == '/':
1276                     if self.input_.startswith('/nick'):
1277                         tokens = self.input_.split(maxsplit=1)
1278                         if len(tokens) == 2:
1279                             self.send('NICK ' + quote(tokens[1]))
1280                         else:
1281                             self.log_msg('? need login name')
1282                     else:
1283                         self.log_msg('? unknown command')
1284                 else:
1285                     self.send('ALL ' + quote(self.input_))
1286                 self.input_ = ""
1287             elif self.mode.name == 'name_thing' and key == '\n':
1288                 if self.input_ == '':
1289                     self.input_ = ' '
1290                 self.send('THING_NAME %s %s' % (quote(self.input_),
1291                                                 quote(self.password)))
1292                 self.switch_mode('edit')
1293             elif self.mode.name == 'annotate' and key == '\n':
1294                 if self.input_ == '':
1295                     self.input_ = ' '
1296                 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1297                                                  quote(self.password)))
1298                 self.switch_mode('edit')
1299             elif self.mode.name == 'portal' and key == '\n':
1300                 if self.input_ == '':
1301                     self.input_ = ' '
1302                 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1303                                                quote(self.password)))
1304                 self.switch_mode('edit')
1305             elif self.mode.name == 'study':
1306                 if self.mode.mode_switch_on_key(self, key):
1307                     continue
1308                 elif key == self.keys['toggle_map_mode']:
1309                     self.toggle_map_mode()
1310                 elif key in self.movement_keys:
1311                     move_explorer(self.movement_keys[key])
1312             elif self.mode.name == 'play':
1313                 if self.mode.mode_switch_on_key(self, key):
1314                     continue
1315                 elif key == self.keys['door'] and task_action_on('door'):
1316                     self.send('TASK:DOOR')
1317                 elif key == self.keys['consume'] and task_action_on('consume'):
1318                     self.send('TASK:INTOXICATE')
1319                 elif key == self.keys['wear'] and task_action_on('wear'):
1320                     self.send('TASK:WEAR')
1321                 elif key == self.keys['spin'] and task_action_on('spin'):
1322                     self.send('TASK:SPIN')
1323                 elif key == self.keys['teleport']:
1324                     if self.game.player.position in self.game.portals:
1325                         self.host = self.game.portals[self.game.player.position]
1326                         self.reconnect()
1327                     else:
1328                         self.flash = True
1329                         self.log_msg('? not standing on portal')
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 == 'write':
1333                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1334                 self.switch_mode('edit')
1335             elif self.mode.name == 'control_tile_draw':
1336                 if self.mode.mode_switch_on_key(self, key):
1337                     continue
1338                 elif key in self.movement_keys:
1339                     move_explorer(self.movement_keys[key])
1340                 elif key == self.keys['toggle_tile_draw']:
1341                     self.tile_draw = False if self.tile_draw else True
1342             elif self.mode.name == 'admin':
1343                 if self.mode.mode_switch_on_key(self, key):
1344                     continue
1345                 elif key in self.movement_keys and task_action_on('move'):
1346                     self.send('TASK:MOVE ' + self.movement_keys[key])
1347             elif self.mode.name == 'edit':
1348                 if self.mode.mode_switch_on_key(self, key):
1349                     continue
1350                 elif key == self.keys['flatten'] and task_action_on('flatten'):
1351                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1352                 elif key == self.keys['install'] and task_action_on('install'):
1353                     self.send('TASK:INSTALL %s' % quote(self.password))
1354                 elif key == self.keys['toggle_map_mode']:
1355                     self.toggle_map_mode()
1356                 elif key in self.movement_keys and task_action_on('move'):
1357                     self.send('TASK:MOVE ' + self.movement_keys[key])
1358
1359 if len(sys.argv) != 2:
1360     raise ArgError('wrong number of arguments, need game host')
1361 host = sys.argv[1]
1362 TUI(host)