7 from plomrogue.game import GameBase
8 from plomrogue.parser import Parser
9 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
10 from plomrogue.things import ThingBase
11 from plomrogue.misc import quote
12 from plomrogue.errors import BrokenSocketConnection, ArgError
17 'long': 'This mode allows you to interact with the map in various ways.'
21 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it. Toggle the map view to show or hide different information layers.'},
23 'short': 'world edit',
24 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the map edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
27 'short': 'name thing',
28 'long': 'Give name to/change name of thing here.'
30 'admin_thing_protect': {
31 'short': 'change thing protection',
32 'long': 'Change protection character for thing here.'
35 'short': 'change terrain',
36 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
39 'short': 'change protection character password',
40 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
43 'short': 'change protection character password',
44 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
46 'control_tile_type': {
47 'short': 'change tiles protection',
48 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
50 'control_tile_draw': {
51 'short': 'change tiles protection',
52 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
55 'short': 'annotate tile',
56 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
59 'short': 'edit portal',
60 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
64 '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:'
68 'long': 'Enter your player name.'
70 'waiting_for_server': {
71 'short': 'waiting for server response',
72 'long': 'Waiting for a server response.'
75 'short': 'waiting for server response',
76 'long': 'Waiting for a server response.'
79 'short': 'set world edit password',
80 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
83 'short': 'become admin',
84 'long': 'This mode allows you to become admin if you know an admin password.'
88 'long': 'This mode allows you access to actions limited to administrators.'
92 from ws4py.client import WebSocketBaseClient
93 class WebSocketClient(WebSocketBaseClient):
95 def __init__(self, recv_handler, *args, **kwargs):
96 super().__init__(*args, **kwargs)
97 self.recv_handler = recv_handler
100 def received_message(self, message):
102 message = str(message)
103 self.recv_handler(message)
106 def plom_closed(self):
107 return self.client_terminated
109 from plomrogue.io_tcp import PlomSocket
110 class PlomSocketClient(PlomSocket):
112 def __init__(self, recv_handler, url):
114 self.recv_handler = recv_handler
115 host, port = url.split(':')
116 super().__init__(socket.create_connection((host, port)))
124 for msg in self.recv():
125 if msg == 'NEED_SSL':
126 self.socket = ssl.wrap_socket(self.socket)
128 self.recv_handler(msg)
129 except BrokenSocketConnection:
130 pass # we assume socket will be known as dead by now
132 def cmd_TURN(game, n):
138 game.turn_complete = False
139 cmd_TURN.argtypes = 'int:nonneg'
141 def cmd_LOGIN_OK(game):
142 game.tui.switch_mode('post_login_wait')
143 game.tui.send('GET_GAMESTATE')
144 game.tui.log_msg('@ welcome')
145 cmd_LOGIN_OK.argtypes = ''
147 def cmd_ADMIN_OK(game):
148 game.tui.is_admin = True
149 game.tui.log_msg('@ you now have admin rights')
150 game.tui.switch_mode('admin')
151 game.tui.do_refresh = True
152 cmd_ADMIN_OK.argtypes = ''
154 def cmd_CHAT(game, msg):
155 game.tui.log_msg('# ' + msg)
156 game.tui.do_refresh = True
157 cmd_CHAT.argtypes = 'string'
159 def cmd_PLAYER_ID(game, player_id):
160 game.player_id = player_id
161 cmd_PLAYER_ID.argtypes = 'int:nonneg'
163 def cmd_THING(game, yx, thing_type, protection, thing_id):
164 t = game.get_thing(thing_id)
166 t = ThingBase(game, thing_id)
170 t.protection = protection
171 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg'
173 def cmd_THING_NAME(game, thing_id, name):
174 t = game.get_thing(thing_id)
177 cmd_THING_NAME.argtypes = 'int:nonneg string'
179 def cmd_THING_CHAR(game, thing_id, c):
180 t = game.get_thing(thing_id)
183 cmd_THING_CHAR.argtypes = 'int:nonneg char'
185 def cmd_MAP(game, geometry, size, content):
186 map_geometry_class = globals()['MapGeometry' + geometry]
187 game.map_geometry = map_geometry_class(size)
188 game.map_content = content
189 if type(game.map_geometry) == MapGeometrySquare:
190 game.tui.movement_keys = {
191 game.tui.keys['square_move_up']: 'UP',
192 game.tui.keys['square_move_left']: 'LEFT',
193 game.tui.keys['square_move_down']: 'DOWN',
194 game.tui.keys['square_move_right']: 'RIGHT',
196 elif type(game.map_geometry) == MapGeometryHex:
197 game.tui.movement_keys = {
198 game.tui.keys['hex_move_upleft']: 'UPLEFT',
199 game.tui.keys['hex_move_upright']: 'UPRIGHT',
200 game.tui.keys['hex_move_right']: 'RIGHT',
201 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
202 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
203 game.tui.keys['hex_move_left']: 'LEFT',
205 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
207 def cmd_FOV(game, content):
209 cmd_FOV.argtypes = 'string'
211 def cmd_MAP_CONTROL(game, content):
212 game.map_control_content = content
213 cmd_MAP_CONTROL.argtypes = 'string'
215 def cmd_GAME_STATE_COMPLETE(game):
216 if game.tui.mode.name == 'post_login_wait':
217 game.tui.switch_mode('play')
218 if game.tui.mode.shows_info:
219 game.tui.query_info()
220 game.turn_complete = True
221 game.tui.do_refresh = True
222 cmd_GAME_STATE_COMPLETE.argtypes = ''
224 def cmd_PORTAL(game, position, msg):
225 game.portals[position] = msg
226 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
228 def cmd_PLAY_ERROR(game, msg):
229 game.tui.log_msg('? ' + msg)
230 game.tui.flash = True
231 game.tui.do_refresh = True
232 cmd_PLAY_ERROR.argtypes = 'string'
234 def cmd_GAME_ERROR(game, msg):
235 game.tui.log_msg('? game error: ' + msg)
236 game.tui.do_refresh = True
237 cmd_GAME_ERROR.argtypes = 'string'
239 def cmd_ARGUMENT_ERROR(game, msg):
240 game.tui.log_msg('? syntax error: ' + msg)
241 game.tui.do_refresh = True
242 cmd_ARGUMENT_ERROR.argtypes = 'string'
244 def cmd_ANNOTATION_HINT(game, position):
245 game.info_hints += [position]
246 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
248 def cmd_ANNOTATION(game, position, msg):
249 game.info_db[position] = msg
250 if game.tui.mode.shows_info:
251 game.tui.do_refresh = True
252 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
254 def cmd_TASKS(game, tasks_comma_separated):
255 game.tasks = tasks_comma_separated.split(',')
256 game.tui.mode_write.legal = 'WRITE' in game.tasks
257 cmd_TASKS.argtypes = 'string'
259 def cmd_THING_TYPE(game, thing_type, symbol_hint):
260 game.thing_types[thing_type] = symbol_hint
261 cmd_THING_TYPE.argtypes = 'string char'
263 def cmd_TERRAIN(game, terrain_char, terrain_desc):
264 game.terrains[terrain_char] = terrain_desc
265 cmd_TERRAIN.argtypes = 'char string'
269 cmd_PONG.argtypes = ''
271 class Game(GameBase):
272 turn_complete = False
276 def __init__(self, *args, **kwargs):
277 super().__init__(*args, **kwargs)
278 self.register_command(cmd_LOGIN_OK)
279 self.register_command(cmd_ADMIN_OK)
280 self.register_command(cmd_PONG)
281 self.register_command(cmd_CHAT)
282 self.register_command(cmd_PLAYER_ID)
283 self.register_command(cmd_TURN)
284 self.register_command(cmd_THING)
285 self.register_command(cmd_THING_TYPE)
286 self.register_command(cmd_THING_NAME)
287 self.register_command(cmd_THING_CHAR)
288 self.register_command(cmd_TERRAIN)
289 self.register_command(cmd_MAP)
290 self.register_command(cmd_MAP_CONTROL)
291 self.register_command(cmd_PORTAL)
292 self.register_command(cmd_ANNOTATION)
293 self.register_command(cmd_ANNOTATION_HINT)
294 self.register_command(cmd_GAME_STATE_COMPLETE)
295 self.register_command(cmd_ARGUMENT_ERROR)
296 self.register_command(cmd_GAME_ERROR)
297 self.register_command(cmd_PLAY_ERROR)
298 self.register_command(cmd_TASKS)
299 self.register_command(cmd_FOV)
300 self.map_content = ''
307 def get_string_options(self, string_option_type):
308 if string_option_type == 'map_geometry':
309 return ['Hex', 'Square']
310 elif string_option_type == 'thing_type':
311 return self.thing_types.keys()
314 def get_command(self, command_name):
315 from functools import partial
316 f = partial(self.commands[command_name], self)
317 f.argtypes = self.commands[command_name].argtypes
322 def __init__(self, name, has_input_prompt=False, shows_info=False,
323 is_intro=False, is_single_char_entry=False):
325 self.short_desc = mode_helps[name]['short']
326 self.available_modes = []
327 self.has_input_prompt = has_input_prompt
328 self.shows_info = shows_info
329 self.is_intro = is_intro
330 self.help_intro = mode_helps[name]['long']
331 self.is_single_char_entry = is_single_char_entry
334 def iter_available_modes(self, tui):
335 for mode_name in self.available_modes:
336 mode = getattr(tui, 'mode_' + mode_name)
339 key = tui.keys['switch_to_' + mode.name]
342 def list_available_modes(self, tui):
344 if len(self.available_modes) > 0:
345 msg = 'Other modes available from here:\n'
346 for mode, key in self.iter_available_modes(tui):
347 msg += '[%s] – %s\n' % (key, mode.short_desc)
350 def mode_switch_on_key(self, tui, key_pressed):
351 for mode, key in self.iter_available_modes(tui):
352 if key_pressed == key:
353 tui.switch_mode(mode.name)
358 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
359 mode_admin = Mode('admin')
360 mode_play = Mode('play')
361 mode_study = Mode('study', shows_info=True)
362 mode_write = Mode('write', is_single_char_entry=True)
363 mode_edit = Mode('edit')
364 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
365 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
366 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
367 mode_control_tile_draw = Mode('control_tile_draw')
368 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
369 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
370 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
371 mode_chat = Mode('chat', has_input_prompt=True)
372 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
373 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
374 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
375 mode_password = Mode('password', has_input_prompt=True)
376 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
380 def __init__(self, host):
383 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
384 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
385 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
386 "control_tile_type", "chat",
387 "study", "play", "edit"]
388 self.mode_control_tile_draw.available_modes = ["admin_enter"]
389 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
390 "password", "chat", "study", "play",
396 self.parser = Parser(self.game)
398 self.do_refresh = True
399 self.queue = queue.Queue()
400 self.login_name = None
401 self.map_mode = 'terrain + things'
402 self.password = 'foo'
403 self.switch_mode('waiting_for_server')
405 'switch_to_chat': 't',
406 'switch_to_play': 'p',
407 'switch_to_password': 'P',
408 'switch_to_annotate': 'M',
409 'switch_to_portal': 'T',
410 'switch_to_study': '?',
411 'switch_to_edit': 'E',
412 'switch_to_write': 'm',
413 'switch_to_name_thing': 'N',
414 'switch_to_admin_enter': 'A',
415 'switch_to_control_pw_type': 'C',
416 'switch_to_control_tile_type': 'Q',
417 'switch_to_admin_thing_protect': 'T',
423 'toggle_map_mode': 'L',
424 'toggle_tile_draw': 'm',
425 'hex_move_upleft': 'w',
426 'hex_move_upright': 'e',
427 'hex_move_right': 'd',
428 'hex_move_downright': 'x',
429 'hex_move_downleft': 'y',
430 'hex_move_left': 'a',
431 'square_move_up': 'w',
432 'square_move_left': 'a',
433 'square_move_down': 's',
434 'square_move_right': 'd',
436 if os.path.isfile('config.json'):
437 with open('config.json', 'r') as f:
438 keys_conf = json.loads(f.read())
440 self.keys[k] = keys_conf[k]
441 self.show_help = False
442 self.disconnected = True
443 self.force_instant_connect = True
444 self.input_lines = []
447 curses.wrapper(self.loop)
451 def handle_recv(msg):
457 self.log_msg('@ attempting connect')
458 socket_client_class = PlomSocketClient
459 if self.host.startswith('ws://') or self.host.startswith('wss://'):
460 socket_client_class = WebSocketClient
462 self.socket = socket_client_class(handle_recv, self.host)
463 self.socket_thread = threading.Thread(target=self.socket.run)
464 self.socket_thread.start()
465 self.disconnected = False
466 self.game.thing_types = {}
467 self.game.terrains = {}
468 time.sleep(0.1) # give potential SSL negotation some time …
469 self.socket.send('TASKS')
470 self.socket.send('TERRAINS')
471 self.socket.send('THING_TYPES')
472 self.switch_mode('login')
473 except ConnectionRefusedError:
474 self.log_msg('@ server connect failure')
475 self.disconnected = True
476 self.switch_mode('waiting_for_server')
477 self.do_refresh = True
480 self.log_msg('@ attempting reconnect')
482 # necessitated by some strange SSL race conditions with ws4py
483 time.sleep(0.1) # FIXME find out why exactly necessary
484 self.switch_mode('waiting_for_server')
489 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
490 raise BrokenSocketConnection
491 self.socket.send(msg)
492 except (BrokenPipeError, BrokenSocketConnection):
493 self.log_msg('@ server disconnected :(')
494 self.disconnected = True
495 self.force_instant_connect = True
496 self.do_refresh = True
498 def log_msg(self, msg):
500 if len(self.log) > 100:
501 self.log = self.log[-100:]
503 def query_info(self):
504 self.send('GET_ANNOTATION ' + str(self.explorer))
506 def restore_input_values(self):
507 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
508 info = self.game.info_db[self.explorer]
511 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
512 self.input_ = self.game.portals[self.explorer]
513 elif self.mode.name == 'password':
514 self.input_ = self.password
515 elif self.mode.name == 'name_thing':
516 if hasattr(self.thing_selected, 'name'):
517 self.input_ = self.thing_selected.name
518 elif self.mode.name == 'admin_thing_protect':
519 if hasattr(self.thing_selected, 'protection'):
520 self.input_ = self.thing_selected.protection
522 def send_tile_control_command(self):
523 self.send('SET_TILE_CONTROL %s %s' %
524 (self.explorer, quote(self.tile_control_char)))
526 def toggle_map_mode(self):
527 if self.map_mode == 'terrain only':
528 self.map_mode = 'terrain + annotations'
529 elif self.map_mode == 'terrain + annotations':
530 self.map_mode = 'terrain + things'
531 elif self.map_mode == 'terrain + things':
532 self.map_mode = 'protections'
533 elif self.map_mode == 'protections':
534 self.map_mode = 'terrain only'
536 def switch_mode(self, mode_name):
537 self.tile_draw = False
538 if mode_name == 'admin_enter' and self.is_admin:
540 elif mode_name in {'name_thing', 'admin_thing_protect'}:
541 player = self.game.get_thing(self.game.player_id)
543 for t in [t for t in self.game.things if t.position == player.position
544 and t.id_ != player.id_]:
549 self.log_msg('? not standing over thing')
552 self.thing_selected = thing
553 self.mode = getattr(self, 'mode_' + mode_name)
554 if self.mode.name == 'control_tile_draw':
555 self.log_msg('@ finished tile protection drawing.')
556 if self.mode.name in {'control_tile_draw', 'control_tile_type',
558 self.map_mode = 'protections'
559 elif self.mode.name != 'edit':
560 self.map_mode = 'terrain + things'
561 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
562 player = self.game.get_thing(self.game.player_id)
563 self.explorer = YX(player.position.y, player.position.x)
564 if self.mode.shows_info:
566 if self.mode.is_single_char_entry:
567 self.show_help = True
568 if self.mode.name == 'waiting_for_server':
569 self.log_msg('@ waiting for server …')
570 elif self.mode.name == 'login':
572 self.send('LOGIN ' + quote(self.login_name))
574 self.log_msg('@ enter username')
575 elif self.mode.name == 'admin_enter':
576 self.log_msg('@ enter admin password:')
577 elif self.mode.name == 'control_pw_type':
578 self.log_msg('@ enter protection character for which you want to change the password:')
579 elif self.mode.name == 'control_tile_type':
580 self.log_msg('@ enter protection character which you want to draw:')
581 elif self.mode.name == 'admin_thing_protect':
582 self.log_msg('@ enter thing protection character:')
583 elif self.mode.name == 'control_pw_pw':
584 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
585 elif self.mode.name == 'control_tile_draw':
586 self.log_msg('@ can draw protection character "%s", turn drawing on/off with [%s], finish with [%s].' % (self.tile_control_char, self.keys['toggle_tile_draw'], self.keys['switch_to_admin_enter']))
588 self.restore_input_values()
590 def loop(self, stdscr):
593 def safe_addstr(y, x, line):
594 if y < self.size.y - 1 or x + len(line) < self.size.x:
595 stdscr.addstr(y, x, line)
596 else: # workaround to <https://stackoverflow.com/q/7063128>
597 cut_i = self.size.x - x - 1
599 last_char = line[cut_i]
600 stdscr.addstr(y, self.size.x - 2, last_char)
601 stdscr.insstr(y, self.size.x - 2, ' ')
602 stdscr.addstr(y, x, cut)
604 def handle_input(msg):
605 command, args = self.parser.parse(msg)
608 def msg_into_lines_of_width(msg, width):
612 for i in range(len(msg)):
613 if x >= width or msg[i] == "\n":
625 def reset_screen_size():
626 self.size = YX(*stdscr.getmaxyx())
627 self.size = self.size - YX(self.size.y % 4, 0)
628 self.size = self.size - YX(0, self.size.x % 4)
629 self.window_width = int(self.size.x / 2)
631 def recalc_input_lines():
632 if not self.mode.has_input_prompt:
633 self.input_lines = []
635 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
638 def move_explorer(direction):
639 target = self.game.map_geometry.move_yx(self.explorer, direction)
641 self.explorer = target
642 if self.mode.shows_info:
645 self.send_tile_control_command()
651 for line in self.log:
652 lines += msg_into_lines_of_width(line, self.window_width)
655 max_y = self.size.y - len(self.input_lines)
656 for i in range(len(lines)):
657 if (i >= max_y - height_header):
659 safe_addstr(max_y - i - 1, self.window_width, lines[i])
662 if not self.game.turn_complete:
664 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
665 info = 'MAP VIEW: %s\n' % self.map_mode
666 if self.game.fov[pos_i] != '.':
667 info += 'outside field of view'
669 terrain_char = self.game.map_content[pos_i]
671 if terrain_char in self.game.terrains:
672 terrain_desc = self.game.terrains[terrain_char]
673 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
674 protection = self.game.map_control_content[pos_i]
675 if protection == '.':
676 protection = 'unprotected'
677 info += 'PROTECTION: %s\n' % protection
678 for t in self.game.things:
679 if t.position == self.explorer:
680 protection = t.protection
681 if protection == '.':
682 protection = 'unprotected'
683 info += 'THING: %s / protection: %s / %s' %\
684 (t.type_, protection, self.game.thing_types[t.type_])
685 if hasattr(t, 'player_char'):
686 info += t.player_char
687 if hasattr(t, 'name'):
688 info += ' (%s)' % t.name
690 if self.explorer in self.game.portals:
691 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
693 info += 'PORTAL: (none)\n'
694 if self.explorer in self.game.info_db:
695 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
697 info += 'ANNOTATION: waiting …'
698 lines = msg_into_lines_of_width(info, self.window_width)
700 for i in range(len(lines)):
701 y = height_header + i
702 if y >= self.size.y - len(self.input_lines):
704 safe_addstr(y, self.window_width, lines[i])
707 y = self.size.y - len(self.input_lines)
708 for i in range(len(self.input_lines)):
709 safe_addstr(y, self.window_width, self.input_lines[i])
713 if not self.game.turn_complete:
715 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
718 help = "hit [%s] for help" % self.keys['help']
719 if self.mode.has_input_prompt:
720 help = "enter /help for help"
721 safe_addstr(1, self.window_width,
722 'MODE: %s – %s' % (self.mode.short_desc, help))
725 if not self.game.turn_complete:
728 for y in range(self.game.map_geometry.size.y):
729 start = self.game.map_geometry.size.x * y
730 end = start + self.game.map_geometry.size.x
731 if self.map_mode == 'protections':
732 map_lines_split += [[c + ' ' for c
733 in self.game.map_control_content[start:end]]]
735 map_lines_split += [[c + ' ' for c
736 in self.game.map_content[start:end]]]
737 if self.map_mode == 'terrain + annotations':
738 for p in self.game.info_hints:
739 map_lines_split[p.y][p.x] = 'A '
740 elif self.map_mode == 'terrain + things':
741 for p in self.game.portals.keys():
742 original = map_lines_split[p.y][p.x]
743 map_lines_split[p.y][p.x] = original[0] + 'P'
745 for t in self.game.things:
746 symbol = self.game.thing_types[t.type_]
748 if hasattr(t, 'player_char'):
749 meta_char = t.player_char
750 if t.position in used_positions:
752 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
753 used_positions += [t.position]
754 player = self.game.get_thing(self.game.player_id)
755 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
756 map_lines_split[self.explorer.y][self.explorer.x] = '??'
757 elif self.map_mode != 'terrain + things':
758 map_lines_split[player.position.y][player.position.x] = '??'
760 if type(self.game.map_geometry) == MapGeometryHex:
762 for line in map_lines_split:
763 map_lines += [indent * ' ' + ''.join(line)]
764 indent = 0 if indent else 1
766 for line in map_lines_split:
767 map_lines += [''.join(line)]
768 window_center = YX(int(self.size.y / 2),
769 int(self.window_width / 2))
770 center = player.position
771 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
772 center = self.explorer
773 center = YX(center.y, center.x * 2)
774 offset = center - window_center
775 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
777 term_y = max(0, -offset.y)
778 term_x = max(0, -offset.x)
779 map_y = max(0, offset.y)
780 map_x = max(0, offset.x)
781 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
782 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
783 safe_addstr(term_y, term_x, to_draw)
788 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
789 self.mode.help_intro)
790 if self.mode.name == 'play':
791 content += "Available actions:\n"
792 if 'MOVE' in self.game.tasks:
793 content += "[%s] – move player\n" % ','.join(self.movement_keys)
794 if 'PICK_UP' in self.game.tasks:
795 content += "[%s] – pick up thing\n" % self.keys['take_thing']
796 if 'DROP' in self.game.tasks:
797 content += "[%s] – drop thing\n" % self.keys['drop_thing']
798 content += '[%s] – teleport\n' % self.keys['teleport']
800 elif self.mode.name == 'study':
801 content += 'Available actions:\n'
802 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
803 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
805 elif self.mode.name == 'edit':
806 content += "Available actions:\n"
807 if 'MOVE' in self.game.tasks:
808 content += "[%s] – move player\n" % ','.join(self.movement_keys)
809 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
810 content += "[%s] – flatten surroundings\n" % self.keys['flatten']
811 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
813 elif self.mode.name == 'control_tile_draw':
814 content += "Available actions:\n"
815 content += "[%s] – toggle tile protection drawing\n" % self.keys['toggle_tile_draw']
817 elif self.mode.name == 'chat':
818 content += '/nick NAME – re-name yourself to NAME\n'
819 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
820 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
821 content += '/%s or /edit – switch to map edit mode\n' % self.keys['switch_to_edit']
822 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
823 elif self.mode.name == 'admin':
824 content += "Available actions:\n"
825 if 'MOVE' in self.game.tasks:
826 content += "[%s] – move player\n" % ','.join(self.movement_keys)
828 content += self.mode.list_available_modes(self)
829 for i in range(self.size.y):
831 self.window_width * (not self.mode.has_input_prompt),
832 ' ' * self.window_width)
834 for line in content.split('\n'):
835 lines += msg_into_lines_of_width(line, self.window_width)
836 for i in range(len(lines)):
840 self.window_width * (not self.mode.has_input_prompt),
846 if self.mode.has_input_prompt:
848 if self.mode.shows_info:
853 if not self.mode.is_intro:
859 curses.curs_set(False) # hide cursor
860 curses.use_default_colors()
863 self.explorer = YX(0, 0)
866 interval = datetime.timedelta(seconds=5)
867 last_ping = datetime.datetime.now() - interval
869 if self.disconnected and self.force_instant_connect:
870 self.force_instant_connect = False
872 now = datetime.datetime.now()
873 if now - last_ping > interval:
874 if self.disconnected:
884 self.do_refresh = False
887 msg = self.queue.get(block=False)
892 key = stdscr.getkey()
893 self.do_refresh = True
896 self.show_help = False
897 if key == 'KEY_RESIZE':
899 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
900 self.input_ = self.input_[:-1]
901 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
902 self.show_help = True
904 self.restore_input_values()
905 elif self.mode.has_input_prompt and key != '\n': # Return key
907 max_length = self.window_width * self.size.y - len(input_prompt) - 1
908 if len(self.input_) > max_length:
909 self.input_ = self.input_[:max_length]
910 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
911 self.show_help = True
912 elif self.mode.name == 'login' and key == '\n':
913 self.login_name = self.input_
914 self.send('LOGIN ' + quote(self.input_))
916 elif self.mode.name == 'control_pw_pw' and key == '\n':
917 if self.input_ == '':
918 self.log_msg('@ aborted')
920 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
921 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
922 self.switch_mode('admin')
923 elif self.mode.name == 'password' and key == '\n':
924 if self.input_ == '':
926 self.password = self.input_
927 self.switch_mode('edit')
928 elif self.mode.name == 'admin_enter' and key == '\n':
929 self.send('BECOME_ADMIN ' + quote(self.input_))
930 self.switch_mode('play')
931 elif self.mode.name == 'control_pw_type' and key == '\n':
932 if len(self.input_) != 1:
933 self.log_msg('@ entered non-single-char, therefore aborted')
934 self.switch_mode('admin')
936 self.tile_control_char = self.input_
937 self.switch_mode('control_pw_pw')
938 elif self.mode.name == 'admin_thing_protect' and key == '\n':
939 if len(self.input_) != 1:
940 self.log_msg('@ entered non-single-char, therefore aborted')
942 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
944 self.log_msg('@ sent new protection character for thing')
945 self.switch_mode('admin')
946 elif self.mode.name == 'control_tile_type' and key == '\n':
947 if len(self.input_) != 1:
948 self.log_msg('@ entered non-single-char, therefore aborted')
949 self.switch_mode('admin')
951 self.tile_control_char = self.input_
952 self.switch_mode('control_tile_draw')
953 elif self.mode.name == 'chat' and key == '\n':
954 if self.input_ == '':
956 if self.input_[0] == '/': # FIXME fails on empty input
957 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
958 self.switch_mode('play')
959 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
960 self.switch_mode('study')
961 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
962 self.switch_mode('edit')
963 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
964 self.switch_mode('admin_enter')
965 elif self.input_.startswith('/nick'):
966 tokens = self.input_.split(maxsplit=1)
968 self.send('NICK ' + quote(tokens[1]))
970 self.log_msg('? need login name')
972 self.log_msg('? unknown command')
974 self.send('ALL ' + quote(self.input_))
976 elif self.mode.name == 'name_thing' and key == '\n':
977 if self.input_ == '':
979 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
981 quote(self.password)))
982 self.switch_mode('edit')
983 elif self.mode.name == 'annotate' and key == '\n':
984 if self.input_ == '':
986 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
987 quote(self.password)))
988 self.switch_mode('edit')
989 elif self.mode.name == 'portal' and key == '\n':
990 if self.input_ == '':
992 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
993 quote(self.password)))
994 self.switch_mode('edit')
995 elif self.mode.name == 'study':
996 if self.mode.mode_switch_on_key(self, key):
998 elif key == self.keys['toggle_map_mode']:
999 self.toggle_map_mode()
1000 elif key in self.movement_keys:
1001 move_explorer(self.movement_keys[key])
1002 elif self.mode.name == 'play':
1003 if self.mode.mode_switch_on_key(self, key):
1005 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
1006 self.send('TASK:PICK_UP')
1007 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
1008 self.send('TASK:DROP')
1009 elif key == self.keys['teleport']:
1010 player = self.game.get_thing(self.game.player_id)
1011 if player.position in self.game.portals:
1012 self.host = self.game.portals[player.position]
1016 self.log_msg('? not standing on portal')
1017 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
1018 self.send('TASK:MOVE ' + self.movement_keys[key])
1019 elif self.mode.name == 'write':
1020 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1021 self.switch_mode('edit')
1022 elif self.mode.name == 'control_tile_draw':
1023 if self.mode.mode_switch_on_key(self, key):
1025 elif key in self.movement_keys:
1026 move_explorer(self.movement_keys[key])
1027 elif key == self.keys['toggle_tile_draw']:
1028 self.tile_draw = False if self.tile_draw else True
1029 elif self.mode.name == 'admin':
1030 if self.mode.mode_switch_on_key(self, key):
1032 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
1033 self.send('TASK:MOVE ' + self.movement_keys[key])
1034 elif self.mode.name == 'edit':
1035 if self.mode.mode_switch_on_key(self, key):
1037 elif key == self.keys['flatten'] and\
1038 'FLATTEN_SURROUNDINGS' in self.game.tasks:
1039 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1040 elif key == self.keys['toggle_map_mode']:
1041 self.toggle_map_mode()
1042 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
1043 self.send('TASK:MOVE ' + self.movement_keys[key])
1045 if len(sys.argv) != 2:
1046 raise ArgError('wrong number of arguments, need game host')