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