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
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 self.socket.send('TASKS')
457 self.socket.send('TERRAINS')
458 self.socket.send('THING_TYPES')
459 self.switch_mode('login')
460 except ConnectionRefusedError:
461 self.log_msg('@ server connect failure')
462 self.disconnected = True
463 self.switch_mode('waiting_for_server')
464 self.do_refresh = True
467 self.log_msg('@ attempting reconnect')
469 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
470 # conditions with ws4py, find out what exactly
471 self.switch_mode('waiting_for_server')
476 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
477 raise BrokenSocketConnection
478 self.socket.send(msg)
479 except (BrokenPipeError, BrokenSocketConnection):
480 self.log_msg('@ server disconnected :(')
481 self.disconnected = True
482 self.force_instant_connect = True
483 self.do_refresh = True
485 def log_msg(self, msg):
487 if len(self.log) > 100:
488 self.log = self.log[-100:]
490 def query_info(self):
491 self.send('GET_ANNOTATION ' + str(self.explorer))
493 def restore_input_values(self):
494 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
495 info = self.game.info_db[self.explorer]
498 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
499 self.input_ = self.game.portals[self.explorer]
500 elif self.mode.name == 'password':
501 self.input_ = self.password
503 def send_tile_control_command(self):
504 self.send('SET_TILE_CONTROL %s %s' %
505 (self.explorer, quote(self.tile_control_char)))
507 def toggle_map_mode(self):
508 if self.map_mode == 'terrain only':
509 self.map_mode = 'terrain + annotations'
510 elif self.map_mode == 'terrain + annotations':
511 self.map_mode = 'terrain + things'
512 elif self.map_mode == 'terrain + things':
513 self.map_mode = 'protections'
514 elif self.map_mode == 'protections':
515 self.map_mode = 'terrain only'
517 def switch_mode(self, mode_name):
518 self.tile_draw = False
519 if mode_name == 'admin_enter' and self.is_admin:
521 self.mode = getattr(self, 'mode_' + mode_name)
522 if self.mode and self.mode.name == 'control_tile_draw':
523 self.log_msg('@ finished tile protection drawing.')
524 if self.mode.name in {'control_tile_draw', 'control_tile_type',
526 self.map_mode = 'protections'
527 elif self.mode.name!= 'edit':
528 self.map_mode = 'terrain + things'
529 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
530 player = self.game.get_thing(self.game.player_id)
531 self.explorer = YX(player.position.y, player.position.x)
532 if self.mode.shows_info:
534 if self.mode.is_single_char_entry:
535 self.show_help = True
536 if self.mode.name == 'waiting_for_server':
537 self.log_msg('@ waiting for server …')
538 elif self.mode.name == 'login':
540 self.send('LOGIN ' + quote(self.login_name))
542 self.log_msg('@ enter username')
543 elif self.mode.name == 'admin_enter':
544 self.log_msg('@ enter admin password:')
545 elif self.mode.name == 'control_pw_type':
546 self.log_msg('@ enter tile protection character for which you want to change the password:')
547 elif self.mode.name == 'control_tile_type':
548 self.log_msg('@ enter tile protection character which you want to draw:')
549 elif self.mode.name == 'control_pw_pw':
550 self.log_msg('@ enter tile protection password for "%s":' % self.tile_control_char)
551 elif self.mode.name == 'control_tile_draw':
552 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']))
554 self.restore_input_values()
556 def loop(self, stdscr):
559 def safe_addstr(y, x, line):
560 if y < self.size.y - 1 or x + len(line) < self.size.x:
561 stdscr.addstr(y, x, line)
562 else: # workaround to <https://stackoverflow.com/q/7063128>
563 cut_i = self.size.x - x - 1
565 last_char = line[cut_i]
566 stdscr.addstr(y, self.size.x - 2, last_char)
567 stdscr.insstr(y, self.size.x - 2, ' ')
568 stdscr.addstr(y, x, cut)
570 def handle_input(msg):
571 command, args = self.parser.parse(msg)
574 def msg_into_lines_of_width(msg, width):
578 for i in range(len(msg)):
579 if x >= width or msg[i] == "\n":
591 def reset_screen_size():
592 self.size = YX(*stdscr.getmaxyx())
593 self.size = self.size - YX(self.size.y % 4, 0)
594 self.size = self.size - YX(0, self.size.x % 4)
595 self.window_width = int(self.size.x / 2)
597 def recalc_input_lines():
598 if not self.mode.has_input_prompt:
599 self.input_lines = []
601 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
604 def move_explorer(direction):
605 target = self.game.map_geometry.move_yx(self.explorer, direction)
607 self.explorer = target
608 if self.mode.shows_info:
611 self.send_tile_control_command()
617 for line in self.log:
618 lines += msg_into_lines_of_width(line, self.window_width)
621 max_y = self.size.y - len(self.input_lines)
622 for i in range(len(lines)):
623 if (i >= max_y - height_header):
625 safe_addstr(max_y - i - 1, self.window_width, lines[i])
628 if not self.game.turn_complete:
630 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
631 info = 'MAP VIEW: %s\n' % self.map_mode
632 if self.game.fov[pos_i] != '.':
633 info += 'outside field of view'
635 terrain_char = self.game.map_content[pos_i]
637 if terrain_char in self.game.terrains:
638 terrain_desc = self.game.terrains[terrain_char]
639 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
640 protection = self.game.map_control_content[pos_i]
641 if protection == '.':
642 protection = 'unprotected'
643 info += 'PROTECTION: %s\n' % protection
644 for t in self.game.things:
645 if t.position == self.explorer:
646 info += 'THING: %s / %s' % (t.type_,
647 self.game.thing_types[t.type_])
648 if hasattr(t, 'player_char'):
649 info += t.player_char
650 if hasattr(t, 'name'):
651 info += ' (%s)' % t.name
653 if self.explorer in self.game.portals:
654 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
656 info += 'PORTAL: (none)\n'
657 if self.explorer in self.game.info_db:
658 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
660 info += 'ANNOTATION: waiting …'
661 lines = msg_into_lines_of_width(info, self.window_width)
663 for i in range(len(lines)):
664 y = height_header + i
665 if y >= self.size.y - len(self.input_lines):
667 safe_addstr(y, self.window_width, lines[i])
670 y = self.size.y - len(self.input_lines)
671 for i in range(len(self.input_lines)):
672 safe_addstr(y, self.window_width, self.input_lines[i])
676 if not self.game.turn_complete:
678 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
681 help = "hit [%s] for help" % self.keys['help']
682 if self.mode.has_input_prompt:
683 help = "enter /help for help"
684 safe_addstr(1, self.window_width,
685 'MODE: %s – %s' % (self.mode.short_desc, help))
688 if not self.game.turn_complete:
691 for y in range(self.game.map_geometry.size.y):
692 start = self.game.map_geometry.size.x * y
693 end = start + self.game.map_geometry.size.x
694 if self.map_mode == 'protections':
695 map_lines_split += [[c + ' ' for c
696 in self.game.map_control_content[start:end]]]
698 map_lines_split += [[c + ' ' for c
699 in self.game.map_content[start:end]]]
700 if self.map_mode == 'terrain + annotations':
701 for p in self.game.info_hints:
702 map_lines_split[p.y][p.x] = 'A '
703 elif self.map_mode == 'terrain + things':
704 for p in self.game.portals.keys():
705 original = map_lines_split[p.y][p.x]
706 map_lines_split[p.y][p.x] = original[0] + 'P'
708 for t in self.game.things:
709 symbol = self.game.thing_types[t.type_]
711 if hasattr(t, 'player_char'):
712 meta_char = t.player_char
713 if t.position in used_positions:
715 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
716 used_positions += [t.position]
717 player = self.game.get_thing(self.game.player_id)
718 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
719 map_lines_split[self.explorer.y][self.explorer.x] = '??'
720 elif self.map_mode != 'terrain + things':
721 map_lines_split[player.position.y][player.position.x] = '??'
723 if type(self.game.map_geometry) == MapGeometryHex:
725 for line in map_lines_split:
726 map_lines += [indent*' ' + ''.join(line)]
727 indent = 0 if indent else 1
729 for line in map_lines_split:
730 map_lines += [''.join(line)]
731 window_center = YX(int(self.size.y / 2),
732 int(self.window_width / 2))
733 center = player.position
734 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
735 center = self.explorer
736 center = YX(center.y, center.x * 2)
737 offset = center - window_center
738 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
740 term_y = max(0, -offset.y)
741 term_x = max(0, -offset.x)
742 map_y = max(0, offset.y)
743 map_x = max(0, offset.x)
744 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
745 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
746 safe_addstr(term_y, term_x, to_draw)
751 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
752 self.mode.help_intro)
753 if self.mode.name == 'play':
754 content += "Available actions:\n"
755 if 'MOVE' in self.game.tasks:
756 content += "[%s] – move player\n" % ','.join(self.movement_keys)
757 if 'PICK_UP' in self.game.tasks:
758 content += "[%s] – pick up thing\n" % self.keys['take_thing']
759 if 'DROP' in self.game.tasks:
760 content += "[%s] – drop thing\n" % self.keys['drop_thing']
761 content += '[%s] – teleport\n' % self.keys['teleport']
763 elif self.mode.name == 'study':
764 content += 'Available actions:\n'
765 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
766 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
768 elif self.mode.name == 'edit':
769 content += "Available actions:\n"
770 if 'MOVE' in self.game.tasks:
771 content += "[%s] – move player\n" % ','.join(self.movement_keys)
772 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
773 content += "[%s] – flatten surroundings\n" % self.keys['flatten']
774 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
776 elif self.mode.name == 'control_tile_draw':
777 content += "Available actions:\n"
778 content += "[%s] – toggle tile protection drawing\n" % self.keys['toggle_tile_draw']
780 elif self.mode.name == 'chat':
781 content += '/nick NAME – re-name yourself to NAME\n'
782 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
783 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
784 content += '/%s or /edit – switch to map edit mode\n' % self.keys['switch_to_edit']
785 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
786 content += self.mode.list_available_modes(self)
787 for i in range(self.size.y):
789 self.window_width * (not self.mode.has_input_prompt),
790 ' '*self.window_width)
792 for line in content.split('\n'):
793 lines += msg_into_lines_of_width(line, self.window_width)
794 for i in range(len(lines)):
798 self.window_width * (not self.mode.has_input_prompt),
803 if self.mode.has_input_prompt:
806 if self.mode.shows_info:
811 if not self.mode.is_intro:
817 curses.curs_set(False) # hide cursor
818 curses.use_default_colors();
821 self.explorer = YX(0, 0)
824 interval = datetime.timedelta(seconds=5)
825 last_ping = datetime.datetime.now() - interval
827 if self.disconnected and self.force_instant_connect:
828 self.force_instant_connect = False
830 now = datetime.datetime.now()
831 if now - last_ping > interval:
832 if self.disconnected:
842 self.do_refresh = False
845 msg = self.queue.get(block=False)
850 key = stdscr.getkey()
851 self.do_refresh = True
854 self.show_help = False
855 if key == 'KEY_RESIZE':
857 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
858 self.input_ = self.input_[:-1]
859 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
860 self.show_help = True
862 self.restore_input_values()
863 elif self.mode.has_input_prompt and key != '\n': # Return key
865 max_length = self.window_width * self.size.y - len(input_prompt) - 1
866 if len(self.input_) > max_length:
867 self.input_ = self.input_[:max_length]
868 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
869 self.show_help = True
870 elif self.mode.name == 'login' and key == '\n':
871 self.login_name = self.input_
872 self.send('LOGIN ' + quote(self.input_))
874 elif self.mode.name == 'control_pw_pw' and key == '\n':
875 if self.input_ == '':
876 self.log_msg('@ aborted')
878 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
879 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
880 self.switch_mode('admin')
881 elif self.mode.name == 'password' and key == '\n':
882 if self.input_ == '':
884 self.password = self.input_
885 self.switch_mode('edit')
886 elif self.mode.name == 'admin_enter' and key == '\n':
887 self.send('BECOME_ADMIN ' + quote(self.input_))
888 self.switch_mode('play')
889 elif self.mode.name == 'control_pw_type' and key == '\n':
890 if len(self.input_) != 1:
891 self.log_msg('@ entered non-single-char, therefore aborted')
892 self.switch_mode('admin')
894 self.tile_control_char = self.input_
895 self.switch_mode('control_pw_pw')
896 elif self.mode.name == 'control_tile_type' and key == '\n':
897 if len(self.input_) != 1:
898 self.log_msg('@ entered non-single-char, therefore aborted')
899 self.switch_mode('admin')
901 self.tile_control_char = self.input_
902 self.switch_mode('control_tile_draw')
903 elif self.mode.name == 'chat' and key == '\n':
904 if self.input_ == '':
906 if self.input_[0] == '/': # FIXME fails on empty input
907 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
908 self.switch_mode('play')
909 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
910 self.switch_mode('study')
911 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
912 self.switch_mode('edit')
913 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
914 self.switch_mode('admin_enter')
915 elif self.input_.startswith('/nick'):
916 tokens = self.input_.split(maxsplit=1)
918 self.send('NICK ' + quote(tokens[1]))
920 self.log_msg('? need login name')
922 self.log_msg('? unknown command')
924 self.send('ALL ' + quote(self.input_))
926 elif self.mode.name == 'annotate' and key == '\n':
927 if self.input_ == '':
929 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
930 quote(self.password)))
931 self.switch_mode('edit')
932 elif self.mode.name == 'portal' and key == '\n':
933 if self.input_ == '':
935 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
936 quote(self.password)))
937 self.switch_mode('edit')
938 elif self.mode.name == 'study':
939 if self.mode.mode_switch_on_key(self, key):
941 elif key == self.keys['toggle_map_mode']:
942 self.toggle_map_mode()
943 elif key in self.movement_keys:
944 move_explorer(self.movement_keys[key])
945 elif self.mode.name == 'play':
946 if self.mode.mode_switch_on_key(self, key):
948 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
949 self.send('TASK:PICK_UP')
950 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
951 self.send('TASK:DROP')
952 elif key == self.keys['teleport']:
953 player = self.game.get_thing(self.game.player_id)
954 if player.position in self.game.portals:
955 self.host = self.game.portals[player.position]
959 self.log_msg('? not standing on portal')
960 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
961 self.send('TASK:MOVE ' + self.movement_keys[key])
962 elif self.mode.name == 'write':
963 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
964 self.switch_mode('edit')
965 elif self.mode.name == 'control_tile_draw':
966 if self.mode.mode_switch_on_key(self, key):
968 elif key in self.movement_keys:
969 move_explorer(self.movement_keys[key])
970 elif key == self.keys['toggle_tile_draw']:
971 self.tile_draw = False if self.tile_draw else True
972 elif self.mode.name == 'admin':
973 if self.mode.mode_switch_on_key(self, key):
975 elif self.mode.name == 'edit':
976 if self.mode.mode_switch_on_key(self, key):
978 elif key == self.keys['flatten'] and\
979 'FLATTEN_SURROUNDINGS' in self.game.tasks:
980 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
981 elif key == self.keys['toggle_map_mode']:
982 self.toggle_map_mode()
983 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
984 self.send('TASK:MOVE ' + self.movement_keys[key])
986 if len(sys.argv) != 2:
987 raise ArgError('wrong number of arguments, need game host')