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