home · contact · privacy
888d9ff4aa27fd4ad2c557116f2bda66eb40dba1
[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         super().draw_screen()
1077         self.stdscr.bkgd(' ', curses.color_pair(1))
1078         self.recalc_input_lines()
1079         if self.mode.has_input_prompt:
1080             self.draw_input()
1081         if self.mode.shows_info:
1082             self.draw_info()
1083         else:
1084             self.draw_history()
1085         self.draw_mode()
1086         if not self.mode.is_intro:
1087             self.draw_stats()
1088             self.draw_map()
1089         if self.show_help:
1090             self.draw_help()
1091         if self.mode.name in {'chat', 'play'}:
1092             self.draw_names()
1093             if self.draw_face:
1094                 self.draw_face_popup()
1095
1096     def handle_server_message(self, msg):
1097         command, args = self.parser.parse(msg)
1098         command(*args)
1099
1100     def on_each_loop_start(self):
1101         prev_disconnected = self.socket.disconnected
1102         self.socket.keep_connection_alive()
1103         if prev_disconnected and not self.socket.disconnected:
1104             self.update_on_connect()
1105         if self.flash:
1106             curses.flash()
1107             self.flash = False
1108
1109     def on_key(self, key, keycode):
1110
1111         def task_action_on(action):
1112             return self.action_tasks[action] in self.game.tasks
1113
1114         def move_explorer(direction):
1115             target = self.game.map_geometry.move_yx(self.explorer, direction)
1116             if target:
1117                 self.info_cached = None
1118                 self.explorer = target
1119                 if self.tile_draw:
1120                     self.send_tile_control_command()
1121             else:
1122                 self.flash = True
1123         def pick_selectable(task_name):
1124             try:
1125                 i = int(self.input_)
1126                 if i < 0 or i >= len(self.selectables):
1127                     self.log('? invalid index, aborted')
1128                 else:
1129                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1130             except ValueError:
1131                 self.log('? invalid index, aborted')
1132             self.input_ = ''
1133             self.switch_mode('play')
1134
1135         def enter_ascii_art(command, height, width,
1136                             with_pw=False, with_size=False):
1137             if with_size and self.ascii_draw_stage == 0:
1138                 width = len(self.input_)
1139                 if width > 36:
1140                     self.log('? input too long, must be max 36; try again')
1141                     # TODO: move max width mechanism server-side
1142                     return
1143                 old_size = self.game.player.carrying.design[0]
1144                 if width != old_size.x:
1145                     # TODO: save remaining design?
1146                     self.game.player.carrying.design[1] = ''
1147                     self.game.player.carrying.design[0] = YX(old_size.y, width)
1148             elif len(self.input_) > width:
1149                 self.log('? input too long, '
1150                              'must be max %s; try again' % width)
1151                 return
1152             self.log('  ' + self.input_)
1153             if with_size and self.input_ in {'', ' '}\
1154                and self.ascii_draw_stage > 0:
1155                 height = self.ascii_draw_stage
1156             else:
1157                 if with_size:
1158                     height = self.ascii_draw_stage + 2
1159                 if len(self.input_) < width:
1160                     self.input_ += ' ' * (width - len(self.input_))
1161                 self.full_ascii_draw += self.input_
1162             if with_size:
1163                 old_size = self.game.player.carrying.design[0]
1164                 self.game.player.carrying.design[0] = YX(height, old_size.x)
1165             self.ascii_draw_stage += 1
1166             if self.ascii_draw_stage < height:
1167                 self.restore_input_values()
1168             else:
1169                 if with_pw and with_size:
1170                     self.send('%s_SIZE %s %s' % (command, YX(height, width),
1171                                                  quote(self.password)))
1172                 if with_pw:
1173                     self.send('%s %s %s' % (command, quote(self.full_ascii_draw),
1174                                             quote(self.password)))
1175                 else:
1176                     self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1177                 self.full_ascii_draw = ""
1178                 self.ascii_draw_stage = 0
1179                 self.input_ = ""
1180                 self.switch_mode('edit')
1181
1182         self.show_help = False
1183         self.draw_face = False
1184         if key == 'KEY_RESIZE':
1185             self.reset_size()
1186         elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1187             self.input_ = self.input_[:-1]
1188         elif (((not self.mode.is_intro) and keycode == 27)  # Escape
1189               or (self.mode.has_input_prompt and key == '\n'
1190                   and self.input_ == ''\
1191                   and self.mode.name in {'chat', 'command_thing',
1192                                          'take_thing', 'drop_thing',
1193                                          'admin_enter'})):
1194             if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1195                 self.log('@ aborted')
1196             self.switch_mode('play')
1197         elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1198             self.show_help = True
1199             self.input_ = ""
1200             self.restore_input_values()
1201         elif self.mode.has_input_prompt and key != '\n':  # Return key
1202             self.input_ += key
1203             max_length = self.right_window_width * self.size.y - len(self.input_prompt) - 1
1204             if len(self.input_) > max_length:
1205                 self.input_ = self.input_[:max_length]
1206         elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1207             self.show_help = True
1208         elif self.mode.name == 'login' and key == '\n':
1209             self.login_name = self.input_
1210             self.send('LOGIN ' + quote(self.input_))
1211             self.input_ = ""
1212         elif self.mode.name == 'enter_face' and key == '\n':
1213             enter_ascii_art('PLAYER_FACE', 3, 6)
1214         elif self.mode.name == 'enter_design' and key == '\n':
1215             if self.game.player.carrying.type_ == 'Hat':
1216                 enter_ascii_art('THING_DESIGN',
1217                                 self.game.player.carrying.design[0].y,
1218                                 self.game.player.carrying.design[0].x, True)
1219             else:
1220                 enter_ascii_art('THING_DESIGN',
1221                                 self.game.player.carrying.design[0].y,
1222                                 self.game.player.carrying.design[0].x,
1223                                 True, True)
1224         elif self.mode.name == 'take_thing' and key == '\n':
1225             pick_selectable('PICK_UP')
1226         elif self.mode.name == 'drop_thing' and key == '\n':
1227             pick_selectable('DROP')
1228         elif self.mode.name == 'command_thing' and key == '\n':
1229             self.send('TASK:COMMAND ' + quote(self.input_))
1230             self.input_ = ""
1231         elif self.mode.name == 'control_pw_pw' and key == '\n':
1232             if self.input_ == '':
1233                 self.log('@ aborted')
1234             else:
1235                 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1236                 self.log('@ sent new password for protection character "%s"' % self.tile_control_char)
1237             self.switch_mode('admin')
1238         elif self.mode.name == 'password' and key == '\n':
1239             if self.input_ == '':
1240                 self.input_ = ' '
1241             self.password = self.input_
1242             self.switch_mode('edit')
1243         elif self.mode.name == 'admin_enter' and key == '\n':
1244             self.send('BECOME_ADMIN ' + quote(self.input_))
1245             self.switch_mode('play')
1246         elif self.mode.name == 'control_pw_type' and key == '\n':
1247             if len(self.input_) != 1:
1248                 self.log('@ entered non-single-char, therefore aborted')
1249                 self.switch_mode('admin')
1250             else:
1251                 self.tile_control_char = self.input_
1252                 self.switch_mode('control_pw_pw')
1253         elif self.mode.name == 'admin_thing_protect' and key == '\n':
1254             if len(self.input_) != 1:
1255                 self.log('@ entered non-single-char, therefore aborted')
1256             else:
1257                 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1258                 self.log('@ sent new protection character for thing')
1259             self.switch_mode('admin')
1260         elif self.mode.name == 'control_tile_type' and key == '\n':
1261             if len(self.input_) != 1:
1262                 self.log('@ entered non-single-char, therefore aborted')
1263                 self.switch_mode('admin')
1264             else:
1265                 self.tile_control_char = self.input_
1266                 self.switch_mode('control_tile_draw')
1267         elif self.mode.name == 'chat' and key == '\n':
1268             if self.input_ == '':
1269                 return
1270             if self.input_[0] == '/':
1271                 if self.input_.startswith('/nick'):
1272                     tokens = self.input_.split(maxsplit=1)
1273                     if len(tokens) == 2:
1274                         self.send('NICK ' + quote(tokens[1]))
1275                     else:
1276                         self.log('? need login name')
1277                 else:
1278                     self.log('? unknown command')
1279             else:
1280                 self.send('ALL ' + quote(self.input_))
1281             self.input_ = ""
1282         elif self.mode.name == 'name_thing' and key == '\n':
1283             if self.input_ == '':
1284                 self.input_ = ' '
1285             self.send('THING_NAME %s %s' % (quote(self.input_),
1286                                             quote(self.password)))
1287             self.switch_mode('edit')
1288         elif self.mode.name == 'annotate' and key == '\n':
1289             if self.input_ == '':
1290                 self.input_ = ' '
1291             self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1292                                              quote(self.password)))
1293             self.switch_mode('edit')
1294         elif self.mode.name == 'portal' and key == '\n':
1295             if self.input_ == '':
1296                 self.input_ = ' '
1297             self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1298                                            quote(self.password)))
1299             self.switch_mode('edit')
1300         elif self.mode.name == 'study':
1301             if self.mode.mode_switch_on_key(self, key):
1302                 return
1303             elif key == self.keys['toggle_map_mode']:
1304                 self.toggle_map_mode()
1305             elif key in self.movement_keys:
1306                 move_explorer(self.movement_keys[key])
1307         elif self.mode.name == 'play':
1308             if self.mode.mode_switch_on_key(self, key):
1309                 return
1310             elif key == self.keys['door'] and task_action_on('door'):
1311                 self.send('TASK:DOOR')
1312             elif key == self.keys['consume'] and task_action_on('consume'):
1313                 self.send('TASK:INTOXICATE')
1314             elif key == self.keys['wear'] and task_action_on('wear'):
1315                 self.send('TASK:WEAR')
1316             elif key == self.keys['spin'] and task_action_on('spin'):
1317                 self.send('TASK:SPIN')
1318             elif key == self.keys['dance'] and task_action_on('dance'):
1319                 self.send('TASK:DANCE')
1320             elif key == self.keys['teleport']:
1321                 if self.game.player.position in self.game.portals:
1322                     self.socket.host = self.game.portals[self.game.player.position]
1323                     self.reconnect()
1324                 else:
1325                     self.flash = True
1326                     self.log('? not standing on portal')
1327             elif key in self.movement_keys and task_action_on('move'):
1328                 self.send('TASK:MOVE ' + self.movement_keys[key])
1329         elif self.mode.name == 'write':
1330             self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1331             self.switch_mode('edit')
1332         elif self.mode.name == 'control_tile_draw':
1333             if self.mode.mode_switch_on_key(self, key):
1334                 return
1335             elif key in self.movement_keys:
1336                 move_explorer(self.movement_keys[key])
1337             elif key == self.keys['toggle_tile_draw']:
1338                 self.tile_draw = False if self.tile_draw else True
1339         elif self.mode.name == 'admin':
1340             if self.mode.mode_switch_on_key(self, key):
1341                 return
1342             elif key == self.keys['toggle_map_mode']:
1343                 self.toggle_map_mode()
1344             elif key in self.movement_keys and task_action_on('move'):
1345                 self.send('TASK:MOVE ' + self.movement_keys[key])
1346         elif self.mode.name == 'edit':
1347             if self.mode.mode_switch_on_key(self, key):
1348                 return
1349             elif key == self.keys['flatten'] and task_action_on('flatten'):
1350                 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1351             elif key == self.keys['install'] and task_action_on('install'):
1352                 self.send('TASK:INSTALL %s' % quote(self.password))
1353             elif key == self.keys['toggle_map_mode']:
1354                 self.toggle_map_mode()
1355             elif key in self.movement_keys and task_action_on('move'):
1356                 self.send('TASK:MOVE ' + self.movement_keys[key])
1357
1358 if len(sys.argv) != 2:
1359     raise ArgError('wrong number of arguments, need game host')
1360 host = sys.argv[1]
1361 RogueChatTUI(host)