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