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.'
31 'short': 'command thing',
32 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
34 'admin_thing_protect': {
35 'short': 'change thing protection',
36 'long': 'Change protection character for thing here.'
39 'short': 'change terrain',
40 '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.'
43 'short': 'change protection character password',
44 '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.'
47 'short': 'change protection character password',
48 '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.'
50 'control_tile_type': {
51 'short': 'change tiles protection',
52 '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.'
54 'control_tile_draw': {
55 'short': 'change tiles protection',
56 '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.'
59 'short': 'annotate tile',
60 '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.'
63 'short': 'edit portal',
64 '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.'
68 '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:'
72 'long': 'Enter your player name.'
74 'waiting_for_server': {
75 'short': 'waiting for server response',
76 'long': 'Waiting for a server response.'
79 'short': 'waiting for server response',
80 'long': 'Waiting for a server response.'
83 'short': 'set world edit password',
84 '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.'
87 'short': 'become admin',
88 'long': 'This mode allows you to become admin if you know an admin password.'
92 'long': 'This mode allows you access to actions limited to administrators.'
96 from ws4py.client import WebSocketBaseClient
97 class WebSocketClient(WebSocketBaseClient):
99 def __init__(self, recv_handler, *args, **kwargs):
100 super().__init__(*args, **kwargs)
101 self.recv_handler = recv_handler
104 def received_message(self, message):
106 message = str(message)
107 self.recv_handler(message)
110 def plom_closed(self):
111 return self.client_terminated
113 from plomrogue.io_tcp import PlomSocket
114 class PlomSocketClient(PlomSocket):
116 def __init__(self, recv_handler, url):
118 self.recv_handler = recv_handler
119 host, port = url.split(':')
120 super().__init__(socket.create_connection((host, port)))
128 for msg in self.recv():
129 if msg == 'NEED_SSL':
130 self.socket = ssl.wrap_socket(self.socket)
132 self.recv_handler(msg)
133 except BrokenSocketConnection:
134 pass # we assume socket will be known as dead by now
136 def cmd_TURN(game, n):
142 game.turn_complete = False
143 cmd_TURN.argtypes = 'int:nonneg'
145 def cmd_LOGIN_OK(game):
146 game.tui.switch_mode('post_login_wait')
147 game.tui.send('GET_GAMESTATE')
148 game.tui.log_msg('@ welcome')
149 cmd_LOGIN_OK.argtypes = ''
151 def cmd_ADMIN_OK(game):
152 game.tui.is_admin = True
153 game.tui.log_msg('@ you now have admin rights')
154 game.tui.switch_mode('admin')
155 game.tui.do_refresh = True
156 cmd_ADMIN_OK.argtypes = ''
158 def cmd_REPLY(game, msg):
159 game.tui.log_msg('#MUSICPLAYER: ' + msg)
160 game.tui.do_refresh = True
161 cmd_REPLY.argtypes = 'string'
163 def cmd_CHAT(game, msg):
164 game.tui.log_msg('# ' + msg)
165 game.tui.do_refresh = True
166 cmd_CHAT.argtypes = 'string'
168 def cmd_PLAYER_ID(game, player_id):
169 game.player_id = player_id
170 cmd_PLAYER_ID.argtypes = 'int:nonneg'
172 def cmd_THING(game, yx, thing_type, protection, thing_id):
173 t = game.get_thing(thing_id)
175 t = ThingBase(game, thing_id)
179 t.protection = protection
180 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg'
182 def cmd_THING_NAME(game, thing_id, name):
183 t = game.get_thing(thing_id)
186 cmd_THING_NAME.argtypes = 'int:nonneg string'
188 def cmd_THING_CHAR(game, thing_id, c):
189 t = game.get_thing(thing_id)
192 cmd_THING_CHAR.argtypes = 'int:nonneg char'
194 def cmd_MAP(game, geometry, size, content):
195 map_geometry_class = globals()['MapGeometry' + geometry]
196 game.map_geometry = map_geometry_class(size)
197 game.map_content = content
198 if type(game.map_geometry) == MapGeometrySquare:
199 game.tui.movement_keys = {
200 game.tui.keys['square_move_up']: 'UP',
201 game.tui.keys['square_move_left']: 'LEFT',
202 game.tui.keys['square_move_down']: 'DOWN',
203 game.tui.keys['square_move_right']: 'RIGHT',
205 elif type(game.map_geometry) == MapGeometryHex:
206 game.tui.movement_keys = {
207 game.tui.keys['hex_move_upleft']: 'UPLEFT',
208 game.tui.keys['hex_move_upright']: 'UPRIGHT',
209 game.tui.keys['hex_move_right']: 'RIGHT',
210 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
211 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
212 game.tui.keys['hex_move_left']: 'LEFT',
214 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
216 def cmd_FOV(game, content):
218 cmd_FOV.argtypes = 'string'
220 def cmd_MAP_CONTROL(game, content):
221 game.map_control_content = content
222 cmd_MAP_CONTROL.argtypes = 'string'
224 def cmd_GAME_STATE_COMPLETE(game):
225 if game.tui.mode.name == 'post_login_wait':
226 game.tui.switch_mode('play')
227 if game.tui.mode.shows_info:
228 game.tui.query_info()
229 game.turn_complete = True
230 game.tui.do_refresh = True
231 cmd_GAME_STATE_COMPLETE.argtypes = ''
233 def cmd_PORTAL(game, position, msg):
234 game.portals[position] = msg
235 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
237 def cmd_PLAY_ERROR(game, msg):
238 game.tui.log_msg('? ' + msg)
239 game.tui.flash = True
240 game.tui.do_refresh = True
241 cmd_PLAY_ERROR.argtypes = 'string'
243 def cmd_GAME_ERROR(game, msg):
244 game.tui.log_msg('? game error: ' + msg)
245 game.tui.do_refresh = True
246 cmd_GAME_ERROR.argtypes = 'string'
248 def cmd_ARGUMENT_ERROR(game, msg):
249 game.tui.log_msg('? syntax error: ' + msg)
250 game.tui.do_refresh = True
251 cmd_ARGUMENT_ERROR.argtypes = 'string'
253 def cmd_ANNOTATION_HINT(game, position):
254 game.info_hints += [position]
255 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
257 def cmd_ANNOTATION(game, position, msg):
258 game.info_db[position] = msg
259 if game.tui.mode.shows_info:
260 game.tui.do_refresh = True
261 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
263 def cmd_TASKS(game, tasks_comma_separated):
264 game.tasks = tasks_comma_separated.split(',')
265 game.tui.mode_write.legal = 'WRITE' in game.tasks
266 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
267 cmd_TASKS.argtypes = 'string'
269 def cmd_THING_TYPE(game, thing_type, symbol_hint):
270 game.thing_types[thing_type] = symbol_hint
271 cmd_THING_TYPE.argtypes = 'string char'
273 def cmd_TERRAIN(game, terrain_char, terrain_desc):
274 game.terrains[terrain_char] = terrain_desc
275 cmd_TERRAIN.argtypes = 'char string'
279 cmd_PONG.argtypes = ''
281 def cmd_DEFAULT_COLORS(game):
282 game.tui.set_default_colors()
283 cmd_DEFAULT_COLORS.argtypes = ''
285 def cmd_RANDOM_COLORS(game):
286 game.tui.set_random_colors()
287 cmd_RANDOM_COLORS.argtypes = ''
289 class Game(GameBase):
290 turn_complete = False
294 def __init__(self, *args, **kwargs):
295 super().__init__(*args, **kwargs)
296 self.register_command(cmd_LOGIN_OK)
297 self.register_command(cmd_ADMIN_OK)
298 self.register_command(cmd_PONG)
299 self.register_command(cmd_CHAT)
300 self.register_command(cmd_REPLY)
301 self.register_command(cmd_PLAYER_ID)
302 self.register_command(cmd_TURN)
303 self.register_command(cmd_THING)
304 self.register_command(cmd_THING_TYPE)
305 self.register_command(cmd_THING_NAME)
306 self.register_command(cmd_THING_CHAR)
307 self.register_command(cmd_TERRAIN)
308 self.register_command(cmd_MAP)
309 self.register_command(cmd_MAP_CONTROL)
310 self.register_command(cmd_PORTAL)
311 self.register_command(cmd_ANNOTATION)
312 self.register_command(cmd_ANNOTATION_HINT)
313 self.register_command(cmd_GAME_STATE_COMPLETE)
314 self.register_command(cmd_ARGUMENT_ERROR)
315 self.register_command(cmd_GAME_ERROR)
316 self.register_command(cmd_PLAY_ERROR)
317 self.register_command(cmd_TASKS)
318 self.register_command(cmd_FOV)
319 self.register_command(cmd_DEFAULT_COLORS)
320 self.register_command(cmd_RANDOM_COLORS)
321 self.map_content = ''
328 def get_string_options(self, string_option_type):
329 if string_option_type == 'map_geometry':
330 return ['Hex', 'Square']
331 elif string_option_type == 'thing_type':
332 return self.thing_types.keys()
335 def get_command(self, command_name):
336 from functools import partial
337 f = partial(self.commands[command_name], self)
338 f.argtypes = self.commands[command_name].argtypes
343 def __init__(self, name, has_input_prompt=False, shows_info=False,
344 is_intro=False, is_single_char_entry=False):
346 self.short_desc = mode_helps[name]['short']
347 self.available_modes = []
348 self.available_actions = []
349 self.has_input_prompt = has_input_prompt
350 self.shows_info = shows_info
351 self.is_intro = is_intro
352 self.help_intro = mode_helps[name]['long']
353 self.is_single_char_entry = is_single_char_entry
356 def iter_available_modes(self, tui):
357 for mode_name in self.available_modes:
358 mode = getattr(tui, 'mode_' + mode_name)
361 key = tui.keys['switch_to_' + mode.name]
364 def list_available_modes(self, tui):
366 if len(self.available_modes) > 0:
367 msg = 'Other modes available from here:\n'
368 for mode, key in self.iter_available_modes(tui):
369 msg += '[%s] – %s\n' % (key, mode.short_desc)
372 def mode_switch_on_key(self, tui, key_pressed):
373 for mode, key in self.iter_available_modes(tui):
374 if key_pressed == key:
375 tui.switch_mode(mode.name)
380 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
381 mode_admin = Mode('admin')
382 mode_play = Mode('play')
383 mode_study = Mode('study', shows_info=True)
384 mode_write = Mode('write', is_single_char_entry=True)
385 mode_edit = Mode('edit')
386 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
387 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
388 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
389 mode_control_tile_draw = Mode('control_tile_draw')
390 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
391 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
392 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
393 mode_chat = Mode('chat', has_input_prompt=True)
394 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
395 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
396 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
397 mode_password = Mode('password', has_input_prompt=True)
398 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
399 mode_command_thing = Mode('command_thing', has_input_prompt=True)
403 def __init__(self, host):
406 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
408 self.mode_play.available_actions = ["move", "take_thing", "drop_thing",
409 "teleport", "door", "consume"]
410 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
411 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
412 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
413 "control_tile_type", "chat",
414 "study", "play", "edit"]
415 self.mode_admin.available_actions = ["move"]
416 self.mode_control_tile_draw.available_modes = ["admin_enter"]
417 self.mode_control_tile_draw.available_actions = ["move_explorer",
419 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
420 "password", "chat", "study", "play",
422 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
427 self.parser = Parser(self.game)
429 self.do_refresh = True
430 self.queue = queue.Queue()
431 self.login_name = None
432 self.map_mode = 'terrain + things'
433 self.password = 'foo'
434 self.switch_mode('waiting_for_server')
436 'switch_to_chat': 't',
437 'switch_to_play': 'p',
438 'switch_to_password': 'P',
439 'switch_to_annotate': 'M',
440 'switch_to_portal': 'T',
441 'switch_to_study': '?',
442 'switch_to_edit': 'E',
443 'switch_to_write': 'm',
444 'switch_to_name_thing': 'N',
445 'switch_to_command_thing': 'O',
446 'switch_to_admin_enter': 'A',
447 'switch_to_control_pw_type': 'C',
448 'switch_to_control_tile_type': 'Q',
449 'switch_to_admin_thing_protect': 'T',
457 'toggle_map_mode': 'L',
458 'toggle_tile_draw': 'm',
459 'hex_move_upleft': 'w',
460 'hex_move_upright': 'e',
461 'hex_move_right': 'd',
462 'hex_move_downright': 'x',
463 'hex_move_downleft': 'y',
464 'hex_move_left': 'a',
465 'square_move_up': 'w',
466 'square_move_left': 'a',
467 'square_move_down': 's',
468 'square_move_right': 'd',
470 if os.path.isfile('config.json'):
471 with open('config.json', 'r') as f:
472 keys_conf = json.loads(f.read())
474 self.keys[k] = keys_conf[k]
475 self.show_help = False
476 self.disconnected = True
477 self.force_instant_connect = True
478 self.input_lines = []
482 self.offset = YX(0,0)
483 curses.wrapper(self.loop)
487 def handle_recv(msg):
493 self.log_msg('@ attempting connect')
494 socket_client_class = PlomSocketClient
495 if self.host.startswith('ws://') or self.host.startswith('wss://'):
496 socket_client_class = WebSocketClient
498 self.socket = socket_client_class(handle_recv, self.host)
499 self.socket_thread = threading.Thread(target=self.socket.run)
500 self.socket_thread.start()
501 self.disconnected = False
502 self.game.thing_types = {}
503 self.game.terrains = {}
504 time.sleep(0.1) # give potential SSL negotation some time …
505 self.socket.send('TASKS')
506 self.socket.send('TERRAINS')
507 self.socket.send('THING_TYPES')
508 self.switch_mode('login')
509 except ConnectionRefusedError:
510 self.log_msg('@ server connect failure')
511 self.disconnected = True
512 self.switch_mode('waiting_for_server')
513 self.do_refresh = True
516 self.log_msg('@ attempting reconnect')
518 # necessitated by some strange SSL race conditions with ws4py
519 time.sleep(0.1) # FIXME find out why exactly necessary
520 self.switch_mode('waiting_for_server')
525 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
526 raise BrokenSocketConnection
527 self.socket.send(msg)
528 except (BrokenPipeError, BrokenSocketConnection):
529 self.log_msg('@ server disconnected :(')
530 self.disconnected = True
531 self.force_instant_connect = True
532 self.do_refresh = True
534 def log_msg(self, msg):
536 if len(self.log) > 100:
537 self.log = self.log[-100:]
539 def query_info(self):
540 self.send('GET_ANNOTATION ' + str(self.explorer))
542 def restore_input_values(self):
543 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
544 info = self.game.info_db[self.explorer]
547 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
548 self.input_ = self.game.portals[self.explorer]
549 elif self.mode.name == 'password':
550 self.input_ = self.password
551 elif self.mode.name == 'name_thing':
552 if hasattr(self.thing_selected, 'name'):
553 self.input_ = self.thing_selected.name
554 elif self.mode.name == 'admin_thing_protect':
555 if hasattr(self.thing_selected, 'protection'):
556 self.input_ = self.thing_selected.protection
558 def send_tile_control_command(self):
559 self.send('SET_TILE_CONTROL %s %s' %
560 (self.explorer, quote(self.tile_control_char)))
562 def toggle_map_mode(self):
563 if self.map_mode == 'terrain only':
564 self.map_mode = 'terrain + annotations'
565 elif self.map_mode == 'terrain + annotations':
566 self.map_mode = 'terrain + things'
567 elif self.map_mode == 'terrain + things':
568 self.map_mode = 'protections'
569 elif self.map_mode == 'protections':
570 self.map_mode = 'terrain only'
572 def switch_mode(self, mode_name):
573 self.tile_draw = False
574 if mode_name == 'admin_enter' and self.is_admin:
576 elif mode_name in {'name_thing', 'admin_thing_protect'}:
577 player = self.game.get_thing(self.game.player_id)
579 for t in [t for t in self.game.things if t.position == player.position
580 and t.id_ != player.id_]:
585 self.log_msg('? not standing over thing')
588 self.thing_selected = thing
589 self.mode = getattr(self, 'mode_' + mode_name)
590 if self.mode.name == 'control_tile_draw':
591 self.log_msg('@ finished tile protection drawing.')
592 if self.mode.name in {'control_tile_draw', 'control_tile_type',
594 self.map_mode = 'protections'
595 elif self.mode.name != 'edit':
596 self.map_mode = 'terrain + things'
597 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
598 player = self.game.get_thing(self.game.player_id)
599 self.explorer = YX(player.position.y, player.position.x)
600 if self.mode.shows_info:
602 if self.mode.is_single_char_entry:
603 self.show_help = True
604 if self.mode.name == 'waiting_for_server':
605 self.log_msg('@ waiting for server …')
606 elif self.mode.name == 'login':
608 self.send('LOGIN ' + quote(self.login_name))
610 self.log_msg('@ enter username')
611 elif self.mode.name == 'command_thing':
612 self.send('TASK:COMMAND ' + quote('HELP'))
613 elif self.mode.name == 'admin_enter':
614 self.log_msg('@ enter admin password:')
615 elif self.mode.name == 'control_pw_type':
616 self.log_msg('@ enter protection character for which you want to change the password:')
617 elif self.mode.name == 'control_tile_type':
618 self.log_msg('@ enter protection character which you want to draw:')
619 elif self.mode.name == 'admin_thing_protect':
620 self.log_msg('@ enter thing protection character:')
621 elif self.mode.name == 'control_pw_pw':
622 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
623 elif self.mode.name == 'control_tile_draw':
624 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']))
626 self.restore_input_values()
628 def set_default_colors(self):
629 curses.init_color(1, 1000, 1000, 1000)
630 curses.init_color(2, 0, 0, 0)
631 self.do_refresh = True
633 def set_random_colors(self):
637 return int(offset + random.random()*375)
639 curses.init_color(1, rand(625), rand(625), rand(625))
640 curses.init_color(2, rand(0), rand(0), rand(0))
641 self.do_refresh = True
643 def loop(self, stdscr):
646 def safe_addstr(y, x, line):
647 if y < self.size.y - 1 or x + len(line) < self.size.x:
648 stdscr.addstr(y, x, line, curses.color_pair(1))
649 else: # workaround to <https://stackoverflow.com/q/7063128>
650 cut_i = self.size.x - x - 1
652 last_char = line[cut_i]
653 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
654 stdscr.insstr(y, self.size.x - 2, ' ')
655 stdscr.addstr(y, x, cut, curses.color_pair(1))
657 def handle_input(msg):
658 command, args = self.parser.parse(msg)
661 def task_action_on(action):
662 return action_tasks[action] in self.game.tasks
664 def msg_into_lines_of_width(msg, width):
668 for i in range(len(msg)):
669 if x >= width or msg[i] == "\n":
681 def reset_screen_size():
682 self.size = YX(*stdscr.getmaxyx())
683 self.size = self.size - YX(self.size.y % 4, 0)
684 self.size = self.size - YX(0, self.size.x % 4)
685 self.window_width = int(self.size.x / 2)
687 def recalc_input_lines():
688 if not self.mode.has_input_prompt:
689 self.input_lines = []
691 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
694 def move_explorer(direction):
695 target = self.game.map_geometry.move_yx(self.explorer, direction)
697 self.explorer = target
698 if self.mode.shows_info:
701 self.send_tile_control_command()
707 for line in self.log:
708 lines += msg_into_lines_of_width(line, self.window_width)
711 max_y = self.size.y - len(self.input_lines)
712 for i in range(len(lines)):
713 if (i >= max_y - height_header):
715 safe_addstr(max_y - i - 1, self.window_width, lines[i])
718 if not self.game.turn_complete:
720 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
721 info = 'MAP VIEW: %s\n' % self.map_mode
722 if self.game.fov[pos_i] != '.':
723 info += 'outside field of view'
725 terrain_char = self.game.map_content[pos_i]
727 if terrain_char in self.game.terrains:
728 terrain_desc = self.game.terrains[terrain_char]
729 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
730 protection = self.game.map_control_content[pos_i]
731 if protection == '.':
732 protection = 'unprotected'
733 info += 'PROTECTION: %s\n' % protection
734 for t in self.game.things:
735 if t.position == self.explorer:
736 protection = t.protection
737 if protection == '.':
739 info += 'THING: %s / %s' % (t.type_,
740 self.game.thing_types[t.type_])
741 if hasattr(t, 'thing_char'):
743 if hasattr(t, 'name'):
744 info += ' (%s)' % t.name
745 info += ' / protection: %s\n' % protection
746 if self.explorer in self.game.portals:
747 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
749 info += 'PORTAL: (none)\n'
750 if self.explorer in self.game.info_db:
751 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
753 info += 'ANNOTATION: waiting …'
754 lines = msg_into_lines_of_width(info, self.window_width)
756 for i in range(len(lines)):
757 y = height_header + i
758 if y >= self.size.y - len(self.input_lines):
760 safe_addstr(y, self.window_width, lines[i])
763 y = self.size.y - len(self.input_lines)
764 for i in range(len(self.input_lines)):
765 safe_addstr(y, self.window_width, self.input_lines[i])
769 if not self.game.turn_complete:
771 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
774 help = "hit [%s] for help" % self.keys['help']
775 if self.mode.has_input_prompt:
776 help = "enter /help for help"
777 safe_addstr(1, self.window_width,
778 'MODE: %s – %s' % (self.mode.short_desc, help))
781 if not self.game.turn_complete and len(self.map_lines) == 0:
783 if self.game.turn_complete:
785 for y in range(self.game.map_geometry.size.y):
786 start = self.game.map_geometry.size.x * y
787 end = start + self.game.map_geometry.size.x
788 if self.map_mode == 'protections':
789 map_lines_split += [[c + ' ' for c
790 in self.game.map_control_content[start:end]]]
792 map_lines_split += [[c + ' ' for c
793 in self.game.map_content[start:end]]]
794 if self.map_mode == 'terrain + annotations':
795 for p in self.game.info_hints:
796 map_lines_split[p.y][p.x] = 'A '
797 elif self.map_mode == 'terrain + things':
798 for p in self.game.portals.keys():
799 original = map_lines_split[p.y][p.x]
800 map_lines_split[p.y][p.x] = original[0] + 'P'
802 for t in self.game.things:
803 symbol = self.game.thing_types[t.type_]
805 if hasattr(t, 'thing_char'):
806 meta_char = t.thing_char
807 if t.position in used_positions:
809 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
810 used_positions += [t.position]
811 player = self.game.get_thing(self.game.player_id)
812 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
813 map_lines_split[self.explorer.y][self.explorer.x] = '??'
814 elif self.map_mode != 'terrain + things':
815 map_lines_split[player.position.y][player.position.x] = '??'
817 if type(self.game.map_geometry) == MapGeometryHex:
819 for line in map_lines_split:
820 self.map_lines += [indent * ' ' + ''.join(line)]
821 indent = 0 if indent else 1
823 for line in map_lines_split:
824 self.map_lines += [''.join(line)]
825 window_center = YX(int(self.size.y / 2),
826 int(self.window_width / 2))
827 center = player.position
828 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
829 center = self.explorer
830 center = YX(center.y, center.x * 2)
831 self.offset = center - window_center
832 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
833 self.offset += YX(0, 1)
834 term_y = max(0, -self.offset.y)
835 term_x = max(0, -self.offset.x)
836 map_y = max(0, self.offset.y)
837 map_x = max(0, self.offset.x)
838 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
839 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
840 safe_addstr(term_y, term_x, to_draw)
845 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
846 self.mode.help_intro)
847 if len(self.mode.available_actions) > 0:
848 content += "Available actions:\n"
849 for action in self.mode.available_actions:
850 if action in action_tasks:
851 if action_tasks[action] not in self.game.tasks:
853 if action == 'move_explorer':
856 key = ','.join(self.movement_keys)
858 key = self.keys[action]
859 content += '[%s] – %s\n' % (key, action_descriptions[action])
861 if self.mode.name == 'chat':
862 content += '/nick NAME – re-name yourself to NAME\n'
863 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
864 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
865 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
866 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
867 content += self.mode.list_available_modes(self)
868 for i in range(self.size.y):
870 self.window_width * (not self.mode.has_input_prompt),
871 ' ' * self.window_width)
873 for line in content.split('\n'):
874 lines += msg_into_lines_of_width(line, self.window_width)
875 for i in range(len(lines)):
879 self.window_width * (not self.mode.has_input_prompt),
884 stdscr.bkgd(' ', curses.color_pair(1))
886 if self.mode.has_input_prompt:
888 if self.mode.shows_info:
893 if not self.mode.is_intro:
899 action_descriptions = {
901 'flatten': 'flatten surroundings',
902 'teleport': 'teleport',
903 'take_thing': 'pick up thing',
904 'drop_thing': 'drop thing',
905 'toggle_map_mode': 'toggle map view',
906 'toggle_tile_draw': 'toggle protection character drawing',
907 'door': 'open/close',
908 'consume': 'consume',
912 'flatten': 'FLATTEN_SURROUNDINGS',
913 'take_thing': 'PICK_UP',
914 'drop_thing': 'DROP',
917 'command': 'COMMAND',
918 'consume': 'INTOXICATE',
921 curses.curs_set(False) # hide cursor
923 self.set_default_colors()
924 curses.init_pair(1, 1, 2)
927 self.explorer = YX(0, 0)
930 interval = datetime.timedelta(seconds=5)
931 last_ping = datetime.datetime.now() - interval
933 if self.disconnected and self.force_instant_connect:
934 self.force_instant_connect = False
936 now = datetime.datetime.now()
937 if now - last_ping > interval:
938 if self.disconnected:
948 self.do_refresh = False
951 msg = self.queue.get(block=False)
956 key = stdscr.getkey()
957 self.do_refresh = True
960 self.show_help = False
961 if key == 'KEY_RESIZE':
963 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
964 self.input_ = self.input_[:-1]
965 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
966 self.show_help = True
968 self.restore_input_values()
969 elif self.mode.has_input_prompt and key != '\n': # Return key
971 max_length = self.window_width * self.size.y - len(input_prompt) - 1
972 if len(self.input_) > max_length:
973 self.input_ = self.input_[:max_length]
974 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
975 self.show_help = True
976 elif self.mode.name == 'login' and key == '\n':
977 self.login_name = self.input_
978 self.send('LOGIN ' + quote(self.input_))
980 elif self.mode.name == 'command_thing' and key == '\n':
981 if self.input_ == '':
982 self.log_msg('@ aborted')
983 self.switch_mode('play')
984 elif task_action_on('command'):
985 self.send('TASK:COMMAND ' + quote(self.input_))
987 elif self.mode.name == 'control_pw_pw' and key == '\n':
988 if self.input_ == '':
989 self.log_msg('@ aborted')
991 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
992 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
993 self.switch_mode('admin')
994 elif self.mode.name == 'password' and key == '\n':
995 if self.input_ == '':
997 self.password = self.input_
998 self.switch_mode('edit')
999 elif self.mode.name == 'admin_enter' and key == '\n':
1000 self.send('BECOME_ADMIN ' + quote(self.input_))
1001 self.switch_mode('play')
1002 elif self.mode.name == 'control_pw_type' and key == '\n':
1003 if len(self.input_) != 1:
1004 self.log_msg('@ entered non-single-char, therefore aborted')
1005 self.switch_mode('admin')
1007 self.tile_control_char = self.input_
1008 self.switch_mode('control_pw_pw')
1009 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1010 if len(self.input_) != 1:
1011 self.log_msg('@ entered non-single-char, therefore aborted')
1013 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1014 quote(self.input_)))
1015 self.log_msg('@ sent new protection character for thing')
1016 self.switch_mode('admin')
1017 elif self.mode.name == 'control_tile_type' and key == '\n':
1018 if len(self.input_) != 1:
1019 self.log_msg('@ entered non-single-char, therefore aborted')
1020 self.switch_mode('admin')
1022 self.tile_control_char = self.input_
1023 self.switch_mode('control_tile_draw')
1024 elif self.mode.name == 'chat' and key == '\n':
1025 if self.input_ == '':
1027 if self.input_[0] == '/': # FIXME fails on empty input
1028 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1029 self.switch_mode('play')
1030 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1031 self.switch_mode('study')
1032 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1033 self.switch_mode('edit')
1034 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1035 self.switch_mode('admin_enter')
1036 elif self.input_.startswith('/nick'):
1037 tokens = self.input_.split(maxsplit=1)
1038 if len(tokens) == 2:
1039 self.send('NICK ' + quote(tokens[1]))
1041 self.log_msg('? need login name')
1043 self.log_msg('? unknown command')
1045 self.send('ALL ' + quote(self.input_))
1047 elif self.mode.name == 'name_thing' and key == '\n':
1048 if self.input_ == '':
1050 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1052 quote(self.password)))
1053 self.switch_mode('edit')
1054 elif self.mode.name == 'annotate' and key == '\n':
1055 if self.input_ == '':
1057 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1058 quote(self.password)))
1059 self.switch_mode('edit')
1060 elif self.mode.name == 'portal' and key == '\n':
1061 if self.input_ == '':
1063 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1064 quote(self.password)))
1065 self.switch_mode('edit')
1066 elif self.mode.name == 'study':
1067 if self.mode.mode_switch_on_key(self, key):
1069 elif key == self.keys['toggle_map_mode']:
1070 self.toggle_map_mode()
1071 elif key in self.movement_keys:
1072 move_explorer(self.movement_keys[key])
1073 elif self.mode.name == 'play':
1074 if self.mode.mode_switch_on_key(self, key):
1076 elif key == self.keys['take_thing'] and task_action_on('take_thing'):
1077 self.send('TASK:PICK_UP')
1078 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1079 self.send('TASK:DROP')
1080 elif key == self.keys['door'] and task_action_on('door'):
1081 self.send('TASK:DOOR')
1082 elif key == self.keys['consume'] and task_action_on('consume'):
1083 self.send('TASK:INTOXICATE')
1084 elif key == self.keys['teleport']:
1085 player = self.game.get_thing(self.game.player_id)
1086 if player.position in self.game.portals:
1087 self.host = self.game.portals[player.position]
1091 self.log_msg('? not standing on portal')
1092 elif key in self.movement_keys and task_action_on('move'):
1093 self.send('TASK:MOVE ' + self.movement_keys[key])
1094 elif self.mode.name == 'write':
1095 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1096 self.switch_mode('edit')
1097 elif self.mode.name == 'control_tile_draw':
1098 if self.mode.mode_switch_on_key(self, key):
1100 elif key in self.movement_keys:
1101 move_explorer(self.movement_keys[key])
1102 elif key == self.keys['toggle_tile_draw']:
1103 self.tile_draw = False if self.tile_draw else True
1104 elif self.mode.name == 'admin':
1105 if self.mode.mode_switch_on_key(self, key):
1107 elif key in self.movement_keys and task_action_on('move'):
1108 self.send('TASK:MOVE ' + self.movement_keys[key])
1109 elif self.mode.name == 'edit':
1110 if self.mode.mode_switch_on_key(self, key):
1112 elif key == self.keys['flatten'] and task_action_on('flatten'):
1113 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1114 elif key == self.keys['toggle_map_mode']:
1115 self.toggle_map_mode()
1116 elif key in self.movement_keys and task_action_on('move'):
1117 self.send('TASK:MOVE ' + self.movement_keys[key])
1119 if len(sys.argv) != 2:
1120 raise ArgError('wrong number of arguments, need game host')