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.'
35 'short': 'take thing',
36 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
38 'admin_thing_protect': {
39 'short': 'change thing protection',
40 'long': 'Change protection character for thing here.'
43 'short': 'change terrain',
44 '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.'
47 'short': 'change protection character password',
48 '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.'
51 'short': 'change protection character password',
52 '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.'
54 'control_tile_type': {
55 'short': 'change tiles protection',
56 '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.'
58 'control_tile_draw': {
59 'short': 'change tiles protection',
60 '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.'
63 'short': 'annotate tile',
64 '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.'
67 'short': 'edit portal',
68 '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.'
72 '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:'
76 'long': 'Enter your player name.'
78 'waiting_for_server': {
79 'short': 'waiting for server response',
80 'long': 'Waiting for a server response.'
83 'short': 'waiting for server response',
84 'long': 'Waiting for a server response.'
87 'short': 'set world edit password',
88 '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.'
91 'short': 'become admin',
92 'long': 'This mode allows you to become admin if you know an admin password.'
96 'long': 'This mode allows you access to actions limited to administrators.'
100 from ws4py.client import WebSocketBaseClient
101 class WebSocketClient(WebSocketBaseClient):
103 def __init__(self, recv_handler, *args, **kwargs):
104 super().__init__(*args, **kwargs)
105 self.recv_handler = recv_handler
108 def received_message(self, message):
110 message = str(message)
111 self.recv_handler(message)
114 def plom_closed(self):
115 return self.client_terminated
117 from plomrogue.io_tcp import PlomSocket
118 class PlomSocketClient(PlomSocket):
120 def __init__(self, recv_handler, url):
122 self.recv_handler = recv_handler
123 host, port = url.split(':')
124 super().__init__(socket.create_connection((host, port)))
132 for msg in self.recv():
133 if msg == 'NEED_SSL':
134 self.socket = ssl.wrap_socket(self.socket)
136 self.recv_handler(msg)
137 except BrokenSocketConnection:
138 pass # we assume socket will be known as dead by now
140 def cmd_TURN(game, n):
141 game.annotations = {}
145 game.turn_complete = False
147 cmd_TURN.argtypes = 'int:nonneg'
149 def cmd_LOGIN_OK(game):
150 game.tui.switch_mode('post_login_wait')
151 game.tui.send('GET_GAMESTATE')
152 game.tui.log_msg('@ welcome')
153 cmd_LOGIN_OK.argtypes = ''
155 def cmd_ADMIN_OK(game):
156 game.tui.is_admin = True
157 game.tui.log_msg('@ you now have admin rights')
158 game.tui.switch_mode('admin')
159 game.tui.do_refresh = True
160 cmd_ADMIN_OK.argtypes = ''
162 def cmd_REPLY(game, msg):
163 game.tui.log_msg('#MUSICPLAYER: ' + msg)
164 game.tui.do_refresh = True
165 cmd_REPLY.argtypes = 'string'
167 def cmd_CHAT(game, msg):
168 game.tui.log_msg('# ' + msg)
169 game.tui.do_refresh = True
170 cmd_CHAT.argtypes = 'string'
172 def cmd_PLAYER_ID(game, player_id):
173 game.player_id = player_id
174 cmd_PLAYER_ID.argtypes = 'int:nonneg'
176 def cmd_THING(game, yx, thing_type, protection, thing_id):
177 t = game.get_thing(thing_id)
179 t = ThingBase(game, thing_id)
183 t.protection = protection
184 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg'
186 def cmd_THING_NAME(game, thing_id, name):
187 t = game.get_thing(thing_id)
190 cmd_THING_NAME.argtypes = 'int:nonneg string'
192 def cmd_THING_CHAR(game, thing_id, c):
193 t = game.get_thing(thing_id)
196 cmd_THING_CHAR.argtypes = 'int:nonneg char'
198 def cmd_MAP(game, geometry, size, content):
199 map_geometry_class = globals()['MapGeometry' + geometry]
200 game.map_geometry = map_geometry_class(size)
201 game.map_content = content
202 if type(game.map_geometry) == MapGeometrySquare:
203 game.tui.movement_keys = {
204 game.tui.keys['square_move_up']: 'UP',
205 game.tui.keys['square_move_left']: 'LEFT',
206 game.tui.keys['square_move_down']: 'DOWN',
207 game.tui.keys['square_move_right']: 'RIGHT',
209 elif type(game.map_geometry) == MapGeometryHex:
210 game.tui.movement_keys = {
211 game.tui.keys['hex_move_upleft']: 'UPLEFT',
212 game.tui.keys['hex_move_upright']: 'UPRIGHT',
213 game.tui.keys['hex_move_right']: 'RIGHT',
214 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
215 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
216 game.tui.keys['hex_move_left']: 'LEFT',
218 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
220 def cmd_FOV(game, content):
222 cmd_FOV.argtypes = 'string'
224 def cmd_MAP_CONTROL(game, content):
225 game.map_control_content = content
226 cmd_MAP_CONTROL.argtypes = 'string'
228 def cmd_GAME_STATE_COMPLETE(game):
229 if game.tui.mode.name == 'post_login_wait':
230 game.tui.switch_mode('play')
231 game.turn_complete = True
232 game.tui.do_refresh = True
233 game.tui.info_cached = None
234 cmd_GAME_STATE_COMPLETE.argtypes = ''
236 def cmd_PORTAL(game, position, msg):
237 game.portals[position] = msg
238 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
240 def cmd_PLAY_ERROR(game, msg):
241 game.tui.log_msg('? ' + msg)
242 game.tui.flash = True
243 game.tui.do_refresh = True
244 cmd_PLAY_ERROR.argtypes = 'string'
246 def cmd_GAME_ERROR(game, msg):
247 game.tui.log_msg('? game error: ' + msg)
248 game.tui.do_refresh = True
249 cmd_GAME_ERROR.argtypes = 'string'
251 def cmd_ARGUMENT_ERROR(game, msg):
252 game.tui.log_msg('? syntax error: ' + msg)
253 game.tui.do_refresh = True
254 cmd_ARGUMENT_ERROR.argtypes = 'string'
256 def cmd_ANNOTATION(game, position, msg):
257 game.annotations[position] = msg
258 if game.tui.mode.shows_info:
259 game.tui.do_refresh = True
260 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
262 def cmd_TASKS(game, tasks_comma_separated):
263 game.tasks = tasks_comma_separated.split(',')
264 game.tui.mode_write.legal = 'WRITE' in game.tasks
265 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
266 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
267 cmd_TASKS.argtypes = 'string'
269 def cmd_THING_TYPE(game, thing_type, symbol_hint):
270 game.thing_types[thing_type] = symbol_hint
271 cmd_THING_TYPE.argtypes = 'string char'
273 def cmd_TERRAIN(game, terrain_char, terrain_desc):
274 game.terrains[terrain_char] = terrain_desc
275 cmd_TERRAIN.argtypes = 'char string'
279 cmd_PONG.argtypes = ''
281 def cmd_DEFAULT_COLORS(game):
282 game.tui.set_default_colors()
283 cmd_DEFAULT_COLORS.argtypes = ''
285 def cmd_RANDOM_COLORS(game):
286 game.tui.set_random_colors()
287 cmd_RANDOM_COLORS.argtypes = ''
289 class Game(GameBase):
290 turn_complete = False
294 def __init__(self, *args, **kwargs):
295 super().__init__(*args, **kwargs)
296 self.register_command(cmd_LOGIN_OK)
297 self.register_command(cmd_ADMIN_OK)
298 self.register_command(cmd_PONG)
299 self.register_command(cmd_CHAT)
300 self.register_command(cmd_REPLY)
301 self.register_command(cmd_PLAYER_ID)
302 self.register_command(cmd_TURN)
303 self.register_command(cmd_THING)
304 self.register_command(cmd_THING_TYPE)
305 self.register_command(cmd_THING_NAME)
306 self.register_command(cmd_THING_CHAR)
307 self.register_command(cmd_TERRAIN)
308 self.register_command(cmd_MAP)
309 self.register_command(cmd_MAP_CONTROL)
310 self.register_command(cmd_PORTAL)
311 self.register_command(cmd_ANNOTATION)
312 self.register_command(cmd_GAME_STATE_COMPLETE)
313 self.register_command(cmd_ARGUMENT_ERROR)
314 self.register_command(cmd_GAME_ERROR)
315 self.register_command(cmd_PLAY_ERROR)
316 self.register_command(cmd_TASKS)
317 self.register_command(cmd_FOV)
318 self.register_command(cmd_DEFAULT_COLORS)
319 self.register_command(cmd_RANDOM_COLORS)
320 self.map_content = ''
322 self.annotations = {}
326 def get_string_options(self, string_option_type):
327 if string_option_type == 'map_geometry':
328 return ['Hex', 'Square']
329 elif string_option_type == 'thing_type':
330 return self.thing_types.keys()
333 def get_command(self, command_name):
334 from functools import partial
335 f = partial(self.commands[command_name], self)
336 f.argtypes = self.commands[command_name].argtypes
341 def __init__(self, name, has_input_prompt=False, shows_info=False,
342 is_intro=False, is_single_char_entry=False):
344 self.short_desc = mode_helps[name]['short']
345 self.available_modes = []
346 self.available_actions = []
347 self.has_input_prompt = has_input_prompt
348 self.shows_info = shows_info
349 self.is_intro = is_intro
350 self.help_intro = mode_helps[name]['long']
351 self.is_single_char_entry = is_single_char_entry
354 def iter_available_modes(self, tui):
355 for mode_name in self.available_modes:
356 mode = getattr(tui, 'mode_' + mode_name)
359 key = tui.keys['switch_to_' + mode.name]
362 def list_available_modes(self, tui):
364 if len(self.available_modes) > 0:
365 msg = 'Other modes available from here:\n'
366 for mode, key in self.iter_available_modes(tui):
367 msg += '[%s] – %s\n' % (key, mode.short_desc)
370 def mode_switch_on_key(self, tui, key_pressed):
371 for mode, key in self.iter_available_modes(tui):
372 if key_pressed == key:
373 tui.switch_mode(mode.name)
378 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
379 mode_admin = Mode('admin')
380 mode_play = Mode('play')
381 mode_study = Mode('study', shows_info=True)
382 mode_write = Mode('write', is_single_char_entry=True)
383 mode_edit = Mode('edit')
384 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
385 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
386 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
387 mode_control_tile_draw = Mode('control_tile_draw')
388 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
389 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
390 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
391 mode_chat = Mode('chat', has_input_prompt=True)
392 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
393 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
394 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
395 mode_password = Mode('password', has_input_prompt=True)
396 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
397 mode_command_thing = Mode('command_thing', has_input_prompt=True)
398 mode_take_thing = Mode('take_thing', has_input_prompt=True)
402 def __init__(self, host):
405 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
406 "command_thing", "take_thing"]
407 self.mode_play.available_actions = ["move", "drop_thing",
408 "teleport", "door", "consume"]
409 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
410 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
411 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
412 "control_tile_type", "chat",
413 "study", "play", "edit"]
414 self.mode_admin.available_actions = ["move"]
415 self.mode_control_tile_draw.available_modes = ["admin_enter"]
416 self.mode_control_tile_draw.available_actions = ["move_explorer",
418 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
419 "password", "chat", "study", "play",
421 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
426 self.parser = Parser(self.game)
428 self.do_refresh = True
429 self.queue = queue.Queue()
430 self.login_name = None
431 self.map_mode = 'terrain + things'
432 self.password = 'foo'
433 self.switch_mode('waiting_for_server')
435 'switch_to_chat': 't',
436 'switch_to_play': 'p',
437 'switch_to_password': 'P',
438 'switch_to_annotate': 'M',
439 'switch_to_portal': 'T',
440 'switch_to_study': '?',
441 'switch_to_edit': 'E',
442 'switch_to_write': 'm',
443 'switch_to_name_thing': 'N',
444 'switch_to_command_thing': 'O',
445 'switch_to_admin_enter': 'A',
446 'switch_to_control_pw_type': 'C',
447 'switch_to_control_tile_type': 'Q',
448 'switch_to_admin_thing_protect': 'T',
450 'switch_to_take_thing': 'z',
456 'toggle_map_mode': 'L',
457 'toggle_tile_draw': 'm',
458 'hex_move_upleft': 'w',
459 'hex_move_upright': 'e',
460 'hex_move_right': 'd',
461 'hex_move_downright': 'x',
462 'hex_move_downleft': 'y',
463 'hex_move_left': 'a',
464 'square_move_up': 'w',
465 'square_move_left': 'a',
466 'square_move_down': 's',
467 'square_move_right': 'd',
469 if os.path.isfile('config.json'):
470 with open('config.json', 'r') as f:
471 keys_conf = json.loads(f.read())
473 self.keys[k] = keys_conf[k]
474 self.show_help = False
475 self.disconnected = True
476 self.force_instant_connect = True
477 self.input_lines = []
481 self.offset = YX(0,0)
482 curses.wrapper(self.loop)
486 def handle_recv(msg):
492 self.log_msg('@ attempting connect')
493 socket_client_class = PlomSocketClient
494 if self.host.startswith('ws://') or self.host.startswith('wss://'):
495 socket_client_class = WebSocketClient
497 self.socket = socket_client_class(handle_recv, self.host)
498 self.socket_thread = threading.Thread(target=self.socket.run)
499 self.socket_thread.start()
500 self.disconnected = False
501 self.game.thing_types = {}
502 self.game.terrains = {}
503 time.sleep(0.1) # give potential SSL negotation some time …
504 self.socket.send('TASKS')
505 self.socket.send('TERRAINS')
506 self.socket.send('THING_TYPES')
507 self.switch_mode('login')
508 except ConnectionRefusedError:
509 self.log_msg('@ server connect failure')
510 self.disconnected = True
511 self.switch_mode('waiting_for_server')
512 self.do_refresh = True
515 self.log_msg('@ attempting reconnect')
517 # necessitated by some strange SSL race conditions with ws4py
518 time.sleep(0.1) # FIXME find out why exactly necessary
519 self.switch_mode('waiting_for_server')
524 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
525 raise BrokenSocketConnection
526 self.socket.send(msg)
527 except (BrokenPipeError, BrokenSocketConnection):
528 self.log_msg('@ server disconnected :(')
529 self.disconnected = True
530 self.force_instant_connect = True
531 self.do_refresh = True
533 def log_msg(self, msg):
535 if len(self.log) > 100:
536 self.log = self.log[-100:]
538 def restore_input_values(self):
539 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
540 self.input_ = self.game.annotations[self.explorer]
541 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
542 self.input_ = self.game.portals[self.explorer]
543 elif self.mode.name == 'password':
544 self.input_ = self.password
545 elif self.mode.name == 'name_thing':
546 if hasattr(self.thing_selected, 'name'):
547 self.input_ = self.thing_selected.name
548 elif self.mode.name == 'admin_thing_protect':
549 if hasattr(self.thing_selected, 'protection'):
550 self.input_ = self.thing_selected.protection
552 def send_tile_control_command(self):
553 self.send('SET_TILE_CONTROL %s %s' %
554 (self.explorer, quote(self.tile_control_char)))
556 def toggle_map_mode(self):
557 if self.map_mode == 'terrain only':
558 self.map_mode = 'terrain + annotations'
559 elif self.map_mode == 'terrain + annotations':
560 self.map_mode = 'terrain + things'
561 elif self.map_mode == 'terrain + things':
562 self.map_mode = 'protections'
563 elif self.map_mode == 'protections':
564 self.map_mode = 'terrain only'
566 def switch_mode(self, mode_name):
567 self.tile_draw = False
568 if mode_name == 'admin_enter' and self.is_admin:
570 elif mode_name in {'name_thing', 'admin_thing_protect'}:
571 player = self.game.get_thing(self.game.player_id)
573 for t in [t for t in self.game.things if t.position == player.position
574 and t.id_ != player.id_]:
579 self.log_msg('? not standing over thing')
582 self.thing_selected = thing
583 self.mode = getattr(self, 'mode_' + mode_name)
584 if self.mode.name == 'control_tile_draw':
585 self.log_msg('@ finished tile protection drawing.')
586 if self.mode.name in {'control_tile_draw', 'control_tile_type',
588 self.map_mode = 'protections'
589 elif self.mode.name != 'edit':
590 self.map_mode = 'terrain + things'
591 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
592 player = self.game.get_thing(self.game.player_id)
593 self.explorer = YX(player.position.y, player.position.x)
594 if self.mode.is_single_char_entry:
595 self.show_help = True
596 if self.mode.name == 'waiting_for_server':
597 self.log_msg('@ waiting for server …')
598 elif self.mode.name == 'login':
600 self.send('LOGIN ' + quote(self.login_name))
602 self.log_msg('@ enter username')
603 elif self.mode.name == 'take_thing':
604 self.log_msg('selectable things:')
605 player = self.game.get_thing(self.game.player_id)
606 selectables = [t for t in self.game.things
607 if t != player and t.type_ != 'Player'
608 and t.position == player.position]
609 if len(selectables) == 0:
612 for t in selectables:
613 self.log_msg(str(t.id_) + ' ' + self.get_thing_info(t))
614 elif self.mode.name == 'command_thing':
615 self.send('TASK:COMMAND ' + quote('HELP'))
616 elif self.mode.name == 'admin_enter':
617 self.log_msg('@ enter admin password:')
618 elif self.mode.name == 'control_pw_type':
619 self.log_msg('@ enter protection character for which you want to change the password:')
620 elif self.mode.name == 'control_tile_type':
621 self.log_msg('@ enter protection character which you want to draw:')
622 elif self.mode.name == 'admin_thing_protect':
623 self.log_msg('@ enter thing protection character:')
624 elif self.mode.name == 'control_pw_pw':
625 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
626 elif self.mode.name == 'control_tile_draw':
627 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']))
629 self.restore_input_values()
631 def set_default_colors(self):
632 curses.init_color(1, 1000, 1000, 1000)
633 curses.init_color(2, 0, 0, 0)
634 self.do_refresh = True
636 def set_random_colors(self):
640 return int(offset + random.random()*375)
642 curses.init_color(1, rand(625), rand(625), rand(625))
643 curses.init_color(2, rand(0), rand(0), rand(0))
644 self.do_refresh = True
648 return self.info_cached
649 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
651 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
652 info_to_cache += 'outside field of view'
654 terrain_char = self.game.map_content[pos_i]
656 if terrain_char in self.game.terrains:
657 terrain_desc = self.game.terrains[terrain_char]
658 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
660 protection = self.game.map_control_content[pos_i]
661 if protection == '.':
662 protection = 'unprotected'
663 info_to_cache += 'PROTECTION: %s\n' % protection
664 for t in self.game.things:
665 if t.position == self.explorer:
666 info_to_cache += 'THING: %s' % self.get_thing_info(t)
667 protection = t.protection
668 if protection == '.':
670 info_to_cache += ' / protection: %s\n' % protection
671 if self.explorer in self.game.portals:
672 info_to_cache += 'PORTAL: ' +\
673 self.game.portals[self.explorer] + '\n'
675 info_to_cache += 'PORTAL: (none)\n'
676 if self.explorer in self.game.annotations:
677 info_to_cache += 'ANNOTATION: ' +\
678 self.game.annotations[self.explorer]
679 self.info_cached = info_to_cache
680 return self.info_cached
682 def get_thing_info(self, t):
684 (t.type_, self.game.thing_types[t.type_])
685 if hasattr(t, 'thing_char'):
687 if hasattr(t, 'name'):
688 info += ' (%s)' % t.name
691 def loop(self, stdscr):
694 def safe_addstr(y, x, line):
695 if y < self.size.y - 1 or x + len(line) < self.size.x:
696 stdscr.addstr(y, x, line, curses.color_pair(1))
697 else: # workaround to <https://stackoverflow.com/q/7063128>
698 cut_i = self.size.x - x - 1
700 last_char = line[cut_i]
701 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
702 stdscr.insstr(y, self.size.x - 2, ' ')
703 stdscr.addstr(y, x, cut, curses.color_pair(1))
705 def handle_input(msg):
706 command, args = self.parser.parse(msg)
709 def task_action_on(action):
710 return action_tasks[action] in self.game.tasks
712 def msg_into_lines_of_width(msg, width):
716 for i in range(len(msg)):
717 if x >= width or msg[i] == "\n":
729 def reset_screen_size():
730 self.size = YX(*stdscr.getmaxyx())
731 self.size = self.size - YX(self.size.y % 4, 0)
732 self.size = self.size - YX(0, self.size.x % 4)
733 self.window_width = int(self.size.x / 2)
735 def recalc_input_lines():
736 if not self.mode.has_input_prompt:
737 self.input_lines = []
739 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
742 def move_explorer(direction):
743 target = self.game.map_geometry.move_yx(self.explorer, direction)
745 self.info_cached = None
746 self.explorer = target
748 self.send_tile_control_command()
754 for line in self.log:
755 lines += msg_into_lines_of_width(line, self.window_width)
758 max_y = self.size.y - len(self.input_lines)
759 for i in range(len(lines)):
760 if (i >= max_y - height_header):
762 safe_addstr(max_y - i - 1, self.window_width, lines[i])
765 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
766 lines = msg_into_lines_of_width(info, self.window_width)
768 for i in range(len(lines)):
769 y = height_header + i
770 if y >= self.size.y - len(self.input_lines):
772 safe_addstr(y, self.window_width, lines[i])
775 y = self.size.y - len(self.input_lines)
776 for i in range(len(self.input_lines)):
777 safe_addstr(y, self.window_width, self.input_lines[i])
781 if not self.game.turn_complete:
783 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
786 help = "hit [%s] for help" % self.keys['help']
787 if self.mode.has_input_prompt:
788 help = "enter /help for help"
789 safe_addstr(1, self.window_width,
790 'MODE: %s – %s' % (self.mode.short_desc, help))
793 if not self.game.turn_complete and len(self.map_lines) == 0:
795 if self.game.turn_complete:
797 for y in range(self.game.map_geometry.size.y):
798 start = self.game.map_geometry.size.x * y
799 end = start + self.game.map_geometry.size.x
800 if self.map_mode == 'protections':
801 map_lines_split += [[c + ' ' for c
802 in self.game.map_control_content[start:end]]]
804 map_lines_split += [[c + ' ' for c
805 in self.game.map_content[start:end]]]
806 if self.map_mode == 'terrain + annotations':
807 for p in self.game.annotations:
808 map_lines_split[p.y][p.x] = 'A '
809 elif self.map_mode == 'terrain + things':
810 for p in self.game.portals.keys():
811 original = map_lines_split[p.y][p.x]
812 map_lines_split[p.y][p.x] = original[0] + 'P'
815 def draw_thing(t, used_positions):
816 symbol = self.game.thing_types[t.type_]
818 if hasattr(t, 'thing_char'):
819 meta_char = t.thing_char
820 if t.position in used_positions:
822 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
823 used_positions += [t.position]
825 for t in [t for t in self.game.things if t.type_ != 'Player']:
826 draw_thing(t, used_positions)
827 for t in [t for t in self.game.things if t.type_ == 'Player']:
828 draw_thing(t, used_positions)
829 player = self.game.get_thing(self.game.player_id)
830 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
831 map_lines_split[self.explorer.y][self.explorer.x] = '??'
832 elif self.map_mode != 'terrain + things':
833 map_lines_split[player.position.y][player.position.x] = '??'
835 if type(self.game.map_geometry) == MapGeometryHex:
837 for line in map_lines_split:
838 self.map_lines += [indent * ' ' + ''.join(line)]
839 indent = 0 if indent else 1
841 for line in map_lines_split:
842 self.map_lines += [''.join(line)]
843 window_center = YX(int(self.size.y / 2),
844 int(self.window_width / 2))
845 center = player.position
846 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
847 center = self.explorer
848 center = YX(center.y, center.x * 2)
849 self.offset = center - window_center
850 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
851 self.offset += YX(0, 1)
852 term_y = max(0, -self.offset.y)
853 term_x = max(0, -self.offset.x)
854 map_y = max(0, self.offset.y)
855 map_x = max(0, self.offset.x)
856 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
857 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
858 safe_addstr(term_y, term_x, to_draw)
863 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
864 self.mode.help_intro)
865 if len(self.mode.available_actions) > 0:
866 content += "Available actions:\n"
867 for action in self.mode.available_actions:
868 if action in action_tasks:
869 if action_tasks[action] not in self.game.tasks:
871 if action == 'move_explorer':
874 key = ','.join(self.movement_keys)
876 key = self.keys[action]
877 content += '[%s] – %s\n' % (key, action_descriptions[action])
879 if self.mode.name == 'chat':
880 content += '/nick NAME – re-name yourself to NAME\n'
881 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
882 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
883 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
884 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
885 content += self.mode.list_available_modes(self)
886 for i in range(self.size.y):
888 self.window_width * (not self.mode.has_input_prompt),
889 ' ' * self.window_width)
891 for line in content.split('\n'):
892 lines += msg_into_lines_of_width(line, self.window_width)
893 for i in range(len(lines)):
897 self.window_width * (not self.mode.has_input_prompt),
902 stdscr.bkgd(' ', curses.color_pair(1))
904 if self.mode.has_input_prompt:
906 if self.mode.shows_info:
911 if not self.mode.is_intro:
917 action_descriptions = {
919 'flatten': 'flatten surroundings',
920 'teleport': 'teleport',
921 'take_thing': 'pick up thing',
922 'drop_thing': 'drop thing',
923 'toggle_map_mode': 'toggle map view',
924 'toggle_tile_draw': 'toggle protection character drawing',
925 'door': 'open/close',
926 'consume': 'consume',
930 'flatten': 'FLATTEN_SURROUNDINGS',
931 'take_thing': 'PICK_UP',
932 'drop_thing': 'DROP',
935 'command': 'COMMAND',
936 'consume': 'INTOXICATE',
939 curses.curs_set(False) # hide cursor
941 self.set_default_colors()
942 curses.init_pair(1, 1, 2)
945 self.explorer = YX(0, 0)
948 interval = datetime.timedelta(seconds=5)
949 last_ping = datetime.datetime.now() - interval
951 if self.disconnected and self.force_instant_connect:
952 self.force_instant_connect = False
954 now = datetime.datetime.now()
955 if now - last_ping > interval:
956 if self.disconnected:
966 self.do_refresh = False
969 msg = self.queue.get(block=False)
974 key = stdscr.getkey()
975 self.do_refresh = True
978 self.show_help = False
979 if key == 'KEY_RESIZE':
981 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
982 self.input_ = self.input_[:-1]
983 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
984 self.show_help = True
986 self.restore_input_values()
987 elif self.mode.has_input_prompt and key != '\n': # Return key
989 max_length = self.window_width * self.size.y - len(input_prompt) - 1
990 if len(self.input_) > max_length:
991 self.input_ = self.input_[:max_length]
992 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
993 self.show_help = True
994 elif self.mode.name == 'login' and key == '\n':
995 self.login_name = self.input_
996 self.send('LOGIN ' + quote(self.input_))
998 elif self.mode.name == 'take_thing' and key == '\n':
999 if self.input_ == '':
1000 self.log_msg('@ aborted')
1002 self.send('TASK:PICK_UP ' + quote(self.input_))
1004 self.switch_mode('play')
1005 elif self.mode.name == 'command_thing' and key == '\n':
1006 if self.input_ == '':
1007 self.log_msg('@ aborted')
1008 self.switch_mode('play')
1009 elif task_action_on('command'):
1010 self.send('TASK:COMMAND ' + quote(self.input_))
1012 elif self.mode.name == 'control_pw_pw' and key == '\n':
1013 if self.input_ == '':
1014 self.log_msg('@ aborted')
1016 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1017 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1018 self.switch_mode('admin')
1019 elif self.mode.name == 'password' and key == '\n':
1020 if self.input_ == '':
1022 self.password = self.input_
1023 self.switch_mode('edit')
1024 elif self.mode.name == 'admin_enter' and key == '\n':
1025 self.send('BECOME_ADMIN ' + quote(self.input_))
1026 self.switch_mode('play')
1027 elif self.mode.name == 'control_pw_type' and key == '\n':
1028 if len(self.input_) != 1:
1029 self.log_msg('@ entered non-single-char, therefore aborted')
1030 self.switch_mode('admin')
1032 self.tile_control_char = self.input_
1033 self.switch_mode('control_pw_pw')
1034 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1035 if len(self.input_) != 1:
1036 self.log_msg('@ entered non-single-char, therefore aborted')
1038 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1039 quote(self.input_)))
1040 self.log_msg('@ sent new protection character for thing')
1041 self.switch_mode('admin')
1042 elif self.mode.name == 'control_tile_type' and key == '\n':
1043 if len(self.input_) != 1:
1044 self.log_msg('@ entered non-single-char, therefore aborted')
1045 self.switch_mode('admin')
1047 self.tile_control_char = self.input_
1048 self.switch_mode('control_tile_draw')
1049 elif self.mode.name == 'chat' and key == '\n':
1050 if self.input_ == '':
1052 if self.input_[0] == '/': # FIXME fails on empty input
1053 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1054 self.switch_mode('play')
1055 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1056 self.switch_mode('study')
1057 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1058 self.switch_mode('edit')
1059 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1060 self.switch_mode('admin_enter')
1061 elif self.input_.startswith('/nick'):
1062 tokens = self.input_.split(maxsplit=1)
1063 if len(tokens) == 2:
1064 self.send('NICK ' + quote(tokens[1]))
1066 self.log_msg('? need login name')
1068 self.log_msg('? unknown command')
1070 self.send('ALL ' + quote(self.input_))
1072 elif self.mode.name == 'name_thing' and key == '\n':
1073 if self.input_ == '':
1075 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1077 quote(self.password)))
1078 self.switch_mode('edit')
1079 elif self.mode.name == 'annotate' and key == '\n':
1080 if self.input_ == '':
1082 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1083 quote(self.password)))
1084 self.switch_mode('edit')
1085 elif self.mode.name == 'portal' and key == '\n':
1086 if self.input_ == '':
1088 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1089 quote(self.password)))
1090 self.switch_mode('edit')
1091 elif self.mode.name == 'study':
1092 if self.mode.mode_switch_on_key(self, key):
1094 elif key == self.keys['toggle_map_mode']:
1095 self.toggle_map_mode()
1096 elif key in self.movement_keys:
1097 move_explorer(self.movement_keys[key])
1098 elif self.mode.name == 'play':
1099 if self.mode.mode_switch_on_key(self, key):
1101 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1102 self.send('TASK:DROP')
1103 elif key == self.keys['door'] and task_action_on('door'):
1104 self.send('TASK:DOOR')
1105 elif key == self.keys['consume'] and task_action_on('consume'):
1106 self.send('TASK:INTOXICATE')
1107 elif key == self.keys['teleport']:
1108 player = self.game.get_thing(self.game.player_id)
1109 if player.position in self.game.portals:
1110 self.host = self.game.portals[player.position]
1114 self.log_msg('? not standing on portal')
1115 elif key in self.movement_keys and task_action_on('move'):
1116 self.send('TASK:MOVE ' + self.movement_keys[key])
1117 elif self.mode.name == 'write':
1118 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1119 self.switch_mode('edit')
1120 elif self.mode.name == 'control_tile_draw':
1121 if self.mode.mode_switch_on_key(self, key):
1123 elif key in self.movement_keys:
1124 move_explorer(self.movement_keys[key])
1125 elif key == self.keys['toggle_tile_draw']:
1126 self.tile_draw = False if self.tile_draw else True
1127 elif self.mode.name == 'admin':
1128 if self.mode.mode_switch_on_key(self, key):
1130 elif key in self.movement_keys and task_action_on('move'):
1131 self.send('TASK:MOVE ' + self.movement_keys[key])
1132 elif self.mode.name == 'edit':
1133 if self.mode.mode_switch_on_key(self, key):
1135 elif key == self.keys['flatten'] and task_action_on('flatten'):
1136 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1137 elif key == self.keys['toggle_map_mode']:
1138 self.toggle_map_mode()
1139 elif key in self.movement_keys and task_action_on('move'):
1140 self.send('TASK:MOVE ' + self.movement_keys[key])
1142 if len(sys.argv) != 2:
1143 raise ArgError('wrong number of arguments, need game host')