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