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