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.'},
24 'long': 'This mode allows you to change the map 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': 'change terrain',
28 '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.'
31 'short': 'change protection character password',
32 'long': 'This mode is the first of two steps to change the password for a tile protection character. First enter the tile protection character for which you want to change the password.'
35 'short': 'change protection character password',
36 'long': 'This mode is the second of two steps to change the password for a tile protection character. Enter the new password for the tile protection character you chose.'
38 'control_tile_type': {
39 'short': 'change tiles protection',
40 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile tile protection character you want to write.'
42 'control_tile_draw': {
43 'short': 'change tiles protection',
44 '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 tile protection character.'
47 'short': 'annotate tile',
48 '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.'
51 'short': 'edit portal',
52 '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.'
56 '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:'
60 'long': 'Enter your player name.'
62 'waiting_for_server': {
63 'short': 'waiting for server response',
64 'long': 'Waiting for a server response.'
67 'short': 'waiting for server response',
68 'long': 'Waiting for a server response.'
71 'short': 'set map edit password',
72 '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.'
75 'short': 'become admin',
76 'long': 'This mode allows you to become admin if you know an admin password.'
80 'long': 'This mode allows you access to actions limited to administrators.'
84 from ws4py.client import WebSocketBaseClient
85 class WebSocketClient(WebSocketBaseClient):
87 def __init__(self, recv_handler, *args, **kwargs):
88 super().__init__(*args, **kwargs)
89 self.recv_handler = recv_handler
92 def received_message(self, message):
94 message = str(message)
95 self.recv_handler(message)
98 def plom_closed(self):
99 return self.client_terminated
101 from plomrogue.io_tcp import PlomSocket
102 class PlomSocketClient(PlomSocket):
104 def __init__(self, recv_handler, url):
106 self.recv_handler = recv_handler
107 host, port = url.split(':')
108 super().__init__(socket.create_connection((host, port)))
116 for msg in self.recv():
117 if msg == 'NEED_SSL':
118 self.socket = ssl.wrap_socket(self.socket)
120 self.recv_handler(msg)
121 except BrokenSocketConnection:
122 pass # we assume socket will be known as dead by now
124 def cmd_TURN(game, n):
130 game.turn_complete = False
131 cmd_TURN.argtypes = 'int:nonneg'
133 def cmd_LOGIN_OK(game):
134 game.tui.switch_mode('post_login_wait')
135 game.tui.send('GET_GAMESTATE')
136 game.tui.log_msg('@ welcome')
137 cmd_LOGIN_OK.argtypes = ''
139 def cmd_ADMIN_OK(game):
140 game.tui.is_admin = True
141 game.tui.log_msg('@ you now have admin rights')
142 game.tui.switch_mode('admin')
143 game.tui.do_refresh = True
144 cmd_ADMIN_OK.argtypes = ''
146 def cmd_CHAT(game, msg):
147 game.tui.log_msg('# ' + msg)
148 game.tui.do_refresh = True
149 cmd_CHAT.argtypes = 'string'
151 def cmd_PLAYER_ID(game, player_id):
152 game.player_id = player_id
153 cmd_PLAYER_ID.argtypes = 'int:nonneg'
155 def cmd_THING(game, yx, thing_type, thing_id):
156 t = game.get_thing(thing_id)
158 t = ThingBase(game, thing_id)
162 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
164 def cmd_THING_NAME(game, thing_id, name):
165 t = game.get_thing(thing_id)
168 cmd_THING_NAME.argtypes = 'int:nonneg string'
170 def cmd_THING_CHAR(game, thing_id, c):
171 t = game.get_thing(thing_id)
174 cmd_THING_CHAR.argtypes = 'int:nonneg char'
176 def cmd_MAP(game, geometry, size, content):
177 map_geometry_class = globals()['MapGeometry' + geometry]
178 game.map_geometry = map_geometry_class(size)
179 game.map_content = content
180 if type(game.map_geometry) == MapGeometrySquare:
181 game.tui.movement_keys = {
182 game.tui.keys['square_move_up']: 'UP',
183 game.tui.keys['square_move_left']: 'LEFT',
184 game.tui.keys['square_move_down']: 'DOWN',
185 game.tui.keys['square_move_right']: 'RIGHT',
187 elif type(game.map_geometry) == MapGeometryHex:
188 game.tui.movement_keys = {
189 game.tui.keys['hex_move_upleft']: 'UPLEFT',
190 game.tui.keys['hex_move_upright']: 'UPRIGHT',
191 game.tui.keys['hex_move_right']: 'RIGHT',
192 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
193 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
194 game.tui.keys['hex_move_left']: 'LEFT',
196 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
198 def cmd_FOV(game, content):
200 cmd_FOV.argtypes = 'string'
202 def cmd_MAP_CONTROL(game, content):
203 game.map_control_content = content
204 cmd_MAP_CONTROL.argtypes = 'string'
206 def cmd_GAME_STATE_COMPLETE(game):
207 if game.tui.mode.name == 'post_login_wait':
208 game.tui.switch_mode('play')
209 if game.tui.mode.shows_info:
210 game.tui.query_info()
211 game.turn_complete = True
212 game.tui.do_refresh = True
213 cmd_GAME_STATE_COMPLETE.argtypes = ''
215 def cmd_PORTAL(game, position, msg):
216 game.portals[position] = msg
217 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
219 def cmd_PLAY_ERROR(game, msg):
220 game.tui.log_msg('? ' + msg)
221 game.tui.flash = True
222 game.tui.do_refresh = True
223 cmd_PLAY_ERROR.argtypes = 'string'
225 def cmd_GAME_ERROR(game, msg):
226 game.tui.log_msg('? game error: ' + msg)
227 game.tui.do_refresh = True
228 cmd_GAME_ERROR.argtypes = 'string'
230 def cmd_ARGUMENT_ERROR(game, msg):
231 game.tui.log_msg('? syntax error: ' + msg)
232 game.tui.do_refresh = True
233 cmd_ARGUMENT_ERROR.argtypes = 'string'
235 def cmd_ANNOTATION_HINT(game, position):
236 game.info_hints += [position]
237 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
239 def cmd_ANNOTATION(game, position, msg):
240 game.info_db[position] = msg
241 game.tui.restore_input_values()
242 if game.tui.mode.shows_info:
243 game.tui.do_refresh = True
244 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
246 def cmd_TASKS(game, tasks_comma_separated):
247 game.tasks = tasks_comma_separated.split(',')
248 game.tui.mode_write.legal = 'WRITE' in game.tasks
249 cmd_TASKS.argtypes = 'string'
251 def cmd_THING_TYPE(game, thing_type, symbol_hint):
252 game.thing_types[thing_type] = symbol_hint
253 cmd_THING_TYPE.argtypes = 'string char'
255 def cmd_TERRAIN(game, terrain_char, terrain_desc):
256 game.terrains[terrain_char] = terrain_desc
257 cmd_TERRAIN.argtypes = 'char string'
261 cmd_PONG.argtypes = ''
263 class Game(GameBase):
264 turn_complete = False
268 def __init__(self, *args, **kwargs):
269 super().__init__(*args, **kwargs)
270 self.register_command(cmd_LOGIN_OK)
271 self.register_command(cmd_ADMIN_OK)
272 self.register_command(cmd_PONG)
273 self.register_command(cmd_CHAT)
274 self.register_command(cmd_PLAYER_ID)
275 self.register_command(cmd_TURN)
276 self.register_command(cmd_THING)
277 self.register_command(cmd_THING_TYPE)
278 self.register_command(cmd_THING_NAME)
279 self.register_command(cmd_THING_CHAR)
280 self.register_command(cmd_TERRAIN)
281 self.register_command(cmd_MAP)
282 self.register_command(cmd_MAP_CONTROL)
283 self.register_command(cmd_PORTAL)
284 self.register_command(cmd_ANNOTATION)
285 self.register_command(cmd_ANNOTATION_HINT)
286 self.register_command(cmd_GAME_STATE_COMPLETE)
287 self.register_command(cmd_ARGUMENT_ERROR)
288 self.register_command(cmd_GAME_ERROR)
289 self.register_command(cmd_PLAY_ERROR)
290 self.register_command(cmd_TASKS)
291 self.register_command(cmd_FOV)
292 self.map_content = ''
299 def get_string_options(self, string_option_type):
300 if string_option_type == 'map_geometry':
301 return ['Hex', 'Square']
302 elif string_option_type == 'thing_type':
303 return self.thing_types.keys()
306 def get_command(self, command_name):
307 from functools import partial
308 f = partial(self.commands[command_name], self)
309 f.argtypes = self.commands[command_name].argtypes
314 def __init__(self, name, has_input_prompt=False, shows_info=False,
315 is_intro=False, is_single_char_entry=False):
317 self.short_desc = mode_helps[name]['short']
318 self.available_modes = []
319 self.has_input_prompt = has_input_prompt
320 self.shows_info = shows_info
321 self.is_intro = is_intro
322 self.help_intro = mode_helps[name]['long']
323 self.is_single_char_entry = is_single_char_entry
326 def iter_available_modes(self, tui):
327 for mode_name in self.available_modes:
328 mode = getattr(tui, 'mode_' + mode_name)
331 key = tui.keys['switch_to_' + mode.name]
334 def list_available_modes(self, tui):
336 if len(self.available_modes) > 0:
337 msg = 'Other modes available from here:\n'
338 for mode, key in self.iter_available_modes(tui):
339 msg += '[%s] – %s\n' % (key, mode.short_desc)
342 def mode_switch_on_key(self, tui, key_pressed):
343 for mode, key in self.iter_available_modes(tui):
344 if key_pressed == key:
345 tui.switch_mode(mode.name)
350 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
351 mode_admin = Mode('admin')
352 mode_play = Mode('play')
353 mode_study = Mode('study', shows_info=True)
354 mode_write = Mode('write', is_single_char_entry=True)
355 mode_edit = Mode('edit')
356 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
357 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
358 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
359 mode_control_tile_draw = Mode('control_tile_draw')
360 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
361 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
362 mode_chat = Mode('chat', has_input_prompt=True)
363 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
364 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
365 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
366 mode_password = Mode('password', has_input_prompt=True)
370 def __init__(self, host):
373 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
374 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
375 self.mode_admin.available_modes = ["control_pw_type",
376 "control_tile_type", "chat",
377 "study", "play", "edit"]
378 self.mode_control_tile_draw.available_modes = ["admin_enter"]
379 self.mode_edit.available_modes = ["write", "annotate", "portal",
380 "password", "chat", "study", "play",
386 self.parser = Parser(self.game)
388 self.do_refresh = True
389 self.queue = queue.Queue()
390 self.login_name = None
391 self.map_mode = 'terrain + things'
392 self.password = 'foo'
393 self.switch_mode('waiting_for_server')
395 'switch_to_chat': 't',
396 'switch_to_play': 'p',
397 'switch_to_password': 'P',
398 'switch_to_annotate': 'M',
399 'switch_to_portal': 'T',
400 'switch_to_study': '?',
401 'switch_to_edit': 'E',
402 'switch_to_write': 'm',
403 'switch_to_admin_enter': 'A',
404 'switch_to_control_pw_type': 'C',
405 'switch_to_control_tile_type': 'Q',
411 'toggle_map_mode': 'L',
412 'toggle_tile_draw': 'm',
413 'hex_move_upleft': 'w',
414 'hex_move_upright': 'e',
415 'hex_move_right': 'd',
416 'hex_move_downright': 'x',
417 'hex_move_downleft': 'y',
418 'hex_move_left': 'a',
419 'square_move_up': 'w',
420 'square_move_left': 'a',
421 'square_move_down': 's',
422 'square_move_right': 'd',
424 if os.path.isfile('config.json'):
425 with open('config.json', 'r') as f:
426 keys_conf = json.loads(f.read())
428 self.keys[k] = keys_conf[k]
429 self.show_help = False
430 self.disconnected = True
431 self.force_instant_connect = True
432 self.input_lines = []
435 curses.wrapper(self.loop)
439 def handle_recv(msg):
445 self.log_msg('@ attempting connect')
446 socket_client_class = PlomSocketClient
447 if self.host.startswith('ws://') or self.host.startswith('wss://'):
448 socket_client_class = WebSocketClient
450 self.socket = socket_client_class(handle_recv, self.host)
451 self.socket_thread = threading.Thread(target=self.socket.run)
452 self.socket_thread.start()
453 self.disconnected = False
454 self.game.thing_types = {}
455 self.game.terrains = {}
456 time.sleep(0.1) # give potential SSL negotation some time …
457 self.socket.send('TASKS')
458 self.socket.send('TERRAINS')
459 self.socket.send('THING_TYPES')
460 self.switch_mode('login')
461 except ConnectionRefusedError:
462 self.log_msg('@ server connect failure')
463 self.disconnected = True
464 self.switch_mode('waiting_for_server')
465 self.do_refresh = True
468 self.log_msg('@ attempting reconnect')
470 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
471 # conditions with ws4py, find out what exactly
472 self.switch_mode('waiting_for_server')
477 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
478 raise BrokenSocketConnection
479 self.socket.send(msg)
480 except (BrokenPipeError, BrokenSocketConnection):
481 self.log_msg('@ server disconnected :(')
482 self.disconnected = True
483 self.force_instant_connect = True
484 self.do_refresh = True
486 def log_msg(self, msg):
488 if len(self.log) > 100:
489 self.log = self.log[-100:]
491 def query_info(self):
492 self.send('GET_ANNOTATION ' + str(self.explorer))
494 def restore_input_values(self):
495 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
496 info = self.game.info_db[self.explorer]
499 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
500 self.input_ = self.game.portals[self.explorer]
501 elif self.mode.name == 'password':
502 self.input_ = self.password
504 def send_tile_control_command(self):
505 self.send('SET_TILE_CONTROL %s %s' %
506 (self.explorer, quote(self.tile_control_char)))
508 def toggle_map_mode(self):
509 if self.map_mode == 'terrain only':
510 self.map_mode = 'terrain + annotations'
511 elif self.map_mode == 'terrain + annotations':
512 self.map_mode = 'terrain + things'
513 elif self.map_mode == 'terrain + things':
514 self.map_mode = 'protections'
515 elif self.map_mode == 'protections':
516 self.map_mode = 'terrain only'
518 def switch_mode(self, mode_name):
519 self.tile_draw = False
520 if mode_name == 'admin_enter' and self.is_admin:
522 self.mode = getattr(self, 'mode_' + mode_name)
523 if self.mode and self.mode.name == 'control_tile_draw':
524 self.log_msg('@ finished tile protection drawing.')
525 if self.mode.name in {'control_tile_draw', 'control_tile_type',
527 self.map_mode = 'protections'
528 elif self.mode.name!= 'edit':
529 self.map_mode = 'terrain + things'
530 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
531 player = self.game.get_thing(self.game.player_id)
532 self.explorer = YX(player.position.y, player.position.x)
533 if self.mode.shows_info:
535 if self.mode.is_single_char_entry:
536 self.show_help = True
537 if self.mode.name == 'waiting_for_server':
538 self.log_msg('@ waiting for server …')
539 elif self.mode.name == 'login':
541 self.send('LOGIN ' + quote(self.login_name))
543 self.log_msg('@ enter username')
544 elif self.mode.name == 'admin_enter':
545 self.log_msg('@ enter admin password:')
546 elif self.mode.name == 'control_pw_type':
547 self.log_msg('@ enter tile protection character for which you want to change the password:')
548 elif self.mode.name == 'control_tile_type':
549 self.log_msg('@ enter tile protection character which you want to draw:')
550 elif self.mode.name == 'control_pw_pw':
551 self.log_msg('@ enter tile protection password for "%s":' % self.tile_control_char)
552 elif self.mode.name == 'control_tile_draw':
553 self.log_msg('@ can draw tile 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']))
555 self.restore_input_values()
557 def loop(self, stdscr):
560 def safe_addstr(y, x, line):
561 if y < self.size.y - 1 or x + len(line) < self.size.x:
562 stdscr.addstr(y, x, line)
563 else: # workaround to <https://stackoverflow.com/q/7063128>
564 cut_i = self.size.x - x - 1
566 last_char = line[cut_i]
567 stdscr.addstr(y, self.size.x - 2, last_char)
568 stdscr.insstr(y, self.size.x - 2, ' ')
569 stdscr.addstr(y, x, cut)
571 def handle_input(msg):
572 command, args = self.parser.parse(msg)
575 def msg_into_lines_of_width(msg, width):
579 for i in range(len(msg)):
580 if x >= width or msg[i] == "\n":
592 def reset_screen_size():
593 self.size = YX(*stdscr.getmaxyx())
594 self.size = self.size - YX(self.size.y % 4, 0)
595 self.size = self.size - YX(0, self.size.x % 4)
596 self.window_width = int(self.size.x / 2)
598 def recalc_input_lines():
599 if not self.mode.has_input_prompt:
600 self.input_lines = []
602 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
605 def move_explorer(direction):
606 target = self.game.map_geometry.move_yx(self.explorer, direction)
608 self.explorer = target
609 if self.mode.shows_info:
612 self.send_tile_control_command()
618 for line in self.log:
619 lines += msg_into_lines_of_width(line, self.window_width)
622 max_y = self.size.y - len(self.input_lines)
623 for i in range(len(lines)):
624 if (i >= max_y - height_header):
626 safe_addstr(max_y - i - 1, self.window_width, lines[i])
629 if not self.game.turn_complete:
631 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
632 info = 'MAP VIEW: %s\n' % self.map_mode
633 if self.game.fov[pos_i] != '.':
634 info += 'outside field of view'
636 terrain_char = self.game.map_content[pos_i]
638 if terrain_char in self.game.terrains:
639 terrain_desc = self.game.terrains[terrain_char]
640 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
641 protection = self.game.map_control_content[pos_i]
642 if protection == '.':
643 protection = 'unprotected'
644 info += 'PROTECTION: %s\n' % protection
645 for t in self.game.things:
646 if t.position == self.explorer:
647 info += 'THING: %s / %s' % (t.type_,
648 self.game.thing_types[t.type_])
649 if hasattr(t, 'player_char'):
650 info += t.player_char
651 if hasattr(t, 'name'):
652 info += ' (%s)' % t.name
654 if self.explorer in self.game.portals:
655 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
657 info += 'PORTAL: (none)\n'
658 if self.explorer in self.game.info_db:
659 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
661 info += 'ANNOTATION: waiting …'
662 lines = msg_into_lines_of_width(info, self.window_width)
664 for i in range(len(lines)):
665 y = height_header + i
666 if y >= self.size.y - len(self.input_lines):
668 safe_addstr(y, self.window_width, lines[i])
671 y = self.size.y - len(self.input_lines)
672 for i in range(len(self.input_lines)):
673 safe_addstr(y, self.window_width, self.input_lines[i])
677 if not self.game.turn_complete:
679 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
682 help = "hit [%s] for help" % self.keys['help']
683 if self.mode.has_input_prompt:
684 help = "enter /help for help"
685 safe_addstr(1, self.window_width,
686 'MODE: %s – %s' % (self.mode.short_desc, help))
689 if not self.game.turn_complete:
692 for y in range(self.game.map_geometry.size.y):
693 start = self.game.map_geometry.size.x * y
694 end = start + self.game.map_geometry.size.x
695 if self.map_mode == 'protections':
696 map_lines_split += [[c + ' ' for c
697 in self.game.map_control_content[start:end]]]
699 map_lines_split += [[c + ' ' for c
700 in self.game.map_content[start:end]]]
701 if self.map_mode == 'terrain + annotations':
702 for p in self.game.info_hints:
703 map_lines_split[p.y][p.x] = 'A '
704 elif self.map_mode == 'terrain + things':
705 for p in self.game.portals.keys():
706 original = map_lines_split[p.y][p.x]
707 map_lines_split[p.y][p.x] = original[0] + 'P'
709 for t in self.game.things:
710 symbol = self.game.thing_types[t.type_]
712 if hasattr(t, 'player_char'):
713 meta_char = t.player_char
714 if t.position in used_positions:
716 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
717 used_positions += [t.position]
718 player = self.game.get_thing(self.game.player_id)
719 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
720 map_lines_split[self.explorer.y][self.explorer.x] = '??'
721 elif self.map_mode != 'terrain + things':
722 map_lines_split[player.position.y][player.position.x] = '??'
724 if type(self.game.map_geometry) == MapGeometryHex:
726 for line in map_lines_split:
727 map_lines += [indent*' ' + ''.join(line)]
728 indent = 0 if indent else 1
730 for line in map_lines_split:
731 map_lines += [''.join(line)]
732 window_center = YX(int(self.size.y / 2),
733 int(self.window_width / 2))
734 center = player.position
735 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
736 center = self.explorer
737 center = YX(center.y, center.x * 2)
738 offset = center - window_center
739 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
741 term_y = max(0, -offset.y)
742 term_x = max(0, -offset.x)
743 map_y = max(0, offset.y)
744 map_x = max(0, offset.x)
745 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
746 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
747 safe_addstr(term_y, term_x, to_draw)
752 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
753 self.mode.help_intro)
754 if self.mode.name == 'play':
755 content += "Available actions:\n"
756 if 'MOVE' in self.game.tasks:
757 content += "[%s] – move player\n" % ','.join(self.movement_keys)
758 if 'PICK_UP' in self.game.tasks:
759 content += "[%s] – pick up thing\n" % self.keys['take_thing']
760 if 'DROP' in self.game.tasks:
761 content += "[%s] – drop thing\n" % self.keys['drop_thing']
762 content += '[%s] – teleport\n' % self.keys['teleport']
764 elif self.mode.name == 'study':
765 content += 'Available actions:\n'
766 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
767 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
769 elif self.mode.name == 'edit':
770 content += "Available actions:\n"
771 if 'MOVE' in self.game.tasks:
772 content += "[%s] – move player\n" % ','.join(self.movement_keys)
773 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
774 content += "[%s] – flatten surroundings\n" % self.keys['flatten']
775 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
777 elif self.mode.name == 'control_tile_draw':
778 content += "Available actions:\n"
779 content += "[%s] – toggle tile protection drawing\n" % self.keys['toggle_tile_draw']
781 elif self.mode.name == 'chat':
782 content += '/nick NAME – re-name yourself to NAME\n'
783 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
784 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
785 content += '/%s or /edit – switch to map edit mode\n' % self.keys['switch_to_edit']
786 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
787 content += self.mode.list_available_modes(self)
788 for i in range(self.size.y):
790 self.window_width * (not self.mode.has_input_prompt),
791 ' '*self.window_width)
793 for line in content.split('\n'):
794 lines += msg_into_lines_of_width(line, self.window_width)
795 for i in range(len(lines)):
799 self.window_width * (not self.mode.has_input_prompt),
804 if self.mode.has_input_prompt:
807 if self.mode.shows_info:
812 if not self.mode.is_intro:
818 curses.curs_set(False) # hide cursor
819 curses.use_default_colors();
822 self.explorer = YX(0, 0)
825 interval = datetime.timedelta(seconds=5)
826 last_ping = datetime.datetime.now() - interval
828 if self.disconnected and self.force_instant_connect:
829 self.force_instant_connect = False
831 now = datetime.datetime.now()
832 if now - last_ping > interval:
833 if self.disconnected:
843 self.do_refresh = False
846 msg = self.queue.get(block=False)
851 key = stdscr.getkey()
852 self.do_refresh = True
855 self.show_help = False
856 if key == 'KEY_RESIZE':
858 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
859 self.input_ = self.input_[:-1]
860 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
861 self.show_help = True
863 self.restore_input_values()
864 elif self.mode.has_input_prompt and key != '\n': # Return key
866 max_length = self.window_width * self.size.y - len(input_prompt) - 1
867 if len(self.input_) > max_length:
868 self.input_ = self.input_[:max_length]
869 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
870 self.show_help = True
871 elif self.mode.name == 'login' and key == '\n':
872 self.login_name = self.input_
873 self.send('LOGIN ' + quote(self.input_))
875 elif self.mode.name == 'control_pw_pw' and key == '\n':
876 if self.input_ == '':
877 self.log_msg('@ aborted')
879 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
880 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
881 self.switch_mode('admin')
882 elif self.mode.name == 'password' and key == '\n':
883 if self.input_ == '':
885 self.password = self.input_
886 self.switch_mode('edit')
887 elif self.mode.name == 'admin_enter' and key == '\n':
888 self.send('BECOME_ADMIN ' + quote(self.input_))
889 self.switch_mode('play')
890 elif self.mode.name == 'control_pw_type' and key == '\n':
891 if len(self.input_) != 1:
892 self.log_msg('@ entered non-single-char, therefore aborted')
893 self.switch_mode('admin')
895 self.tile_control_char = self.input_
896 self.switch_mode('control_pw_pw')
897 elif self.mode.name == 'control_tile_type' and key == '\n':
898 if len(self.input_) != 1:
899 self.log_msg('@ entered non-single-char, therefore aborted')
900 self.switch_mode('admin')
902 self.tile_control_char = self.input_
903 self.switch_mode('control_tile_draw')
904 elif self.mode.name == 'chat' and key == '\n':
905 if self.input_ == '':
907 if self.input_[0] == '/': # FIXME fails on empty input
908 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
909 self.switch_mode('play')
910 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
911 self.switch_mode('study')
912 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
913 self.switch_mode('edit')
914 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
915 self.switch_mode('admin_enter')
916 elif self.input_.startswith('/nick'):
917 tokens = self.input_.split(maxsplit=1)
919 self.send('NICK ' + quote(tokens[1]))
921 self.log_msg('? need login name')
923 self.log_msg('? unknown command')
925 self.send('ALL ' + quote(self.input_))
927 elif self.mode.name == 'annotate' and key == '\n':
928 if self.input_ == '':
930 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
931 quote(self.password)))
932 self.switch_mode('edit')
933 elif self.mode.name == 'portal' and key == '\n':
934 if self.input_ == '':
936 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
937 quote(self.password)))
938 self.switch_mode('edit')
939 elif self.mode.name == 'study':
940 if self.mode.mode_switch_on_key(self, key):
942 elif key == self.keys['toggle_map_mode']:
943 self.toggle_map_mode()
944 elif key in self.movement_keys:
945 move_explorer(self.movement_keys[key])
946 elif self.mode.name == 'play':
947 if self.mode.mode_switch_on_key(self, key):
949 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
950 self.send('TASK:PICK_UP')
951 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
952 self.send('TASK:DROP')
953 elif key == self.keys['teleport']:
954 player = self.game.get_thing(self.game.player_id)
955 if player.position in self.game.portals:
956 self.host = self.game.portals[player.position]
960 self.log_msg('? not standing on portal')
961 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
962 self.send('TASK:MOVE ' + self.movement_keys[key])
963 elif self.mode.name == 'write':
964 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
965 self.switch_mode('edit')
966 elif self.mode.name == 'control_tile_draw':
967 if self.mode.mode_switch_on_key(self, key):
969 elif key in self.movement_keys:
970 move_explorer(self.movement_keys[key])
971 elif key == self.keys['toggle_tile_draw']:
972 self.tile_draw = False if self.tile_draw else True
973 elif self.mode.name == 'admin':
974 if self.mode.mode_switch_on_key(self, key):
976 elif self.mode.name == 'edit':
977 if self.mode.mode_switch_on_key(self, key):
979 elif key == self.keys['flatten'] and\
980 'FLATTEN_SURROUNDINGS' in self.game.tasks:
981 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
982 elif key == self.keys['toggle_map_mode']:
983 self.toggle_map_mode()
984 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
985 self.send('TASK:MOVE ' + self.movement_keys[key])
987 if len(sys.argv) != 2:
988 raise ArgError('wrong number of arguments, need game host')