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