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 world 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.available_actions = []
328 self.has_input_prompt = has_input_prompt
329 self.shows_info = shows_info
330 self.is_intro = is_intro
331 self.help_intro = mode_helps[name]['long']
332 self.is_single_char_entry = is_single_char_entry
335 def iter_available_modes(self, tui):
336 for mode_name in self.available_modes:
337 mode = getattr(tui, 'mode_' + mode_name)
340 key = tui.keys['switch_to_' + mode.name]
343 def list_available_modes(self, tui):
345 if len(self.available_modes) > 0:
346 msg = 'Other modes available from here:\n'
347 for mode, key in self.iter_available_modes(tui):
348 msg += '[%s] – %s\n' % (key, mode.short_desc)
351 def mode_switch_on_key(self, tui, key_pressed):
352 for mode, key in self.iter_available_modes(tui):
353 if key_pressed == key:
354 tui.switch_mode(mode.name)
359 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
360 mode_admin = Mode('admin')
361 mode_play = Mode('play')
362 mode_study = Mode('study', shows_info=True)
363 mode_write = Mode('write', is_single_char_entry=True)
364 mode_edit = Mode('edit')
365 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
366 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
367 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
368 mode_control_tile_draw = Mode('control_tile_draw')
369 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
370 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
371 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
372 mode_chat = Mode('chat', has_input_prompt=True)
373 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
374 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
375 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
376 mode_password = Mode('password', has_input_prompt=True)
377 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
381 def __init__(self, host):
384 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
385 self.mode_play.available_actions = ["move", "take_thing", "drop_thing",
387 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
388 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
389 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
390 "control_tile_type", "chat",
391 "study", "play", "edit"]
392 self.mode_admin.available_actions = ["move"]
393 self.mode_control_tile_draw.available_modes = ["admin_enter"]
394 self.mode_control_tile_draw.available_actions = ["move_explorer",
396 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
397 "password", "chat", "study", "play",
399 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
404 self.parser = Parser(self.game)
406 self.do_refresh = True
407 self.queue = queue.Queue()
408 self.login_name = None
409 self.map_mode = 'terrain + things'
410 self.password = 'foo'
411 self.switch_mode('waiting_for_server')
413 'switch_to_chat': 't',
414 'switch_to_play': 'p',
415 'switch_to_password': 'P',
416 'switch_to_annotate': 'M',
417 'switch_to_portal': 'T',
418 'switch_to_study': '?',
419 'switch_to_edit': 'E',
420 'switch_to_write': 'm',
421 'switch_to_name_thing': 'N',
422 'switch_to_admin_enter': 'A',
423 'switch_to_control_pw_type': 'C',
424 'switch_to_control_tile_type': 'Q',
425 'switch_to_admin_thing_protect': 'T',
432 'toggle_map_mode': 'L',
433 'toggle_tile_draw': 'm',
434 'hex_move_upleft': 'w',
435 'hex_move_upright': 'e',
436 'hex_move_right': 'd',
437 'hex_move_downright': 'x',
438 'hex_move_downleft': 'y',
439 'hex_move_left': 'a',
440 'square_move_up': 'w',
441 'square_move_left': 'a',
442 'square_move_down': 's',
443 'square_move_right': 'd',
445 if os.path.isfile('config.json'):
446 with open('config.json', 'r') as f:
447 keys_conf = json.loads(f.read())
449 self.keys[k] = keys_conf[k]
450 self.show_help = False
451 self.disconnected = True
452 self.force_instant_connect = True
453 self.input_lines = []
456 curses.wrapper(self.loop)
460 def handle_recv(msg):
466 self.log_msg('@ attempting connect')
467 socket_client_class = PlomSocketClient
468 if self.host.startswith('ws://') or self.host.startswith('wss://'):
469 socket_client_class = WebSocketClient
471 self.socket = socket_client_class(handle_recv, self.host)
472 self.socket_thread = threading.Thread(target=self.socket.run)
473 self.socket_thread.start()
474 self.disconnected = False
475 self.game.thing_types = {}
476 self.game.terrains = {}
477 time.sleep(0.1) # give potential SSL negotation some time …
478 self.socket.send('TASKS')
479 self.socket.send('TERRAINS')
480 self.socket.send('THING_TYPES')
481 self.switch_mode('login')
482 except ConnectionRefusedError:
483 self.log_msg('@ server connect failure')
484 self.disconnected = True
485 self.switch_mode('waiting_for_server')
486 self.do_refresh = True
489 self.log_msg('@ attempting reconnect')
491 # necessitated by some strange SSL race conditions with ws4py
492 time.sleep(0.1) # FIXME find out why exactly necessary
493 self.switch_mode('waiting_for_server')
498 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
499 raise BrokenSocketConnection
500 self.socket.send(msg)
501 except (BrokenPipeError, BrokenSocketConnection):
502 self.log_msg('@ server disconnected :(')
503 self.disconnected = True
504 self.force_instant_connect = True
505 self.do_refresh = True
507 def log_msg(self, msg):
509 if len(self.log) > 100:
510 self.log = self.log[-100:]
512 def query_info(self):
513 self.send('GET_ANNOTATION ' + str(self.explorer))
515 def restore_input_values(self):
516 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
517 info = self.game.info_db[self.explorer]
520 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
521 self.input_ = self.game.portals[self.explorer]
522 elif self.mode.name == 'password':
523 self.input_ = self.password
524 elif self.mode.name == 'name_thing':
525 if hasattr(self.thing_selected, 'name'):
526 self.input_ = self.thing_selected.name
527 elif self.mode.name == 'admin_thing_protect':
528 if hasattr(self.thing_selected, 'protection'):
529 self.input_ = self.thing_selected.protection
531 def send_tile_control_command(self):
532 self.send('SET_TILE_CONTROL %s %s' %
533 (self.explorer, quote(self.tile_control_char)))
535 def toggle_map_mode(self):
536 if self.map_mode == 'terrain only':
537 self.map_mode = 'terrain + annotations'
538 elif self.map_mode == 'terrain + annotations':
539 self.map_mode = 'terrain + things'
540 elif self.map_mode == 'terrain + things':
541 self.map_mode = 'protections'
542 elif self.map_mode == 'protections':
543 self.map_mode = 'terrain only'
545 def switch_mode(self, mode_name):
546 self.tile_draw = False
547 if mode_name == 'admin_enter' and self.is_admin:
549 elif mode_name in {'name_thing', 'admin_thing_protect'}:
550 player = self.game.get_thing(self.game.player_id)
552 for t in [t for t in self.game.things if t.position == player.position
553 and t.id_ != player.id_]:
558 self.log_msg('? not standing over thing')
561 self.thing_selected = thing
562 self.mode = getattr(self, 'mode_' + mode_name)
563 if self.mode.name == 'control_tile_draw':
564 self.log_msg('@ finished tile protection drawing.')
565 if self.mode.name in {'control_tile_draw', 'control_tile_type',
567 self.map_mode = 'protections'
568 elif self.mode.name != 'edit':
569 self.map_mode = 'terrain + things'
570 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
571 player = self.game.get_thing(self.game.player_id)
572 self.explorer = YX(player.position.y, player.position.x)
573 if self.mode.shows_info:
575 if self.mode.is_single_char_entry:
576 self.show_help = True
577 if self.mode.name == 'waiting_for_server':
578 self.log_msg('@ waiting for server …')
579 elif self.mode.name == 'login':
581 self.send('LOGIN ' + quote(self.login_name))
583 self.log_msg('@ enter username')
584 elif self.mode.name == 'admin_enter':
585 self.log_msg('@ enter admin password:')
586 elif self.mode.name == 'control_pw_type':
587 self.log_msg('@ enter protection character for which you want to change the password:')
588 elif self.mode.name == 'control_tile_type':
589 self.log_msg('@ enter protection character which you want to draw:')
590 elif self.mode.name == 'admin_thing_protect':
591 self.log_msg('@ enter thing protection character:')
592 elif self.mode.name == 'control_pw_pw':
593 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
594 elif self.mode.name == 'control_tile_draw':
595 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']))
597 self.restore_input_values()
599 def loop(self, stdscr):
602 def safe_addstr(y, x, line):
603 if y < self.size.y - 1 or x + len(line) < self.size.x:
604 stdscr.addstr(y, x, line)
605 else: # workaround to <https://stackoverflow.com/q/7063128>
606 cut_i = self.size.x - x - 1
608 last_char = line[cut_i]
609 stdscr.addstr(y, self.size.x - 2, last_char)
610 stdscr.insstr(y, self.size.x - 2, ' ')
611 stdscr.addstr(y, x, cut)
613 def handle_input(msg):
614 command, args = self.parser.parse(msg)
617 def task_action_on(action):
618 return action_tasks[action] in self.game.tasks
620 def msg_into_lines_of_width(msg, width):
624 for i in range(len(msg)):
625 if x >= width or msg[i] == "\n":
637 def reset_screen_size():
638 self.size = YX(*stdscr.getmaxyx())
639 self.size = self.size - YX(self.size.y % 4, 0)
640 self.size = self.size - YX(0, self.size.x % 4)
641 self.window_width = int(self.size.x / 2)
643 def recalc_input_lines():
644 if not self.mode.has_input_prompt:
645 self.input_lines = []
647 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
650 def move_explorer(direction):
651 target = self.game.map_geometry.move_yx(self.explorer, direction)
653 self.explorer = target
654 if self.mode.shows_info:
657 self.send_tile_control_command()
663 for line in self.log:
664 lines += msg_into_lines_of_width(line, self.window_width)
667 max_y = self.size.y - len(self.input_lines)
668 for i in range(len(lines)):
669 if (i >= max_y - height_header):
671 safe_addstr(max_y - i - 1, self.window_width, lines[i])
674 if not self.game.turn_complete:
676 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
677 info = 'MAP VIEW: %s\n' % self.map_mode
678 if self.game.fov[pos_i] != '.':
679 info += 'outside field of view'
681 terrain_char = self.game.map_content[pos_i]
683 if terrain_char in self.game.terrains:
684 terrain_desc = self.game.terrains[terrain_char]
685 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
686 protection = self.game.map_control_content[pos_i]
687 if protection == '.':
688 protection = 'unprotected'
689 info += 'PROTECTION: %s\n' % protection
690 for t in self.game.things:
691 if t.position == self.explorer:
692 protection = t.protection
693 if protection == '.':
695 info += 'THING: %s / %s' % (t.type_,
696 self.game.thing_types[t.type_])
697 if hasattr(t, 'thing_char'):
699 if hasattr(t, 'name'):
700 info += ' (%s)' % t.name
701 info += ' / protection: %s\n' % protection
702 if self.explorer in self.game.portals:
703 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
705 info += 'PORTAL: (none)\n'
706 if self.explorer in self.game.info_db:
707 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
709 info += 'ANNOTATION: waiting …'
710 lines = msg_into_lines_of_width(info, self.window_width)
712 for i in range(len(lines)):
713 y = height_header + i
714 if y >= self.size.y - len(self.input_lines):
716 safe_addstr(y, self.window_width, lines[i])
719 y = self.size.y - len(self.input_lines)
720 for i in range(len(self.input_lines)):
721 safe_addstr(y, self.window_width, self.input_lines[i])
725 if not self.game.turn_complete:
727 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
730 help = "hit [%s] for help" % self.keys['help']
731 if self.mode.has_input_prompt:
732 help = "enter /help for help"
733 safe_addstr(1, self.window_width,
734 'MODE: %s – %s' % (self.mode.short_desc, help))
737 if not self.game.turn_complete:
740 for y in range(self.game.map_geometry.size.y):
741 start = self.game.map_geometry.size.x * y
742 end = start + self.game.map_geometry.size.x
743 if self.map_mode == 'protections':
744 map_lines_split += [[c + ' ' for c
745 in self.game.map_control_content[start:end]]]
747 map_lines_split += [[c + ' ' for c
748 in self.game.map_content[start:end]]]
749 if self.map_mode == 'terrain + annotations':
750 for p in self.game.info_hints:
751 map_lines_split[p.y][p.x] = 'A '
752 elif self.map_mode == 'terrain + things':
753 for p in self.game.portals.keys():
754 original = map_lines_split[p.y][p.x]
755 map_lines_split[p.y][p.x] = original[0] + 'P'
757 for t in self.game.things:
758 symbol = self.game.thing_types[t.type_]
760 if hasattr(t, 'thing_char'):
761 meta_char = t.thing_char
762 if t.position in used_positions:
764 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
765 used_positions += [t.position]
766 player = self.game.get_thing(self.game.player_id)
767 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
768 map_lines_split[self.explorer.y][self.explorer.x] = '??'
769 elif self.map_mode != 'terrain + things':
770 map_lines_split[player.position.y][player.position.x] = '??'
772 if type(self.game.map_geometry) == MapGeometryHex:
774 for line in map_lines_split:
775 map_lines += [indent * ' ' + ''.join(line)]
776 indent = 0 if indent else 1
778 for line in map_lines_split:
779 map_lines += [''.join(line)]
780 window_center = YX(int(self.size.y / 2),
781 int(self.window_width / 2))
782 center = player.position
783 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
784 center = self.explorer
785 center = YX(center.y, center.x * 2)
786 offset = center - window_center
787 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
789 term_y = max(0, -offset.y)
790 term_x = max(0, -offset.x)
791 map_y = max(0, offset.y)
792 map_x = max(0, offset.x)
793 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
794 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
795 safe_addstr(term_y, term_x, to_draw)
800 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
801 self.mode.help_intro)
802 if len(self.mode.available_actions) > 0:
803 content += "Available actions:\n"
804 for action in self.mode.available_actions:
805 if action in action_tasks:
806 if action_tasks[action] not in self.game.tasks:
808 if action == 'move_explorer':
811 key = ','.join(self.movement_keys)
813 key = self.keys[action]
814 content += '[%s] – %s\n' % (key, action_descriptions[action])
816 if self.mode.name == 'chat':
817 content += '/nick NAME – re-name yourself to NAME\n'
818 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
819 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
820 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
821 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
822 content += self.mode.list_available_modes(self)
823 for i in range(self.size.y):
825 self.window_width * (not self.mode.has_input_prompt),
826 ' ' * self.window_width)
828 for line in content.split('\n'):
829 lines += msg_into_lines_of_width(line, self.window_width)
830 for i in range(len(lines)):
834 self.window_width * (not self.mode.has_input_prompt),
840 if self.mode.has_input_prompt:
842 if self.mode.shows_info:
847 if not self.mode.is_intro:
853 action_descriptions = {
855 'flatten': 'flatten surroundings',
856 'teleport': 'teleport',
857 'take_thing': 'pick up thing',
858 'drop_thing': 'drop thing',
859 'toggle_map_mode': 'toggle map view',
860 'toggle_tile_draw': 'toggle protection character drawing',
861 'door': 'open/close',
865 'flatten': 'FLATTEN_SURROUNDINGS',
866 'take_thing': 'PICK_UP',
867 'drop_thing': 'DROP',
872 curses.curs_set(False) # hide cursor
873 curses.use_default_colors()
876 self.explorer = YX(0, 0)
879 interval = datetime.timedelta(seconds=5)
880 last_ping = datetime.datetime.now() - interval
882 if self.disconnected and self.force_instant_connect:
883 self.force_instant_connect = False
885 now = datetime.datetime.now()
886 if now - last_ping > interval:
887 if self.disconnected:
897 self.do_refresh = False
900 msg = self.queue.get(block=False)
905 key = stdscr.getkey()
906 self.do_refresh = True
909 self.show_help = False
910 if key == 'KEY_RESIZE':
912 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
913 self.input_ = self.input_[:-1]
914 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
915 self.show_help = True
917 self.restore_input_values()
918 elif self.mode.has_input_prompt and key != '\n': # Return key
920 max_length = self.window_width * self.size.y - len(input_prompt) - 1
921 if len(self.input_) > max_length:
922 self.input_ = self.input_[:max_length]
923 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
924 self.show_help = True
925 elif self.mode.name == 'login' and key == '\n':
926 self.login_name = self.input_
927 self.send('LOGIN ' + quote(self.input_))
929 elif self.mode.name == 'control_pw_pw' and key == '\n':
930 if self.input_ == '':
931 self.log_msg('@ aborted')
933 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
934 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
935 self.switch_mode('admin')
936 elif self.mode.name == 'password' and key == '\n':
937 if self.input_ == '':
939 self.password = self.input_
940 self.switch_mode('edit')
941 elif self.mode.name == 'admin_enter' and key == '\n':
942 self.send('BECOME_ADMIN ' + quote(self.input_))
943 self.switch_mode('play')
944 elif self.mode.name == 'control_pw_type' and key == '\n':
945 if len(self.input_) != 1:
946 self.log_msg('@ entered non-single-char, therefore aborted')
947 self.switch_mode('admin')
949 self.tile_control_char = self.input_
950 self.switch_mode('control_pw_pw')
951 elif self.mode.name == 'admin_thing_protect' and key == '\n':
952 if len(self.input_) != 1:
953 self.log_msg('@ entered non-single-char, therefore aborted')
955 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
957 self.log_msg('@ sent new protection character for thing')
958 self.switch_mode('admin')
959 elif self.mode.name == 'control_tile_type' and key == '\n':
960 if len(self.input_) != 1:
961 self.log_msg('@ entered non-single-char, therefore aborted')
962 self.switch_mode('admin')
964 self.tile_control_char = self.input_
965 self.switch_mode('control_tile_draw')
966 elif self.mode.name == 'chat' and key == '\n':
967 if self.input_ == '':
969 if self.input_[0] == '/': # FIXME fails on empty input
970 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
971 self.switch_mode('play')
972 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
973 self.switch_mode('study')
974 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
975 self.switch_mode('edit')
976 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
977 self.switch_mode('admin_enter')
978 elif self.input_.startswith('/nick'):
979 tokens = self.input_.split(maxsplit=1)
981 self.send('NICK ' + quote(tokens[1]))
983 self.log_msg('? need login name')
985 self.log_msg('? unknown command')
987 self.send('ALL ' + quote(self.input_))
989 elif self.mode.name == 'name_thing' and key == '\n':
990 if self.input_ == '':
992 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
994 quote(self.password)))
995 self.switch_mode('edit')
996 elif self.mode.name == 'annotate' and key == '\n':
997 if self.input_ == '':
999 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1000 quote(self.password)))
1001 self.switch_mode('edit')
1002 elif self.mode.name == 'portal' and key == '\n':
1003 if self.input_ == '':
1005 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1006 quote(self.password)))
1007 self.switch_mode('edit')
1008 elif self.mode.name == 'study':
1009 if self.mode.mode_switch_on_key(self, key):
1011 elif key == self.keys['toggle_map_mode']:
1012 self.toggle_map_mode()
1013 elif key in self.movement_keys:
1014 move_explorer(self.movement_keys[key])
1015 elif self.mode.name == 'play':
1016 if self.mode.mode_switch_on_key(self, key):
1018 elif key == self.keys['take_thing'] and task_action_on('take_thing'):
1019 self.send('TASK:PICK_UP')
1020 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1021 self.send('TASK:DROP')
1022 elif key == self.keys['door'] and task_action_on('door'):
1023 self.send('TASK:DOOR')
1024 elif key == self.keys['teleport']:
1025 player = self.game.get_thing(self.game.player_id)
1026 if player.position in self.game.portals:
1027 self.host = self.game.portals[player.position]
1031 self.log_msg('? not standing on portal')
1032 elif key in self.movement_keys and task_action_on('move'):
1033 self.send('TASK:MOVE ' + self.movement_keys[key])
1034 elif self.mode.name == 'write':
1035 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1036 self.switch_mode('edit')
1037 elif self.mode.name == 'control_tile_draw':
1038 if self.mode.mode_switch_on_key(self, key):
1040 elif key in self.movement_keys:
1041 move_explorer(self.movement_keys[key])
1042 elif key == self.keys['toggle_tile_draw']:
1043 self.tile_draw = False if self.tile_draw else True
1044 elif self.mode.name == 'admin':
1045 if self.mode.mode_switch_on_key(self, key):
1047 elif key in self.movement_keys and task_action_on('move'):
1048 self.send('TASK:MOVE ' + self.movement_keys[key])
1049 elif self.mode.name == 'edit':
1050 if self.mode.mode_switch_on_key(self, key):
1052 elif key == self.keys['flatten'] and task_action_on('flatten'):
1053 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1054 elif key == self.keys['toggle_map_mode']:
1055 self.toggle_map_mode()
1056 elif key in self.movement_keys and task_action_on('move'):
1057 self.send('TASK:MOVE ' + self.movement_keys[key])
1059 if len(sys.argv) != 2:
1060 raise ArgError('wrong number of arguments, need game host')