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',
431 'toggle_map_mode': 'L',
432 'toggle_tile_draw': 'm',
433 'hex_move_upleft': 'w',
434 'hex_move_upright': 'e',
435 'hex_move_right': 'd',
436 'hex_move_downright': 'x',
437 'hex_move_downleft': 'y',
438 'hex_move_left': 'a',
439 'square_move_up': 'w',
440 'square_move_left': 'a',
441 'square_move_down': 's',
442 'square_move_right': 'd',
444 if os.path.isfile('config.json'):
445 with open('config.json', 'r') as f:
446 keys_conf = json.loads(f.read())
448 self.keys[k] = keys_conf[k]
449 self.show_help = False
450 self.disconnected = True
451 self.force_instant_connect = True
452 self.input_lines = []
455 curses.wrapper(self.loop)
459 def handle_recv(msg):
465 self.log_msg('@ attempting connect')
466 socket_client_class = PlomSocketClient
467 if self.host.startswith('ws://') or self.host.startswith('wss://'):
468 socket_client_class = WebSocketClient
470 self.socket = socket_client_class(handle_recv, self.host)
471 self.socket_thread = threading.Thread(target=self.socket.run)
472 self.socket_thread.start()
473 self.disconnected = False
474 self.game.thing_types = {}
475 self.game.terrains = {}
476 time.sleep(0.1) # give potential SSL negotation some time …
477 self.socket.send('TASKS')
478 self.socket.send('TERRAINS')
479 self.socket.send('THING_TYPES')
480 self.switch_mode('login')
481 except ConnectionRefusedError:
482 self.log_msg('@ server connect failure')
483 self.disconnected = True
484 self.switch_mode('waiting_for_server')
485 self.do_refresh = True
488 self.log_msg('@ attempting reconnect')
490 # necessitated by some strange SSL race conditions with ws4py
491 time.sleep(0.1) # FIXME find out why exactly necessary
492 self.switch_mode('waiting_for_server')
497 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
498 raise BrokenSocketConnection
499 self.socket.send(msg)
500 except (BrokenPipeError, BrokenSocketConnection):
501 self.log_msg('@ server disconnected :(')
502 self.disconnected = True
503 self.force_instant_connect = True
504 self.do_refresh = True
506 def log_msg(self, msg):
508 if len(self.log) > 100:
509 self.log = self.log[-100:]
511 def query_info(self):
512 self.send('GET_ANNOTATION ' + str(self.explorer))
514 def restore_input_values(self):
515 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
516 info = self.game.info_db[self.explorer]
519 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
520 self.input_ = self.game.portals[self.explorer]
521 elif self.mode.name == 'password':
522 self.input_ = self.password
523 elif self.mode.name == 'name_thing':
524 if hasattr(self.thing_selected, 'name'):
525 self.input_ = self.thing_selected.name
526 elif self.mode.name == 'admin_thing_protect':
527 if hasattr(self.thing_selected, 'protection'):
528 self.input_ = self.thing_selected.protection
530 def send_tile_control_command(self):
531 self.send('SET_TILE_CONTROL %s %s' %
532 (self.explorer, quote(self.tile_control_char)))
534 def toggle_map_mode(self):
535 if self.map_mode == 'terrain only':
536 self.map_mode = 'terrain + annotations'
537 elif self.map_mode == 'terrain + annotations':
538 self.map_mode = 'terrain + things'
539 elif self.map_mode == 'terrain + things':
540 self.map_mode = 'protections'
541 elif self.map_mode == 'protections':
542 self.map_mode = 'terrain only'
544 def switch_mode(self, mode_name):
545 self.tile_draw = False
546 if mode_name == 'admin_enter' and self.is_admin:
548 elif mode_name in {'name_thing', 'admin_thing_protect'}:
549 player = self.game.get_thing(self.game.player_id)
551 for t in [t for t in self.game.things if t.position == player.position
552 and t.id_ != player.id_]:
557 self.log_msg('? not standing over thing')
560 self.thing_selected = thing
561 self.mode = getattr(self, 'mode_' + mode_name)
562 if self.mode.name == 'control_tile_draw':
563 self.log_msg('@ finished tile protection drawing.')
564 if self.mode.name in {'control_tile_draw', 'control_tile_type',
566 self.map_mode = 'protections'
567 elif self.mode.name != 'edit':
568 self.map_mode = 'terrain + things'
569 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
570 player = self.game.get_thing(self.game.player_id)
571 self.explorer = YX(player.position.y, player.position.x)
572 if self.mode.shows_info:
574 if self.mode.is_single_char_entry:
575 self.show_help = True
576 if self.mode.name == 'waiting_for_server':
577 self.log_msg('@ waiting for server …')
578 elif self.mode.name == 'login':
580 self.send('LOGIN ' + quote(self.login_name))
582 self.log_msg('@ enter username')
583 elif self.mode.name == 'admin_enter':
584 self.log_msg('@ enter admin password:')
585 elif self.mode.name == 'control_pw_type':
586 self.log_msg('@ enter protection character for which you want to change the password:')
587 elif self.mode.name == 'control_tile_type':
588 self.log_msg('@ enter protection character which you want to draw:')
589 elif self.mode.name == 'admin_thing_protect':
590 self.log_msg('@ enter thing protection character:')
591 elif self.mode.name == 'control_pw_pw':
592 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
593 elif self.mode.name == 'control_tile_draw':
594 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']))
596 self.restore_input_values()
598 def loop(self, stdscr):
601 def safe_addstr(y, x, line):
602 if y < self.size.y - 1 or x + len(line) < self.size.x:
603 stdscr.addstr(y, x, line)
604 else: # workaround to <https://stackoverflow.com/q/7063128>
605 cut_i = self.size.x - x - 1
607 last_char = line[cut_i]
608 stdscr.addstr(y, self.size.x - 2, last_char)
609 stdscr.insstr(y, self.size.x - 2, ' ')
610 stdscr.addstr(y, x, cut)
612 def handle_input(msg):
613 command, args = self.parser.parse(msg)
616 def task_action_on(action):
617 return action_tasks[action] in self.game.tasks
619 def msg_into_lines_of_width(msg, width):
623 for i in range(len(msg)):
624 if x >= width or msg[i] == "\n":
636 def reset_screen_size():
637 self.size = YX(*stdscr.getmaxyx())
638 self.size = self.size - YX(self.size.y % 4, 0)
639 self.size = self.size - YX(0, self.size.x % 4)
640 self.window_width = int(self.size.x / 2)
642 def recalc_input_lines():
643 if not self.mode.has_input_prompt:
644 self.input_lines = []
646 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
649 def move_explorer(direction):
650 target = self.game.map_geometry.move_yx(self.explorer, direction)
652 self.explorer = target
653 if self.mode.shows_info:
656 self.send_tile_control_command()
662 for line in self.log:
663 lines += msg_into_lines_of_width(line, self.window_width)
666 max_y = self.size.y - len(self.input_lines)
667 for i in range(len(lines)):
668 if (i >= max_y - height_header):
670 safe_addstr(max_y - i - 1, self.window_width, lines[i])
673 if not self.game.turn_complete:
675 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
676 info = 'MAP VIEW: %s\n' % self.map_mode
677 if self.game.fov[pos_i] != '.':
678 info += 'outside field of view'
680 terrain_char = self.game.map_content[pos_i]
682 if terrain_char in self.game.terrains:
683 terrain_desc = self.game.terrains[terrain_char]
684 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
685 protection = self.game.map_control_content[pos_i]
686 if protection == '.':
687 protection = 'unprotected'
688 info += 'PROTECTION: %s\n' % protection
689 for t in self.game.things:
690 if t.position == self.explorer:
691 protection = t.protection
692 if protection == '.':
693 protection = 'unprotected'
694 info += 'THING: %s / protection: %s / %s' %\
695 (t.type_, protection, self.game.thing_types[t.type_])
696 if hasattr(t, 'player_char'):
697 info += t.player_char
698 if hasattr(t, 'name'):
699 info += ' (%s)' % t.name
701 if self.explorer in self.game.portals:
702 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
704 info += 'PORTAL: (none)\n'
705 if self.explorer in self.game.info_db:
706 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
708 info += 'ANNOTATION: waiting …'
709 lines = msg_into_lines_of_width(info, self.window_width)
711 for i in range(len(lines)):
712 y = height_header + i
713 if y >= self.size.y - len(self.input_lines):
715 safe_addstr(y, self.window_width, lines[i])
718 y = self.size.y - len(self.input_lines)
719 for i in range(len(self.input_lines)):
720 safe_addstr(y, self.window_width, self.input_lines[i])
724 if not self.game.turn_complete:
726 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
729 help = "hit [%s] for help" % self.keys['help']
730 if self.mode.has_input_prompt:
731 help = "enter /help for help"
732 safe_addstr(1, self.window_width,
733 'MODE: %s – %s' % (self.mode.short_desc, help))
736 if not self.game.turn_complete:
739 for y in range(self.game.map_geometry.size.y):
740 start = self.game.map_geometry.size.x * y
741 end = start + self.game.map_geometry.size.x
742 if self.map_mode == 'protections':
743 map_lines_split += [[c + ' ' for c
744 in self.game.map_control_content[start:end]]]
746 map_lines_split += [[c + ' ' for c
747 in self.game.map_content[start:end]]]
748 if self.map_mode == 'terrain + annotations':
749 for p in self.game.info_hints:
750 map_lines_split[p.y][p.x] = 'A '
751 elif self.map_mode == 'terrain + things':
752 for p in self.game.portals.keys():
753 original = map_lines_split[p.y][p.x]
754 map_lines_split[p.y][p.x] = original[0] + 'P'
756 for t in self.game.things:
757 symbol = self.game.thing_types[t.type_]
759 if hasattr(t, 'player_char'):
760 meta_char = t.player_char
761 if t.position in used_positions:
763 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
764 used_positions += [t.position]
765 player = self.game.get_thing(self.game.player_id)
766 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
767 map_lines_split[self.explorer.y][self.explorer.x] = '??'
768 elif self.map_mode != 'terrain + things':
769 map_lines_split[player.position.y][player.position.x] = '??'
771 if type(self.game.map_geometry) == MapGeometryHex:
773 for line in map_lines_split:
774 map_lines += [indent * ' ' + ''.join(line)]
775 indent = 0 if indent else 1
777 for line in map_lines_split:
778 map_lines += [''.join(line)]
779 window_center = YX(int(self.size.y / 2),
780 int(self.window_width / 2))
781 center = player.position
782 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
783 center = self.explorer
784 center = YX(center.y, center.x * 2)
785 offset = center - window_center
786 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
788 term_y = max(0, -offset.y)
789 term_x = max(0, -offset.x)
790 map_y = max(0, offset.y)
791 map_x = max(0, offset.x)
792 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
793 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
794 safe_addstr(term_y, term_x, to_draw)
799 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
800 self.mode.help_intro)
801 if len(self.mode.available_actions) > 0:
802 content += "Available actions:\n"
803 for action in self.mode.available_actions:
804 if action in action_tasks:
805 if action_tasks[action] not in self.game.tasks:
807 if action == 'move_explorer':
810 key = ','.join(self.movement_keys)
812 key = self.keys[action]
813 content += '[%s] – %s\n' % (key, action_descriptions[action])
815 if self.mode.name == 'chat':
816 content += '/nick NAME – re-name yourself to NAME\n'
817 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
818 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
819 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
820 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
821 content += self.mode.list_available_modes(self)
822 for i in range(self.size.y):
824 self.window_width * (not self.mode.has_input_prompt),
825 ' ' * self.window_width)
827 for line in content.split('\n'):
828 lines += msg_into_lines_of_width(line, self.window_width)
829 for i in range(len(lines)):
833 self.window_width * (not self.mode.has_input_prompt),
839 if self.mode.has_input_prompt:
841 if self.mode.shows_info:
846 if not self.mode.is_intro:
852 action_descriptions = {
854 'flatten': 'flatten surroundings',
855 'teleport': 'teleport',
856 'take_thing': 'pick up thing',
857 'drop_thing': 'drop thing',
858 'toggle_map_mode': 'toggle map view',
859 'toggle_tile_draw': 'toggle protection character drawing',
863 'flatten': 'FLATTEN_SURROUNDINGS',
864 'take_thing': 'PICK_UP',
865 'drop_thing': 'DROP',
869 curses.curs_set(False) # hide cursor
870 curses.use_default_colors()
873 self.explorer = YX(0, 0)
876 interval = datetime.timedelta(seconds=5)
877 last_ping = datetime.datetime.now() - interval
879 if self.disconnected and self.force_instant_connect:
880 self.force_instant_connect = False
882 now = datetime.datetime.now()
883 if now - last_ping > interval:
884 if self.disconnected:
894 self.do_refresh = False
897 msg = self.queue.get(block=False)
902 key = stdscr.getkey()
903 self.do_refresh = True
906 self.show_help = False
907 if key == 'KEY_RESIZE':
909 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
910 self.input_ = self.input_[:-1]
911 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
912 self.show_help = True
914 self.restore_input_values()
915 elif self.mode.has_input_prompt and key != '\n': # Return key
917 max_length = self.window_width * self.size.y - len(input_prompt) - 1
918 if len(self.input_) > max_length:
919 self.input_ = self.input_[:max_length]
920 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
921 self.show_help = True
922 elif self.mode.name == 'login' and key == '\n':
923 self.login_name = self.input_
924 self.send('LOGIN ' + quote(self.input_))
926 elif self.mode.name == 'control_pw_pw' and key == '\n':
927 if self.input_ == '':
928 self.log_msg('@ aborted')
930 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
931 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
932 self.switch_mode('admin')
933 elif self.mode.name == 'password' and key == '\n':
934 if self.input_ == '':
936 self.password = self.input_
937 self.switch_mode('edit')
938 elif self.mode.name == 'admin_enter' and key == '\n':
939 self.send('BECOME_ADMIN ' + quote(self.input_))
940 self.switch_mode('play')
941 elif self.mode.name == 'control_pw_type' and key == '\n':
942 if len(self.input_) != 1:
943 self.log_msg('@ entered non-single-char, therefore aborted')
944 self.switch_mode('admin')
946 self.tile_control_char = self.input_
947 self.switch_mode('control_pw_pw')
948 elif self.mode.name == 'admin_thing_protect' and key == '\n':
949 if len(self.input_) != 1:
950 self.log_msg('@ entered non-single-char, therefore aborted')
952 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
954 self.log_msg('@ sent new protection character for thing')
955 self.switch_mode('admin')
956 elif self.mode.name == 'control_tile_type' and key == '\n':
957 if len(self.input_) != 1:
958 self.log_msg('@ entered non-single-char, therefore aborted')
959 self.switch_mode('admin')
961 self.tile_control_char = self.input_
962 self.switch_mode('control_tile_draw')
963 elif self.mode.name == 'chat' and key == '\n':
964 if self.input_ == '':
966 if self.input_[0] == '/': # FIXME fails on empty input
967 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
968 self.switch_mode('play')
969 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
970 self.switch_mode('study')
971 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
972 self.switch_mode('edit')
973 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
974 self.switch_mode('admin_enter')
975 elif self.input_.startswith('/nick'):
976 tokens = self.input_.split(maxsplit=1)
978 self.send('NICK ' + quote(tokens[1]))
980 self.log_msg('? need login name')
982 self.log_msg('? unknown command')
984 self.send('ALL ' + quote(self.input_))
986 elif self.mode.name == 'name_thing' and key == '\n':
987 if self.input_ == '':
989 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
991 quote(self.password)))
992 self.switch_mode('edit')
993 elif self.mode.name == 'annotate' and key == '\n':
994 if self.input_ == '':
996 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
997 quote(self.password)))
998 self.switch_mode('edit')
999 elif self.mode.name == 'portal' and key == '\n':
1000 if self.input_ == '':
1002 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1003 quote(self.password)))
1004 self.switch_mode('edit')
1005 elif self.mode.name == 'study':
1006 if self.mode.mode_switch_on_key(self, key):
1008 elif key == self.keys['toggle_map_mode']:
1009 self.toggle_map_mode()
1010 elif key in self.movement_keys:
1011 move_explorer(self.movement_keys[key])
1012 elif self.mode.name == 'play':
1013 if self.mode.mode_switch_on_key(self, key):
1015 elif key == self.keys['take_thing'] and task_action_on('take_thing'):
1016 self.send('TASK:PICK_UP')
1017 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1018 self.send('TASK:DROP')
1019 elif key == self.keys['teleport']:
1020 player = self.game.get_thing(self.game.player_id)
1021 if player.position in self.game.portals:
1022 self.host = self.game.portals[player.position]
1026 self.log_msg('? not standing on portal')
1027 elif key in self.movement_keys and task_action_on('move'):
1028 self.send('TASK:MOVE ' + self.movement_keys[key])
1029 elif self.mode.name == 'write':
1030 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1031 self.switch_mode('edit')
1032 elif self.mode.name == 'control_tile_draw':
1033 if self.mode.mode_switch_on_key(self, key):
1035 elif key in self.movement_keys:
1036 move_explorer(self.movement_keys[key])
1037 elif key == self.keys['toggle_tile_draw']:
1038 self.tile_draw = False if self.tile_draw else True
1039 elif self.mode.name == 'admin':
1040 if self.mode.mode_switch_on_key(self, key):
1042 elif key in self.movement_keys and task_action_on('move'):
1043 self.send('TASK:MOVE ' + self.movement_keys[key])
1044 elif self.mode.name == 'edit':
1045 if self.mode.mode_switch_on_key(self, key):
1047 elif key == self.keys['flatten'] and task_action_on('flatten'):
1048 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1049 elif key == self.keys['toggle_map_mode']:
1050 self.toggle_map_mode()
1051 elif key in self.movement_keys and task_action_on('move'):
1052 self.send('TASK:MOVE ' + self.movement_keys[key])
1054 if len(sys.argv) != 2:
1055 raise ArgError('wrong number of arguments, need game host')