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 = []
467 curses.wrapper(self.loop)
471 def handle_recv(msg):
477 self.log_msg('@ attempting connect')
478 socket_client_class = PlomSocketClient
479 if self.host.startswith('ws://') or self.host.startswith('wss://'):
480 socket_client_class = WebSocketClient
482 self.socket = socket_client_class(handle_recv, self.host)
483 self.socket_thread = threading.Thread(target=self.socket.run)
484 self.socket_thread.start()
485 self.disconnected = False
486 self.game.thing_types = {}
487 self.game.terrains = {}
488 time.sleep(0.1) # give potential SSL negotation some time …
489 self.socket.send('TASKS')
490 self.socket.send('TERRAINS')
491 self.socket.send('THING_TYPES')
492 self.switch_mode('login')
493 except ConnectionRefusedError:
494 self.log_msg('@ server connect failure')
495 self.disconnected = True
496 self.switch_mode('waiting_for_server')
497 self.do_refresh = True
500 self.log_msg('@ attempting reconnect')
502 # necessitated by some strange SSL race conditions with ws4py
503 time.sleep(0.1) # FIXME find out why exactly necessary
504 self.switch_mode('waiting_for_server')
509 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
510 raise BrokenSocketConnection
511 self.socket.send(msg)
512 except (BrokenPipeError, BrokenSocketConnection):
513 self.log_msg('@ server disconnected :(')
514 self.disconnected = True
515 self.force_instant_connect = True
516 self.do_refresh = True
518 def log_msg(self, msg):
520 if len(self.log) > 100:
521 self.log = self.log[-100:]
523 def query_info(self):
524 self.send('GET_ANNOTATION ' + str(self.explorer))
526 def restore_input_values(self):
527 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
528 info = self.game.info_db[self.explorer]
531 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
532 self.input_ = self.game.portals[self.explorer]
533 elif self.mode.name == 'password':
534 self.input_ = self.password
535 elif self.mode.name == 'name_thing':
536 if hasattr(self.thing_selected, 'name'):
537 self.input_ = self.thing_selected.name
538 elif self.mode.name == 'admin_thing_protect':
539 if hasattr(self.thing_selected, 'protection'):
540 self.input_ = self.thing_selected.protection
542 def send_tile_control_command(self):
543 self.send('SET_TILE_CONTROL %s %s' %
544 (self.explorer, quote(self.tile_control_char)))
546 def toggle_map_mode(self):
547 if self.map_mode == 'terrain only':
548 self.map_mode = 'terrain + annotations'
549 elif self.map_mode == 'terrain + annotations':
550 self.map_mode = 'terrain + things'
551 elif self.map_mode == 'terrain + things':
552 self.map_mode = 'protections'
553 elif self.map_mode == 'protections':
554 self.map_mode = 'terrain only'
556 def switch_mode(self, mode_name):
557 self.tile_draw = False
558 if mode_name == 'admin_enter' and self.is_admin:
560 elif mode_name in {'name_thing', 'admin_thing_protect'}:
561 player = self.game.get_thing(self.game.player_id)
563 for t in [t for t in self.game.things if t.position == player.position
564 and t.id_ != player.id_]:
569 self.log_msg('? not standing over thing')
572 self.thing_selected = thing
573 self.mode = getattr(self, 'mode_' + mode_name)
574 if self.mode.name == 'control_tile_draw':
575 self.log_msg('@ finished tile protection drawing.')
576 if self.mode.name in {'control_tile_draw', 'control_tile_type',
578 self.map_mode = 'protections'
579 elif self.mode.name != 'edit':
580 self.map_mode = 'terrain + things'
581 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
582 player = self.game.get_thing(self.game.player_id)
583 self.explorer = YX(player.position.y, player.position.x)
584 if self.mode.shows_info:
586 if self.mode.is_single_char_entry:
587 self.show_help = True
588 if self.mode.name == 'waiting_for_server':
589 self.log_msg('@ waiting for server …')
590 elif self.mode.name == 'login':
592 self.send('LOGIN ' + quote(self.login_name))
594 self.log_msg('@ enter username')
595 elif self.mode.name == 'admin_enter':
596 self.log_msg('@ enter admin password:')
597 elif self.mode.name == 'control_pw_type':
598 self.log_msg('@ enter protection character for which you want to change the password:')
599 elif self.mode.name == 'control_tile_type':
600 self.log_msg('@ enter protection character which you want to draw:')
601 elif self.mode.name == 'admin_thing_protect':
602 self.log_msg('@ enter thing protection character:')
603 elif self.mode.name == 'control_pw_pw':
604 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
605 elif self.mode.name == 'control_tile_draw':
606 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']))
608 self.restore_input_values()
610 def set_default_colors(self):
611 curses.init_color(1, 1000, 1000, 1000)
612 curses.init_color(2, 0, 0, 0)
613 self.do_refresh = True
615 def set_random_colors(self):
619 return int(offset + random.random()*375)
621 curses.init_color(1, rand(625), rand(625), rand(625))
622 curses.init_color(2, rand(0), rand(0), rand(0))
623 self.do_refresh = True
625 def loop(self, stdscr):
628 def safe_addstr(y, x, line):
629 if y < self.size.y - 1 or x + len(line) < self.size.x:
630 stdscr.addstr(y, x, line, curses.color_pair(1))
631 else: # workaround to <https://stackoverflow.com/q/7063128>
632 cut_i = self.size.x - x - 1
634 last_char = line[cut_i]
635 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
636 stdscr.insstr(y, self.size.x - 2, ' ')
637 stdscr.addstr(y, x, cut, curses.color_pair(1))
639 def handle_input(msg):
640 command, args = self.parser.parse(msg)
643 def task_action_on(action):
644 return action_tasks[action] in self.game.tasks
646 def msg_into_lines_of_width(msg, width):
650 for i in range(len(msg)):
651 if x >= width or msg[i] == "\n":
663 def reset_screen_size():
664 self.size = YX(*stdscr.getmaxyx())
665 self.size = self.size - YX(self.size.y % 4, 0)
666 self.size = self.size - YX(0, self.size.x % 4)
667 self.window_width = int(self.size.x / 2)
669 def recalc_input_lines():
670 if not self.mode.has_input_prompt:
671 self.input_lines = []
673 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
676 def move_explorer(direction):
677 target = self.game.map_geometry.move_yx(self.explorer, direction)
679 self.explorer = target
680 if self.mode.shows_info:
683 self.send_tile_control_command()
689 for line in self.log:
690 lines += msg_into_lines_of_width(line, self.window_width)
693 max_y = self.size.y - len(self.input_lines)
694 for i in range(len(lines)):
695 if (i >= max_y - height_header):
697 safe_addstr(max_y - i - 1, self.window_width, lines[i])
700 if not self.game.turn_complete:
702 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
703 info = 'MAP VIEW: %s\n' % self.map_mode
704 if self.game.fov[pos_i] != '.':
705 info += 'outside field of view'
707 terrain_char = self.game.map_content[pos_i]
709 if terrain_char in self.game.terrains:
710 terrain_desc = self.game.terrains[terrain_char]
711 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
712 protection = self.game.map_control_content[pos_i]
713 if protection == '.':
714 protection = 'unprotected'
715 info += 'PROTECTION: %s\n' % protection
716 for t in self.game.things:
717 if t.position == self.explorer:
718 protection = t.protection
719 if protection == '.':
721 info += 'THING: %s / %s' % (t.type_,
722 self.game.thing_types[t.type_])
723 if hasattr(t, 'thing_char'):
725 if hasattr(t, 'name'):
726 info += ' (%s)' % t.name
727 info += ' / protection: %s\n' % protection
728 if self.explorer in self.game.portals:
729 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
731 info += 'PORTAL: (none)\n'
732 if self.explorer in self.game.info_db:
733 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
735 info += 'ANNOTATION: waiting …'
736 lines = msg_into_lines_of_width(info, self.window_width)
738 for i in range(len(lines)):
739 y = height_header + i
740 if y >= self.size.y - len(self.input_lines):
742 safe_addstr(y, self.window_width, lines[i])
745 y = self.size.y - len(self.input_lines)
746 for i in range(len(self.input_lines)):
747 safe_addstr(y, self.window_width, self.input_lines[i])
751 if not self.game.turn_complete:
753 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
756 help = "hit [%s] for help" % self.keys['help']
757 if self.mode.has_input_prompt:
758 help = "enter /help for help"
759 safe_addstr(1, self.window_width,
760 'MODE: %s – %s' % (self.mode.short_desc, help))
763 if not self.game.turn_complete:
766 for y in range(self.game.map_geometry.size.y):
767 start = self.game.map_geometry.size.x * y
768 end = start + self.game.map_geometry.size.x
769 if self.map_mode == 'protections':
770 map_lines_split += [[c + ' ' for c
771 in self.game.map_control_content[start:end]]]
773 map_lines_split += [[c + ' ' for c
774 in self.game.map_content[start:end]]]
775 if self.map_mode == 'terrain + annotations':
776 for p in self.game.info_hints:
777 map_lines_split[p.y][p.x] = 'A '
778 elif self.map_mode == 'terrain + things':
779 for p in self.game.portals.keys():
780 original = map_lines_split[p.y][p.x]
781 map_lines_split[p.y][p.x] = original[0] + 'P'
783 for t in self.game.things:
784 symbol = self.game.thing_types[t.type_]
786 if hasattr(t, 'thing_char'):
787 meta_char = t.thing_char
788 if t.position in used_positions:
790 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
791 used_positions += [t.position]
792 player = self.game.get_thing(self.game.player_id)
793 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
794 map_lines_split[self.explorer.y][self.explorer.x] = '??'
795 elif self.map_mode != 'terrain + things':
796 map_lines_split[player.position.y][player.position.x] = '??'
798 if type(self.game.map_geometry) == MapGeometryHex:
800 for line in map_lines_split:
801 map_lines += [indent * ' ' + ''.join(line)]
802 indent = 0 if indent else 1
804 for line in map_lines_split:
805 map_lines += [''.join(line)]
806 window_center = YX(int(self.size.y / 2),
807 int(self.window_width / 2))
808 center = player.position
809 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
810 center = self.explorer
811 center = YX(center.y, center.x * 2)
812 offset = center - window_center
813 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
815 term_y = max(0, -offset.y)
816 term_x = max(0, -offset.x)
817 map_y = max(0, offset.y)
818 map_x = max(0, offset.x)
819 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
820 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
821 safe_addstr(term_y, term_x, to_draw)
826 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
827 self.mode.help_intro)
828 if len(self.mode.available_actions) > 0:
829 content += "Available actions:\n"
830 for action in self.mode.available_actions:
831 if action in action_tasks:
832 if action_tasks[action] not in self.game.tasks:
834 if action == 'move_explorer':
837 key = ','.join(self.movement_keys)
839 key = self.keys[action]
840 content += '[%s] – %s\n' % (key, action_descriptions[action])
842 if self.mode.name == 'chat':
843 content += '/nick NAME – re-name yourself to NAME\n'
844 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
845 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
846 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
847 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
848 content += self.mode.list_available_modes(self)
849 for i in range(self.size.y):
851 self.window_width * (not self.mode.has_input_prompt),
852 ' ' * self.window_width)
854 for line in content.split('\n'):
855 lines += msg_into_lines_of_width(line, self.window_width)
856 for i in range(len(lines)):
860 self.window_width * (not self.mode.has_input_prompt),
865 stdscr.bkgd(' ', curses.color_pair(1))
867 if self.mode.has_input_prompt:
869 if self.mode.shows_info:
874 if not self.mode.is_intro:
880 action_descriptions = {
882 'flatten': 'flatten surroundings',
883 'teleport': 'teleport',
884 'take_thing': 'pick up thing',
885 'drop_thing': 'drop thing',
886 'toggle_map_mode': 'toggle map view',
887 'toggle_tile_draw': 'toggle protection character drawing',
888 'door': 'open/close',
889 'consume': 'consume',
893 'flatten': 'FLATTEN_SURROUNDINGS',
894 'take_thing': 'PICK_UP',
895 'drop_thing': 'DROP',
898 'consume': 'INTOXICATE',
901 curses.curs_set(False) # hide cursor
903 self.set_default_colors()
904 curses.init_pair(1, 1, 2)
907 self.explorer = YX(0, 0)
910 interval = datetime.timedelta(seconds=5)
911 last_ping = datetime.datetime.now() - interval
913 if self.disconnected and self.force_instant_connect:
914 self.force_instant_connect = False
916 now = datetime.datetime.now()
917 if now - last_ping > interval:
918 if self.disconnected:
928 self.do_refresh = False
931 msg = self.queue.get(block=False)
936 key = stdscr.getkey()
937 self.do_refresh = True
940 self.show_help = False
941 if key == 'KEY_RESIZE':
943 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
944 self.input_ = self.input_[:-1]
945 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
946 self.show_help = True
948 self.restore_input_values()
949 elif self.mode.has_input_prompt and key != '\n': # Return key
951 max_length = self.window_width * self.size.y - len(input_prompt) - 1
952 if len(self.input_) > max_length:
953 self.input_ = self.input_[:max_length]
954 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
955 self.show_help = True
956 elif self.mode.name == 'login' and key == '\n':
957 self.login_name = self.input_
958 self.send('LOGIN ' + quote(self.input_))
960 elif self.mode.name == 'control_pw_pw' and key == '\n':
961 if self.input_ == '':
962 self.log_msg('@ aborted')
964 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
965 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
966 self.switch_mode('admin')
967 elif self.mode.name == 'password' and key == '\n':
968 if self.input_ == '':
970 self.password = self.input_
971 self.switch_mode('edit')
972 elif self.mode.name == 'admin_enter' and key == '\n':
973 self.send('BECOME_ADMIN ' + quote(self.input_))
974 self.switch_mode('play')
975 elif self.mode.name == 'control_pw_type' and key == '\n':
976 if len(self.input_) != 1:
977 self.log_msg('@ entered non-single-char, therefore aborted')
978 self.switch_mode('admin')
980 self.tile_control_char = self.input_
981 self.switch_mode('control_pw_pw')
982 elif self.mode.name == 'admin_thing_protect' and key == '\n':
983 if len(self.input_) != 1:
984 self.log_msg('@ entered non-single-char, therefore aborted')
986 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
988 self.log_msg('@ sent new protection character for thing')
989 self.switch_mode('admin')
990 elif self.mode.name == 'control_tile_type' and key == '\n':
991 if len(self.input_) != 1:
992 self.log_msg('@ entered non-single-char, therefore aborted')
993 self.switch_mode('admin')
995 self.tile_control_char = self.input_
996 self.switch_mode('control_tile_draw')
997 elif self.mode.name == 'chat' and key == '\n':
998 if self.input_ == '':
1000 if self.input_[0] == '/': # FIXME fails on empty input
1001 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1002 self.switch_mode('play')
1003 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1004 self.switch_mode('study')
1005 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1006 self.switch_mode('edit')
1007 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1008 self.switch_mode('admin_enter')
1009 elif self.input_.startswith('/nick'):
1010 tokens = self.input_.split(maxsplit=1)
1011 if len(tokens) == 2:
1012 self.send('NICK ' + quote(tokens[1]))
1014 self.log_msg('? need login name')
1016 self.log_msg('? unknown command')
1018 self.send('ALL ' + quote(self.input_))
1020 elif self.mode.name == 'name_thing' and key == '\n':
1021 if self.input_ == '':
1023 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1025 quote(self.password)))
1026 self.switch_mode('edit')
1027 elif self.mode.name == 'annotate' and key == '\n':
1028 if self.input_ == '':
1030 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1031 quote(self.password)))
1032 self.switch_mode('edit')
1033 elif self.mode.name == 'portal' and key == '\n':
1034 if self.input_ == '':
1036 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1037 quote(self.password)))
1038 self.switch_mode('edit')
1039 elif self.mode.name == 'study':
1040 if self.mode.mode_switch_on_key(self, key):
1042 elif key == self.keys['toggle_map_mode']:
1043 self.toggle_map_mode()
1044 elif key in self.movement_keys:
1045 move_explorer(self.movement_keys[key])
1046 elif self.mode.name == 'play':
1047 if self.mode.mode_switch_on_key(self, key):
1049 elif key == self.keys['take_thing'] and task_action_on('take_thing'):
1050 self.send('TASK:PICK_UP')
1051 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1052 self.send('TASK:DROP')
1053 elif key == self.keys['door'] and task_action_on('door'):
1054 self.send('TASK:DOOR')
1055 elif key == self.keys['consume'] and task_action_on('consume'):
1056 self.send('TASK:INTOXICATE')
1057 elif key == self.keys['teleport']:
1058 player = self.game.get_thing(self.game.player_id)
1059 if player.position in self.game.portals:
1060 self.host = self.game.portals[player.position]
1064 self.log_msg('? not standing on portal')
1065 elif key in self.movement_keys and task_action_on('move'):
1066 self.send('TASK:MOVE ' + self.movement_keys[key])
1067 elif self.mode.name == 'write':
1068 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1069 self.switch_mode('edit')
1070 elif self.mode.name == 'control_tile_draw':
1071 if self.mode.mode_switch_on_key(self, key):
1073 elif key in self.movement_keys:
1074 move_explorer(self.movement_keys[key])
1075 elif key == self.keys['toggle_tile_draw']:
1076 self.tile_draw = False if self.tile_draw else True
1077 elif self.mode.name == 'admin':
1078 if self.mode.mode_switch_on_key(self, key):
1080 elif key in self.movement_keys and task_action_on('move'):
1081 self.send('TASK:MOVE ' + self.movement_keys[key])
1082 elif self.mode.name == 'edit':
1083 if self.mode.mode_switch_on_key(self, key):
1085 elif key == self.keys['flatten'] and task_action_on('flatten'):
1086 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1087 elif key == self.keys['toggle_map_mode']:
1088 self.toggle_map_mode()
1089 elif key in self.movement_keys and task_action_on('move'):
1090 self.send('TASK:MOVE ' + self.movement_keys[key])
1092 if len(sys.argv) != 2:
1093 raise ArgError('wrong number of arguments, need game host')