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