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'
794 def draw_thing(t, used_positions):
795 symbol = self.game.thing_types[t.type_]
797 if hasattr(t, 'thing_char'):
798 meta_char = t.thing_char
799 if t.position in used_positions:
801 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
802 used_positions += [t.position]
804 for t in [t for t in self.game.things if t.type_ != 'Player']:
805 draw_thing(t, used_positions)
806 for t in [t for t in self.game.things if t.type_ == 'Player']:
807 draw_thing(t, used_positions)
808 player = self.game.get_thing(self.game.player_id)
809 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
810 map_lines_split[self.explorer.y][self.explorer.x] = '??'
811 elif self.map_mode != 'terrain + things':
812 map_lines_split[player.position.y][player.position.x] = '??'
814 if type(self.game.map_geometry) == MapGeometryHex:
816 for line in map_lines_split:
817 self.map_lines += [indent * ' ' + ''.join(line)]
818 indent = 0 if indent else 1
820 for line in map_lines_split:
821 self.map_lines += [''.join(line)]
822 window_center = YX(int(self.size.y / 2),
823 int(self.window_width / 2))
824 center = player.position
825 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
826 center = self.explorer
827 center = YX(center.y, center.x * 2)
828 self.offset = center - window_center
829 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
830 self.offset += YX(0, 1)
831 term_y = max(0, -self.offset.y)
832 term_x = max(0, -self.offset.x)
833 map_y = max(0, self.offset.y)
834 map_x = max(0, self.offset.x)
835 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
836 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
837 safe_addstr(term_y, term_x, to_draw)
842 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
843 self.mode.help_intro)
844 if len(self.mode.available_actions) > 0:
845 content += "Available actions:\n"
846 for action in self.mode.available_actions:
847 if action in action_tasks:
848 if action_tasks[action] not in self.game.tasks:
850 if action == 'move_explorer':
853 key = ','.join(self.movement_keys)
855 key = self.keys[action]
856 content += '[%s] – %s\n' % (key, action_descriptions[action])
858 if self.mode.name == 'chat':
859 content += '/nick NAME – re-name yourself to NAME\n'
860 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
861 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
862 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
863 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
864 content += self.mode.list_available_modes(self)
865 for i in range(self.size.y):
867 self.window_width * (not self.mode.has_input_prompt),
868 ' ' * self.window_width)
870 for line in content.split('\n'):
871 lines += msg_into_lines_of_width(line, self.window_width)
872 for i in range(len(lines)):
876 self.window_width * (not self.mode.has_input_prompt),
881 stdscr.bkgd(' ', curses.color_pair(1))
883 if self.mode.has_input_prompt:
885 if self.mode.shows_info:
890 if not self.mode.is_intro:
896 action_descriptions = {
898 'flatten': 'flatten surroundings',
899 'teleport': 'teleport',
900 'take_thing': 'pick up thing',
901 'drop_thing': 'drop thing',
902 'toggle_map_mode': 'toggle map view',
903 'toggle_tile_draw': 'toggle protection character drawing',
904 'door': 'open/close',
905 'consume': 'consume',
909 'flatten': 'FLATTEN_SURROUNDINGS',
910 'take_thing': 'PICK_UP',
911 'drop_thing': 'DROP',
914 'command': 'COMMAND',
915 'consume': 'INTOXICATE',
918 curses.curs_set(False) # hide cursor
920 self.set_default_colors()
921 curses.init_pair(1, 1, 2)
924 self.explorer = YX(0, 0)
927 interval = datetime.timedelta(seconds=5)
928 last_ping = datetime.datetime.now() - interval
930 if self.disconnected and self.force_instant_connect:
931 self.force_instant_connect = False
933 now = datetime.datetime.now()
934 if now - last_ping > interval:
935 if self.disconnected:
945 self.do_refresh = False
948 msg = self.queue.get(block=False)
953 key = stdscr.getkey()
954 self.do_refresh = True
957 self.show_help = False
958 if key == 'KEY_RESIZE':
960 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
961 self.input_ = self.input_[:-1]
962 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
963 self.show_help = True
965 self.restore_input_values()
966 elif self.mode.has_input_prompt and key != '\n': # Return key
968 max_length = self.window_width * self.size.y - len(input_prompt) - 1
969 if len(self.input_) > max_length:
970 self.input_ = self.input_[:max_length]
971 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
972 self.show_help = True
973 elif self.mode.name == 'login' and key == '\n':
974 self.login_name = self.input_
975 self.send('LOGIN ' + quote(self.input_))
977 elif self.mode.name == 'command_thing' and key == '\n':
978 if self.input_ == '':
979 self.log_msg('@ aborted')
980 self.switch_mode('play')
981 elif task_action_on('command'):
982 self.send('TASK:COMMAND ' + quote(self.input_))
984 elif self.mode.name == 'control_pw_pw' and key == '\n':
985 if self.input_ == '':
986 self.log_msg('@ aborted')
988 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
989 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
990 self.switch_mode('admin')
991 elif self.mode.name == 'password' and key == '\n':
992 if self.input_ == '':
994 self.password = self.input_
995 self.switch_mode('edit')
996 elif self.mode.name == 'admin_enter' and key == '\n':
997 self.send('BECOME_ADMIN ' + quote(self.input_))
998 self.switch_mode('play')
999 elif self.mode.name == 'control_pw_type' and key == '\n':
1000 if len(self.input_) != 1:
1001 self.log_msg('@ entered non-single-char, therefore aborted')
1002 self.switch_mode('admin')
1004 self.tile_control_char = self.input_
1005 self.switch_mode('control_pw_pw')
1006 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1007 if len(self.input_) != 1:
1008 self.log_msg('@ entered non-single-char, therefore aborted')
1010 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1011 quote(self.input_)))
1012 self.log_msg('@ sent new protection character for thing')
1013 self.switch_mode('admin')
1014 elif self.mode.name == 'control_tile_type' and key == '\n':
1015 if len(self.input_) != 1:
1016 self.log_msg('@ entered non-single-char, therefore aborted')
1017 self.switch_mode('admin')
1019 self.tile_control_char = self.input_
1020 self.switch_mode('control_tile_draw')
1021 elif self.mode.name == 'chat' and key == '\n':
1022 if self.input_ == '':
1024 if self.input_[0] == '/': # FIXME fails on empty input
1025 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1026 self.switch_mode('play')
1027 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1028 self.switch_mode('study')
1029 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1030 self.switch_mode('edit')
1031 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1032 self.switch_mode('admin_enter')
1033 elif self.input_.startswith('/nick'):
1034 tokens = self.input_.split(maxsplit=1)
1035 if len(tokens) == 2:
1036 self.send('NICK ' + quote(tokens[1]))
1038 self.log_msg('? need login name')
1040 self.log_msg('? unknown command')
1042 self.send('ALL ' + quote(self.input_))
1044 elif self.mode.name == 'name_thing' and key == '\n':
1045 if self.input_ == '':
1047 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1049 quote(self.password)))
1050 self.switch_mode('edit')
1051 elif self.mode.name == 'annotate' and key == '\n':
1052 if self.input_ == '':
1054 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1055 quote(self.password)))
1056 self.switch_mode('edit')
1057 elif self.mode.name == 'portal' and key == '\n':
1058 if self.input_ == '':
1060 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1061 quote(self.password)))
1062 self.switch_mode('edit')
1063 elif self.mode.name == 'study':
1064 if self.mode.mode_switch_on_key(self, key):
1066 elif key == self.keys['toggle_map_mode']:
1067 self.toggle_map_mode()
1068 elif key in self.movement_keys:
1069 move_explorer(self.movement_keys[key])
1070 elif self.mode.name == 'play':
1071 if self.mode.mode_switch_on_key(self, key):
1073 elif key == self.keys['take_thing'] and task_action_on('take_thing'):
1074 self.send('TASK:PICK_UP')
1075 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1076 self.send('TASK:DROP')
1077 elif key == self.keys['door'] and task_action_on('door'):
1078 self.send('TASK:DOOR')
1079 elif key == self.keys['consume'] and task_action_on('consume'):
1080 self.send('TASK:INTOXICATE')
1081 elif key == self.keys['teleport']:
1082 player = self.game.get_thing(self.game.player_id)
1083 if player.position in self.game.portals:
1084 self.host = self.game.portals[player.position]
1088 self.log_msg('? not standing on portal')
1089 elif key in self.movement_keys and task_action_on('move'):
1090 self.send('TASK:MOVE ' + self.movement_keys[key])
1091 elif self.mode.name == 'write':
1092 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1093 self.switch_mode('edit')
1094 elif self.mode.name == 'control_tile_draw':
1095 if self.mode.mode_switch_on_key(self, key):
1097 elif key in self.movement_keys:
1098 move_explorer(self.movement_keys[key])
1099 elif key == self.keys['toggle_tile_draw']:
1100 self.tile_draw = False if self.tile_draw else True
1101 elif self.mode.name == 'admin':
1102 if self.mode.mode_switch_on_key(self, key):
1104 elif key in self.movement_keys and task_action_on('move'):
1105 self.send('TASK:MOVE ' + self.movement_keys[key])
1106 elif self.mode.name == 'edit':
1107 if self.mode.mode_switch_on_key(self, key):
1109 elif key == self.keys['flatten'] and task_action_on('flatten'):
1110 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1111 elif key == self.keys['toggle_map_mode']:
1112 self.toggle_map_mode()
1113 elif key in self.movement_keys and task_action_on('move'):
1114 self.send('TASK:MOVE ' + self.movement_keys[key])
1116 if len(sys.argv) != 2:
1117 raise ArgError('wrong number of arguments, need game host')