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):
137 game.annotations = {}
141 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 game.turn_complete = True
228 game.tui.do_refresh = True
229 game.tui.info_cached = None
230 cmd_GAME_STATE_COMPLETE.argtypes = ''
232 def cmd_PORTAL(game, position, msg):
233 game.portals[position] = msg
234 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
236 def cmd_PLAY_ERROR(game, msg):
237 game.tui.log_msg('? ' + msg)
238 game.tui.flash = True
239 game.tui.do_refresh = True
240 cmd_PLAY_ERROR.argtypes = 'string'
242 def cmd_GAME_ERROR(game, msg):
243 game.tui.log_msg('? game error: ' + msg)
244 game.tui.do_refresh = True
245 cmd_GAME_ERROR.argtypes = 'string'
247 def cmd_ARGUMENT_ERROR(game, msg):
248 game.tui.log_msg('? syntax error: ' + msg)
249 game.tui.do_refresh = True
250 cmd_ARGUMENT_ERROR.argtypes = 'string'
252 def cmd_ANNOTATION(game, position, msg):
253 game.annotations[position] = msg
254 if game.tui.mode.shows_info:
255 game.tui.do_refresh = True
256 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
258 def cmd_TASKS(game, tasks_comma_separated):
259 game.tasks = tasks_comma_separated.split(',')
260 game.tui.mode_write.legal = 'WRITE' in game.tasks
261 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
262 cmd_TASKS.argtypes = 'string'
264 def cmd_THING_TYPE(game, thing_type, symbol_hint):
265 game.thing_types[thing_type] = symbol_hint
266 cmd_THING_TYPE.argtypes = 'string char'
268 def cmd_TERRAIN(game, terrain_char, terrain_desc):
269 game.terrains[terrain_char] = terrain_desc
270 cmd_TERRAIN.argtypes = 'char string'
274 cmd_PONG.argtypes = ''
276 def cmd_DEFAULT_COLORS(game):
277 game.tui.set_default_colors()
278 cmd_DEFAULT_COLORS.argtypes = ''
280 def cmd_RANDOM_COLORS(game):
281 game.tui.set_random_colors()
282 cmd_RANDOM_COLORS.argtypes = ''
284 class Game(GameBase):
285 turn_complete = False
289 def __init__(self, *args, **kwargs):
290 super().__init__(*args, **kwargs)
291 self.register_command(cmd_LOGIN_OK)
292 self.register_command(cmd_ADMIN_OK)
293 self.register_command(cmd_PONG)
294 self.register_command(cmd_CHAT)
295 self.register_command(cmd_REPLY)
296 self.register_command(cmd_PLAYER_ID)
297 self.register_command(cmd_TURN)
298 self.register_command(cmd_THING)
299 self.register_command(cmd_THING_TYPE)
300 self.register_command(cmd_THING_NAME)
301 self.register_command(cmd_THING_CHAR)
302 self.register_command(cmd_TERRAIN)
303 self.register_command(cmd_MAP)
304 self.register_command(cmd_MAP_CONTROL)
305 self.register_command(cmd_PORTAL)
306 self.register_command(cmd_ANNOTATION)
307 self.register_command(cmd_GAME_STATE_COMPLETE)
308 self.register_command(cmd_ARGUMENT_ERROR)
309 self.register_command(cmd_GAME_ERROR)
310 self.register_command(cmd_PLAY_ERROR)
311 self.register_command(cmd_TASKS)
312 self.register_command(cmd_FOV)
313 self.register_command(cmd_DEFAULT_COLORS)
314 self.register_command(cmd_RANDOM_COLORS)
315 self.map_content = ''
317 self.annotations = {}
321 def get_string_options(self, string_option_type):
322 if string_option_type == 'map_geometry':
323 return ['Hex', 'Square']
324 elif string_option_type == 'thing_type':
325 return self.thing_types.keys()
328 def get_command(self, command_name):
329 from functools import partial
330 f = partial(self.commands[command_name], self)
331 f.argtypes = self.commands[command_name].argtypes
336 def __init__(self, name, has_input_prompt=False, shows_info=False,
337 is_intro=False, is_single_char_entry=False):
339 self.short_desc = mode_helps[name]['short']
340 self.available_modes = []
341 self.available_actions = []
342 self.has_input_prompt = has_input_prompt
343 self.shows_info = shows_info
344 self.is_intro = is_intro
345 self.help_intro = mode_helps[name]['long']
346 self.is_single_char_entry = is_single_char_entry
349 def iter_available_modes(self, tui):
350 for mode_name in self.available_modes:
351 mode = getattr(tui, 'mode_' + mode_name)
354 key = tui.keys['switch_to_' + mode.name]
357 def list_available_modes(self, tui):
359 if len(self.available_modes) > 0:
360 msg = 'Other modes available from here:\n'
361 for mode, key in self.iter_available_modes(tui):
362 msg += '[%s] – %s\n' % (key, mode.short_desc)
365 def mode_switch_on_key(self, tui, key_pressed):
366 for mode, key in self.iter_available_modes(tui):
367 if key_pressed == key:
368 tui.switch_mode(mode.name)
373 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
374 mode_admin = Mode('admin')
375 mode_play = Mode('play')
376 mode_study = Mode('study', shows_info=True)
377 mode_write = Mode('write', is_single_char_entry=True)
378 mode_edit = Mode('edit')
379 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
380 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
381 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
382 mode_control_tile_draw = Mode('control_tile_draw')
383 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
384 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
385 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
386 mode_chat = Mode('chat', has_input_prompt=True)
387 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
388 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
389 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
390 mode_password = Mode('password', has_input_prompt=True)
391 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
392 mode_command_thing = Mode('command_thing', has_input_prompt=True)
396 def __init__(self, host):
399 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
401 self.mode_play.available_actions = ["move", "take_thing", "drop_thing",
402 "teleport", "door", "consume"]
403 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
404 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
405 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
406 "control_tile_type", "chat",
407 "study", "play", "edit"]
408 self.mode_admin.available_actions = ["move"]
409 self.mode_control_tile_draw.available_modes = ["admin_enter"]
410 self.mode_control_tile_draw.available_actions = ["move_explorer",
412 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
413 "password", "chat", "study", "play",
415 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
420 self.parser = Parser(self.game)
422 self.do_refresh = True
423 self.queue = queue.Queue()
424 self.login_name = None
425 self.map_mode = 'terrain + things'
426 self.password = 'foo'
427 self.switch_mode('waiting_for_server')
429 'switch_to_chat': 't',
430 'switch_to_play': 'p',
431 'switch_to_password': 'P',
432 'switch_to_annotate': 'M',
433 'switch_to_portal': 'T',
434 'switch_to_study': '?',
435 'switch_to_edit': 'E',
436 'switch_to_write': 'm',
437 'switch_to_name_thing': 'N',
438 'switch_to_command_thing': 'O',
439 'switch_to_admin_enter': 'A',
440 'switch_to_control_pw_type': 'C',
441 'switch_to_control_tile_type': 'Q',
442 'switch_to_admin_thing_protect': 'T',
450 'toggle_map_mode': 'L',
451 'toggle_tile_draw': 'm',
452 'hex_move_upleft': 'w',
453 'hex_move_upright': 'e',
454 'hex_move_right': 'd',
455 'hex_move_downright': 'x',
456 'hex_move_downleft': 'y',
457 'hex_move_left': 'a',
458 'square_move_up': 'w',
459 'square_move_left': 'a',
460 'square_move_down': 's',
461 'square_move_right': 'd',
463 if os.path.isfile('config.json'):
464 with open('config.json', 'r') as f:
465 keys_conf = json.loads(f.read())
467 self.keys[k] = keys_conf[k]
468 self.show_help = False
469 self.disconnected = True
470 self.force_instant_connect = True
471 self.input_lines = []
475 self.offset = YX(0,0)
476 curses.wrapper(self.loop)
480 def handle_recv(msg):
486 self.log_msg('@ attempting connect')
487 socket_client_class = PlomSocketClient
488 if self.host.startswith('ws://') or self.host.startswith('wss://'):
489 socket_client_class = WebSocketClient
491 self.socket = socket_client_class(handle_recv, self.host)
492 self.socket_thread = threading.Thread(target=self.socket.run)
493 self.socket_thread.start()
494 self.disconnected = False
495 self.game.thing_types = {}
496 self.game.terrains = {}
497 time.sleep(0.1) # give potential SSL negotation some time …
498 self.socket.send('TASKS')
499 self.socket.send('TERRAINS')
500 self.socket.send('THING_TYPES')
501 self.switch_mode('login')
502 except ConnectionRefusedError:
503 self.log_msg('@ server connect failure')
504 self.disconnected = True
505 self.switch_mode('waiting_for_server')
506 self.do_refresh = True
509 self.log_msg('@ attempting reconnect')
511 # necessitated by some strange SSL race conditions with ws4py
512 time.sleep(0.1) # FIXME find out why exactly necessary
513 self.switch_mode('waiting_for_server')
518 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
519 raise BrokenSocketConnection
520 self.socket.send(msg)
521 except (BrokenPipeError, BrokenSocketConnection):
522 self.log_msg('@ server disconnected :(')
523 self.disconnected = True
524 self.force_instant_connect = True
525 self.do_refresh = True
527 def log_msg(self, msg):
529 if len(self.log) > 100:
530 self.log = self.log[-100:]
532 def restore_input_values(self):
533 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
534 self.input_ = self.game.annotations[self.explorer]
535 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
536 self.input_ = self.game.portals[self.explorer]
537 elif self.mode.name == 'password':
538 self.input_ = self.password
539 elif self.mode.name == 'name_thing':
540 if hasattr(self.thing_selected, 'name'):
541 self.input_ = self.thing_selected.name
542 elif self.mode.name == 'admin_thing_protect':
543 if hasattr(self.thing_selected, 'protection'):
544 self.input_ = self.thing_selected.protection
546 def send_tile_control_command(self):
547 self.send('SET_TILE_CONTROL %s %s' %
548 (self.explorer, quote(self.tile_control_char)))
550 def toggle_map_mode(self):
551 if self.map_mode == 'terrain only':
552 self.map_mode = 'terrain + annotations'
553 elif self.map_mode == 'terrain + annotations':
554 self.map_mode = 'terrain + things'
555 elif self.map_mode == 'terrain + things':
556 self.map_mode = 'protections'
557 elif self.map_mode == 'protections':
558 self.map_mode = 'terrain only'
560 def switch_mode(self, mode_name):
561 self.tile_draw = False
562 if mode_name == 'admin_enter' and self.is_admin:
564 elif mode_name in {'name_thing', 'admin_thing_protect'}:
565 player = self.game.get_thing(self.game.player_id)
567 for t in [t for t in self.game.things if t.position == player.position
568 and t.id_ != player.id_]:
573 self.log_msg('? not standing over thing')
576 self.thing_selected = thing
577 self.mode = getattr(self, 'mode_' + mode_name)
578 if self.mode.name == 'control_tile_draw':
579 self.log_msg('@ finished tile protection drawing.')
580 if self.mode.name in {'control_tile_draw', 'control_tile_type',
582 self.map_mode = 'protections'
583 elif self.mode.name != 'edit':
584 self.map_mode = 'terrain + things'
585 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
586 player = self.game.get_thing(self.game.player_id)
587 self.explorer = YX(player.position.y, player.position.x)
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 == 'command_thing':
598 self.send('TASK:COMMAND ' + quote('HELP'))
599 elif self.mode.name == 'admin_enter':
600 self.log_msg('@ enter admin password:')
601 elif self.mode.name == 'control_pw_type':
602 self.log_msg('@ enter protection character for which you want to change the password:')
603 elif self.mode.name == 'control_tile_type':
604 self.log_msg('@ enter protection character which you want to draw:')
605 elif self.mode.name == 'admin_thing_protect':
606 self.log_msg('@ enter thing protection character:')
607 elif self.mode.name == 'control_pw_pw':
608 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
609 elif self.mode.name == 'control_tile_draw':
610 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']))
612 self.restore_input_values()
614 def set_default_colors(self):
615 curses.init_color(1, 1000, 1000, 1000)
616 curses.init_color(2, 0, 0, 0)
617 self.do_refresh = True
619 def set_random_colors(self):
623 return int(offset + random.random()*375)
625 curses.init_color(1, rand(625), rand(625), rand(625))
626 curses.init_color(2, rand(0), rand(0), rand(0))
627 self.do_refresh = True
631 return self.info_cached
632 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
634 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
635 info_to_cache += 'outside field of view'
637 terrain_char = self.game.map_content[pos_i]
639 if terrain_char in self.game.terrains:
640 terrain_desc = self.game.terrains[terrain_char]
641 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
643 protection = self.game.map_control_content[pos_i]
644 if protection == '.':
645 protection = 'unprotected'
646 info_to_cache += 'PROTECTION: %s\n' % protection
647 for t in self.game.things:
648 if t.position == self.explorer:
649 protection = t.protection
650 if protection == '.':
652 info_to_cache += 'THING: %s / %s' %\
653 (t.type_, self.game.thing_types[t.type_])
654 if hasattr(t, 'thing_char'):
655 info_to_cache += t.thing_char
656 if hasattr(t, 'name'):
657 info_to_cache += ' (%s)' % t.name
658 info_to_cache += ' / protection: %s\n' % protection
659 if self.explorer in self.game.portals:
660 info_to_cache += 'PORTAL: ' +\
661 self.game.portals[self.explorer] + '\n'
663 info_to_cache += 'PORTAL: (none)\n'
664 if self.explorer in self.game.annotations:
665 info_to_cache += 'ANNOTATION: ' +\
666 self.game.annotations[self.explorer]
667 self.info_cached = info_to_cache
668 return self.info_cached
670 def loop(self, stdscr):
673 def safe_addstr(y, x, line):
674 if y < self.size.y - 1 or x + len(line) < self.size.x:
675 stdscr.addstr(y, x, line, curses.color_pair(1))
676 else: # workaround to <https://stackoverflow.com/q/7063128>
677 cut_i = self.size.x - x - 1
679 last_char = line[cut_i]
680 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
681 stdscr.insstr(y, self.size.x - 2, ' ')
682 stdscr.addstr(y, x, cut, curses.color_pair(1))
684 def handle_input(msg):
685 command, args = self.parser.parse(msg)
688 def task_action_on(action):
689 return action_tasks[action] in self.game.tasks
691 def msg_into_lines_of_width(msg, width):
695 for i in range(len(msg)):
696 if x >= width or msg[i] == "\n":
708 def reset_screen_size():
709 self.size = YX(*stdscr.getmaxyx())
710 self.size = self.size - YX(self.size.y % 4, 0)
711 self.size = self.size - YX(0, self.size.x % 4)
712 self.window_width = int(self.size.x / 2)
714 def recalc_input_lines():
715 if not self.mode.has_input_prompt:
716 self.input_lines = []
718 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
721 def move_explorer(direction):
722 target = self.game.map_geometry.move_yx(self.explorer, direction)
724 self.info_cached = None
725 self.explorer = target
727 self.send_tile_control_command()
733 for line in self.log:
734 lines += msg_into_lines_of_width(line, self.window_width)
737 max_y = self.size.y - len(self.input_lines)
738 for i in range(len(lines)):
739 if (i >= max_y - height_header):
741 safe_addstr(max_y - i - 1, self.window_width, lines[i])
744 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
745 lines = msg_into_lines_of_width(info, self.window_width)
747 for i in range(len(lines)):
748 y = height_header + i
749 if y >= self.size.y - len(self.input_lines):
751 safe_addstr(y, self.window_width, lines[i])
754 y = self.size.y - len(self.input_lines)
755 for i in range(len(self.input_lines)):
756 safe_addstr(y, self.window_width, self.input_lines[i])
760 if not self.game.turn_complete:
762 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
765 help = "hit [%s] for help" % self.keys['help']
766 if self.mode.has_input_prompt:
767 help = "enter /help for help"
768 safe_addstr(1, self.window_width,
769 'MODE: %s – %s' % (self.mode.short_desc, help))
772 if not self.game.turn_complete and len(self.map_lines) == 0:
774 if self.game.turn_complete:
776 for y in range(self.game.map_geometry.size.y):
777 start = self.game.map_geometry.size.x * y
778 end = start + self.game.map_geometry.size.x
779 if self.map_mode == 'protections':
780 map_lines_split += [[c + ' ' for c
781 in self.game.map_control_content[start:end]]]
783 map_lines_split += [[c + ' ' for c
784 in self.game.map_content[start:end]]]
785 if self.map_mode == 'terrain + annotations':
786 for p in self.game.annotations:
787 map_lines_split[p.y][p.x] = 'A '
788 elif self.map_mode == 'terrain + things':
789 for p in self.game.portals.keys():
790 original = map_lines_split[p.y][p.x]
791 map_lines_split[p.y][p.x] = original[0] + 'P'
793 for t in self.game.things:
794 symbol = self.game.thing_types[t.type_]
796 if hasattr(t, 'thing_char'):
797 meta_char = t.thing_char
798 if t.position in used_positions:
800 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
801 used_positions += [t.position]
802 player = self.game.get_thing(self.game.player_id)
803 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
804 map_lines_split[self.explorer.y][self.explorer.x] = '??'
805 elif self.map_mode != 'terrain + things':
806 map_lines_split[player.position.y][player.position.x] = '??'
808 if type(self.game.map_geometry) == MapGeometryHex:
810 for line in map_lines_split:
811 self.map_lines += [indent * ' ' + ''.join(line)]
812 indent = 0 if indent else 1
814 for line in map_lines_split:
815 self.map_lines += [''.join(line)]
816 window_center = YX(int(self.size.y / 2),
817 int(self.window_width / 2))
818 center = player.position
819 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
820 center = self.explorer
821 center = YX(center.y, center.x * 2)
822 self.offset = center - window_center
823 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
824 self.offset += YX(0, 1)
825 term_y = max(0, -self.offset.y)
826 term_x = max(0, -self.offset.x)
827 map_y = max(0, self.offset.y)
828 map_x = max(0, self.offset.x)
829 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
830 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
831 safe_addstr(term_y, term_x, to_draw)
836 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
837 self.mode.help_intro)
838 if len(self.mode.available_actions) > 0:
839 content += "Available actions:\n"
840 for action in self.mode.available_actions:
841 if action in action_tasks:
842 if action_tasks[action] not in self.game.tasks:
844 if action == 'move_explorer':
847 key = ','.join(self.movement_keys)
849 key = self.keys[action]
850 content += '[%s] – %s\n' % (key, action_descriptions[action])
852 if self.mode.name == 'chat':
853 content += '/nick NAME – re-name yourself to NAME\n'
854 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
855 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
856 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
857 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
858 content += self.mode.list_available_modes(self)
859 for i in range(self.size.y):
861 self.window_width * (not self.mode.has_input_prompt),
862 ' ' * self.window_width)
864 for line in content.split('\n'):
865 lines += msg_into_lines_of_width(line, self.window_width)
866 for i in range(len(lines)):
870 self.window_width * (not self.mode.has_input_prompt),
875 stdscr.bkgd(' ', curses.color_pair(1))
877 if self.mode.has_input_prompt:
879 if self.mode.shows_info:
884 if not self.mode.is_intro:
890 action_descriptions = {
892 'flatten': 'flatten surroundings',
893 'teleport': 'teleport',
894 'take_thing': 'pick up thing',
895 'drop_thing': 'drop thing',
896 'toggle_map_mode': 'toggle map view',
897 'toggle_tile_draw': 'toggle protection character drawing',
898 'door': 'open/close',
899 'consume': 'consume',
903 'flatten': 'FLATTEN_SURROUNDINGS',
904 'take_thing': 'PICK_UP',
905 'drop_thing': 'DROP',
908 'command': 'COMMAND',
909 'consume': 'INTOXICATE',
912 curses.curs_set(False) # hide cursor
914 self.set_default_colors()
915 curses.init_pair(1, 1, 2)
918 self.explorer = YX(0, 0)
921 interval = datetime.timedelta(seconds=5)
922 last_ping = datetime.datetime.now() - interval
924 if self.disconnected and self.force_instant_connect:
925 self.force_instant_connect = False
927 now = datetime.datetime.now()
928 if now - last_ping > interval:
929 if self.disconnected:
939 self.do_refresh = False
942 msg = self.queue.get(block=False)
947 key = stdscr.getkey()
948 self.do_refresh = True
951 self.show_help = False
952 if key == 'KEY_RESIZE':
954 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
955 self.input_ = self.input_[:-1]
956 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
957 self.show_help = True
959 self.restore_input_values()
960 elif self.mode.has_input_prompt and key != '\n': # Return key
962 max_length = self.window_width * self.size.y - len(input_prompt) - 1
963 if len(self.input_) > max_length:
964 self.input_ = self.input_[:max_length]
965 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
966 self.show_help = True
967 elif self.mode.name == 'login' and key == '\n':
968 self.login_name = self.input_
969 self.send('LOGIN ' + quote(self.input_))
971 elif self.mode.name == 'command_thing' and key == '\n':
972 if self.input_ == '':
973 self.log_msg('@ aborted')
974 self.switch_mode('play')
975 elif task_action_on('command'):
976 self.send('TASK:COMMAND ' + quote(self.input_))
978 elif self.mode.name == 'control_pw_pw' and key == '\n':
979 if self.input_ == '':
980 self.log_msg('@ aborted')
982 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
983 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
984 self.switch_mode('admin')
985 elif self.mode.name == 'password' and key == '\n':
986 if self.input_ == '':
988 self.password = self.input_
989 self.switch_mode('edit')
990 elif self.mode.name == 'admin_enter' and key == '\n':
991 self.send('BECOME_ADMIN ' + quote(self.input_))
992 self.switch_mode('play')
993 elif self.mode.name == 'control_pw_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_pw_pw')
1000 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1001 if len(self.input_) != 1:
1002 self.log_msg('@ entered non-single-char, therefore aborted')
1004 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1005 quote(self.input_)))
1006 self.log_msg('@ sent new protection character for thing')
1007 self.switch_mode('admin')
1008 elif self.mode.name == 'control_tile_type' and key == '\n':
1009 if len(self.input_) != 1:
1010 self.log_msg('@ entered non-single-char, therefore aborted')
1011 self.switch_mode('admin')
1013 self.tile_control_char = self.input_
1014 self.switch_mode('control_tile_draw')
1015 elif self.mode.name == 'chat' and key == '\n':
1016 if self.input_ == '':
1018 if self.input_[0] == '/': # FIXME fails on empty input
1019 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1020 self.switch_mode('play')
1021 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1022 self.switch_mode('study')
1023 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1024 self.switch_mode('edit')
1025 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1026 self.switch_mode('admin_enter')
1027 elif self.input_.startswith('/nick'):
1028 tokens = self.input_.split(maxsplit=1)
1029 if len(tokens) == 2:
1030 self.send('NICK ' + quote(tokens[1]))
1032 self.log_msg('? need login name')
1034 self.log_msg('? unknown command')
1036 self.send('ALL ' + quote(self.input_))
1038 elif self.mode.name == 'name_thing' and key == '\n':
1039 if self.input_ == '':
1041 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1043 quote(self.password)))
1044 self.switch_mode('edit')
1045 elif self.mode.name == 'annotate' and key == '\n':
1046 if self.input_ == '':
1048 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1049 quote(self.password)))
1050 self.switch_mode('edit')
1051 elif self.mode.name == 'portal' and key == '\n':
1052 if self.input_ == '':
1054 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1055 quote(self.password)))
1056 self.switch_mode('edit')
1057 elif self.mode.name == 'study':
1058 if self.mode.mode_switch_on_key(self, key):
1060 elif key == self.keys['toggle_map_mode']:
1061 self.toggle_map_mode()
1062 elif key in self.movement_keys:
1063 move_explorer(self.movement_keys[key])
1064 elif self.mode.name == 'play':
1065 if self.mode.mode_switch_on_key(self, key):
1067 elif key == self.keys['take_thing'] and task_action_on('take_thing'):
1068 self.send('TASK:PICK_UP')
1069 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1070 self.send('TASK:DROP')
1071 elif key == self.keys['door'] and task_action_on('door'):
1072 self.send('TASK:DOOR')
1073 elif key == self.keys['consume'] and task_action_on('consume'):
1074 self.send('TASK:INTOXICATE')
1075 elif key == self.keys['teleport']:
1076 player = self.game.get_thing(self.game.player_id)
1077 if player.position in self.game.portals:
1078 self.host = self.game.portals[player.position]
1082 self.log_msg('? not standing on portal')
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 == 'write':
1086 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1087 self.switch_mode('edit')
1088 elif self.mode.name == 'control_tile_draw':
1089 if self.mode.mode_switch_on_key(self, key):
1091 elif key in self.movement_keys:
1092 move_explorer(self.movement_keys[key])
1093 elif key == self.keys['toggle_tile_draw']:
1094 self.tile_draw = False if self.tile_draw else True
1095 elif self.mode.name == 'admin':
1096 if self.mode.mode_switch_on_key(self, key):
1098 elif key in self.movement_keys and task_action_on('move'):
1099 self.send('TASK:MOVE ' + self.movement_keys[key])
1100 elif self.mode.name == 'edit':
1101 if self.mode.mode_switch_on_key(self, key):
1103 elif key == self.keys['flatten'] and task_action_on('flatten'):
1104 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1105 elif key == self.keys['toggle_map_mode']:
1106 self.toggle_map_mode()
1107 elif key in self.movement_keys and task_action_on('move'):
1108 self.send('TASK:MOVE ' + self.movement_keys[key])
1110 if len(sys.argv) != 2:
1111 raise ArgError('wrong number of arguments, need game host')