home · contact · privacy
Add terrain editing access control via passwords.
[plomrogue2] / rogue_chat_curses.py
1 #!/usr/bin/env python3
2 import curses
3 import queue
4 import threading
5 from plomrogue.game import GameBase
6 from plomrogue.parser import Parser
7 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
8 from plomrogue.things import ThingBase
9 from plomrogue.misc import quote
10 from plomrogue.errors import BrokenSocketConnection
11
12 from ws4py.client import WebSocketBaseClient
13 class WebSocketClient(WebSocketBaseClient):
14
15     def __init__(self, recv_handler, *args, **kwargs):
16         super().__init__(*args, **kwargs)
17         self.recv_handler = recv_handler
18         self.connect()
19
20     def received_message(self, message):
21         if message.is_text:
22             message = str(message)
23             self.recv_handler(message)
24
25     @property
26     def plom_closed(self):
27         return self.client_terminated
28
29 from plomrogue.io_tcp import PlomSocket
30 class PlomSocketClient(PlomSocket):
31
32     def __init__(self, recv_handler, url):
33         import socket
34         self.recv_handler = recv_handler
35         host, port = url.split(':')
36         super().__init__(socket.create_connection((host, port)))
37
38     def close(self):
39         self.socket.close()
40
41     def run(self):
42         import ssl
43         try:
44             for msg in self.recv():
45                 if msg == 'NEED_SSL':
46                     self.socket = ssl.wrap_socket(self.socket)
47                     continue
48                 self.recv_handler(msg)
49         except BrokenSocketConnection:
50             pass  # we assume socket will be known as dead by now
51
52 def cmd_TURN(game, n):
53     game.turn = n
54     game.things = []
55     game.portals = {}
56     game.turn_complete = False
57 cmd_TURN.argtypes = 'int:nonneg'
58
59 def cmd_LOGIN_OK(game):
60     game.tui.switch_mode('post_login_wait')
61     game.tui.send('GET_GAMESTATE')
62     game.tui.log_msg('@ welcome')
63 cmd_LOGIN_OK.argtypes = ''
64
65 def cmd_CHAT(game, msg):
66     game.tui.log_msg('# ' + msg)
67     game.tui.do_refresh = True
68 cmd_CHAT.argtypes = 'string'
69
70 def cmd_PLAYER_ID(game, player_id):
71     game.player_id = player_id
72 cmd_PLAYER_ID.argtypes = 'int:nonneg'
73
74 def cmd_THING_POS(game, thing_id, position):
75     t = game.get_thing(thing_id, True)
76     t.position = position
77 cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg'
78
79 def cmd_THING_NAME(game, thing_id, name):
80     t = game.get_thing(thing_id, True)
81     t.name = name
82 cmd_THING_NAME.argtypes = 'int:nonneg string'
83
84 def cmd_MAP(game, geometry, size, content):
85     map_geometry_class = globals()['MapGeometry' + geometry]
86     game.map_geometry = map_geometry_class(size)
87     game.map_content = content
88     if type(game.map_geometry) == MapGeometrySquare:
89         game.tui.movement_keys = {
90             game.tui.keys['square_move_up']: 'UP',
91             game.tui.keys['square_move_left']: 'LEFT',
92             game.tui.keys['square_move_down']: 'DOWN',
93             game.tui.keys['square_move_right']: 'RIGHT',
94         }
95     elif type(game.map_geometry) == MapGeometryHex:
96         game.tui.movement_keys = {
97             game.tui.keys['hex_move_upleft']: 'UPLEFT',
98             game.tui.keys['hex_move_upright']: 'UPRIGHT',
99             game.tui.keys['hex_move_right']: 'RIGHT',
100             game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
101             game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
102             game.tui.keys['hex_move_left']: 'LEFT',
103         }
104 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
105
106 def cmd_MAP_CONTROL(game, content):
107     game.map_control_content = content
108 cmd_MAP_CONTROL.argtypes = 'string'
109
110 def cmd_GAME_STATE_COMPLETE(game):
111     game.info_db = {}
112     if game.tui.mode.name == 'post_login_wait':
113         game.tui.switch_mode('play')
114         game.tui.help()
115     if game.tui.mode.shows_info:
116         game.tui.query_info()
117     player = game.get_thing(game.player_id, False)
118     if player.position in game.portals:
119         game.tui.teleport_target_host = game.portals[player.position]
120         game.tui.switch_mode('teleport')
121     game.turn_complete = True
122     game.tui.do_refresh = True
123 cmd_GAME_STATE_COMPLETE.argtypes = ''
124
125 def cmd_PORTAL(game, position, msg):
126     game.portals[position] = msg
127 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
128
129 def cmd_PLAY_ERROR(game, msg):
130     game.tui.flash()
131     game.tui.do_refresh = True
132 cmd_PLAY_ERROR.argtypes = 'string'
133
134 def cmd_GAME_ERROR(game, msg):
135     game.tui.log_msg('? game error: ' + msg)
136     game.tui.do_refresh = True
137 cmd_GAME_ERROR.argtypes = 'string'
138
139 def cmd_ARGUMENT_ERROR(game, msg):
140     game.tui.log_msg('? syntax error: ' + msg)
141     game.tui.do_refresh = True
142 cmd_ARGUMENT_ERROR.argtypes = 'string'
143
144 def cmd_ANNOTATION(game, position, msg):
145     game.info_db[position] = msg
146     if game.tui.mode.shows_info:
147         game.tui.do_refresh = True
148 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
149
150 def cmd_TASKS(game, tasks_comma_separated):
151     game.tasks = tasks_comma_separated.split(',')
152 cmd_TASKS.argtypes = 'string'
153
154 def cmd_PONG(game):
155     pass
156 cmd_PONG.argtypes = ''
157
158 class Game(GameBase):
159     thing_type = ThingBase
160     turn_complete = False
161     tasks = {}
162
163     def __init__(self, *args, **kwargs):
164         super().__init__(*args, **kwargs)
165         self.register_command(cmd_LOGIN_OK)
166         self.register_command(cmd_PONG)
167         self.register_command(cmd_CHAT)
168         self.register_command(cmd_PLAYER_ID)
169         self.register_command(cmd_TURN)
170         self.register_command(cmd_THING_POS)
171         self.register_command(cmd_THING_NAME)
172         self.register_command(cmd_MAP)
173         self.register_command(cmd_MAP_CONTROL)
174         self.register_command(cmd_PORTAL)
175         self.register_command(cmd_ANNOTATION)
176         self.register_command(cmd_GAME_STATE_COMPLETE)
177         self.register_command(cmd_ARGUMENT_ERROR)
178         self.register_command(cmd_GAME_ERROR)
179         self.register_command(cmd_PLAY_ERROR)
180         self.register_command(cmd_TASKS)
181         self.map_content = ''
182         self.player_id = -1
183         self.info_db = {}
184         self.portals = {}
185
186     def get_string_options(self, string_option_type):
187         if string_option_type == 'map_geometry':
188             return ['Hex', 'Square']
189         return None
190
191     def get_command(self, command_name):
192         from functools import partial
193         f = partial(self.commands[command_name], self)
194         f.argtypes = self.commands[command_name].argtypes
195         return f
196
197 class TUI:
198
199     class Mode:
200
201         def __init__(self, name, has_input_prompt=False, shows_info=False,
202                      is_intro = False):
203             self.name = name
204             self.has_input_prompt = has_input_prompt
205             self.shows_info = shows_info
206             self.is_intro = is_intro
207
208     def __init__(self, host):
209         import os
210         import json
211         self.host = host
212         self.mode_play = self.Mode('play')
213         self.mode_study = self.Mode('study', shows_info=True)
214         self.mode_edit = self.Mode('edit')
215         self.mode_annotate = self.Mode('annotate', has_input_prompt=True, shows_info=True)
216         self.mode_portal = self.Mode('portal', has_input_prompt=True, shows_info=True)
217         self.mode_chat = self.Mode('chat', has_input_prompt=True)
218         self.mode_waiting_for_server = self.Mode('waiting_for_server', is_intro=True)
219         self.mode_login = self.Mode('login', has_input_prompt=True, is_intro=True)
220         self.mode_post_login_wait = self.Mode('post_login_wait', is_intro=True)
221         self.mode_teleport = self.Mode('teleport', has_input_prompt=True)
222         self.mode_password = self.Mode('password', has_input_prompt=True)
223         self.game = Game()
224         self.game.tui = self
225         self.parser = Parser(self.game)
226         self.log = []
227         self.do_refresh = True
228         self.queue = queue.Queue()
229         self.login_name = None
230         self.map_mode = 'terrain'
231         self.password = 'foo'
232         self.switch_mode('waiting_for_server')
233         self.keys = {
234             'switch_to_chat': 't',
235             'switch_to_play': 'p',
236             'switch_to_password': 'p',
237             'switch_to_annotate': 'm',
238             'switch_to_portal': 'P',
239             'switch_to_study': '?',
240             'switch_to_edit': 'm',
241             'flatten': 'F',
242             'toggle_map_mode': 'M',
243             'hex_move_upleft': 'w',
244             'hex_move_upright': 'e',
245             'hex_move_right': 'd',
246             'hex_move_downright': 'x',
247             'hex_move_downleft': 'y',
248             'hex_move_left': 'a',
249             'square_move_up': 'w',
250             'square_move_left': 'a',
251             'square_move_down': 's',
252             'square_move_right': 'd',
253         }
254         if os.path.isfile('config.json'):
255             with open('config.json', 'r') as f:
256                 keys_conf = json.loads(f.read())
257             for k in keys_conf:
258                 self.keys[k] = keys_conf[k]
259         curses.wrapper(self.loop)
260
261     def flash(self):
262         curses.flash()
263
264     def send(self, msg):
265         try:
266             if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
267                 raise BrokenSocketConnection
268             self.socket.send(msg)
269         except (BrokenPipeError, BrokenSocketConnection):
270             self.log_msg('@ server disconnected :(')
271             self.do_refresh = True
272
273     def log_msg(self, msg):
274         self.log += [msg]
275         if len(self.log) > 100:
276             self.log = self.log[-100:]
277
278     def query_info(self):
279         self.send('GET_ANNOTATION ' + str(self.explorer))
280
281     def switch_mode(self, mode_name, keep_position = False):
282         self.map_mode = 'terrain'
283         self.mode = getattr(self, 'mode_' + mode_name)
284         if self.mode.shows_info and not keep_position:
285             player = self.game.get_thing(self.game.player_id, False)
286             self.explorer = YX(player.position.y, player.position.x)
287         if self.mode.name == 'waiting_for_server':
288             self.log_msg('@ waiting for server …')
289         elif self.mode.name == 'login':
290             if self.login_name:
291                 self.send('LOGIN ' + quote(self.login_name))
292             else:
293                 self.log_msg('@ enter username')
294         elif self.mode.name == 'teleport':
295             self.log_msg("@ May teleport to %s" % (self.teleport_target_host)),
296             self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
297         elif self.mode.name == 'annotate' and self.explorer in self.game.info_db:
298             info = self.game.info_db[self.explorer]
299             if info != '(none)':
300                 self.input_ = info
301         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
302             self.input_ = self.game.portals[self.explorer]
303         elif self.mode.name == 'password':
304             self.input_ = self.password
305
306     def help(self):
307         self.log_msg("HELP:");
308         self.log_msg("chat mode commands:");
309         self.log_msg("  /nick NAME - re-name yourself to NAME");
310         self.log_msg("  /msg USER TEXT - send TEXT to USER");
311         self.log_msg("  /help - show this help");
312         self.log_msg("  /%s or /play - switch to play mode" % self.keys['switch_to_play']);
313         self.log_msg("  /%s or /study - switch to study mode" % self.keys['switch_to_study']);
314         self.log_msg("commands common to study and play mode:");
315         if 'MOVE' in self.game.tasks:
316             self.log_msg("  %s - move" % ','.join(self.movement_keys));
317         self.log_msg("  %s - switch to chat mode" % self.keys['switch_to_chat']);
318         self.log_msg("commands specific to play mode:");
319         self.log_msg("  %s - enter terrain password" % self.keys['switch_to_password']);
320         if 'WRITE' in self.game.tasks:
321             self.log_msg("  %s - write following ASCII character" % self.keys['switch_to_edit']);
322         if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
323             self.log_msg("  %s - flatten surroundings" % self.keys['flatten']);
324         self.log_msg("  %s - switch to study mode" % self.keys['switch_to_study']);
325         self.log_msg("commands specific to study mode:");
326         self.log_msg("  %s - switch to play mode" % self.keys['switch_to_play']);
327         if 'MOVE' not in self.game.tasks:
328             self.log_msg("  %s - move" % ','.join(self.movement_keys));
329         self.log_msg("  %s - annotate terrain" % self.keys['switch_to_annotate']);
330         self.log_msg("  %s - toggle terrain/control view" % self.keys['toggle_map_mode']);
331
332     def loop(self, stdscr):
333         import time
334         import datetime
335
336         def safe_addstr(y, x, line):
337             if y < self.size.y - 1 or x + len(line) < self.size.x:
338                 stdscr.addstr(y, x, line)
339             else:  # workaround to <https://stackoverflow.com/q/7063128>
340                 cut_i = self.size.x - x - 1
341                 cut = line[:cut_i]
342                 last_char = line[cut_i]
343                 stdscr.addstr(y, self.size.x - 2, last_char)
344                 stdscr.insstr(y, self.size.x - 2, ' ')
345                 stdscr.addstr(y, x, cut)
346
347         def connect():
348
349             def handle_recv(msg):
350                 if msg == 'BYE':
351                     self.socket.close()
352                 else:
353                     self.queue.put(msg)
354
355             socket_client_class = PlomSocketClient
356             if self.host.startswith('ws://') or self.host.startswith('wss://'):
357                 socket_client_class = WebSocketClient
358             while True:
359                 try:
360                     self.socket = socket_client_class(handle_recv, self.host)
361                     self.socket_thread = threading.Thread(target=self.socket.run)
362                     self.socket_thread.start()
363                     self.socket.send('TASKS')
364                     self.switch_mode('login')
365                     return
366                 except ConnectionRefusedError:
367                     self.log_msg('@ server connect failure, trying again …')
368                     draw_screen()
369                     stdscr.refresh()
370                     time.sleep(1)
371
372         def reconnect():
373             self.send('QUIT')
374             time.sleep(0.1)  # FIXME necessitated by some some strange SSL race
375                              # conditions with ws4py, find out what exactly
376             self.switch_mode('waiting_for_server')
377             connect()
378
379         def handle_input(msg):
380             command, args = self.parser.parse(msg)
381             command(*args)
382
383         def msg_into_lines_of_width(msg, width):
384             chunk = ''
385             lines = []
386             x = 0
387             for i in range(len(msg)):
388                 if x >= width or msg[i] == "\n":
389                     lines += [chunk]
390                     chunk = ''
391                     x = 0
392                 if msg[i] != "\n":
393                     chunk += msg[i]
394                 x += 1
395             lines += [chunk]
396             return lines
397
398         def reset_screen_size():
399             self.size = YX(*stdscr.getmaxyx())
400             self.size = self.size - YX(self.size.y % 4, 0)
401             self.size = self.size - YX(0, self.size.x % 4)
402             self.window_width = int(self.size.x / 2)
403
404         def recalc_input_lines():
405             if not self.mode.has_input_prompt:
406                 self.input_lines = []
407             else:
408                 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
409                                                            self.window_width)
410
411         def move_explorer(direction):
412             target = self.game.map_geometry.move(self.explorer, direction)
413             if target:
414                 self.explorer = target
415                 self.query_info()
416             else:
417                 self.flash()
418
419         def draw_history():
420             lines = []
421             for line in self.log:
422                 lines += msg_into_lines_of_width(line, self.window_width)
423             lines.reverse()
424             height_header = 2
425             max_y = self.size.y - len(self.input_lines)
426             for i in range(len(lines)):
427                 if (i >= max_y - height_header):
428                     break
429                 safe_addstr(max_y - i - 1, self.window_width, lines[i])
430
431         def draw_info():
432             if not self.game.turn_complete:
433                 return
434             pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
435             info = 'TERRAIN: %s\n' % self.game.map_content[pos_i]
436             for t in self.game.things:
437                 if t.position == self.explorer:
438                     info += 'PLAYER @: %s\n' % t.name
439             if self.explorer in self.game.portals:
440                 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
441             else:
442                 info += 'PORTAL: (none)\n'
443             if self.explorer in self.game.info_db:
444                 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
445             else:
446                 info += 'ANNOTATION: waiting …'
447             lines = msg_into_lines_of_width(info, self.window_width)
448             height_header = 2
449             for i in range(len(lines)):
450                 y = height_header + i
451                 if y >= self.size.y - len(self.input_lines):
452                     break
453                 safe_addstr(y, self.window_width, lines[i])
454
455         def draw_input():
456             y = self.size.y - len(self.input_lines)
457             for i in range(len(self.input_lines)):
458                 safe_addstr(y, self.window_width, self.input_lines[i])
459                 y += 1
460
461         def draw_turn():
462             if not self.game.turn_complete:
463                 return
464             safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
465
466         def draw_mode():
467             safe_addstr(1, self.window_width, 'MODE: ' + self.mode.name)
468
469         def draw_map():
470             if not self.game.turn_complete:
471                 return
472             map_lines_split = []
473             map_content = self.game.map_content
474             if self.map_mode == 'control':
475                 map_content = self.game.map_control_content
476             for y in range(self.game.map_geometry.size.y):
477                 start = self.game.map_geometry.size.x * y
478                 end = start + self.game.map_geometry.size.x
479                 map_lines_split += [list(map_content[start:end])]
480             if self.map_mode == 'terrain':
481                 for t in self.game.things:
482                     map_lines_split[t.position.y][t.position.x] = '@'
483             if self.mode.shows_info:
484                 map_lines_split[self.explorer.y][self.explorer.x] = '?'
485             map_lines = []
486             if type(self.game.map_geometry) == MapGeometryHex:
487                 indent = 0
488                 for line in map_lines_split:
489                     map_lines += [indent*' ' + ' '.join(line)]
490                     indent = 0 if indent else 1
491             else:
492                 for line in map_lines_split:
493                     map_lines += [' '.join(line)]
494             window_center = YX(int(self.size.y / 2),
495                                int(self.window_width / 2))
496             player = self.game.get_thing(self.game.player_id, False)
497             center = player.position
498             if self.mode.shows_info:
499                 center = self.explorer
500             center = YX(center.y, center.x * 2)
501             offset = center - window_center
502             if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
503                 offset += YX(0, 1)
504             term_y = max(0, -offset.y)
505             term_x = max(0, -offset.x)
506             map_y = max(0, offset.y)
507             map_x = max(0, offset.x)
508             while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
509                 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
510                 safe_addstr(term_y, term_x, to_draw)
511                 term_y += 1
512                 map_y += 1
513
514         def draw_screen():
515             stdscr.clear()
516             recalc_input_lines()
517             if self.mode.has_input_prompt:
518                 draw_input()
519             if self.mode.shows_info:
520                 draw_info()
521             else:
522                 draw_history()
523             draw_mode()
524             if not self.mode.is_intro:
525                 draw_turn()
526                 draw_map()
527
528         curses.curs_set(False)  # hide cursor
529         curses.use_default_colors();
530         stdscr.timeout(10)
531         reset_screen_size()
532         self.explorer = YX(0, 0)
533         self.input_ = ''
534         input_prompt = '> '
535         connect()
536         last_ping = datetime.datetime.now()
537         interval = datetime.timedelta(seconds=30)
538         while True:
539             now = datetime.datetime.now()
540             if now - last_ping > interval:
541                 self.send('PING')
542                 last_ping = now
543             if self.do_refresh:
544                 draw_screen()
545                 self.do_refresh = False
546             while True:
547                 try:
548                     msg = self.queue.get(block=False)
549                     handle_input(msg)
550                 except queue.Empty:
551                     break
552             try:
553                 key = stdscr.getkey()
554                 self.do_refresh = True
555             except curses.error:
556                 continue
557             if key == 'KEY_RESIZE':
558                 reset_screen_size()
559             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
560                 self.input_ = self.input_[:-1]
561             elif self.mode.has_input_prompt and key != '\n':  # Return key
562                 self.input_ += key
563                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
564                 if len(self.input_) > max_length:
565                     self.input_ = self.input_[:max_length]
566             elif self.mode == self.mode_login and key == '\n':
567                 self.login_name = self.input_
568                 self.send('LOGIN ' + quote(self.input_))
569                 self.input_ = ""
570             elif self.mode == self.mode_password and key == '\n':
571                 if self.input_ == '':
572                     self.input_ = ' '
573                 self.password = self.input_
574                 self.input_ = ""
575                 self.switch_mode('play')
576             elif self.mode == self.mode_chat and key == '\n':
577                 if self.input_[0] == '/':
578                     if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
579                         self.switch_mode('play')
580                     elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
581                         self.switch_mode('study')
582                     elif self.input_ == '/help':
583                         self.help()
584                     elif self.input_ == '/reconnect':
585                         reconnect()
586                     elif self.input_.startswith('/nick'):
587                         tokens = self.input_.split(maxsplit=1)
588                         if len(tokens) == 2:
589                             self.send('NICK ' + quote(tokens[1]))
590                         else:
591                             self.log_msg('? need login name')
592                     elif self.input_.startswith('/msg'):
593                         tokens = self.input_.split(maxsplit=2)
594                         if len(tokens) == 3:
595                             self.send('QUERY %s %s' % (quote(tokens[1]),
596                                                               quote(tokens[2])))
597                         else:
598                             self.log_msg('? need message target and message')
599                     else:
600                         self.log_msg('? unknown command')
601                 else:
602                     self.send('ALL ' + quote(self.input_))
603                 self.input_ = ""
604             elif self.mode == self.mode_annotate and key == '\n':
605                 if self.input_ == '':
606                     self.input_ = ' '
607                 self.send('ANNOTATE %s %s' % (self.explorer, quote(self.input_)))
608                 self.input_ = ""
609                 self.switch_mode('study', keep_position=True)
610             elif self.mode == self.mode_portal and key == '\n':
611                 if self.input_ == '':
612                     self.input_ = ' '
613                 self.send('PORTAL %s %s' % (self.explorer, quote(self.input_)))
614                 self.input_ = ""
615                 self.switch_mode('study', keep_position=True)
616             elif self.mode == self.mode_teleport and key == '\n':
617                 if self.input_ == 'YES!':
618                     self.host = self.teleport_target_host
619                     reconnect()
620                 else:
621                     self.log_msg('@ teleport aborted')
622                     self.switch_mode('play')
623                 self.input_ = ''
624             elif self.mode == self.mode_study:
625                 if key == self.keys['switch_to_chat']:
626                     self.switch_mode('chat')
627                 elif key == self.keys['switch_to_play']:
628                     self.switch_mode('play')
629                 elif key == self.keys['switch_to_annotate']:
630                     self.switch_mode('annotate', keep_position=True)
631                 elif key == self.keys['switch_to_portal']:
632                     self.switch_mode('portal', keep_position=True)
633                 elif key == self.keys['toggle_map_mode']:
634                     if self.map_mode == 'terrain':
635                         self.map_mode = 'control'
636                     else:
637                         self.map_mode = 'terrain'
638                 elif key in self.movement_keys:
639                     move_explorer(self.movement_keys[key])
640             elif self.mode == self.mode_play:
641                 if key == self.keys['switch_to_chat']:
642                     self.switch_mode('chat')
643                 elif key == self.keys['switch_to_study']:
644                     self.switch_mode('study')
645                 elif key == self.keys['switch_to_password']:
646                     self.switch_mode('password')
647                 if key == self.keys['switch_to_edit'] and\
648                    'WRITE' in self.game.tasks:
649                     self.switch_mode('edit')
650                 elif key == self.keys['flatten'] and\
651                      'FLATTEN_SURROUNDINGS' in self.game.tasks:
652                     self.send('TASK:FLATTEN_SURROUNDINGS')
653                 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
654                     self.send('TASK:MOVE ' + self.movement_keys[key])
655             elif self.mode == self.mode_edit:
656                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
657                 self.switch_mode('play')
658
659 TUI('localhost:5000')