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