home · contact · privacy
60c46b270b30dac481408f23bc46181f79288732
[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 loop(self):
885
886         def handle_input(msg):
887             command, args = self.parser.parse(msg)
888             command(*args)
889
890         def task_action_on(action):
891             return self.action_tasks[action] in self.game.tasks
892
893         def recalc_input_lines():
894             if not self.mode.has_input_prompt:
895                 self.input_lines = []
896             else:
897                 self.input_lines = msg_into_lines_of_width(self.input_prompt
898                                                            + self.input_ + '█',
899                                                            self.right_window_width)
900
901         def move_explorer(direction):
902             target = self.game.map_geometry.move_yx(self.explorer, direction)
903             if target:
904                 self.info_cached = None
905                 self.explorer = target
906                 if self.tile_draw:
907                     self.send_tile_control_command()
908             else:
909                 self.flash = True
910
911         def draw_history():
912             lines = []
913             for line in self._log:
914                 lines += msg_into_lines_of_width(line, self.right_window_width)
915             lines.reverse()
916             height_header = 2
917             max_y = self.size.y - len(self.input_lines)
918             for i in range(len(lines)):
919                 if (i >= max_y - height_header):
920                     break
921                 self.addstr(max_y - i - 1, self.left_window_width, lines[i])
922
923         def draw_info():
924             info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
925             lines = msg_into_lines_of_width(info, self.right_window_width)
926             height_header = 2
927             for i in range(len(lines)):
928                 y = height_header + i
929                 if y >= self.size.y - len(self.input_lines):
930                     break
931                 self.addstr(y, self.left_window_width, lines[i])
932
933         def draw_input():
934             y = self.size.y - len(self.input_lines)
935             for i in range(len(self.input_lines)):
936                 self.addstr(y, self.left_window_width, self.input_lines[i])
937                 y += 1
938
939         def draw_stats():
940             stats = 'ENERGY: %s BLADDER: %s' % (self.game.energy,
941                                                 self.game.bladder_pressure)
942             self.addstr(0, self.left_window_width, stats)
943
944         def draw_mode():
945             help = "hit [%s] for help" % self.keys['help']
946             if self.mode.has_input_prompt:
947                 help = "enter /help for help"
948             self.addstr(1, self.left_window_width,
949                         'MODE: %s – %s' % (self.mode.short_desc, help))
950
951         def draw_map():
952             if (not self.game.turn_complete) and len(self.map_lines) == 0:
953                 return
954             if self.game.turn_complete:
955                 map_lines_split = []
956                 for y in range(self.game.map_geometry.size.y):
957                     start = self.game.map_geometry.size.x * y
958                     end = start + self.game.map_geometry.size.x
959                     if self.map_mode == 'protections':
960                         map_lines_split += [[c + ' ' for c
961                                              in self.game.map_control_content[start:end]]]
962                     else:
963                         map_lines_split += [[c + ' ' for c
964                                              in self.game.map_content[start:end]]]
965                 if self.map_mode == 'terrain + annotations':
966                     for p in self.game.annotations:
967                         map_lines_split[p.y][p.x] = 'A '
968                 elif self.map_mode == 'terrain + things':
969                     for p in self.game.portals.keys():
970                         original = map_lines_split[p.y][p.x]
971                         map_lines_split[p.y][p.x] = original[0] + 'P'
972                     used_positions = []
973
974                     def draw_thing(t, used_positions):
975                         symbol = self.game.thing_types[t.type_]
976                         meta_char = ' '
977                         if hasattr(t, 'thing_char'):
978                             meta_char = t.thing_char
979                         if t.position in used_positions:
980                             meta_char = '+'
981                         if hasattr(t, 'carrying') and t.carrying:
982                             meta_char = '$'
983                         map_lines_split[t.position.y][t.position.x] = symbol + meta_char
984                         used_positions += [t.position]
985
986                     for t in [t for t in self.game.things if t.type_ != 'Player']:
987                         draw_thing(t, used_positions)
988                     for t in [t for t in self.game.things if t.type_ == 'Player']:
989                         draw_thing(t, used_positions)
990                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
991                     map_lines_split[self.explorer.y][self.explorer.x] = '??'
992                 elif self.map_mode != 'terrain + things':
993                     map_lines_split[self.game.player.position.y]\
994                         [self.game.player.position.x] = '??'
995                 self.map_lines = []
996                 if type(self.game.map_geometry) == MapGeometryHex:
997                     indent = 0
998                     for line in map_lines_split:
999                         self.map_lines += [indent * ' ' + ''.join(line)]
1000                         indent = 0 if indent else 1
1001                 else:
1002                     for line in map_lines_split:
1003                         self.map_lines += [''.join(line)]
1004                 window_center = YX(int(self.size.y / 2),
1005                                    int(self.left_window_width / 2))
1006                 center = self.game.player.position
1007                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1008                     center = self.explorer
1009                 center = YX(center.y, center.x * 2)
1010                 self.offset = center - window_center
1011                 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1012                     self.offset += YX(0, 1)
1013             term_y = max(0, -self.offset.y)
1014             term_x = max(0, -self.offset.x)
1015             map_y = max(0, self.offset.y)
1016             map_x = max(0, self.offset.x)
1017             while term_y < self.size.y and map_y < len(self.map_lines):
1018                 to_draw = self.map_lines[map_y][map_x:self.left_window_width + self.offset.x]
1019                 self.addstr(term_y, term_x, to_draw)
1020                 term_y += 1
1021                 map_y += 1
1022
1023         def draw_names():
1024             players = [t for t in self.game.things if t.type_ == 'Player']
1025             players.sort(key=lambda t: len(t.name))
1026             players.reverse()
1027             shrink_offset = max(0, (self.size.y - self.left_window_width // 2) // 2)
1028             y = 0
1029             for t in players:
1030                 offset_y = y - shrink_offset
1031                 max_len = max(5, (self.left_window_width // 2) - (offset_y * 2) - 8)
1032                 name = t.name[:]
1033                 if len(name) > max_len:
1034                     name = name[:max_len - 1] + '…'
1035                 self.addstr(y, 0, '@%s:%s' % (t.thing_char, name))
1036                 y += 1
1037                 if y >= self.size.y:
1038                     break
1039
1040         def draw_face_popup():
1041             t = self.game.get_thing(self.draw_face)
1042             if not t or not hasattr(t, 'face'):
1043                 self.draw_face = False
1044                 return
1045
1046             start_x = self.left_window_width - 10
1047             def draw_body_part(body_part, end_y):
1048                 self.addstr(end_y - 3, start_x, '----------')
1049                 self.addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1050                 self.addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1051                 self.addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1052
1053             if hasattr(t, 'face'):
1054                 draw_body_part(t.face, self.size.y - 3)
1055             if hasattr(t, 'hat'):
1056                 draw_body_part(t.hat, self.size.y - 6)
1057             self.addstr(self.size.y - 2, start_x, '----------')
1058             name = t.name[:]
1059             if len(name) > 7:
1060                 name = name[:6 - 1] + '…'
1061             self.addstr(self.size.y - 1, start_x, '@%s:%s' % (t.thing_char, name))
1062
1063         def draw_help():
1064             content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1065                                              self.mode.help_intro)
1066             if len(self.mode.available_actions) > 0:
1067                 content += "Available actions:\n"
1068                 for action in self.mode.available_actions:
1069                     if action in self.action_tasks:
1070                         if self.action_tasks[action] not in self.game.tasks:
1071                             continue
1072                     if action == 'move_explorer':
1073                         action = 'move'
1074                     if action == 'move':
1075                         key = ','.join(self.movement_keys)
1076                     else:
1077                         key = self.keys[action]
1078                     content += '[%s] – %s\n' % (key, self.action_descriptions[action])
1079                 content += '\n'
1080             content += self.mode.list_available_modes(self)
1081             for i in range(self.size.y):
1082                 self.addstr(i,
1083                             self.left_window_width * (not self.mode.has_input_prompt),
1084                             ' ' * self.left_window_width)
1085             lines = []
1086             for line in content.split('\n'):
1087                 lines += msg_into_lines_of_width(line, self.right_window_width)
1088             for i in range(len(lines)):
1089                 if i >= self.size.y:
1090                     break
1091                 self.addstr(i,
1092                             self.left_window_width * (not self.mode.has_input_prompt),
1093                             lines[i])
1094
1095         def draw_screen():
1096             self.stdscr.clear()
1097             self.stdscr.bkgd(' ', curses.color_pair(1))
1098             recalc_input_lines()
1099             if self.mode.has_input_prompt:
1100                 draw_input()
1101             if self.mode.shows_info:
1102                 draw_info()
1103             else:
1104                 draw_history()
1105             draw_mode()
1106             if not self.mode.is_intro:
1107                 draw_stats()
1108                 draw_map()
1109             if self.show_help:
1110                 draw_help()
1111             if self.mode.name in {'chat', 'play'}:
1112                 draw_names()
1113                 if self.draw_face:
1114                     draw_face_popup()
1115
1116         def pick_selectable(task_name):
1117             try:
1118                 i = int(self.input_)
1119                 if i < 0 or i >= len(self.selectables):
1120                     self.log('? invalid index, aborted')
1121                 else:
1122                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1123             except ValueError:
1124                 self.log('? invalid index, aborted')
1125             self.input_ = ''
1126             self.switch_mode('play')
1127
1128         def enter_ascii_art(command, height, width,
1129                             with_pw=False, with_size=False):
1130             if with_size and self.ascii_draw_stage == 0:
1131                 width = len(self.input_)
1132                 if width > 36:
1133                     self.log('? input too long, must be max 36; try again')
1134                     # TODO: move max width mechanism server-side
1135                     return
1136                 old_size = self.game.player.carrying.design[0]
1137                 if width != old_size.x:
1138                     # TODO: save remaining design?
1139                     self.game.player.carrying.design[1] = ''
1140                     self.game.player.carrying.design[0] = YX(old_size.y, width)
1141             elif len(self.input_) > width:
1142                 self.log('? input too long, '
1143                              'must be max %s; try again' % width)
1144                 return
1145             self.log('  ' + self.input_)
1146             if with_size and self.input_ in {'', ' '}\
1147                and self.ascii_draw_stage > 0:
1148                 height = self.ascii_draw_stage
1149             else:
1150                 if with_size:
1151                     height = self.ascii_draw_stage + 2
1152                 if len(self.input_) < width:
1153                     self.input_ += ' ' * (width - len(self.input_))
1154                 self.full_ascii_draw += self.input_
1155             if with_size:
1156                 old_size = self.game.player.carrying.design[0]
1157                 self.game.player.carrying.design[0] = YX(height, old_size.x)
1158             self.ascii_draw_stage += 1
1159             if self.ascii_draw_stage < height:
1160                 self.restore_input_values()
1161             else:
1162                 if with_pw and with_size:
1163                     self.send('%s_SIZE %s %s' % (command, YX(height, width),
1164                                                  quote(self.password)))
1165                 if with_pw:
1166                     self.send('%s %s %s' % (command, quote(self.full_ascii_draw),
1167                                             quote(self.password)))
1168                 else:
1169                     self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1170                 self.full_ascii_draw = ""
1171                 self.ascii_draw_stage = 0
1172                 self.input_ = ""
1173                 self.switch_mode('edit')
1174
1175         prev_disconnected = self.socket.disconnected
1176         self.socket.keep_connection_alive()
1177         if prev_disconnected and not self.socket.disconnected:
1178             self.update_on_connect()
1179         if self.flash:
1180             curses.flash()
1181             self.flash = False
1182         if self.do_refresh:
1183             draw_screen()
1184             self.do_refresh = False
1185         for msg in self.socket.get_message():
1186             handle_input(msg)
1187         key, keycode = self.get_key_and_keycode()
1188         self.show_help = False
1189         self.draw_face = False
1190         if key == 'KEY_RESIZE':
1191             self.reset_size()
1192         elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1193             self.input_ = self.input_[:-1]
1194         elif (((not self.mode.is_intro) and keycode == 27)  # Escape
1195               or (self.mode.has_input_prompt and key == '\n'
1196                   and self.input_ == ''\
1197                   and self.mode.name in {'chat', 'command_thing',
1198                                          'take_thing', 'drop_thing',
1199                                          'admin_enter'})):
1200             if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1201                 self.log('@ aborted')
1202             self.switch_mode('play')
1203         elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1204             self.show_help = True
1205             self.input_ = ""
1206             self.restore_input_values()
1207         elif self.mode.has_input_prompt and key != '\n':  # Return key
1208             self.input_ += key
1209             max_length = self.right_window_width * self.size.y - len(self.input_prompt) - 1
1210             if len(self.input_) > max_length:
1211                 self.input_ = self.input_[:max_length]
1212         elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1213             self.show_help = True
1214         elif self.mode.name == 'login' and key == '\n':
1215             self.login_name = self.input_
1216             self.send('LOGIN ' + quote(self.input_))
1217             self.input_ = ""
1218         elif self.mode.name == 'enter_face' and key == '\n':
1219             enter_ascii_art('PLAYER_FACE', 3, 6)
1220         elif self.mode.name == 'enter_design' and key == '\n':
1221             if self.game.player.carrying.type_ == 'Hat':
1222                 enter_ascii_art('THING_DESIGN',
1223                                 self.game.player.carrying.design[0].y,
1224                                 self.game.player.carrying.design[0].x, True)
1225             else:
1226                 enter_ascii_art('THING_DESIGN',
1227                                 self.game.player.carrying.design[0].y,
1228                                 self.game.player.carrying.design[0].x,
1229                                 True, True)
1230         elif self.mode.name == 'take_thing' and key == '\n':
1231             pick_selectable('PICK_UP')
1232         elif self.mode.name == 'drop_thing' and key == '\n':
1233             pick_selectable('DROP')
1234         elif self.mode.name == 'command_thing' and key == '\n':
1235             self.send('TASK:COMMAND ' + quote(self.input_))
1236             self.input_ = ""
1237         elif self.mode.name == 'control_pw_pw' and key == '\n':
1238             if self.input_ == '':
1239                 self.log('@ aborted')
1240             else:
1241                 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1242                 self.log('@ sent new password for protection character "%s"' % self.tile_control_char)
1243             self.switch_mode('admin')
1244         elif self.mode.name == 'password' and key == '\n':
1245             if self.input_ == '':
1246                 self.input_ = ' '
1247             self.password = self.input_
1248             self.switch_mode('edit')
1249         elif self.mode.name == 'admin_enter' and key == '\n':
1250             self.send('BECOME_ADMIN ' + quote(self.input_))
1251             self.switch_mode('play')
1252         elif self.mode.name == 'control_pw_type' and key == '\n':
1253             if len(self.input_) != 1:
1254                 self.log('@ entered non-single-char, therefore aborted')
1255                 self.switch_mode('admin')
1256             else:
1257                 self.tile_control_char = self.input_
1258                 self.switch_mode('control_pw_pw')
1259         elif self.mode.name == 'admin_thing_protect' and key == '\n':
1260             if len(self.input_) != 1:
1261                 self.log('@ entered non-single-char, therefore aborted')
1262             else:
1263                 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1264                 self.log('@ sent new protection character for thing')
1265             self.switch_mode('admin')
1266         elif self.mode.name == 'control_tile_type' and key == '\n':
1267             if len(self.input_) != 1:
1268                 self.log('@ entered non-single-char, therefore aborted')
1269                 self.switch_mode('admin')
1270             else:
1271                 self.tile_control_char = self.input_
1272                 self.switch_mode('control_tile_draw')
1273         elif self.mode.name == 'chat' and key == '\n':
1274             if self.input_ == '':
1275                 return
1276             if self.input_[0] == '/':
1277                 if self.input_.startswith('/nick'):
1278                     tokens = self.input_.split(maxsplit=1)
1279                     if len(tokens) == 2:
1280                         self.send('NICK ' + quote(tokens[1]))
1281                     else:
1282                         self.log('? need login name')
1283                 else:
1284                     self.log('? unknown command')
1285             else:
1286                 self.send('ALL ' + quote(self.input_))
1287             self.input_ = ""
1288         elif self.mode.name == 'name_thing' and key == '\n':
1289             if self.input_ == '':
1290                 self.input_ = ' '
1291             self.send('THING_NAME %s %s' % (quote(self.input_),
1292                                             quote(self.password)))
1293             self.switch_mode('edit')
1294         elif self.mode.name == 'annotate' and key == '\n':
1295             if self.input_ == '':
1296                 self.input_ = ' '
1297             self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1298                                              quote(self.password)))
1299             self.switch_mode('edit')
1300         elif self.mode.name == 'portal' and key == '\n':
1301             if self.input_ == '':
1302                 self.input_ = ' '
1303             self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1304                                            quote(self.password)))
1305             self.switch_mode('edit')
1306         elif self.mode.name == 'study':
1307             if self.mode.mode_switch_on_key(self, key):
1308                 return
1309             elif key == self.keys['toggle_map_mode']:
1310                 self.toggle_map_mode()
1311             elif key in self.movement_keys:
1312                 move_explorer(self.movement_keys[key])
1313         elif self.mode.name == 'play':
1314             if self.mode.mode_switch_on_key(self, key):
1315                 return
1316             elif key == self.keys['door'] and task_action_on('door'):
1317                 self.send('TASK:DOOR')
1318             elif key == self.keys['consume'] and task_action_on('consume'):
1319                 self.send('TASK:INTOXICATE')
1320             elif key == self.keys['wear'] and task_action_on('wear'):
1321                 self.send('TASK:WEAR')
1322             elif key == self.keys['spin'] and task_action_on('spin'):
1323                 self.send('TASK:SPIN')
1324             elif key == self.keys['dance'] and task_action_on('dance'):
1325                 self.send('TASK:DANCE')
1326             elif key == self.keys['teleport']:
1327                 if self.game.player.position in self.game.portals:
1328                     self.socket.host = self.game.portals[self.game.player.position]
1329                     self.reconnect()
1330                 else:
1331                     self.flash = True
1332                     self.log('? not standing on portal')
1333             elif key in self.movement_keys and task_action_on('move'):
1334                 self.send('TASK:MOVE ' + self.movement_keys[key])
1335         elif self.mode.name == 'write':
1336             self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1337             self.switch_mode('edit')
1338         elif self.mode.name == 'control_tile_draw':
1339             if self.mode.mode_switch_on_key(self, key):
1340                 return
1341             elif key in self.movement_keys:
1342                 move_explorer(self.movement_keys[key])
1343             elif key == self.keys['toggle_tile_draw']:
1344                 self.tile_draw = False if self.tile_draw else True
1345         elif self.mode.name == 'admin':
1346             if self.mode.mode_switch_on_key(self, key):
1347                 return
1348             elif key == self.keys['toggle_map_mode']:
1349                 self.toggle_map_mode()
1350             elif key in self.movement_keys and task_action_on('move'):
1351                 self.send('TASK:MOVE ' + self.movement_keys[key])
1352         elif self.mode.name == 'edit':
1353             if self.mode.mode_switch_on_key(self, key):
1354                 return
1355             elif key == self.keys['flatten'] and task_action_on('flatten'):
1356                 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1357             elif key == self.keys['install'] and task_action_on('install'):
1358                 self.send('TASK:INSTALL %s' % quote(self.password))
1359             elif key == self.keys['toggle_map_mode']:
1360                 self.toggle_map_mode()
1361             elif key in self.movement_keys and task_action_on('move'):
1362                 self.send('TASK:MOVE ' + self.movement_keys[key])
1363
1364 if len(sys.argv) != 2:
1365     raise ArgError('wrong number of arguments, need game host')
1366 host = sys.argv[1]
1367 RogueChatTUI(host)