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