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