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
18 'long': 'This mode allows you to interact with the map in various ways.'
23 '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.'},
25 'short': 'world edit',
27 '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.'
30 'short': 'name thing',
32 'long': 'Give name to/change name of thing here.'
35 'short': 'command thing',
37 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
40 'short': 'take thing',
42 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
44 'admin_thing_protect': {
45 'short': 'change thing protection',
46 'intro': '@ enter thing protection character:',
47 'long': 'Change protection character for thing here.'
50 'short': 'change terrain',
52 '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.'
55 'short': 'change protection character password',
56 'intro': '@ enter protection character for which you want to change the password:',
57 '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.'
60 'short': 'change protection character password',
62 '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.'
64 'control_tile_type': {
65 'short': 'change tiles protection',
66 'intro': '@ enter protection character which you want to draw:',
67 '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.'
69 'control_tile_draw': {
70 'short': 'change tiles protection',
72 '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.'
75 'short': 'annotate tile',
77 '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.'
80 'short': 'edit portal',
82 '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.'
87 '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:'
92 'long': 'Enter your player name.'
94 'waiting_for_server': {
95 'short': 'waiting for server response',
96 'intro': '@ waiting for server …',
97 'long': 'Waiting for a server response.'
100 'short': 'waiting for server response',
102 'long': 'Waiting for a server response.'
105 'short': 'set world edit password',
107 '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.'
110 'short': 'become admin',
111 'intro': '@ enter admin password:',
112 'long': 'This mode allows you to become admin if you know an admin password.'
117 'long': 'This mode allows you access to actions limited to administrators.'
121 from ws4py.client import WebSocketBaseClient
122 class WebSocketClient(WebSocketBaseClient):
124 def __init__(self, recv_handler, *args, **kwargs):
125 super().__init__(*args, **kwargs)
126 self.recv_handler = recv_handler
129 def received_message(self, message):
131 message = str(message)
132 self.recv_handler(message)
135 def plom_closed(self):
136 return self.client_terminated
138 from plomrogue.io_tcp import PlomSocket
139 class PlomSocketClient(PlomSocket):
141 def __init__(self, recv_handler, url):
143 self.recv_handler = recv_handler
144 host, port = url.split(':')
145 super().__init__(socket.create_connection((host, port)))
153 for msg in self.recv():
154 if msg == 'NEED_SSL':
155 self.socket = ssl.wrap_socket(self.socket)
157 self.recv_handler(msg)
158 except BrokenSocketConnection:
159 pass # we assume socket will be known as dead by now
161 def cmd_TURN(game, n):
162 game.annotations = {}
166 game.turn_complete = False
168 cmd_TURN.argtypes = 'int:nonneg'
170 def cmd_LOGIN_OK(game):
171 game.tui.switch_mode('post_login_wait')
172 game.tui.send('GET_GAMESTATE')
173 game.tui.log_msg('@ welcome')
174 cmd_LOGIN_OK.argtypes = ''
176 def cmd_ADMIN_OK(game):
177 game.tui.is_admin = True
178 game.tui.log_msg('@ you now have admin rights')
179 game.tui.switch_mode('admin')
180 game.tui.do_refresh = True
181 cmd_ADMIN_OK.argtypes = ''
183 def cmd_REPLY(game, msg):
184 game.tui.log_msg('#MUSICPLAYER: ' + msg)
185 game.tui.do_refresh = True
186 cmd_REPLY.argtypes = 'string'
188 def cmd_CHAT(game, msg):
189 game.tui.log_msg('# ' + msg)
190 game.tui.do_refresh = True
191 cmd_CHAT.argtypes = 'string'
193 def cmd_PLAYER_ID(game, player_id):
194 game.player_id = player_id
195 cmd_PLAYER_ID.argtypes = 'int:nonneg'
197 def cmd_THING(game, yx, thing_type, protection, thing_id):
198 t = game.get_thing(thing_id)
200 t = ThingBase(game, thing_id)
204 t.protection = protection
205 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg'
207 def cmd_THING_NAME(game, thing_id, name):
208 t = game.get_thing(thing_id)
211 cmd_THING_NAME.argtypes = 'int:nonneg string'
213 def cmd_THING_CHAR(game, thing_id, c):
214 t = game.get_thing(thing_id)
217 cmd_THING_CHAR.argtypes = 'int:nonneg char'
219 def cmd_MAP(game, geometry, size, content):
220 map_geometry_class = globals()['MapGeometry' + geometry]
221 game.map_geometry = map_geometry_class(size)
222 game.map_content = content
223 if type(game.map_geometry) == MapGeometrySquare:
224 game.tui.movement_keys = {
225 game.tui.keys['square_move_up']: 'UP',
226 game.tui.keys['square_move_left']: 'LEFT',
227 game.tui.keys['square_move_down']: 'DOWN',
228 game.tui.keys['square_move_right']: 'RIGHT',
230 elif type(game.map_geometry) == MapGeometryHex:
231 game.tui.movement_keys = {
232 game.tui.keys['hex_move_upleft']: 'UPLEFT',
233 game.tui.keys['hex_move_upright']: 'UPRIGHT',
234 game.tui.keys['hex_move_right']: 'RIGHT',
235 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
236 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
237 game.tui.keys['hex_move_left']: 'LEFT',
239 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
241 def cmd_FOV(game, content):
243 cmd_FOV.argtypes = 'string'
245 def cmd_MAP_CONTROL(game, content):
246 game.map_control_content = content
247 cmd_MAP_CONTROL.argtypes = 'string'
249 def cmd_GAME_STATE_COMPLETE(game):
250 if game.tui.mode.name == 'post_login_wait':
251 game.tui.switch_mode('play')
252 game.turn_complete = True
253 game.tui.do_refresh = True
254 game.tui.info_cached = None
255 cmd_GAME_STATE_COMPLETE.argtypes = ''
257 def cmd_PORTAL(game, position, msg):
258 game.portals[position] = msg
259 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
261 def cmd_PLAY_ERROR(game, msg):
262 game.tui.log_msg('? ' + msg)
263 game.tui.flash = True
264 game.tui.do_refresh = True
265 cmd_PLAY_ERROR.argtypes = 'string'
267 def cmd_GAME_ERROR(game, msg):
268 game.tui.log_msg('? game error: ' + msg)
269 game.tui.do_refresh = True
270 cmd_GAME_ERROR.argtypes = 'string'
272 def cmd_ARGUMENT_ERROR(game, msg):
273 game.tui.log_msg('? syntax error: ' + msg)
274 game.tui.do_refresh = True
275 cmd_ARGUMENT_ERROR.argtypes = 'string'
277 def cmd_ANNOTATION(game, position, msg):
278 game.annotations[position] = msg
279 if game.tui.mode.shows_info:
280 game.tui.do_refresh = True
281 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
283 def cmd_TASKS(game, tasks_comma_separated):
284 game.tasks = tasks_comma_separated.split(',')
285 game.tui.mode_write.legal = 'WRITE' in game.tasks
286 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
287 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
288 cmd_TASKS.argtypes = 'string'
290 def cmd_THING_TYPE(game, thing_type, symbol_hint):
291 game.thing_types[thing_type] = symbol_hint
292 cmd_THING_TYPE.argtypes = 'string char'
294 def cmd_TERRAIN(game, terrain_char, terrain_desc):
295 game.terrains[terrain_char] = terrain_desc
296 cmd_TERRAIN.argtypes = 'char string'
300 cmd_PONG.argtypes = ''
302 def cmd_DEFAULT_COLORS(game):
303 game.tui.set_default_colors()
304 cmd_DEFAULT_COLORS.argtypes = ''
306 def cmd_RANDOM_COLORS(game):
307 game.tui.set_random_colors()
308 cmd_RANDOM_COLORS.argtypes = ''
310 class Game(GameBase):
311 turn_complete = False
315 def __init__(self, *args, **kwargs):
316 super().__init__(*args, **kwargs)
317 self.register_command(cmd_LOGIN_OK)
318 self.register_command(cmd_ADMIN_OK)
319 self.register_command(cmd_PONG)
320 self.register_command(cmd_CHAT)
321 self.register_command(cmd_REPLY)
322 self.register_command(cmd_PLAYER_ID)
323 self.register_command(cmd_TURN)
324 self.register_command(cmd_THING)
325 self.register_command(cmd_THING_TYPE)
326 self.register_command(cmd_THING_NAME)
327 self.register_command(cmd_THING_CHAR)
328 self.register_command(cmd_TERRAIN)
329 self.register_command(cmd_MAP)
330 self.register_command(cmd_MAP_CONTROL)
331 self.register_command(cmd_PORTAL)
332 self.register_command(cmd_ANNOTATION)
333 self.register_command(cmd_GAME_STATE_COMPLETE)
334 self.register_command(cmd_ARGUMENT_ERROR)
335 self.register_command(cmd_GAME_ERROR)
336 self.register_command(cmd_PLAY_ERROR)
337 self.register_command(cmd_TASKS)
338 self.register_command(cmd_FOV)
339 self.register_command(cmd_DEFAULT_COLORS)
340 self.register_command(cmd_RANDOM_COLORS)
341 self.map_content = ''
343 self.annotations = {}
347 def get_string_options(self, string_option_type):
348 if string_option_type == 'map_geometry':
349 return ['Hex', 'Square']
350 elif string_option_type == 'thing_type':
351 return self.thing_types.keys()
354 def get_command(self, command_name):
355 from functools import partial
356 f = partial(self.commands[command_name], self)
357 f.argtypes = self.commands[command_name].argtypes
362 def __init__(self, name, has_input_prompt=False, shows_info=False,
363 is_intro=False, is_single_char_entry=False):
365 self.short_desc = mode_helps[name]['short']
366 self.available_modes = []
367 self.available_actions = []
368 self.has_input_prompt = has_input_prompt
369 self.shows_info = shows_info
370 self.is_intro = is_intro
371 self.help_intro = mode_helps[name]['long']
372 self.intro_msg = mode_helps[name]['intro']
373 self.is_single_char_entry = is_single_char_entry
376 def iter_available_modes(self, tui):
377 for mode_name in self.available_modes:
378 mode = getattr(tui, 'mode_' + mode_name)
381 key = tui.keys['switch_to_' + mode.name]
384 def list_available_modes(self, tui):
386 if len(self.available_modes) > 0:
387 msg = 'Other modes available from here:\n'
388 for mode, key in self.iter_available_modes(tui):
389 msg += '[%s] – %s\n' % (key, mode.short_desc)
392 def mode_switch_on_key(self, tui, key_pressed):
393 for mode, key in self.iter_available_modes(tui):
394 if key_pressed == key:
395 tui.switch_mode(mode.name)
400 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
401 mode_admin = Mode('admin')
402 mode_play = Mode('play')
403 mode_study = Mode('study', shows_info=True)
404 mode_write = Mode('write', is_single_char_entry=True)
405 mode_edit = Mode('edit')
406 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
407 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
408 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
409 mode_control_tile_draw = Mode('control_tile_draw')
410 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
411 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
412 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
413 mode_chat = Mode('chat', has_input_prompt=True)
414 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
415 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
416 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
417 mode_password = Mode('password', has_input_prompt=True)
418 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
419 mode_command_thing = Mode('command_thing', has_input_prompt=True)
420 mode_take_thing = Mode('take_thing', has_input_prompt=True)
424 def __init__(self, host):
427 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
428 "command_thing", "take_thing"]
429 self.mode_play.available_actions = ["move", "drop_thing",
430 "teleport", "door", "consume"]
431 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
432 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
433 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
434 "control_tile_type", "chat",
435 "study", "play", "edit"]
436 self.mode_admin.available_actions = ["move"]
437 self.mode_control_tile_draw.available_modes = ["admin_enter"]
438 self.mode_control_tile_draw.available_actions = ["move_explorer",
440 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
441 "password", "chat", "study", "play",
443 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
448 self.parser = Parser(self.game)
450 self.do_refresh = True
451 self.queue = queue.Queue()
452 self.login_name = None
453 self.map_mode = 'terrain + things'
454 self.password = 'foo'
455 self.switch_mode('waiting_for_server')
457 'switch_to_chat': 't',
458 'switch_to_play': 'p',
459 'switch_to_password': 'P',
460 'switch_to_annotate': 'M',
461 'switch_to_portal': 'T',
462 'switch_to_study': '?',
463 'switch_to_edit': 'E',
464 'switch_to_write': 'm',
465 'switch_to_name_thing': 'N',
466 'switch_to_command_thing': 'O',
467 'switch_to_admin_enter': 'A',
468 'switch_to_control_pw_type': 'C',
469 'switch_to_control_tile_type': 'Q',
470 'switch_to_admin_thing_protect': 'T',
472 'switch_to_take_thing': 'z',
478 'toggle_map_mode': 'L',
479 'toggle_tile_draw': 'm',
480 'hex_move_upleft': 'w',
481 'hex_move_upright': 'e',
482 'hex_move_right': 'd',
483 'hex_move_downright': 'x',
484 'hex_move_downleft': 'y',
485 'hex_move_left': 'a',
486 'square_move_up': 'w',
487 'square_move_left': 'a',
488 'square_move_down': 's',
489 'square_move_right': 'd',
491 if os.path.isfile('config.json'):
492 with open('config.json', 'r') as f:
493 keys_conf = json.loads(f.read())
495 self.keys[k] = keys_conf[k]
496 self.show_help = False
497 self.disconnected = True
498 self.force_instant_connect = True
499 self.input_lines = []
503 self.offset = YX(0,0)
504 curses.wrapper(self.loop)
508 def handle_recv(msg):
514 self.log_msg('@ attempting connect')
515 socket_client_class = PlomSocketClient
516 if self.host.startswith('ws://') or self.host.startswith('wss://'):
517 socket_client_class = WebSocketClient
519 self.socket = socket_client_class(handle_recv, self.host)
520 self.socket_thread = threading.Thread(target=self.socket.run)
521 self.socket_thread.start()
522 self.disconnected = False
523 self.game.thing_types = {}
524 self.game.terrains = {}
525 time.sleep(0.1) # give potential SSL negotation some time …
526 self.socket.send('TASKS')
527 self.socket.send('TERRAINS')
528 self.socket.send('THING_TYPES')
529 self.switch_mode('login')
530 except ConnectionRefusedError:
531 self.log_msg('@ server connect failure')
532 self.disconnected = True
533 self.switch_mode('waiting_for_server')
534 self.do_refresh = True
537 self.log_msg('@ attempting reconnect')
539 # necessitated by some strange SSL race conditions with ws4py
540 time.sleep(0.1) # FIXME find out why exactly necessary
541 self.switch_mode('waiting_for_server')
546 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
547 raise BrokenSocketConnection
548 self.socket.send(msg)
549 except (BrokenPipeError, BrokenSocketConnection):
550 self.log_msg('@ server disconnected :(')
551 self.disconnected = True
552 self.force_instant_connect = True
553 self.do_refresh = True
555 def log_msg(self, msg):
557 if len(self.log) > 100:
558 self.log = self.log[-100:]
560 def restore_input_values(self):
561 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
562 self.input_ = self.game.annotations[self.explorer]
563 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
564 self.input_ = self.game.portals[self.explorer]
565 elif self.mode.name == 'password':
566 self.input_ = self.password
567 elif self.mode.name == 'name_thing':
568 if hasattr(self.thing_selected, 'name'):
569 self.input_ = self.thing_selected.name
570 elif self.mode.name == 'admin_thing_protect':
571 if hasattr(self.thing_selected, 'protection'):
572 self.input_ = self.thing_selected.protection
574 def send_tile_control_command(self):
575 self.send('SET_TILE_CONTROL %s %s' %
576 (self.explorer, quote(self.tile_control_char)))
578 def toggle_map_mode(self):
579 if self.map_mode == 'terrain only':
580 self.map_mode = 'terrain + annotations'
581 elif self.map_mode == 'terrain + annotations':
582 self.map_mode = 'terrain + things'
583 elif self.map_mode == 'terrain + things':
584 self.map_mode = 'protections'
585 elif self.map_mode == 'protections':
586 self.map_mode = 'terrain only'
588 def switch_mode(self, mode_name):
589 if self.mode and self.mode.name == 'control_tile_draw':
590 self.log_msg('@ finished tile protection drawing.')
591 self.tile_draw = False
592 if mode_name == 'admin_enter' and self.is_admin:
594 elif mode_name in {'name_thing', 'admin_thing_protect'}:
595 player = self.game.get_thing(self.game.player_id)
597 for t in [t for t in self.game.things if t.position == player.position
598 and t.id_ != player.id_]:
603 self.log_msg('? not standing over thing')
606 self.thing_selected = thing
607 self.mode = getattr(self, 'mode_' + mode_name)
608 if self.mode.name in {'control_tile_draw', 'control_tile_type',
610 self.map_mode = 'protections'
611 elif self.mode.name != 'edit':
612 self.map_mode = 'terrain + things'
613 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
614 player = self.game.get_thing(self.game.player_id)
615 self.explorer = YX(player.position.y, player.position.x)
616 if self.mode.is_single_char_entry:
617 self.show_help = True
618 if len(self.mode.intro_msg) > 0:
619 self.log_msg(self.mode.intro_msg)
620 if self.mode.name == 'login':
622 self.send('LOGIN ' + quote(self.login_name))
624 self.log_msg('@ enter username')
625 elif self.mode.name == 'take_thing':
626 self.log_msg('selectable things:')
627 player = self.game.get_thing(self.game.player_id)
628 selectables = [t for t in self.game.things
629 if t != player and t.type_ != 'Player'
630 and t.position == player.position]
631 if len(selectables) == 0:
634 for t in selectables:
635 self.log_msg(str(t.id_) + ' ' + self.get_thing_info(t))
636 elif self.mode.name == 'command_thing':
637 self.send('TASK:COMMAND ' + quote('HELP'))
638 elif self.mode.name == 'control_pw_pw':
639 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
640 elif self.mode.name == 'control_tile_draw':
641 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']))
643 self.restore_input_values()
645 def set_default_colors(self):
646 curses.init_color(1, 1000, 1000, 1000)
647 curses.init_color(2, 0, 0, 0)
648 self.do_refresh = True
650 def set_random_colors(self):
654 return int(offset + random.random()*375)
656 curses.init_color(1, rand(625), rand(625), rand(625))
657 curses.init_color(2, rand(0), rand(0), rand(0))
658 self.do_refresh = True
662 return self.info_cached
663 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
665 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
666 info_to_cache += 'outside field of view'
668 terrain_char = self.game.map_content[pos_i]
670 if terrain_char in self.game.terrains:
671 terrain_desc = self.game.terrains[terrain_char]
672 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
674 protection = self.game.map_control_content[pos_i]
675 if protection == '.':
676 protection = 'unprotected'
677 info_to_cache += 'PROTECTION: %s\n' % protection
678 for t in self.game.things:
679 if t.position == self.explorer:
680 info_to_cache += 'THING: %s' % self.get_thing_info(t)
681 protection = t.protection
682 if protection == '.':
684 info_to_cache += ' / protection: %s\n' % protection
685 if self.explorer in self.game.portals:
686 info_to_cache += 'PORTAL: ' +\
687 self.game.portals[self.explorer] + '\n'
689 info_to_cache += 'PORTAL: (none)\n'
690 if self.explorer in self.game.annotations:
691 info_to_cache += 'ANNOTATION: ' +\
692 self.game.annotations[self.explorer]
693 self.info_cached = info_to_cache
694 return self.info_cached
696 def get_thing_info(self, t):
698 (t.type_, self.game.thing_types[t.type_])
699 if hasattr(t, 'thing_char'):
701 if hasattr(t, 'name'):
702 info += ' (%s)' % t.name
705 def loop(self, stdscr):
708 def safe_addstr(y, x, line):
709 if y < self.size.y - 1 or x + len(line) < self.size.x:
710 stdscr.addstr(y, x, line, curses.color_pair(1))
711 else: # workaround to <https://stackoverflow.com/q/7063128>
712 cut_i = self.size.x - x - 1
714 last_char = line[cut_i]
715 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
716 stdscr.insstr(y, self.size.x - 2, ' ')
717 stdscr.addstr(y, x, cut, curses.color_pair(1))
719 def handle_input(msg):
720 command, args = self.parser.parse(msg)
723 def task_action_on(action):
724 return action_tasks[action] in self.game.tasks
726 def msg_into_lines_of_width(msg, width):
730 for i in range(len(msg)):
731 if x >= width or msg[i] == "\n":
743 def reset_screen_size():
744 self.size = YX(*stdscr.getmaxyx())
745 self.size = self.size - YX(self.size.y % 4, 0)
746 self.size = self.size - YX(0, self.size.x % 4)
747 self.window_width = int(self.size.x / 2)
749 def recalc_input_lines():
750 if not self.mode.has_input_prompt:
751 self.input_lines = []
753 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
756 def move_explorer(direction):
757 target = self.game.map_geometry.move_yx(self.explorer, direction)
759 self.info_cached = None
760 self.explorer = target
762 self.send_tile_control_command()
768 for line in self.log:
769 lines += msg_into_lines_of_width(line, self.window_width)
772 max_y = self.size.y - len(self.input_lines)
773 for i in range(len(lines)):
774 if (i >= max_y - height_header):
776 safe_addstr(max_y - i - 1, self.window_width, lines[i])
779 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
780 lines = msg_into_lines_of_width(info, self.window_width)
782 for i in range(len(lines)):
783 y = height_header + i
784 if y >= self.size.y - len(self.input_lines):
786 safe_addstr(y, self.window_width, lines[i])
789 y = self.size.y - len(self.input_lines)
790 for i in range(len(self.input_lines)):
791 safe_addstr(y, self.window_width, self.input_lines[i])
795 if not self.game.turn_complete:
797 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
800 help = "hit [%s] for help" % self.keys['help']
801 if self.mode.has_input_prompt:
802 help = "enter /help for help"
803 safe_addstr(1, self.window_width,
804 'MODE: %s – %s' % (self.mode.short_desc, help))
807 if not self.game.turn_complete and len(self.map_lines) == 0:
809 if self.game.turn_complete:
811 for y in range(self.game.map_geometry.size.y):
812 start = self.game.map_geometry.size.x * y
813 end = start + self.game.map_geometry.size.x
814 if self.map_mode == 'protections':
815 map_lines_split += [[c + ' ' for c
816 in self.game.map_control_content[start:end]]]
818 map_lines_split += [[c + ' ' for c
819 in self.game.map_content[start:end]]]
820 if self.map_mode == 'terrain + annotations':
821 for p in self.game.annotations:
822 map_lines_split[p.y][p.x] = 'A '
823 elif self.map_mode == 'terrain + things':
824 for p in self.game.portals.keys():
825 original = map_lines_split[p.y][p.x]
826 map_lines_split[p.y][p.x] = original[0] + 'P'
829 def draw_thing(t, used_positions):
830 symbol = self.game.thing_types[t.type_]
832 if hasattr(t, 'thing_char'):
833 meta_char = t.thing_char
834 if t.position in used_positions:
836 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
837 used_positions += [t.position]
839 for t in [t for t in self.game.things if t.type_ != 'Player']:
840 draw_thing(t, used_positions)
841 for t in [t for t in self.game.things if t.type_ == 'Player']:
842 draw_thing(t, used_positions)
843 player = self.game.get_thing(self.game.player_id)
844 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
845 map_lines_split[self.explorer.y][self.explorer.x] = '??'
846 elif self.map_mode != 'terrain + things':
847 map_lines_split[player.position.y][player.position.x] = '??'
849 if type(self.game.map_geometry) == MapGeometryHex:
851 for line in map_lines_split:
852 self.map_lines += [indent * ' ' + ''.join(line)]
853 indent = 0 if indent else 1
855 for line in map_lines_split:
856 self.map_lines += [''.join(line)]
857 window_center = YX(int(self.size.y / 2),
858 int(self.window_width / 2))
859 center = player.position
860 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
861 center = self.explorer
862 center = YX(center.y, center.x * 2)
863 self.offset = center - window_center
864 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
865 self.offset += YX(0, 1)
866 term_y = max(0, -self.offset.y)
867 term_x = max(0, -self.offset.x)
868 map_y = max(0, self.offset.y)
869 map_x = max(0, self.offset.x)
870 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
871 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
872 safe_addstr(term_y, term_x, to_draw)
877 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
878 self.mode.help_intro)
879 if len(self.mode.available_actions) > 0:
880 content += "Available actions:\n"
881 for action in self.mode.available_actions:
882 if action in action_tasks:
883 if action_tasks[action] not in self.game.tasks:
885 if action == 'move_explorer':
888 key = ','.join(self.movement_keys)
890 key = self.keys[action]
891 content += '[%s] – %s\n' % (key, action_descriptions[action])
893 if self.mode.name == 'chat':
894 content += '/nick NAME – re-name yourself to NAME\n'
895 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
896 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
897 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
898 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
899 content += self.mode.list_available_modes(self)
900 for i in range(self.size.y):
902 self.window_width * (not self.mode.has_input_prompt),
903 ' ' * self.window_width)
905 for line in content.split('\n'):
906 lines += msg_into_lines_of_width(line, self.window_width)
907 for i in range(len(lines)):
911 self.window_width * (not self.mode.has_input_prompt),
916 stdscr.bkgd(' ', curses.color_pair(1))
918 if self.mode.has_input_prompt:
920 if self.mode.shows_info:
925 if not self.mode.is_intro:
931 action_descriptions = {
933 'flatten': 'flatten surroundings',
934 'teleport': 'teleport',
935 'take_thing': 'pick up thing',
936 'drop_thing': 'drop thing',
937 'toggle_map_mode': 'toggle map view',
938 'toggle_tile_draw': 'toggle protection character drawing',
939 'door': 'open/close',
940 'consume': 'consume',
944 'flatten': 'FLATTEN_SURROUNDINGS',
945 'take_thing': 'PICK_UP',
946 'drop_thing': 'DROP',
949 'command': 'COMMAND',
950 'consume': 'INTOXICATE',
953 curses.curs_set(False) # hide cursor
955 self.set_default_colors()
956 curses.init_pair(1, 1, 2)
959 self.explorer = YX(0, 0)
962 interval = datetime.timedelta(seconds=5)
963 last_ping = datetime.datetime.now() - interval
965 if self.disconnected and self.force_instant_connect:
966 self.force_instant_connect = False
968 now = datetime.datetime.now()
969 if now - last_ping > interval:
970 if self.disconnected:
980 self.do_refresh = False
983 msg = self.queue.get(block=False)
988 key = stdscr.getkey()
989 self.do_refresh = True
992 self.show_help = False
993 if key == 'KEY_RESIZE':
995 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
996 self.input_ = self.input_[:-1]
997 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
998 self.show_help = True
1000 self.restore_input_values()
1001 elif self.mode.has_input_prompt and key != '\n': # Return key
1003 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1004 if len(self.input_) > max_length:
1005 self.input_ = self.input_[:max_length]
1006 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1007 self.show_help = True
1008 elif self.mode.name == 'login' and key == '\n':
1009 self.login_name = self.input_
1010 self.send('LOGIN ' + quote(self.input_))
1012 elif self.mode.name == 'take_thing' and key == '\n':
1013 if self.input_ == '':
1014 self.log_msg('@ aborted')
1016 self.send('TASK:PICK_UP ' + quote(self.input_))
1018 self.switch_mode('play')
1019 elif self.mode.name == 'command_thing' and key == '\n':
1020 if self.input_ == '':
1021 self.log_msg('@ aborted')
1022 self.switch_mode('play')
1023 elif task_action_on('command'):
1024 self.send('TASK:COMMAND ' + quote(self.input_))
1026 elif self.mode.name == 'control_pw_pw' and key == '\n':
1027 if self.input_ == '':
1028 self.log_msg('@ aborted')
1030 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1031 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1032 self.switch_mode('admin')
1033 elif self.mode.name == 'password' and key == '\n':
1034 if self.input_ == '':
1036 self.password = self.input_
1037 self.switch_mode('edit')
1038 elif self.mode.name == 'admin_enter' and key == '\n':
1039 self.send('BECOME_ADMIN ' + quote(self.input_))
1040 self.switch_mode('play')
1041 elif self.mode.name == 'control_pw_type' and key == '\n':
1042 if len(self.input_) != 1:
1043 self.log_msg('@ entered non-single-char, therefore aborted')
1044 self.switch_mode('admin')
1046 self.tile_control_char = self.input_
1047 self.switch_mode('control_pw_pw')
1048 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1049 if len(self.input_) != 1:
1050 self.log_msg('@ entered non-single-char, therefore aborted')
1052 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1053 quote(self.input_)))
1054 self.log_msg('@ sent new protection character for thing')
1055 self.switch_mode('admin')
1056 elif self.mode.name == 'control_tile_type' and key == '\n':
1057 if len(self.input_) != 1:
1058 self.log_msg('@ entered non-single-char, therefore aborted')
1059 self.switch_mode('admin')
1061 self.tile_control_char = self.input_
1062 self.switch_mode('control_tile_draw')
1063 elif self.mode.name == 'chat' and key == '\n':
1064 if self.input_ == '':
1066 if self.input_[0] == '/': # FIXME fails on empty input
1067 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1068 self.switch_mode('play')
1069 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1070 self.switch_mode('study')
1071 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1072 self.switch_mode('edit')
1073 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1074 self.switch_mode('admin_enter')
1075 elif self.input_.startswith('/nick'):
1076 tokens = self.input_.split(maxsplit=1)
1077 if len(tokens) == 2:
1078 self.send('NICK ' + quote(tokens[1]))
1080 self.log_msg('? need login name')
1082 self.log_msg('? unknown command')
1084 self.send('ALL ' + quote(self.input_))
1086 elif self.mode.name == 'name_thing' and key == '\n':
1087 if self.input_ == '':
1089 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1091 quote(self.password)))
1092 self.switch_mode('edit')
1093 elif self.mode.name == 'annotate' and key == '\n':
1094 if self.input_ == '':
1096 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1097 quote(self.password)))
1098 self.switch_mode('edit')
1099 elif self.mode.name == 'portal' and key == '\n':
1100 if self.input_ == '':
1102 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1103 quote(self.password)))
1104 self.switch_mode('edit')
1105 elif self.mode.name == 'study':
1106 if self.mode.mode_switch_on_key(self, key):
1108 elif key == self.keys['toggle_map_mode']:
1109 self.toggle_map_mode()
1110 elif key in self.movement_keys:
1111 move_explorer(self.movement_keys[key])
1112 elif self.mode.name == 'play':
1113 if self.mode.mode_switch_on_key(self, key):
1115 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1116 self.send('TASK:DROP')
1117 elif key == self.keys['door'] and task_action_on('door'):
1118 self.send('TASK:DOOR')
1119 elif key == self.keys['consume'] and task_action_on('consume'):
1120 self.send('TASK:INTOXICATE')
1121 elif key == self.keys['teleport']:
1122 player = self.game.get_thing(self.game.player_id)
1123 if player.position in self.game.portals:
1124 self.host = self.game.portals[player.position]
1128 self.log_msg('? not standing on portal')
1129 elif key in self.movement_keys and task_action_on('move'):
1130 self.send('TASK:MOVE ' + self.movement_keys[key])
1131 elif self.mode.name == 'write':
1132 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1133 self.switch_mode('edit')
1134 elif self.mode.name == 'control_tile_draw':
1135 if self.mode.mode_switch_on_key(self, key):
1137 elif key in self.movement_keys:
1138 move_explorer(self.movement_keys[key])
1139 elif key == self.keys['toggle_tile_draw']:
1140 self.tile_draw = False if self.tile_draw else True
1141 elif self.mode.name == 'admin':
1142 if self.mode.mode_switch_on_key(self, key):
1144 elif key in self.movement_keys and task_action_on('move'):
1145 self.send('TASK:MOVE ' + self.movement_keys[key])
1146 elif self.mode.name == 'edit':
1147 if self.mode.mode_switch_on_key(self, key):
1149 elif key == self.keys['flatten'] and task_action_on('flatten'):
1150 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1151 elif key == self.keys['toggle_map_mode']:
1152 self.toggle_map_mode()
1153 elif key in self.movement_keys and task_action_on('move'):
1154 self.send('TASK:MOVE ' + self.movement_keys[key])
1156 if len(sys.argv) != 2:
1157 raise ArgError('wrong number of arguments, need game host')