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