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 def cmd_DEFAULT_COLORS(game):
272 game.tui.set_default_colors()
273 cmd_DEFAULT_COLORS.argtypes = ''
275 def cmd_RANDOM_COLORS(game):
276 game.tui.set_random_colors()
277 cmd_RANDOM_COLORS.argtypes = ''
279 class Game(GameBase):
280 turn_complete = False
284 def __init__(self, *args, **kwargs):
285 super().__init__(*args, **kwargs)
286 self.register_command(cmd_LOGIN_OK)
287 self.register_command(cmd_ADMIN_OK)
288 self.register_command(cmd_PONG)
289 self.register_command(cmd_CHAT)
290 self.register_command(cmd_PLAYER_ID)
291 self.register_command(cmd_TURN)
292 self.register_command(cmd_THING)
293 self.register_command(cmd_THING_TYPE)
294 self.register_command(cmd_THING_NAME)
295 self.register_command(cmd_THING_CHAR)
296 self.register_command(cmd_TERRAIN)
297 self.register_command(cmd_MAP)
298 self.register_command(cmd_MAP_CONTROL)
299 self.register_command(cmd_PORTAL)
300 self.register_command(cmd_ANNOTATION)
301 self.register_command(cmd_ANNOTATION_HINT)
302 self.register_command(cmd_GAME_STATE_COMPLETE)
303 self.register_command(cmd_ARGUMENT_ERROR)
304 self.register_command(cmd_GAME_ERROR)
305 self.register_command(cmd_PLAY_ERROR)
306 self.register_command(cmd_TASKS)
307 self.register_command(cmd_FOV)
308 self.register_command(cmd_DEFAULT_COLORS)
309 self.register_command(cmd_RANDOM_COLORS)
310 self.map_content = ''
317 def get_string_options(self, string_option_type):
318 if string_option_type == 'map_geometry':
319 return ['Hex', 'Square']
320 elif string_option_type == 'thing_type':
321 return self.thing_types.keys()
324 def get_command(self, command_name):
325 from functools import partial
326 f = partial(self.commands[command_name], self)
327 f.argtypes = self.commands[command_name].argtypes
332 def __init__(self, name, has_input_prompt=False, shows_info=False,
333 is_intro=False, is_single_char_entry=False):
335 self.short_desc = mode_helps[name]['short']
336 self.available_modes = []
337 self.available_actions = []
338 self.has_input_prompt = has_input_prompt
339 self.shows_info = shows_info
340 self.is_intro = is_intro
341 self.help_intro = mode_helps[name]['long']
342 self.is_single_char_entry = is_single_char_entry
345 def iter_available_modes(self, tui):
346 for mode_name in self.available_modes:
347 mode = getattr(tui, 'mode_' + mode_name)
350 key = tui.keys['switch_to_' + mode.name]
353 def list_available_modes(self, tui):
355 if len(self.available_modes) > 0:
356 msg = 'Other modes available from here:\n'
357 for mode, key in self.iter_available_modes(tui):
358 msg += '[%s] – %s\n' % (key, mode.short_desc)
361 def mode_switch_on_key(self, tui, key_pressed):
362 for mode, key in self.iter_available_modes(tui):
363 if key_pressed == key:
364 tui.switch_mode(mode.name)
369 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
370 mode_admin = Mode('admin')
371 mode_play = Mode('play')
372 mode_study = Mode('study', shows_info=True)
373 mode_write = Mode('write', is_single_char_entry=True)
374 mode_edit = Mode('edit')
375 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
376 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
377 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
378 mode_control_tile_draw = Mode('control_tile_draw')
379 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
380 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
381 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
382 mode_chat = Mode('chat', has_input_prompt=True)
383 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
384 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
385 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
386 mode_password = Mode('password', has_input_prompt=True)
387 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
391 def __init__(self, host):
394 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
395 self.mode_play.available_actions = ["move", "take_thing", "drop_thing",
396 "teleport", "door", "consume"]
397 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
398 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
399 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
400 "control_tile_type", "chat",
401 "study", "play", "edit"]
402 self.mode_admin.available_actions = ["move"]
403 self.mode_control_tile_draw.available_modes = ["admin_enter"]
404 self.mode_control_tile_draw.available_actions = ["move_explorer",
406 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
407 "password", "chat", "study", "play",
409 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
414 self.parser = Parser(self.game)
416 self.do_refresh = True
417 self.queue = queue.Queue()
418 self.login_name = None
419 self.map_mode = 'terrain + things'
420 self.password = 'foo'
421 self.switch_mode('waiting_for_server')
423 'switch_to_chat': 't',
424 'switch_to_play': 'p',
425 'switch_to_password': 'P',
426 'switch_to_annotate': 'M',
427 'switch_to_portal': 'T',
428 'switch_to_study': '?',
429 'switch_to_edit': 'E',
430 'switch_to_write': 'm',
431 'switch_to_name_thing': 'N',
432 'switch_to_admin_enter': 'A',
433 'switch_to_control_pw_type': 'C',
434 'switch_to_control_tile_type': 'Q',
435 'switch_to_admin_thing_protect': 'T',
443 'toggle_map_mode': 'L',
444 'toggle_tile_draw': 'm',
445 'hex_move_upleft': 'w',
446 'hex_move_upright': 'e',
447 'hex_move_right': 'd',
448 'hex_move_downright': 'x',
449 'hex_move_downleft': 'y',
450 'hex_move_left': 'a',
451 'square_move_up': 'w',
452 'square_move_left': 'a',
453 'square_move_down': 's',
454 'square_move_right': 'd',
456 if os.path.isfile('config.json'):
457 with open('config.json', 'r') as f:
458 keys_conf = json.loads(f.read())
460 self.keys[k] = keys_conf[k]
461 self.show_help = False
462 self.disconnected = True
463 self.force_instant_connect = True
464 self.input_lines = []
468 self.offset = YX(0,0)
469 curses.wrapper(self.loop)
473 def handle_recv(msg):
479 self.log_msg('@ attempting connect')
480 socket_client_class = PlomSocketClient
481 if self.host.startswith('ws://') or self.host.startswith('wss://'):
482 socket_client_class = WebSocketClient
484 self.socket = socket_client_class(handle_recv, self.host)
485 self.socket_thread = threading.Thread(target=self.socket.run)
486 self.socket_thread.start()
487 self.disconnected = False
488 self.game.thing_types = {}
489 self.game.terrains = {}
490 time.sleep(0.1) # give potential SSL negotation some time …
491 self.socket.send('TASKS')
492 self.socket.send('TERRAINS')
493 self.socket.send('THING_TYPES')
494 self.switch_mode('login')
495 except ConnectionRefusedError:
496 self.log_msg('@ server connect failure')
497 self.disconnected = True
498 self.switch_mode('waiting_for_server')
499 self.do_refresh = True
502 self.log_msg('@ attempting reconnect')
504 # necessitated by some strange SSL race conditions with ws4py
505 time.sleep(0.1) # FIXME find out why exactly necessary
506 self.switch_mode('waiting_for_server')
511 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
512 raise BrokenSocketConnection
513 self.socket.send(msg)
514 except (BrokenPipeError, BrokenSocketConnection):
515 self.log_msg('@ server disconnected :(')
516 self.disconnected = True
517 self.force_instant_connect = True
518 self.do_refresh = True
520 def log_msg(self, msg):
522 if len(self.log) > 100:
523 self.log = self.log[-100:]
525 def query_info(self):
526 self.send('GET_ANNOTATION ' + str(self.explorer))
528 def restore_input_values(self):
529 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
530 info = self.game.info_db[self.explorer]
533 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
534 self.input_ = self.game.portals[self.explorer]
535 elif self.mode.name == 'password':
536 self.input_ = self.password
537 elif self.mode.name == 'name_thing':
538 if hasattr(self.thing_selected, 'name'):
539 self.input_ = self.thing_selected.name
540 elif self.mode.name == 'admin_thing_protect':
541 if hasattr(self.thing_selected, 'protection'):
542 self.input_ = self.thing_selected.protection
544 def send_tile_control_command(self):
545 self.send('SET_TILE_CONTROL %s %s' %
546 (self.explorer, quote(self.tile_control_char)))
548 def toggle_map_mode(self):
549 if self.map_mode == 'terrain only':
550 self.map_mode = 'terrain + annotations'
551 elif self.map_mode == 'terrain + annotations':
552 self.map_mode = 'terrain + things'
553 elif self.map_mode == 'terrain + things':
554 self.map_mode = 'protections'
555 elif self.map_mode == 'protections':
556 self.map_mode = 'terrain only'
558 def switch_mode(self, mode_name):
559 self.tile_draw = False
560 if mode_name == 'admin_enter' and self.is_admin:
562 elif mode_name in {'name_thing', 'admin_thing_protect'}:
563 player = self.game.get_thing(self.game.player_id)
565 for t in [t for t in self.game.things if t.position == player.position
566 and t.id_ != player.id_]:
571 self.log_msg('? not standing over thing')
574 self.thing_selected = thing
575 self.mode = getattr(self, 'mode_' + mode_name)
576 if self.mode.name == 'control_tile_draw':
577 self.log_msg('@ finished tile protection drawing.')
578 if self.mode.name in {'control_tile_draw', 'control_tile_type',
580 self.map_mode = 'protections'
581 elif self.mode.name != 'edit':
582 self.map_mode = 'terrain + things'
583 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
584 player = self.game.get_thing(self.game.player_id)
585 self.explorer = YX(player.position.y, player.position.x)
586 if self.mode.shows_info:
588 if self.mode.is_single_char_entry:
589 self.show_help = True
590 if self.mode.name == 'waiting_for_server':
591 self.log_msg('@ waiting for server …')
592 elif self.mode.name == 'login':
594 self.send('LOGIN ' + quote(self.login_name))
596 self.log_msg('@ enter username')
597 elif self.mode.name == 'admin_enter':
598 self.log_msg('@ enter admin password:')
599 elif self.mode.name == 'control_pw_type':
600 self.log_msg('@ enter protection character for which you want to change the password:')
601 elif self.mode.name == 'control_tile_type':
602 self.log_msg('@ enter protection character which you want to draw:')
603 elif self.mode.name == 'admin_thing_protect':
604 self.log_msg('@ enter thing protection character:')
605 elif self.mode.name == 'control_pw_pw':
606 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
607 elif self.mode.name == 'control_tile_draw':
608 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']))
610 self.restore_input_values()
612 def set_default_colors(self):
613 curses.init_color(1, 1000, 1000, 1000)
614 curses.init_color(2, 0, 0, 0)
615 self.do_refresh = True
617 def set_random_colors(self):
621 return int(offset + random.random()*375)
623 curses.init_color(1, rand(625), rand(625), rand(625))
624 curses.init_color(2, rand(0), rand(0), rand(0))
625 self.do_refresh = True
627 def loop(self, stdscr):
630 def safe_addstr(y, x, line):
631 if y < self.size.y - 1 or x + len(line) < self.size.x:
632 stdscr.addstr(y, x, line, curses.color_pair(1))
633 else: # workaround to <https://stackoverflow.com/q/7063128>
634 cut_i = self.size.x - x - 1
636 last_char = line[cut_i]
637 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
638 stdscr.insstr(y, self.size.x - 2, ' ')
639 stdscr.addstr(y, x, cut, curses.color_pair(1))
641 def handle_input(msg):
642 command, args = self.parser.parse(msg)
645 def task_action_on(action):
646 return action_tasks[action] in self.game.tasks
648 def msg_into_lines_of_width(msg, width):
652 for i in range(len(msg)):
653 if x >= width or msg[i] == "\n":
665 def reset_screen_size():
666 self.size = YX(*stdscr.getmaxyx())
667 self.size = self.size - YX(self.size.y % 4, 0)
668 self.size = self.size - YX(0, self.size.x % 4)
669 self.window_width = int(self.size.x / 2)
671 def recalc_input_lines():
672 if not self.mode.has_input_prompt:
673 self.input_lines = []
675 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
678 def move_explorer(direction):
679 target = self.game.map_geometry.move_yx(self.explorer, direction)
681 self.explorer = target
682 if self.mode.shows_info:
685 self.send_tile_control_command()
691 for line in self.log:
692 lines += msg_into_lines_of_width(line, self.window_width)
695 max_y = self.size.y - len(self.input_lines)
696 for i in range(len(lines)):
697 if (i >= max_y - height_header):
699 safe_addstr(max_y - i - 1, self.window_width, lines[i])
702 if not self.game.turn_complete:
704 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
705 info = 'MAP VIEW: %s\n' % self.map_mode
706 if self.game.fov[pos_i] != '.':
707 info += 'outside field of view'
709 terrain_char = self.game.map_content[pos_i]
711 if terrain_char in self.game.terrains:
712 terrain_desc = self.game.terrains[terrain_char]
713 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
714 protection = self.game.map_control_content[pos_i]
715 if protection == '.':
716 protection = 'unprotected'
717 info += 'PROTECTION: %s\n' % protection
718 for t in self.game.things:
719 if t.position == self.explorer:
720 protection = t.protection
721 if protection == '.':
723 info += 'THING: %s / %s' % (t.type_,
724 self.game.thing_types[t.type_])
725 if hasattr(t, 'thing_char'):
727 if hasattr(t, 'name'):
728 info += ' (%s)' % t.name
729 info += ' / protection: %s\n' % protection
730 if self.explorer in self.game.portals:
731 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
733 info += 'PORTAL: (none)\n'
734 if self.explorer in self.game.info_db:
735 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
737 info += 'ANNOTATION: waiting …'
738 lines = msg_into_lines_of_width(info, self.window_width)
740 for i in range(len(lines)):
741 y = height_header + i
742 if y >= self.size.y - len(self.input_lines):
744 safe_addstr(y, self.window_width, lines[i])
747 y = self.size.y - len(self.input_lines)
748 for i in range(len(self.input_lines)):
749 safe_addstr(y, self.window_width, self.input_lines[i])
753 if not self.game.turn_complete:
755 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
758 help = "hit [%s] for help" % self.keys['help']
759 if self.mode.has_input_prompt:
760 help = "enter /help for help"
761 safe_addstr(1, self.window_width,
762 'MODE: %s – %s' % (self.mode.short_desc, help))
765 if not self.game.turn_complete and len(self.map_lines) == 0:
767 if self.game.turn_complete:
769 for y in range(self.game.map_geometry.size.y):
770 start = self.game.map_geometry.size.x * y
771 end = start + self.game.map_geometry.size.x
772 if self.map_mode == 'protections':
773 map_lines_split += [[c + ' ' for c
774 in self.game.map_control_content[start:end]]]
776 map_lines_split += [[c + ' ' for c
777 in self.game.map_content[start:end]]]
778 if self.map_mode == 'terrain + annotations':
779 for p in self.game.info_hints:
780 map_lines_split[p.y][p.x] = 'A '
781 elif self.map_mode == 'terrain + things':
782 for p in self.game.portals.keys():
783 original = map_lines_split[p.y][p.x]
784 map_lines_split[p.y][p.x] = original[0] + 'P'
786 for t in self.game.things:
787 symbol = self.game.thing_types[t.type_]
789 if hasattr(t, 'thing_char'):
790 meta_char = t.thing_char
791 if t.position in used_positions:
793 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
794 used_positions += [t.position]
795 player = self.game.get_thing(self.game.player_id)
796 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
797 map_lines_split[self.explorer.y][self.explorer.x] = '??'
798 elif self.map_mode != 'terrain + things':
799 map_lines_split[player.position.y][player.position.x] = '??'
801 if type(self.game.map_geometry) == MapGeometryHex:
803 for line in map_lines_split:
804 self.map_lines += [indent * ' ' + ''.join(line)]
805 indent = 0 if indent else 1
807 for line in map_lines_split:
808 self.map_lines += [''.join(line)]
809 window_center = YX(int(self.size.y / 2),
810 int(self.window_width / 2))
811 center = player.position
812 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
813 center = self.explorer
814 center = YX(center.y, center.x * 2)
815 self.offset = center - window_center
816 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
817 self.offset += YX(0, 1)
818 term_y = max(0, -self.offset.y)
819 term_x = max(0, -self.offset.x)
820 map_y = max(0, self.offset.y)
821 map_x = max(0, self.offset.x)
822 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
823 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
824 safe_addstr(term_y, term_x, to_draw)
829 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
830 self.mode.help_intro)
831 if len(self.mode.available_actions) > 0:
832 content += "Available actions:\n"
833 for action in self.mode.available_actions:
834 if action in action_tasks:
835 if action_tasks[action] not in self.game.tasks:
837 if action == 'move_explorer':
840 key = ','.join(self.movement_keys)
842 key = self.keys[action]
843 content += '[%s] – %s\n' % (key, action_descriptions[action])
845 if self.mode.name == 'chat':
846 content += '/nick NAME – re-name yourself to NAME\n'
847 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
848 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
849 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
850 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
851 content += self.mode.list_available_modes(self)
852 for i in range(self.size.y):
854 self.window_width * (not self.mode.has_input_prompt),
855 ' ' * self.window_width)
857 for line in content.split('\n'):
858 lines += msg_into_lines_of_width(line, self.window_width)
859 for i in range(len(lines)):
863 self.window_width * (not self.mode.has_input_prompt),
868 stdscr.bkgd(' ', curses.color_pair(1))
870 if self.mode.has_input_prompt:
872 if self.mode.shows_info:
877 if not self.mode.is_intro:
883 action_descriptions = {
885 'flatten': 'flatten surroundings',
886 'teleport': 'teleport',
887 'take_thing': 'pick up thing',
888 'drop_thing': 'drop thing',
889 'toggle_map_mode': 'toggle map view',
890 'toggle_tile_draw': 'toggle protection character drawing',
891 'door': 'open/close',
892 'consume': 'consume',
896 'flatten': 'FLATTEN_SURROUNDINGS',
897 'take_thing': 'PICK_UP',
898 'drop_thing': 'DROP',
901 'consume': 'INTOXICATE',
904 curses.curs_set(False) # hide cursor
906 self.set_default_colors()
907 curses.init_pair(1, 1, 2)
910 self.explorer = YX(0, 0)
913 interval = datetime.timedelta(seconds=5)
914 last_ping = datetime.datetime.now() - interval
916 if self.disconnected and self.force_instant_connect:
917 self.force_instant_connect = False
919 now = datetime.datetime.now()
920 if now - last_ping > interval:
921 if self.disconnected:
931 self.do_refresh = False
934 msg = self.queue.get(block=False)
939 key = stdscr.getkey()
940 self.do_refresh = True
943 self.show_help = False
944 if key == 'KEY_RESIZE':
946 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
947 self.input_ = self.input_[:-1]
948 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
949 self.show_help = True
951 self.restore_input_values()
952 elif self.mode.has_input_prompt and key != '\n': # Return key
954 max_length = self.window_width * self.size.y - len(input_prompt) - 1
955 if len(self.input_) > max_length:
956 self.input_ = self.input_[:max_length]
957 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
958 self.show_help = True
959 elif self.mode.name == 'login' and key == '\n':
960 self.login_name = self.input_
961 self.send('LOGIN ' + quote(self.input_))
963 elif self.mode.name == 'control_pw_pw' and key == '\n':
964 if self.input_ == '':
965 self.log_msg('@ aborted')
967 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
968 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
969 self.switch_mode('admin')
970 elif self.mode.name == 'password' and key == '\n':
971 if self.input_ == '':
973 self.password = self.input_
974 self.switch_mode('edit')
975 elif self.mode.name == 'admin_enter' and key == '\n':
976 self.send('BECOME_ADMIN ' + quote(self.input_))
977 self.switch_mode('play')
978 elif self.mode.name == 'control_pw_type' and key == '\n':
979 if len(self.input_) != 1:
980 self.log_msg('@ entered non-single-char, therefore aborted')
981 self.switch_mode('admin')
983 self.tile_control_char = self.input_
984 self.switch_mode('control_pw_pw')
985 elif self.mode.name == 'admin_thing_protect' and key == '\n':
986 if len(self.input_) != 1:
987 self.log_msg('@ entered non-single-char, therefore aborted')
989 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
991 self.log_msg('@ sent new protection character for thing')
992 self.switch_mode('admin')
993 elif self.mode.name == 'control_tile_type' and key == '\n':
994 if len(self.input_) != 1:
995 self.log_msg('@ entered non-single-char, therefore aborted')
996 self.switch_mode('admin')
998 self.tile_control_char = self.input_
999 self.switch_mode('control_tile_draw')
1000 elif self.mode.name == 'chat' and key == '\n':
1001 if self.input_ == '':
1003 if self.input_[0] == '/': # FIXME fails on empty input
1004 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1005 self.switch_mode('play')
1006 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1007 self.switch_mode('study')
1008 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1009 self.switch_mode('edit')
1010 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1011 self.switch_mode('admin_enter')
1012 elif self.input_.startswith('/nick'):
1013 tokens = self.input_.split(maxsplit=1)
1014 if len(tokens) == 2:
1015 self.send('NICK ' + quote(tokens[1]))
1017 self.log_msg('? need login name')
1019 self.log_msg('? unknown command')
1021 self.send('ALL ' + quote(self.input_))
1023 elif self.mode.name == 'name_thing' and key == '\n':
1024 if self.input_ == '':
1026 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1028 quote(self.password)))
1029 self.switch_mode('edit')
1030 elif self.mode.name == 'annotate' and key == '\n':
1031 if self.input_ == '':
1033 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1034 quote(self.password)))
1035 self.switch_mode('edit')
1036 elif self.mode.name == 'portal' and key == '\n':
1037 if self.input_ == '':
1039 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1040 quote(self.password)))
1041 self.switch_mode('edit')
1042 elif self.mode.name == 'study':
1043 if self.mode.mode_switch_on_key(self, key):
1045 elif key == self.keys['toggle_map_mode']:
1046 self.toggle_map_mode()
1047 elif key in self.movement_keys:
1048 move_explorer(self.movement_keys[key])
1049 elif self.mode.name == 'play':
1050 if self.mode.mode_switch_on_key(self, key):
1052 elif key == self.keys['take_thing'] and task_action_on('take_thing'):
1053 self.send('TASK:PICK_UP')
1054 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1055 self.send('TASK:DROP')
1056 elif key == self.keys['door'] and task_action_on('door'):
1057 self.send('TASK:DOOR')
1058 elif key == self.keys['consume'] and task_action_on('consume'):
1059 self.send('TASK:INTOXICATE')
1060 elif key == self.keys['teleport']:
1061 player = self.game.get_thing(self.game.player_id)
1062 if player.position in self.game.portals:
1063 self.host = self.game.portals[player.position]
1067 self.log_msg('? not standing on portal')
1068 elif key in self.movement_keys and task_action_on('move'):
1069 self.send('TASK:MOVE ' + self.movement_keys[key])
1070 elif self.mode.name == 'write':
1071 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1072 self.switch_mode('edit')
1073 elif self.mode.name == 'control_tile_draw':
1074 if self.mode.mode_switch_on_key(self, key):
1076 elif key in self.movement_keys:
1077 move_explorer(self.movement_keys[key])
1078 elif key == self.keys['toggle_tile_draw']:
1079 self.tile_draw = False if self.tile_draw else True
1080 elif self.mode.name == 'admin':
1081 if self.mode.mode_switch_on_key(self, key):
1083 elif key in self.movement_keys and task_action_on('move'):
1084 self.send('TASK:MOVE ' + self.movement_keys[key])
1085 elif self.mode.name == 'edit':
1086 if self.mode.mode_switch_on_key(self, key):
1088 elif key == self.keys['flatten'] and task_action_on('flatten'):
1089 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1090 elif key == self.keys['toggle_map_mode']:
1091 self.toggle_map_mode()
1092 elif key in self.movement_keys and task_action_on('move'):
1093 self.send('TASK:MOVE ' + self.movement_keys[key])
1095 if len(sys.argv) != 2:
1096 raise ArgError('wrong number of arguments, need game host')