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