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