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