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