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