home · contact · privacy
More TUI code refactoring.
[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.do_refresh = True
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.store_widechar = False
562         self.input_prompt = '> '
563         self.action_descriptions = {
564             'move': 'move',
565             'flatten': 'flatten surroundings',
566             'teleport': 'teleport',
567             'take_thing': 'pick up thing',
568             'drop_thing': 'drop thing',
569             'toggle_map_mode': 'toggle map view',
570             'toggle_tile_draw': 'toggle protection character drawing',
571             'install': '(un-)install',
572             'wear': '(un-)wear',
573             'door': 'open/close',
574             'consume': 'consume',
575             'spin': 'spin',
576             'dance': 'dance',
577         }
578         self.action_tasks = {
579             'flatten': 'FLATTEN_SURROUNDINGS',
580             'take_thing': 'PICK_UP',
581             'drop_thing': 'DROP',
582             'door': 'DOOR',
583             'install': 'INSTALL',
584             'wear': 'WEAR',
585             'move': 'MOVE',
586             'command': 'COMMAND',
587             'consume': 'INTOXICATE',
588             'spin': 'SPIN',
589             'dance': 'DANCE',
590         }
591         super().__init__(*args, **kwargs)
592
593     def update_on_connect(self):
594         self.socket.send('TASKS')
595         self.socket.send('TERRAINS')
596         self.socket.send('THING_TYPES')
597         self.switch_mode('login')
598
599     def reconnect(self):
600         import time
601         self.log('@ attempting reconnect')
602         self.socket.send('QUIT')
603         # necessitated by some strange SSL race conditions with ws4py
604         time.sleep(0.1)  # FIXME find out why exactly necessary
605         self.switch_mode('waiting_for_server')
606         self.socket.connect()
607         self.update_on_connect()
608
609     def send(self, msg):
610         self.socket.send(msg)
611         if self.socket.disconnected:
612             self.do_refresh = True
613
614     def socket_log(self, msg):
615         self.log('@ ' + msg)
616
617     def log_msg(self, msg):
618         super().log(msg)
619         #self.log += [msg]
620         if len(self._log) > 100:
621             self.log = self._log[-100:]
622
623     def restore_input_values(self):
624         if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
625             self.input_ = self.game.annotations[self.explorer]
626         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
627             self.input_ = self.game.portals[self.explorer]
628         elif self.mode.name == 'password':
629             self.input_ = self.password
630         elif self.mode.name == 'name_thing':
631             if hasattr(self.game.player.carrying, 'name'):
632                 self.input_ = self.game.player.carrying.name
633         elif self.mode.name == 'admin_thing_protect':
634             if hasattr(self.game.player.carrying, 'protection'):
635                 self.input_ = self.game.player.carrying.protection
636         elif self.mode.name == 'enter_face':
637             start = self.ascii_draw_stage * 6
638             end = (self.ascii_draw_stage + 1) * 6
639             self.input_ = self.game.player.face[start:end]
640         elif self.mode.name == 'enter_design':
641             width = self.game.player.carrying.design[0].x
642             start = self.ascii_draw_stage * width
643             end = (self.ascii_draw_stage + 1) * width
644             self.input_ = self.game.player.carrying.design[1][start:end]
645
646     def send_tile_control_command(self):
647         self.send('SET_TILE_CONTROL %s %s' %
648                   (self.explorer, quote(self.tile_control_char)))
649
650     def toggle_map_mode(self):
651         if self.map_mode == 'terrain only':
652             self.map_mode = 'terrain + annotations'
653         elif self.map_mode == 'terrain + annotations':
654             self.map_mode = 'terrain + things'
655         elif self.map_mode == 'terrain + things':
656             self.map_mode = 'protections'
657         elif self.map_mode == 'protections':
658             self.map_mode = 'terrain only'
659
660     def switch_mode(self, mode_name):
661
662         def fail(msg, return_mode='play'):
663             self.log('? ' + msg)
664             self.flash = True
665             self.switch_mode(return_mode)
666
667         if self.mode and self.mode.name == 'control_tile_draw':
668             self.log('@ finished tile protection drawing.')
669         self.draw_face = False
670         self.tile_draw = False
671         self.ascii_draw_stage = 0
672         self.full_ascii_draw = ''
673         if mode_name == 'command_thing' and\
674            (not self.game.player.carrying or
675             not self.game.player.carrying.commandable):
676             return fail('not carrying anything commandable')
677         if mode_name == 'name_thing' and not self.game.player.carrying:
678             return fail('not carrying anything to re-name', 'edit')
679         if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
680             return fail('not carrying anything to protect')
681         if mode_name == 'take_thing' and self.game.player.carrying:
682             return fail('already carrying something')
683         if mode_name == 'drop_thing' and not self.game.player.carrying:
684             return fail('not carrying anything droppable')
685         if mode_name == 'enter_design' and\
686            (not self.game.player.carrying or
687             not hasattr(self.game.player.carrying, 'design')):
688             return fail('not carrying designable to edit', 'edit')
689         if mode_name == 'admin_enter' and self.is_admin:
690             mode_name = 'admin'
691         self.mode = getattr(self, 'mode_' + mode_name)
692         if self.mode.name in {'control_tile_draw', 'control_tile_type',
693                               'control_pw_type'}:
694             self.map_mode = 'protections'
695         elif self.mode.name != 'edit':
696             self.map_mode = 'terrain + things'
697         if self.mode.shows_info or self.mode.name == 'control_tile_draw':
698             self.explorer = YX(self.game.player.position.y,
699                                self.game.player.position.x)
700         if self.mode.is_single_char_entry:
701             self.show_help = True
702         if len(self.mode.intro_msg) > 0:
703             self.log(self.mode.intro_msg)
704         if self.mode.name == 'login':
705             if self.login_name:
706                 self.send('LOGIN ' + quote(self.login_name))
707             else:
708                 self.log('@ enter username')
709         elif self.mode.name == 'take_thing':
710             self.log('Portable things in reach for pick-up:')
711             directed_moves = {
712                 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
713             }
714             if type(self.game.map_geometry) == MapGeometrySquare:
715                 directed_moves['UP'] = YX(-1, 0)
716                 directed_moves['DOWN'] = YX(1, 0)
717             elif type(self.game.map_geometry) == MapGeometryHex:
718                 if self.game.player.position.y % 2:
719                     directed_moves['UPLEFT'] = YX(-1, 0)
720                     directed_moves['UPRIGHT'] = YX(-1, 1)
721                     directed_moves['DOWNLEFT'] = YX(1, 0)
722                     directed_moves['DOWNRIGHT'] = YX(1, 1)
723                 else:
724                     directed_moves['UPLEFT'] = YX(-1, -1)
725                     directed_moves['UPRIGHT'] = YX(-1, 0)
726                     directed_moves['DOWNLEFT'] = YX(1, -1)
727                     directed_moves['DOWNRIGHT'] = YX(1, 0)
728             select_range = {}
729             for direction in directed_moves:
730                 move = directed_moves[direction]
731                 select_range[direction] = self.game.player.position + move
732             self.selectables = []
733             directions = []
734             for direction in select_range:
735                 for t in [t for t in self.game.things
736                           if t.portable and t.position == select_range[direction]]:
737                     self.selectables += [t.id_]
738                     directions += [direction]
739             if len(self.selectables) == 0:
740                 return fail('nothing to pick-up')
741             else:
742                 for i in range(len(self.selectables)):
743                     t = self.game.get_thing(self.selectables[i])
744                     self.log('%s %s: %s' % (i, directions[i],
745                                                 self.get_thing_info(t)))
746         elif self.mode.name == 'drop_thing':
747             self.log('Direction to drop thing to:')
748             self.selectables =\
749                 ['HERE'] + list(self.game.tui.movement_keys.values())
750             for i in range(len(self.selectables)):
751                 self.log(str(i) + ': ' + self.selectables[i])
752         elif self.mode.name == 'enter_design':
753             if self.game.player.carrying.type_ == 'Hat':
754                 self.log('@ The design you enter must be %s lines of max %s '
755                              'characters width each'
756                              % (self.game.player.carrying.design[0].y,
757                                 self.game.player.carrying.design[0].x))
758                 self.log('@ Legal characters: ' + self.game.players_hat_chars)
759                 self.log('@ (Eat cookies to extend the ASCII characters available for drawing.)')
760             else:
761                 self.log('@ Width of first line determines maximum width for remaining design')
762                 self.log('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
763         elif self.mode.name == 'command_thing':
764             self.send('TASK:COMMAND ' + quote('HELP'))
765         elif self.mode.name == 'control_pw_pw':
766             self.log('@ enter protection password for "%s":' % self.tile_control_char)
767         elif self.mode.name == 'control_tile_draw':
768             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']))
769         self.input_ = ""
770         self.restore_input_values()
771
772     def set_default_colors(self):
773         if curses.can_change_color():
774             curses.init_color(7, 1000, 1000, 1000)
775             curses.init_color(0, 0, 0, 0)
776         self.do_refresh = True
777
778     def set_random_colors(self):
779
780         def rand(offset):
781             import random
782             return int(offset + random.random()*375)
783
784         if curses.can_change_color():
785             curses.init_color(7, rand(625), rand(625), rand(625))
786             curses.init_color(0, rand(0), rand(0), rand(0))
787         self.do_refresh = True
788
789     def get_info(self):
790         if self.info_cached:
791             return self.info_cached
792         pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
793         info_to_cache = ''
794         if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
795             info_to_cache += 'outside field of view'
796         else:
797             for t in self.game.things:
798                 if t.position == self.explorer:
799                     info_to_cache += '%s' % self.get_thing_info(t, True)
800             terrain_char = self.game.map_content[pos_i]
801             terrain_desc = '?'
802             if terrain_char in self.game.terrains:
803                 terrain_desc = self.game.terrains[terrain_char]
804             info_to_cache += 'TERRAIN: %s (%s' % (terrain_char,
805                                                        terrain_desc)
806             protection = self.game.map_control_content[pos_i]
807             if protection != '.':
808                 info_to_cache += '/protection:%s' % protection
809             info_to_cache += ')\n'
810             if self.explorer in self.game.portals:
811                 info_to_cache += 'PORTAL: ' +\
812                     self.game.portals[self.explorer] + '\n'
813             if self.explorer in self.game.annotations:
814                 info_to_cache += 'ANNOTATION: ' +\
815                     self.game.annotations[self.explorer]
816         self.info_cached = info_to_cache
817         return self.info_cached
818
819     def get_thing_info(self, t, detailed=False):
820         info = ''
821         if detailed:
822             info += '- '
823         info += self.game.thing_types[t.type_]
824         if hasattr(t, 'thing_char'):
825             info += t.thing_char
826         if hasattr(t, 'name'):
827             info += ': %s' % t.name
828         info += ' (%s' % t.type_
829         if hasattr(t, 'installed'):
830             info += '/installed'
831         if t.type_ == 'Bottle':
832             if t.thing_char == '_':
833                 info += '/empty'
834             elif t.thing_char == '~':
835                 info += '/full'
836         if detailed:
837             protection = t.protection
838             if protection != '.':
839                 info += '/protection:%s' % protection
840             info += ')\n'
841             if hasattr(t, 'hat') or hasattr(t, 'face'):
842                 info += '----------\n'
843             if hasattr(t, 'hat'):
844                 info += '| %s |\n' % t.hat[0:6]
845                 info += '| %s |\n' % t.hat[6:12]
846                 info += '| %s |\n' % t.hat[12:18]
847             if hasattr(t, 'face'):
848                 info += '| %s |\n' % t.face[0:6]
849                 info += '| %s |\n' % t.face[6:12]
850                 info += '| %s |\n' % t.face[12:18]
851                 info += '----------\n'
852             if hasattr(t, 'design'):
853                 line_length = t.design[0].x
854                 lines = []
855                 for i in range(t.design[0].y):
856                     start = i * line_length
857                     end = (i + 1) * line_length
858                     lines += [t.design[1][start:end]]
859                 info += '-' * (line_length + 4) + '\n'
860                 for line in lines:
861                     info += '| %s |\n' % line
862                 info += '-' * (line_length + 4) + '\n'
863         else:
864             info += ')'
865         return info
866
867     def reset_size(self):
868         super().reset_size()
869         self.left_window_width = min(52, int(self.size.x / 2))
870         self.right_window_width = self.size.x - self.left_window_width
871
872     def addstr(self, y, x, line, ignore=None):
873         super().addstr(y, x, line, curses.color_pair(1))
874
875     def init_loop(self):
876         self.switch_mode('waiting_for_server')
877         curses.start_color()
878         self.set_default_colors()
879         curses.init_pair(1, 7, 0)
880         if not curses.can_change_color():
881             self.log('@ unfortunately, your terminal does not seem to '
882                          'support re-definition of colors; you might miss out '
883                          'on some color effects')
884         super().init_loop()
885
886     def loop(self):
887
888         def handle_input(msg):
889             command, args = self.parser.parse(msg)
890             command(*args)
891
892         def task_action_on(action):
893             return self.action_tasks[action] in self.game.tasks
894
895         def recalc_input_lines():
896             if not self.mode.has_input_prompt:
897                 self.input_lines = []
898             else:
899                 self.input_lines = msg_into_lines_of_width(self.input_prompt
900                                                            + self.input_ + '█',
901                                                            self.right_window_width)
902
903         def move_explorer(direction):
904             target = self.game.map_geometry.move_yx(self.explorer, direction)
905             if target:
906                 self.info_cached = None
907                 self.explorer = target
908                 if self.tile_draw:
909                     self.send_tile_control_command()
910             else:
911                 self.flash = True
912
913         def draw_history():
914             lines = []
915             for line in self._log:
916                 lines += msg_into_lines_of_width(line, self.right_window_width)
917             lines.reverse()
918             height_header = 2
919             max_y = self.size.y - len(self.input_lines)
920             for i in range(len(lines)):
921                 if (i >= max_y - height_header):
922                     break
923                 self.addstr(max_y - i - 1, self.left_window_width, lines[i])
924
925         def draw_info():
926             info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
927             lines = msg_into_lines_of_width(info, self.right_window_width)
928             height_header = 2
929             for i in range(len(lines)):
930                 y = height_header + i
931                 if y >= self.size.y - len(self.input_lines):
932                     break
933                 self.addstr(y, self.left_window_width, lines[i])
934
935         def draw_input():
936             y = self.size.y - len(self.input_lines)
937             for i in range(len(self.input_lines)):
938                 self.addstr(y, self.left_window_width, self.input_lines[i])
939                 y += 1
940
941         def draw_stats():
942             stats = 'ENERGY: %s BLADDER: %s' % (self.game.energy,
943                                                 self.game.bladder_pressure)
944             self.addstr(0, self.left_window_width, stats)
945
946         def draw_mode():
947             help = "hit [%s] for help" % self.keys['help']
948             if self.mode.has_input_prompt:
949                 help = "enter /help for help"
950             self.addstr(1, self.left_window_width,
951                         'MODE: %s – %s' % (self.mode.short_desc, help))
952
953         def draw_map():
954             if (not self.game.turn_complete) and len(self.map_lines) == 0:
955                 return
956             if self.game.turn_complete:
957                 map_lines_split = []
958                 for y in range(self.game.map_geometry.size.y):
959                     start = self.game.map_geometry.size.x * y
960                     end = start + self.game.map_geometry.size.x
961                     if self.map_mode == 'protections':
962                         map_lines_split += [[c + ' ' for c
963                                              in self.game.map_control_content[start:end]]]
964                     else:
965                         map_lines_split += [[c + ' ' for c
966                                              in self.game.map_content[start:end]]]
967                 if self.map_mode == 'terrain + annotations':
968                     for p in self.game.annotations:
969                         map_lines_split[p.y][p.x] = 'A '
970                 elif self.map_mode == 'terrain + things':
971                     for p in self.game.portals.keys():
972                         original = map_lines_split[p.y][p.x]
973                         map_lines_split[p.y][p.x] = original[0] + 'P'
974                     used_positions = []
975
976                     def draw_thing(t, used_positions):
977                         symbol = self.game.thing_types[t.type_]
978                         meta_char = ' '
979                         if hasattr(t, 'thing_char'):
980                             meta_char = t.thing_char
981                         if t.position in used_positions:
982                             meta_char = '+'
983                         if hasattr(t, 'carrying') and t.carrying:
984                             meta_char = '$'
985                         map_lines_split[t.position.y][t.position.x] = symbol + meta_char
986                         used_positions += [t.position]
987
988                     for t in [t for t in self.game.things if t.type_ != 'Player']:
989                         draw_thing(t, used_positions)
990                     for t in [t for t in self.game.things if t.type_ == 'Player']:
991                         draw_thing(t, used_positions)
992                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
993                     map_lines_split[self.explorer.y][self.explorer.x] = '??'
994                 elif self.map_mode != 'terrain + things':
995                     map_lines_split[self.game.player.position.y]\
996                         [self.game.player.position.x] = '??'
997                 self.map_lines = []
998                 if type(self.game.map_geometry) == MapGeometryHex:
999                     indent = 0
1000                     for line in map_lines_split:
1001                         self.map_lines += [indent * ' ' + ''.join(line)]
1002                         indent = 0 if indent else 1
1003                 else:
1004                     for line in map_lines_split:
1005                         self.map_lines += [''.join(line)]
1006                 window_center = YX(int(self.size.y / 2),
1007                                    int(self.left_window_width / 2))
1008                 center = self.game.player.position
1009                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1010                     center = self.explorer
1011                 center = YX(center.y, center.x * 2)
1012                 self.offset = center - window_center
1013                 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1014                     self.offset += YX(0, 1)
1015             term_y = max(0, -self.offset.y)
1016             term_x = max(0, -self.offset.x)
1017             map_y = max(0, self.offset.y)
1018             map_x = max(0, self.offset.x)
1019             while term_y < self.size.y and map_y < len(self.map_lines):
1020                 to_draw = self.map_lines[map_y][map_x:self.left_window_width + self.offset.x]
1021                 self.addstr(term_y, term_x, to_draw)
1022                 term_y += 1
1023                 map_y += 1
1024
1025         def draw_names():
1026             players = [t for t in self.game.things if t.type_ == 'Player']
1027             players.sort(key=lambda t: len(t.name))
1028             players.reverse()
1029             shrink_offset = max(0, (self.size.y - self.left_window_width // 2) // 2)
1030             y = 0
1031             for t in players:
1032                 offset_y = y - shrink_offset
1033                 max_len = max(5, (self.left_window_width // 2) - (offset_y * 2) - 8)
1034                 name = t.name[:]
1035                 if len(name) > max_len:
1036                     name = name[:max_len - 1] + '…'
1037                 self.addstr(y, 0, '@%s:%s' % (t.thing_char, name))
1038                 y += 1
1039                 if y >= self.size.y:
1040                     break
1041
1042         def draw_face_popup():
1043             t = self.game.get_thing(self.draw_face)
1044             if not t or not hasattr(t, 'face'):
1045                 self.draw_face = False
1046                 return
1047
1048             start_x = self.left_window_width - 10
1049             def draw_body_part(body_part, end_y):
1050                 self.addstr(end_y - 3, start_x, '----------')
1051                 self.addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1052                 self.addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1053                 self.addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1054
1055             if hasattr(t, 'face'):
1056                 draw_body_part(t.face, self.size.y - 3)
1057             if hasattr(t, 'hat'):
1058                 draw_body_part(t.hat, self.size.y - 6)
1059             self.addstr(self.size.y - 2, start_x, '----------')
1060             name = t.name[:]
1061             if len(name) > 7:
1062                 name = name[:6 - 1] + '…'
1063             self.addstr(self.size.y - 1, start_x, '@%s:%s' % (t.thing_char, name))
1064
1065         def draw_help():
1066             content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1067                                              self.mode.help_intro)
1068             if len(self.mode.available_actions) > 0:
1069                 content += "Available actions:\n"
1070                 for action in self.mode.available_actions:
1071                     if action in self.action_tasks:
1072                         if self.action_tasks[action] not in self.game.tasks:
1073                             continue
1074                     if action == 'move_explorer':
1075                         action = 'move'
1076                     if action == 'move':
1077                         key = ','.join(self.movement_keys)
1078                     else:
1079                         key = self.keys[action]
1080                     content += '[%s] – %s\n' % (key, self.action_descriptions[action])
1081                 content += '\n'
1082             content += self.mode.list_available_modes(self)
1083             for i in range(self.size.y):
1084                 self.addstr(i,
1085                             self.left_window_width * (not self.mode.has_input_prompt),
1086                             ' ' * self.left_window_width)
1087             lines = []
1088             for line in content.split('\n'):
1089                 lines += msg_into_lines_of_width(line, self.right_window_width)
1090             for i in range(len(lines)):
1091                 if i >= self.size.y:
1092                     break
1093                 self.addstr(i,
1094                             self.left_window_width * (not self.mode.has_input_prompt),
1095                             lines[i])
1096
1097         def draw_screen():
1098             self.stdscr.clear()
1099             self.stdscr.bkgd(' ', curses.color_pair(1))
1100             recalc_input_lines()
1101             if self.mode.has_input_prompt:
1102                 draw_input()
1103             if self.mode.shows_info:
1104                 draw_info()
1105             else:
1106                 draw_history()
1107             draw_mode()
1108             if not self.mode.is_intro:
1109                 draw_stats()
1110                 draw_map()
1111             if self.show_help:
1112                 draw_help()
1113             if self.mode.name in {'chat', 'play'}:
1114                 draw_names()
1115                 if self.draw_face:
1116                     draw_face_popup()
1117
1118         def pick_selectable(task_name):
1119             try:
1120                 i = int(self.input_)
1121                 if i < 0 or i >= len(self.selectables):
1122                     self.log('? invalid index, aborted')
1123                 else:
1124                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1125             except ValueError:
1126                 self.log('? invalid index, aborted')
1127             self.input_ = ''
1128             self.switch_mode('play')
1129
1130         def enter_ascii_art(command, height, width,
1131                             with_pw=False, with_size=False):
1132             if with_size and self.ascii_draw_stage == 0:
1133                 width = len(self.input_)
1134                 if width > 36:
1135                     self.log('? input too long, must be max 36; try again')
1136                     # TODO: move max width mechanism server-side
1137                     return
1138                 old_size = self.game.player.carrying.design[0]
1139                 if width != old_size.x:
1140                     # TODO: save remaining design?
1141                     self.game.player.carrying.design[1] = ''
1142                     self.game.player.carrying.design[0] = YX(old_size.y, width)
1143             elif len(self.input_) > width:
1144                 self.log('? input too long, '
1145                              'must be max %s; try again' % width)
1146                 return
1147             self.log('  ' + self.input_)
1148             if with_size and self.input_ in {'', ' '}\
1149                and self.ascii_draw_stage > 0:
1150                 height = self.ascii_draw_stage
1151             else:
1152                 if with_size:
1153                     height = self.ascii_draw_stage + 2
1154                 if len(self.input_) < width:
1155                     self.input_ += ' ' * (width - len(self.input_))
1156                 self.full_ascii_draw += self.input_
1157             if with_size:
1158                 old_size = self.game.player.carrying.design[0]
1159                 self.game.player.carrying.design[0] = YX(height, old_size.x)
1160             self.ascii_draw_stage += 1
1161             if self.ascii_draw_stage < height:
1162                 self.restore_input_values()
1163             else:
1164                 if with_pw and with_size:
1165                     self.send('%s_SIZE %s %s' % (command, YX(height, width),
1166                                                  quote(self.password)))
1167                 if with_pw:
1168                     self.send('%s %s %s' % (command, quote(self.full_ascii_draw),
1169                                             quote(self.password)))
1170                 else:
1171                     self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1172                 self.full_ascii_draw = ""
1173                 self.ascii_draw_stage = 0
1174                 self.input_ = ""
1175                 self.switch_mode('edit')
1176
1177         prev_disconnected = self.socket.disconnected
1178         self.socket.keep_connection_alive()
1179         if prev_disconnected and not self.socket.disconnected:
1180             self.update_on_connect()
1181         if self.flash:
1182             curses.flash()
1183             self.flash = False
1184         if self.do_refresh:
1185             draw_screen()
1186             self.do_refresh = False
1187         for msg in self.socket.get_message():
1188             handle_input(msg)
1189         try:
1190             key = self.stdscr.getkey()
1191             self.do_refresh = True
1192         except curses.error:
1193             return
1194         keycode = None
1195         if len(key) == 1:
1196             keycode = ord(key)
1197             # workaround for <https://stackoverflow.com/a/56390915>
1198             if self.store_widechar:
1199                 self.store_widechar = False
1200                 key = bytes([195, keycode]).decode()
1201             if keycode == 195:
1202                 self.store_widechar = True
1203                 return
1204         self.show_help = False
1205         self.draw_face = False
1206         if key == 'KEY_RESIZE':
1207             self.reset_size()
1208         elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1209             self.input_ = self.input_[:-1]
1210         elif (((not self.mode.is_intro) and keycode == 27)  # Escape
1211               or (self.mode.has_input_prompt and key == '\n'
1212                   and self.input_ == ''\
1213                   and self.mode.name in {'chat', 'command_thing',
1214                                          'take_thing', 'drop_thing',
1215                                          'admin_enter'})):
1216             if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1217                 self.log('@ aborted')
1218             self.switch_mode('play')
1219         elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1220             self.show_help = True
1221             self.input_ = ""
1222             self.restore_input_values()
1223         elif self.mode.has_input_prompt and key != '\n':  # Return key
1224             self.input_ += key
1225             max_length = self.right_window_width * self.size.y - len(self.input_prompt) - 1
1226             if len(self.input_) > max_length:
1227                 self.input_ = self.input_[:max_length]
1228         elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1229             self.show_help = True
1230         elif self.mode.name == 'login' and key == '\n':
1231             self.login_name = self.input_
1232             self.send('LOGIN ' + quote(self.input_))
1233             self.input_ = ""
1234         elif self.mode.name == 'enter_face' and key == '\n':
1235             enter_ascii_art('PLAYER_FACE', 3, 6)
1236         elif self.mode.name == 'enter_design' and key == '\n':
1237             if self.game.player.carrying.type_ == 'Hat':
1238                 enter_ascii_art('THING_DESIGN',
1239                                 self.game.player.carrying.design[0].y,
1240                                 self.game.player.carrying.design[0].x, True)
1241             else:
1242                 enter_ascii_art('THING_DESIGN',
1243                                 self.game.player.carrying.design[0].y,
1244                                 self.game.player.carrying.design[0].x,
1245                                 True, True)
1246         elif self.mode.name == 'take_thing' and key == '\n':
1247             pick_selectable('PICK_UP')
1248         elif self.mode.name == 'drop_thing' and key == '\n':
1249             pick_selectable('DROP')
1250         elif self.mode.name == 'command_thing' and key == '\n':
1251             self.send('TASK:COMMAND ' + quote(self.input_))
1252             self.input_ = ""
1253         elif self.mode.name == 'control_pw_pw' and key == '\n':
1254             if self.input_ == '':
1255                 self.log('@ aborted')
1256             else:
1257                 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1258                 self.log('@ sent new password for protection character "%s"' % self.tile_control_char)
1259             self.switch_mode('admin')
1260         elif self.mode.name == 'password' and key == '\n':
1261             if self.input_ == '':
1262                 self.input_ = ' '
1263             self.password = self.input_
1264             self.switch_mode('edit')
1265         elif self.mode.name == 'admin_enter' and key == '\n':
1266             self.send('BECOME_ADMIN ' + quote(self.input_))
1267             self.switch_mode('play')
1268         elif self.mode.name == 'control_pw_type' and key == '\n':
1269             if len(self.input_) != 1:
1270                 self.log('@ entered non-single-char, therefore aborted')
1271                 self.switch_mode('admin')
1272             else:
1273                 self.tile_control_char = self.input_
1274                 self.switch_mode('control_pw_pw')
1275         elif self.mode.name == 'admin_thing_protect' and key == '\n':
1276             if len(self.input_) != 1:
1277                 self.log('@ entered non-single-char, therefore aborted')
1278             else:
1279                 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1280                 self.log('@ sent new protection character for thing')
1281             self.switch_mode('admin')
1282         elif self.mode.name == 'control_tile_type' and key == '\n':
1283             if len(self.input_) != 1:
1284                 self.log('@ entered non-single-char, therefore aborted')
1285                 self.switch_mode('admin')
1286             else:
1287                 self.tile_control_char = self.input_
1288                 self.switch_mode('control_tile_draw')
1289         elif self.mode.name == 'chat' and key == '\n':
1290             if self.input_ == '':
1291                 return
1292             if self.input_[0] == '/':
1293                 if self.input_.startswith('/nick'):
1294                     tokens = self.input_.split(maxsplit=1)
1295                     if len(tokens) == 2:
1296                         self.send('NICK ' + quote(tokens[1]))
1297                     else:
1298                         self.log('? need login name')
1299                 else:
1300                     self.log('? unknown command')
1301             else:
1302                 self.send('ALL ' + quote(self.input_))
1303             self.input_ = ""
1304         elif self.mode.name == 'name_thing' and key == '\n':
1305             if self.input_ == '':
1306                 self.input_ = ' '
1307             self.send('THING_NAME %s %s' % (quote(self.input_),
1308                                             quote(self.password)))
1309             self.switch_mode('edit')
1310         elif self.mode.name == 'annotate' and key == '\n':
1311             if self.input_ == '':
1312                 self.input_ = ' '
1313             self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1314                                              quote(self.password)))
1315             self.switch_mode('edit')
1316         elif self.mode.name == 'portal' and key == '\n':
1317             if self.input_ == '':
1318                 self.input_ = ' '
1319             self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1320                                            quote(self.password)))
1321             self.switch_mode('edit')
1322         elif self.mode.name == 'study':
1323             if self.mode.mode_switch_on_key(self, key):
1324                 return
1325             elif key == self.keys['toggle_map_mode']:
1326                 self.toggle_map_mode()
1327             elif key in self.movement_keys:
1328                 move_explorer(self.movement_keys[key])
1329         elif self.mode.name == 'play':
1330             if self.mode.mode_switch_on_key(self, key):
1331                 return
1332             elif key == self.keys['door'] and task_action_on('door'):
1333                 self.send('TASK:DOOR')
1334             elif key == self.keys['consume'] and task_action_on('consume'):
1335                 self.send('TASK:INTOXICATE')
1336             elif key == self.keys['wear'] and task_action_on('wear'):
1337                 self.send('TASK:WEAR')
1338             elif key == self.keys['spin'] and task_action_on('spin'):
1339                 self.send('TASK:SPIN')
1340             elif key == self.keys['dance'] and task_action_on('dance'):
1341                 self.send('TASK:DANCE')
1342             elif key == self.keys['teleport']:
1343                 if self.game.player.position in self.game.portals:
1344                     self.socket.host = self.game.portals[self.game.player.position]
1345                     self.reconnect()
1346                 else:
1347                     self.flash = True
1348                     self.log('? not standing on portal')
1349             elif key in self.movement_keys and task_action_on('move'):
1350                 self.send('TASK:MOVE ' + self.movement_keys[key])
1351         elif self.mode.name == 'write':
1352             self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1353             self.switch_mode('edit')
1354         elif self.mode.name == 'control_tile_draw':
1355             if self.mode.mode_switch_on_key(self, key):
1356                 return
1357             elif key in self.movement_keys:
1358                 move_explorer(self.movement_keys[key])
1359             elif key == self.keys['toggle_tile_draw']:
1360                 self.tile_draw = False if self.tile_draw else True
1361         elif self.mode.name == 'admin':
1362             if self.mode.mode_switch_on_key(self, key):
1363                 return
1364             elif key == self.keys['toggle_map_mode']:
1365                 self.toggle_map_mode()
1366             elif key in self.movement_keys and task_action_on('move'):
1367                 self.send('TASK:MOVE ' + self.movement_keys[key])
1368         elif self.mode.name == 'edit':
1369             if self.mode.mode_switch_on_key(self, key):
1370                 return
1371             elif key == self.keys['flatten'] and task_action_on('flatten'):
1372                 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1373             elif key == self.keys['install'] and task_action_on('install'):
1374                 self.send('TASK:INSTALL %s' % quote(self.password))
1375             elif key == self.keys['toggle_map_mode']:
1376                 self.toggle_map_mode()
1377             elif key in self.movement_keys and task_action_on('move'):
1378                 self.send('TASK:MOVE ' + self.movement_keys[key])
1379
1380 if len(sys.argv) != 2:
1381     raise ArgError('wrong number of arguments, need game host')
1382 host = sys.argv[1]
1383 RogueChatTUI(host)