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 self.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(self.selectables) == 0:
634 for i in range(len(self.selectables)):
635 t = self.selectables[i]
636 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
637 elif self.mode.name == 'command_thing':
638 self.send('TASK:COMMAND ' + quote('HELP'))
639 elif self.mode.name == 'control_pw_pw':
640 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
641 elif self.mode.name == 'control_tile_draw':
642 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']))
644 self.restore_input_values()
646 def set_default_colors(self):
647 curses.init_color(1, 1000, 1000, 1000)
648 curses.init_color(2, 0, 0, 0)
649 self.do_refresh = True
651 def set_random_colors(self):
655 return int(offset + random.random()*375)
657 curses.init_color(1, rand(625), rand(625), rand(625))
658 curses.init_color(2, rand(0), rand(0), rand(0))
659 self.do_refresh = True
663 return self.info_cached
664 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
666 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
667 info_to_cache += 'outside field of view'
669 terrain_char = self.game.map_content[pos_i]
671 if terrain_char in self.game.terrains:
672 terrain_desc = self.game.terrains[terrain_char]
673 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
675 protection = self.game.map_control_content[pos_i]
676 if protection == '.':
677 protection = 'unprotected'
678 info_to_cache += 'PROTECTION: %s\n' % protection
679 for t in self.game.things:
680 if t.position == self.explorer:
681 info_to_cache += 'THING: %s' % self.get_thing_info(t)
682 protection = t.protection
683 if protection == '.':
685 info_to_cache += ' / protection: %s\n' % protection
686 if self.explorer in self.game.portals:
687 info_to_cache += 'PORTAL: ' +\
688 self.game.portals[self.explorer] + '\n'
690 info_to_cache += 'PORTAL: (none)\n'
691 if self.explorer in self.game.annotations:
692 info_to_cache += 'ANNOTATION: ' +\
693 self.game.annotations[self.explorer]
694 self.info_cached = info_to_cache
695 return self.info_cached
697 def get_thing_info(self, t):
699 (t.type_, self.game.thing_types[t.type_])
700 if hasattr(t, 'thing_char'):
702 if hasattr(t, 'name'):
703 info += ' (%s)' % t.name
706 def loop(self, stdscr):
709 def safe_addstr(y, x, line):
710 if y < self.size.y - 1 or x + len(line) < self.size.x:
711 stdscr.addstr(y, x, line, curses.color_pair(1))
712 else: # workaround to <https://stackoverflow.com/q/7063128>
713 cut_i = self.size.x - x - 1
715 last_char = line[cut_i]
716 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
717 stdscr.insstr(y, self.size.x - 2, ' ')
718 stdscr.addstr(y, x, cut, curses.color_pair(1))
720 def handle_input(msg):
721 command, args = self.parser.parse(msg)
724 def task_action_on(action):
725 return action_tasks[action] in self.game.tasks
727 def msg_into_lines_of_width(msg, width):
731 for i in range(len(msg)):
732 if x >= width or msg[i] == "\n":
744 def reset_screen_size():
745 self.size = YX(*stdscr.getmaxyx())
746 self.size = self.size - YX(self.size.y % 4, 0)
747 self.size = self.size - YX(0, self.size.x % 4)
748 self.window_width = int(self.size.x / 2)
750 def recalc_input_lines():
751 if not self.mode.has_input_prompt:
752 self.input_lines = []
754 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
757 def move_explorer(direction):
758 target = self.game.map_geometry.move_yx(self.explorer, direction)
760 self.info_cached = None
761 self.explorer = target
763 self.send_tile_control_command()
769 for line in self.log:
770 lines += msg_into_lines_of_width(line, self.window_width)
773 max_y = self.size.y - len(self.input_lines)
774 for i in range(len(lines)):
775 if (i >= max_y - height_header):
777 safe_addstr(max_y - i - 1, self.window_width, lines[i])
780 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
781 lines = msg_into_lines_of_width(info, self.window_width)
783 for i in range(len(lines)):
784 y = height_header + i
785 if y >= self.size.y - len(self.input_lines):
787 safe_addstr(y, self.window_width, lines[i])
790 y = self.size.y - len(self.input_lines)
791 for i in range(len(self.input_lines)):
792 safe_addstr(y, self.window_width, self.input_lines[i])
796 if not self.game.turn_complete:
798 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
801 help = "hit [%s] for help" % self.keys['help']
802 if self.mode.has_input_prompt:
803 help = "enter /help for help"
804 safe_addstr(1, self.window_width,
805 'MODE: %s – %s' % (self.mode.short_desc, help))
808 if not self.game.turn_complete and len(self.map_lines) == 0:
810 if self.game.turn_complete:
812 for y in range(self.game.map_geometry.size.y):
813 start = self.game.map_geometry.size.x * y
814 end = start + self.game.map_geometry.size.x
815 if self.map_mode == 'protections':
816 map_lines_split += [[c + ' ' for c
817 in self.game.map_control_content[start:end]]]
819 map_lines_split += [[c + ' ' for c
820 in self.game.map_content[start:end]]]
821 if self.map_mode == 'terrain + annotations':
822 for p in self.game.annotations:
823 map_lines_split[p.y][p.x] = 'A '
824 elif self.map_mode == 'terrain + things':
825 for p in self.game.portals.keys():
826 original = map_lines_split[p.y][p.x]
827 map_lines_split[p.y][p.x] = original[0] + 'P'
830 def draw_thing(t, used_positions):
831 symbol = self.game.thing_types[t.type_]
833 if hasattr(t, 'thing_char'):
834 meta_char = t.thing_char
835 if t.position in used_positions:
837 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
838 used_positions += [t.position]
840 for t in [t for t in self.game.things if t.type_ != 'Player']:
841 draw_thing(t, used_positions)
842 for t in [t for t in self.game.things if t.type_ == 'Player']:
843 draw_thing(t, used_positions)
844 player = self.game.get_thing(self.game.player_id)
845 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
846 map_lines_split[self.explorer.y][self.explorer.x] = '??'
847 elif self.map_mode != 'terrain + things':
848 map_lines_split[player.position.y][player.position.x] = '??'
850 if type(self.game.map_geometry) == MapGeometryHex:
852 for line in map_lines_split:
853 self.map_lines += [indent * ' ' + ''.join(line)]
854 indent = 0 if indent else 1
856 for line in map_lines_split:
857 self.map_lines += [''.join(line)]
858 window_center = YX(int(self.size.y / 2),
859 int(self.window_width / 2))
860 center = player.position
861 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
862 center = self.explorer
863 center = YX(center.y, center.x * 2)
864 self.offset = center - window_center
865 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
866 self.offset += YX(0, 1)
867 term_y = max(0, -self.offset.y)
868 term_x = max(0, -self.offset.x)
869 map_y = max(0, self.offset.y)
870 map_x = max(0, self.offset.x)
871 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
872 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
873 safe_addstr(term_y, term_x, to_draw)
878 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
879 self.mode.help_intro)
880 if len(self.mode.available_actions) > 0:
881 content += "Available actions:\n"
882 for action in self.mode.available_actions:
883 if action in action_tasks:
884 if action_tasks[action] not in self.game.tasks:
886 if action == 'move_explorer':
889 key = ','.join(self.movement_keys)
891 key = self.keys[action]
892 content += '[%s] – %s\n' % (key, action_descriptions[action])
894 if self.mode.name == 'chat':
895 content += '/nick NAME – re-name yourself to NAME\n'
896 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
897 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
898 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
899 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
900 content += self.mode.list_available_modes(self)
901 for i in range(self.size.y):
903 self.window_width * (not self.mode.has_input_prompt),
904 ' ' * self.window_width)
906 for line in content.split('\n'):
907 lines += msg_into_lines_of_width(line, self.window_width)
908 for i in range(len(lines)):
912 self.window_width * (not self.mode.has_input_prompt),
917 stdscr.bkgd(' ', curses.color_pair(1))
919 if self.mode.has_input_prompt:
921 if self.mode.shows_info:
926 if not self.mode.is_intro:
932 action_descriptions = {
934 'flatten': 'flatten surroundings',
935 'teleport': 'teleport',
936 'take_thing': 'pick up thing',
937 'drop_thing': 'drop thing',
938 'toggle_map_mode': 'toggle map view',
939 'toggle_tile_draw': 'toggle protection character drawing',
940 'door': 'open/close',
941 'consume': 'consume',
945 'flatten': 'FLATTEN_SURROUNDINGS',
946 'take_thing': 'PICK_UP',
947 'drop_thing': 'DROP',
950 'command': 'COMMAND',
951 'consume': 'INTOXICATE',
954 curses.curs_set(False) # hide cursor
956 self.set_default_colors()
957 curses.init_pair(1, 1, 2)
960 self.explorer = YX(0, 0)
963 interval = datetime.timedelta(seconds=5)
964 last_ping = datetime.datetime.now() - interval
966 if self.disconnected and self.force_instant_connect:
967 self.force_instant_connect = False
969 now = datetime.datetime.now()
970 if now - last_ping > interval:
971 if self.disconnected:
981 self.do_refresh = False
984 msg = self.queue.get(block=False)
989 key = stdscr.getkey()
990 self.do_refresh = True
993 self.show_help = False
994 if key == 'KEY_RESIZE':
996 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
997 self.input_ = self.input_[:-1]
998 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
999 self.show_help = True
1001 self.restore_input_values()
1002 elif self.mode.has_input_prompt and key != '\n': # Return key
1004 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1005 if len(self.input_) > max_length:
1006 self.input_ = self.input_[:max_length]
1007 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1008 self.show_help = True
1009 elif self.mode.name == 'login' and key == '\n':
1010 self.login_name = self.input_
1011 self.send('LOGIN ' + quote(self.input_))
1013 elif self.mode.name == 'take_thing' and key == '\n':
1014 if self.input_ == '':
1015 self.log_msg('@ aborted')
1018 i = int(self.input_)
1019 if i < 0 or i >= len(self.selectables):
1020 self.log_msg('? invalid index, aborted')
1022 self.send('TASK:PICK_UP %s' % self.selectables[i].id_)
1024 self.log_msg('? invalid index, aborted')
1026 self.switch_mode('play')
1027 elif self.mode.name == 'command_thing' and key == '\n':
1028 if self.input_ == '':
1029 self.log_msg('@ aborted')
1030 self.switch_mode('play')
1031 elif task_action_on('command'):
1032 self.send('TASK:COMMAND ' + quote(self.input_))
1034 elif self.mode.name == 'control_pw_pw' and key == '\n':
1035 if self.input_ == '':
1036 self.log_msg('@ aborted')
1038 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1039 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1040 self.switch_mode('admin')
1041 elif self.mode.name == 'password' and key == '\n':
1042 if self.input_ == '':
1044 self.password = self.input_
1045 self.switch_mode('edit')
1046 elif self.mode.name == 'admin_enter' and key == '\n':
1047 self.send('BECOME_ADMIN ' + quote(self.input_))
1048 self.switch_mode('play')
1049 elif self.mode.name == 'control_pw_type' and key == '\n':
1050 if len(self.input_) != 1:
1051 self.log_msg('@ entered non-single-char, therefore aborted')
1052 self.switch_mode('admin')
1054 self.tile_control_char = self.input_
1055 self.switch_mode('control_pw_pw')
1056 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1057 if len(self.input_) != 1:
1058 self.log_msg('@ entered non-single-char, therefore aborted')
1060 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1061 quote(self.input_)))
1062 self.log_msg('@ sent new protection character for thing')
1063 self.switch_mode('admin')
1064 elif self.mode.name == 'control_tile_type' and key == '\n':
1065 if len(self.input_) != 1:
1066 self.log_msg('@ entered non-single-char, therefore aborted')
1067 self.switch_mode('admin')
1069 self.tile_control_char = self.input_
1070 self.switch_mode('control_tile_draw')
1071 elif self.mode.name == 'chat' and key == '\n':
1072 if self.input_ == '':
1074 if self.input_[0] == '/': # FIXME fails on empty input
1075 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1076 self.switch_mode('play')
1077 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1078 self.switch_mode('study')
1079 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1080 self.switch_mode('edit')
1081 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1082 self.switch_mode('admin_enter')
1083 elif self.input_.startswith('/nick'):
1084 tokens = self.input_.split(maxsplit=1)
1085 if len(tokens) == 2:
1086 self.send('NICK ' + quote(tokens[1]))
1088 self.log_msg('? need login name')
1090 self.log_msg('? unknown command')
1092 self.send('ALL ' + quote(self.input_))
1094 elif self.mode.name == 'name_thing' and key == '\n':
1095 if self.input_ == '':
1097 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1099 quote(self.password)))
1100 self.switch_mode('edit')
1101 elif self.mode.name == 'annotate' and key == '\n':
1102 if self.input_ == '':
1104 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1105 quote(self.password)))
1106 self.switch_mode('edit')
1107 elif self.mode.name == 'portal' and key == '\n':
1108 if self.input_ == '':
1110 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1111 quote(self.password)))
1112 self.switch_mode('edit')
1113 elif self.mode.name == 'study':
1114 if self.mode.mode_switch_on_key(self, key):
1116 elif key == self.keys['toggle_map_mode']:
1117 self.toggle_map_mode()
1118 elif key in self.movement_keys:
1119 move_explorer(self.movement_keys[key])
1120 elif self.mode.name == 'play':
1121 if self.mode.mode_switch_on_key(self, key):
1123 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1124 self.send('TASK:DROP')
1125 elif key == self.keys['door'] and task_action_on('door'):
1126 self.send('TASK:DOOR')
1127 elif key == self.keys['consume'] and task_action_on('consume'):
1128 self.send('TASK:INTOXICATE')
1129 elif key == self.keys['teleport']:
1130 player = self.game.get_thing(self.game.player_id)
1131 if player.position in self.game.portals:
1132 self.host = self.game.portals[player.position]
1136 self.log_msg('? not standing on portal')
1137 elif key in self.movement_keys and task_action_on('move'):
1138 self.send('TASK:MOVE ' + self.movement_keys[key])
1139 elif self.mode.name == 'write':
1140 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1141 self.switch_mode('edit')
1142 elif self.mode.name == 'control_tile_draw':
1143 if self.mode.mode_switch_on_key(self, key):
1145 elif key in self.movement_keys:
1146 move_explorer(self.movement_keys[key])
1147 elif key == self.keys['toggle_tile_draw']:
1148 self.tile_draw = False if self.tile_draw else True
1149 elif self.mode.name == 'admin':
1150 if self.mode.mode_switch_on_key(self, key):
1152 elif key in self.movement_keys and task_action_on('move'):
1153 self.send('TASK:MOVE ' + self.movement_keys[key])
1154 elif self.mode.name == 'edit':
1155 if self.mode.mode_switch_on_key(self, key):
1157 elif key == self.keys['flatten'] and task_action_on('flatten'):
1158 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1159 elif key == self.keys['toggle_map_mode']:
1160 self.toggle_map_mode()
1161 elif key in self.movement_keys and task_action_on('move'):
1162 self.send('TASK:MOVE ' + self.movement_keys[key])
1164 if len(sys.argv) != 2:
1165 raise ArgError('wrong number of arguments, need game host')