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',
41 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
42 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
45 'short': 'drop thing',
46 'intro': 'Enter number of direction to which you want to drop thing.',
47 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
49 'admin_thing_protect': {
50 'short': 'change thing protection',
51 'intro': '@ enter thing protection character:',
52 'long': 'Change protection character for thing here.'
55 'short': 'enter your face',
56 'intro': '@ enter face line (enter nothing to abort):',
57 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
60 'short': 'change terrain',
62 '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.'
65 'short': 'change protection character password',
66 'intro': '@ enter protection character for which you want to change the password:',
67 '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.'
70 'short': 'change protection character password',
72 '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.'
74 'control_tile_type': {
75 'short': 'change tiles protection',
76 'intro': '@ enter protection character which you want to draw:',
77 '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.'
79 'control_tile_draw': {
80 'short': 'change tiles protection',
82 '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.'
85 'short': 'annotate tile',
87 '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.'
90 'short': 'edit portal',
92 '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.'
97 '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:\n\n/nick NAME – re-name yourself to NAME'
102 'long': 'Enter your player name.'
104 'waiting_for_server': {
105 'short': 'waiting for server response',
106 'intro': '@ waiting for server …',
107 'long': 'Waiting for a server response.'
110 'short': 'waiting for server response',
112 'long': 'Waiting for a server response.'
115 'short': 'set world edit password',
117 '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.'
120 'short': 'become admin',
121 'intro': '@ enter admin password:',
122 'long': 'This mode allows you to become admin if you know an admin password.'
127 'long': 'This mode allows you access to actions limited to administrators.'
131 from ws4py.client import WebSocketBaseClient
132 class WebSocketClient(WebSocketBaseClient):
134 def __init__(self, recv_handler, *args, **kwargs):
135 super().__init__(*args, **kwargs)
136 self.recv_handler = recv_handler
139 def received_message(self, message):
141 message = str(message)
142 self.recv_handler(message)
145 def plom_closed(self):
146 return self.client_terminated
148 from plomrogue.io_tcp import PlomSocket
149 class PlomSocketClient(PlomSocket):
151 def __init__(self, recv_handler, url):
153 self.recv_handler = recv_handler
154 host, port = url.split(':')
155 super().__init__(socket.create_connection((host, port)))
163 for msg in self.recv():
164 if msg == 'NEED_SSL':
165 self.socket = ssl.wrap_socket(self.socket)
167 self.recv_handler(msg)
168 except BrokenSocketConnection:
169 pass # we assume socket will be known as dead by now
171 def cmd_TURN(game, n):
172 game.annotations = {}
176 game.turn_complete = False
178 cmd_TURN.argtypes = 'int:nonneg'
180 def cmd_LOGIN_OK(game):
181 game.tui.switch_mode('post_login_wait')
182 game.tui.send('GET_GAMESTATE')
183 game.tui.log_msg('@ welcome')
184 cmd_LOGIN_OK.argtypes = ''
186 def cmd_ADMIN_OK(game):
187 game.tui.is_admin = True
188 game.tui.log_msg('@ you now have admin rights')
189 game.tui.switch_mode('admin')
190 game.tui.do_refresh = True
191 cmd_ADMIN_OK.argtypes = ''
193 def cmd_REPLY(game, msg):
194 game.tui.log_msg('#MUSICPLAYER: ' + msg)
195 game.tui.do_refresh = True
196 cmd_REPLY.argtypes = 'string'
198 def cmd_CHAT(game, msg):
199 game.tui.log_msg('# ' + msg)
200 game.tui.do_refresh = True
201 cmd_CHAT.argtypes = 'string'
203 def cmd_PLAYER_ID(game, player_id):
204 game.player_id = player_id
205 cmd_PLAYER_ID.argtypes = 'int:nonneg'
207 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
208 t = game.get_thing(thing_id)
210 t = ThingBase(game, thing_id)
214 t.protection = protection
215 t.portable = portable
216 t.commandable = commandable
217 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
219 def cmd_THING_NAME(game, thing_id, name):
220 t = game.get_thing(thing_id)
222 cmd_THING_NAME.argtypes = 'int:pos string'
224 def cmd_THING_FACE(game, thing_id, face):
225 t = game.get_thing(thing_id)
227 cmd_THING_FACE.argtypes = 'int:pos string'
229 def cmd_THING_HAT(game, thing_id, hat):
230 t = game.get_thing(thing_id)
232 cmd_THING_HAT.argtypes = 'int:pos string'
234 def cmd_THING_CHAR(game, thing_id, c):
235 t = game.get_thing(thing_id)
237 cmd_THING_CHAR.argtypes = 'int:pos char'
239 def cmd_MAP(game, geometry, size, content):
240 map_geometry_class = globals()['MapGeometry' + geometry]
241 game.map_geometry = map_geometry_class(size)
242 game.map_content = content
243 if type(game.map_geometry) == MapGeometrySquare:
244 game.tui.movement_keys = {
245 game.tui.keys['square_move_up']: 'UP',
246 game.tui.keys['square_move_left']: 'LEFT',
247 game.tui.keys['square_move_down']: 'DOWN',
248 game.tui.keys['square_move_right']: 'RIGHT',
250 elif type(game.map_geometry) == MapGeometryHex:
251 game.tui.movement_keys = {
252 game.tui.keys['hex_move_upleft']: 'UPLEFT',
253 game.tui.keys['hex_move_upright']: 'UPRIGHT',
254 game.tui.keys['hex_move_right']: 'RIGHT',
255 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
256 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
257 game.tui.keys['hex_move_left']: 'LEFT',
259 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
261 def cmd_FOV(game, content):
263 cmd_FOV.argtypes = 'string'
265 def cmd_MAP_CONTROL(game, content):
266 game.map_control_content = content
267 cmd_MAP_CONTROL.argtypes = 'string'
269 def cmd_GAME_STATE_COMPLETE(game):
270 if game.tui.mode.name == 'post_login_wait':
271 game.tui.switch_mode('play')
272 game.turn_complete = True
273 game.tui.do_refresh = True
274 game.tui.info_cached = None
275 cmd_GAME_STATE_COMPLETE.argtypes = ''
277 def cmd_PORTAL(game, position, msg):
278 game.portals[position] = msg
279 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
281 def cmd_PLAY_ERROR(game, msg):
282 game.tui.log_msg('? ' + msg)
283 game.tui.flash = True
284 game.tui.do_refresh = True
285 cmd_PLAY_ERROR.argtypes = 'string'
287 def cmd_GAME_ERROR(game, msg):
288 game.tui.log_msg('? game error: ' + msg)
289 game.tui.do_refresh = True
290 cmd_GAME_ERROR.argtypes = 'string'
292 def cmd_ARGUMENT_ERROR(game, msg):
293 game.tui.log_msg('? syntax error: ' + msg)
294 game.tui.do_refresh = True
295 cmd_ARGUMENT_ERROR.argtypes = 'string'
297 def cmd_ANNOTATION(game, position, msg):
298 game.annotations[position] = msg
299 if game.tui.mode.shows_info:
300 game.tui.do_refresh = True
301 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
303 def cmd_TASKS(game, tasks_comma_separated):
304 game.tasks = tasks_comma_separated.split(',')
305 game.tui.mode_write.legal = 'WRITE' in game.tasks
306 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
307 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
308 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
309 cmd_TASKS.argtypes = 'string'
311 def cmd_THING_TYPE(game, thing_type, symbol_hint):
312 game.thing_types[thing_type] = symbol_hint
313 cmd_THING_TYPE.argtypes = 'string char'
315 def cmd_THING_INSTALLED(game, thing_id):
316 game.get_thing(thing_id).installed = True
317 cmd_THING_INSTALLED.argtypes = 'int:pos'
319 def cmd_THING_CARRYING(game, thing_id, carried_id):
320 game.get_thing(thing_id).carrying = game.get_thing(carried_id)
321 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
323 def cmd_TERRAIN(game, terrain_char, terrain_desc):
324 game.terrains[terrain_char] = terrain_desc
325 cmd_TERRAIN.argtypes = 'char string'
329 cmd_PONG.argtypes = ''
331 def cmd_DEFAULT_COLORS(game):
332 game.tui.set_default_colors()
333 cmd_DEFAULT_COLORS.argtypes = ''
335 def cmd_RANDOM_COLORS(game):
336 game.tui.set_random_colors()
337 cmd_RANDOM_COLORS.argtypes = ''
339 class Game(GameBase):
340 turn_complete = False
344 def __init__(self, *args, **kwargs):
345 super().__init__(*args, **kwargs)
346 self.register_command(cmd_LOGIN_OK)
347 self.register_command(cmd_ADMIN_OK)
348 self.register_command(cmd_PONG)
349 self.register_command(cmd_CHAT)
350 self.register_command(cmd_REPLY)
351 self.register_command(cmd_PLAYER_ID)
352 self.register_command(cmd_TURN)
353 self.register_command(cmd_THING)
354 self.register_command(cmd_THING_TYPE)
355 self.register_command(cmd_THING_NAME)
356 self.register_command(cmd_THING_CHAR)
357 self.register_command(cmd_THING_FACE)
358 self.register_command(cmd_THING_HAT)
359 self.register_command(cmd_THING_CARRYING)
360 self.register_command(cmd_THING_INSTALLED)
361 self.register_command(cmd_TERRAIN)
362 self.register_command(cmd_MAP)
363 self.register_command(cmd_MAP_CONTROL)
364 self.register_command(cmd_PORTAL)
365 self.register_command(cmd_ANNOTATION)
366 self.register_command(cmd_GAME_STATE_COMPLETE)
367 self.register_command(cmd_ARGUMENT_ERROR)
368 self.register_command(cmd_GAME_ERROR)
369 self.register_command(cmd_PLAY_ERROR)
370 self.register_command(cmd_TASKS)
371 self.register_command(cmd_FOV)
372 self.register_command(cmd_DEFAULT_COLORS)
373 self.register_command(cmd_RANDOM_COLORS)
374 self.map_content = ''
376 self.annotations = {}
380 def get_string_options(self, string_option_type):
381 if string_option_type == 'map_geometry':
382 return ['Hex', 'Square']
383 elif string_option_type == 'thing_type':
384 return self.thing_types.keys()
387 def get_command(self, command_name):
388 from functools import partial
389 f = partial(self.commands[command_name], self)
390 f.argtypes = self.commands[command_name].argtypes
395 def __init__(self, name, has_input_prompt=False, shows_info=False,
396 is_intro=False, is_single_char_entry=False):
398 self.short_desc = mode_helps[name]['short']
399 self.available_modes = []
400 self.available_actions = []
401 self.has_input_prompt = has_input_prompt
402 self.shows_info = shows_info
403 self.is_intro = is_intro
404 self.help_intro = mode_helps[name]['long']
405 self.intro_msg = mode_helps[name]['intro']
406 self.is_single_char_entry = is_single_char_entry
409 def iter_available_modes(self, tui):
410 for mode_name in self.available_modes:
411 mode = getattr(tui, 'mode_' + mode_name)
414 key = tui.keys['switch_to_' + mode.name]
417 def list_available_modes(self, tui):
419 if len(self.available_modes) > 0:
420 msg = 'Other modes available from here:\n'
421 for mode, key in self.iter_available_modes(tui):
422 msg += '[%s] – %s\n' % (key, mode.short_desc)
425 def mode_switch_on_key(self, tui, key_pressed):
426 for mode, key in self.iter_available_modes(tui):
427 if key_pressed == key:
428 tui.switch_mode(mode.name)
433 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
434 mode_admin = Mode('admin')
435 mode_play = Mode('play')
436 mode_study = Mode('study', shows_info=True)
437 mode_write = Mode('write', is_single_char_entry=True)
438 mode_edit = Mode('edit')
439 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
440 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
441 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
442 mode_control_tile_draw = Mode('control_tile_draw')
443 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
444 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
445 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
446 mode_chat = Mode('chat', has_input_prompt=True)
447 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
448 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
449 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
450 mode_password = Mode('password', has_input_prompt=True)
451 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
452 mode_command_thing = Mode('command_thing', has_input_prompt=True)
453 mode_take_thing = Mode('take_thing', has_input_prompt=True)
454 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
455 mode_enter_face = Mode('enter_face', has_input_prompt=True)
459 def __init__(self, host):
462 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
463 "command_thing", "take_thing",
465 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
467 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
468 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
469 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
470 "control_tile_type", "chat",
471 "study", "play", "edit"]
472 self.mode_admin.available_actions = ["move"]
473 self.mode_control_tile_draw.available_modes = ["admin_enter"]
474 self.mode_control_tile_draw.available_actions = ["move_explorer",
476 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
477 "password", "chat", "study", "play",
478 "admin_enter", "enter_face"]
479 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
484 self.parser = Parser(self.game)
486 self.do_refresh = True
487 self.queue = queue.Queue()
488 self.login_name = None
489 self.map_mode = 'terrain + things'
490 self.password = 'foo'
491 self.switch_mode('waiting_for_server')
493 'switch_to_chat': 't',
494 'switch_to_play': 'p',
495 'switch_to_password': 'P',
496 'switch_to_annotate': 'M',
497 'switch_to_portal': 'T',
498 'switch_to_study': '?',
499 'switch_to_edit': 'E',
500 'switch_to_write': 'm',
501 'switch_to_name_thing': 'N',
502 'switch_to_command_thing': 'O',
503 'switch_to_admin_enter': 'A',
504 'switch_to_control_pw_type': 'C',
505 'switch_to_control_tile_type': 'Q',
506 'switch_to_admin_thing_protect': 'T',
508 'switch_to_enter_face': 'f',
509 'switch_to_take_thing': 'z',
510 'switch_to_drop_thing': 'u',
517 'toggle_map_mode': 'L',
518 'toggle_tile_draw': 'm',
519 'hex_move_upleft': 'w',
520 'hex_move_upright': 'e',
521 'hex_move_right': 'd',
522 'hex_move_downright': 'x',
523 'hex_move_downleft': 'y',
524 'hex_move_left': 'a',
525 'square_move_up': 'w',
526 'square_move_left': 'a',
527 'square_move_down': 's',
528 'square_move_right': 'd',
530 if os.path.isfile('config.json'):
531 with open('config.json', 'r') as f:
532 keys_conf = json.loads(f.read())
534 self.keys[k] = keys_conf[k]
535 self.show_help = False
536 self.disconnected = True
537 self.force_instant_connect = True
538 self.input_lines = []
542 self.offset = YX(0,0)
543 curses.wrapper(self.loop)
547 def handle_recv(msg):
553 self.log_msg('@ attempting connect')
554 socket_client_class = PlomSocketClient
555 if self.host.startswith('ws://') or self.host.startswith('wss://'):
556 socket_client_class = WebSocketClient
558 self.socket = socket_client_class(handle_recv, self.host)
559 self.socket_thread = threading.Thread(target=self.socket.run)
560 self.socket_thread.start()
561 self.disconnected = False
562 self.game.thing_types = {}
563 self.game.terrains = {}
564 time.sleep(0.1) # give potential SSL negotation some time …
565 self.socket.send('TASKS')
566 self.socket.send('TERRAINS')
567 self.socket.send('THING_TYPES')
568 self.switch_mode('login')
569 except ConnectionRefusedError:
570 self.log_msg('@ server connect failure')
571 self.disconnected = True
572 self.switch_mode('waiting_for_server')
573 self.do_refresh = True
576 self.log_msg('@ attempting reconnect')
578 # necessitated by some strange SSL race conditions with ws4py
579 time.sleep(0.1) # FIXME find out why exactly necessary
580 self.switch_mode('waiting_for_server')
585 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
586 raise BrokenSocketConnection
587 self.socket.send(msg)
588 except (BrokenPipeError, BrokenSocketConnection):
589 self.log_msg('@ server disconnected :(')
590 self.disconnected = True
591 self.force_instant_connect = True
592 self.do_refresh = True
594 def log_msg(self, msg):
596 if len(self.log) > 100:
597 self.log = self.log[-100:]
599 def restore_input_values(self):
600 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
601 self.input_ = self.game.annotations[self.explorer]
602 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
603 self.input_ = self.game.portals[self.explorer]
604 elif self.mode.name == 'password':
605 self.input_ = self.password
606 elif self.mode.name == 'name_thing':
607 if hasattr(self.thing_selected, 'name'):
608 self.input_ = self.thing_selected.name
609 elif self.mode.name == 'admin_thing_protect':
610 if hasattr(self.thing_selected, 'protection'):
611 self.input_ = self.thing_selected.protection
613 def send_tile_control_command(self):
614 self.send('SET_TILE_CONTROL %s %s' %
615 (self.explorer, quote(self.tile_control_char)))
617 def toggle_map_mode(self):
618 if self.map_mode == 'terrain only':
619 self.map_mode = 'terrain + annotations'
620 elif self.map_mode == 'terrain + annotations':
621 self.map_mode = 'terrain + things'
622 elif self.map_mode == 'terrain + things':
623 self.map_mode = 'protections'
624 elif self.map_mode == 'protections':
625 self.map_mode = 'terrain only'
627 def switch_mode(self, mode_name):
628 if self.mode and self.mode.name == 'control_tile_draw':
629 self.log_msg('@ finished tile protection drawing.')
630 self.tile_draw = False
631 player = self.game.get_thing(self.game.player_id)
632 if mode_name == 'command_thing' and\
633 (not hasattr(player, 'carrying') or not player.carrying.commandable):
634 self.log_msg('? not carrying anything commandable')
636 self.switch_mode('play')
638 if mode_name == 'admin_enter' and self.is_admin:
640 elif mode_name in {'name_thing', 'admin_thing_protect'}:
642 for t in [t for t in self.game.things if t.position == player.position
643 and t.id_ != player.id_]:
648 self.log_msg('? not standing over thing')
651 self.thing_selected = thing
652 self.mode = getattr(self, 'mode_' + mode_name)
653 if self.mode.name in {'control_tile_draw', 'control_tile_type',
655 self.map_mode = 'protections'
656 elif self.mode.name != 'edit':
657 self.map_mode = 'terrain + things'
658 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
659 player = self.game.get_thing(self.game.player_id)
660 self.explorer = YX(player.position.y, player.position.x)
661 if self.mode.is_single_char_entry:
662 self.show_help = True
663 if len(self.mode.intro_msg) > 0:
664 self.log_msg(self.mode.intro_msg)
665 if self.mode.name == 'login':
667 self.send('LOGIN ' + quote(self.login_name))
669 self.log_msg('@ enter username')
670 elif self.mode.name == 'take_thing':
671 self.log_msg('Portable things in reach for pick-up:')
672 player = self.game.get_thing(self.game.player_id)
673 select_range = [player.position,
674 player.position + YX(0,-1),
675 player.position + YX(0, 1),
676 player.position + YX(-1, 0),
677 player.position + YX(1, 0)]
678 if type(self.game.map_geometry) == MapGeometryHex:
679 if player.position.y % 2:
680 select_range += [player.position + YX(-1, 1),
681 player.position + YX(1, 1)]
683 select_range += [player.position + YX(-1, -1),
684 player.position + YX(1, -1)]
685 self.selectables = [t.id_ for t in self.game.things
686 if t.portable and t.position in select_range]
687 if len(self.selectables) == 0:
690 self.switch_mode('play')
693 for i in range(len(self.selectables)):
694 t = self.game.get_thing(self.selectables[i])
695 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
696 elif self.mode.name == 'drop_thing':
697 self.log_msg('Direction to drop thing to:')
699 ['HERE'] + list(self.game.tui.movement_keys.values())
700 for i in range(len(self.selectables)):
701 self.log_msg(str(i) + ': ' + self.selectables[i])
702 elif self.mode.name == 'command_thing':
703 self.send('TASK:COMMAND ' + quote('HELP'))
704 elif self.mode.name == 'control_pw_pw':
705 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
706 elif self.mode.name == 'control_tile_draw':
707 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']))
709 self.restore_input_values()
711 def set_default_colors(self):
712 curses.init_color(1, 1000, 1000, 1000)
713 curses.init_color(2, 0, 0, 0)
714 self.do_refresh = True
716 def set_random_colors(self):
720 return int(offset + random.random()*375)
722 curses.init_color(1, rand(625), rand(625), rand(625))
723 curses.init_color(2, rand(0), rand(0), rand(0))
724 self.do_refresh = True
728 return self.info_cached
729 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
731 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
732 info_to_cache += 'outside field of view'
734 for t in self.game.things:
735 if t.position == self.explorer:
736 info_to_cache += 'THING: %s' % self.get_thing_info(t)
737 protection = t.protection
738 if protection == '.':
740 info_to_cache += ' / protection: %s\n' % protection
741 if hasattr(t, 'hat'):
742 info_to_cache += t.hat[0:6] + '\n'
743 info_to_cache += t.hat[6:12] + '\n'
744 info_to_cache += t.hat[12:18] + '\n'
745 if hasattr(t, 'face'):
746 info_to_cache += t.face[0:6] + '\n'
747 info_to_cache += t.face[6:12] + '\n'
748 info_to_cache += t.face[12:18] + '\n'
749 terrain_char = self.game.map_content[pos_i]
751 if terrain_char in self.game.terrains:
752 terrain_desc = self.game.terrains[terrain_char]
753 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
755 protection = self.game.map_control_content[pos_i]
756 if protection == '.':
757 protection = 'unprotected'
758 info_to_cache += 'PROTECTION: %s\n' % protection
759 if self.explorer in self.game.portals:
760 info_to_cache += 'PORTAL: ' +\
761 self.game.portals[self.explorer] + '\n'
763 info_to_cache += 'PORTAL: (none)\n'
764 if self.explorer in self.game.annotations:
765 info_to_cache += 'ANNOTATION: ' +\
766 self.game.annotations[self.explorer]
767 self.info_cached = info_to_cache
768 return self.info_cached
770 def get_thing_info(self, t):
772 (t.type_, self.game.thing_types[t.type_])
773 if hasattr(t, 'thing_char'):
775 if hasattr(t, 'name'):
776 info += ' (%s)' % t.name
777 if hasattr(t, 'installed'):
778 info += ' / installed'
781 def loop(self, stdscr):
784 def safe_addstr(y, x, line):
785 if y < self.size.y - 1 or x + len(line) < self.size.x:
786 stdscr.addstr(y, x, line, curses.color_pair(1))
787 else: # workaround to <https://stackoverflow.com/q/7063128>
788 cut_i = self.size.x - x - 1
790 last_char = line[cut_i]
791 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
792 stdscr.insstr(y, self.size.x - 2, ' ')
793 stdscr.addstr(y, x, cut, curses.color_pair(1))
795 def handle_input(msg):
796 command, args = self.parser.parse(msg)
799 def task_action_on(action):
800 return action_tasks[action] in self.game.tasks
802 def msg_into_lines_of_width(msg, width):
806 for i in range(len(msg)):
807 if x >= width or msg[i] == "\n":
819 def reset_screen_size():
820 self.size = YX(*stdscr.getmaxyx())
821 self.size = self.size - YX(self.size.y % 4, 0)
822 self.size = self.size - YX(0, self.size.x % 4)
823 self.window_width = int(self.size.x / 2)
825 def recalc_input_lines():
826 if not self.mode.has_input_prompt:
827 self.input_lines = []
829 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
832 def move_explorer(direction):
833 target = self.game.map_geometry.move_yx(self.explorer, direction)
835 self.info_cached = None
836 self.explorer = target
838 self.send_tile_control_command()
844 for line in self.log:
845 lines += msg_into_lines_of_width(line, self.window_width)
848 max_y = self.size.y - len(self.input_lines)
849 for i in range(len(lines)):
850 if (i >= max_y - height_header):
852 safe_addstr(max_y - i - 1, self.window_width, lines[i])
855 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
856 lines = msg_into_lines_of_width(info, self.window_width)
858 for i in range(len(lines)):
859 y = height_header + i
860 if y >= self.size.y - len(self.input_lines):
862 safe_addstr(y, self.window_width, lines[i])
865 y = self.size.y - len(self.input_lines)
866 for i in range(len(self.input_lines)):
867 safe_addstr(y, self.window_width, self.input_lines[i])
871 if not self.game.turn_complete:
873 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
876 help = "hit [%s] for help" % self.keys['help']
877 if self.mode.has_input_prompt:
878 help = "enter /help for help"
879 safe_addstr(1, self.window_width,
880 'MODE: %s – %s' % (self.mode.short_desc, help))
883 if not self.game.turn_complete and len(self.map_lines) == 0:
885 if self.game.turn_complete:
887 for y in range(self.game.map_geometry.size.y):
888 start = self.game.map_geometry.size.x * y
889 end = start + self.game.map_geometry.size.x
890 if self.map_mode == 'protections':
891 map_lines_split += [[c + ' ' for c
892 in self.game.map_control_content[start:end]]]
894 map_lines_split += [[c + ' ' for c
895 in self.game.map_content[start:end]]]
896 if self.map_mode == 'terrain + annotations':
897 for p in self.game.annotations:
898 map_lines_split[p.y][p.x] = 'A '
899 elif self.map_mode == 'terrain + things':
900 for p in self.game.portals.keys():
901 original = map_lines_split[p.y][p.x]
902 map_lines_split[p.y][p.x] = original[0] + 'P'
905 def draw_thing(t, used_positions):
906 symbol = self.game.thing_types[t.type_]
908 if hasattr(t, 'thing_char'):
909 meta_char = t.thing_char
910 if t.position in used_positions:
912 if hasattr(t, 'carrying') and t.carrying:
914 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
915 used_positions += [t.position]
917 for t in [t for t in self.game.things if t.type_ != 'Player']:
918 draw_thing(t, used_positions)
919 for t in [t for t in self.game.things if t.type_ == 'Player']:
920 draw_thing(t, used_positions)
921 player = self.game.get_thing(self.game.player_id)
922 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
923 map_lines_split[self.explorer.y][self.explorer.x] = '??'
924 elif self.map_mode != 'terrain + things':
925 map_lines_split[player.position.y][player.position.x] = '??'
927 if type(self.game.map_geometry) == MapGeometryHex:
929 for line in map_lines_split:
930 self.map_lines += [indent * ' ' + ''.join(line)]
931 indent = 0 if indent else 1
933 for line in map_lines_split:
934 self.map_lines += [''.join(line)]
935 window_center = YX(int(self.size.y / 2),
936 int(self.window_width / 2))
937 center = player.position
938 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
939 center = self.explorer
940 center = YX(center.y, center.x * 2)
941 self.offset = center - window_center
942 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
943 self.offset += YX(0, 1)
944 term_y = max(0, -self.offset.y)
945 term_x = max(0, -self.offset.x)
946 map_y = max(0, self.offset.y)
947 map_x = max(0, self.offset.x)
948 while term_y < self.size.y and map_y < len(self.map_lines):
949 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
950 safe_addstr(term_y, term_x, to_draw)
955 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
956 self.mode.help_intro)
957 if len(self.mode.available_actions) > 0:
958 content += "Available actions:\n"
959 for action in self.mode.available_actions:
960 if action in action_tasks:
961 if action_tasks[action] not in self.game.tasks:
963 if action == 'move_explorer':
966 key = ','.join(self.movement_keys)
968 key = self.keys[action]
969 content += '[%s] – %s\n' % (key, action_descriptions[action])
971 content += self.mode.list_available_modes(self)
972 for i in range(self.size.y):
974 self.window_width * (not self.mode.has_input_prompt),
975 ' ' * self.window_width)
977 for line in content.split('\n'):
978 lines += msg_into_lines_of_width(line, self.window_width)
979 for i in range(len(lines)):
983 self.window_width * (not self.mode.has_input_prompt),
988 stdscr.bkgd(' ', curses.color_pair(1))
990 if self.mode.has_input_prompt:
992 if self.mode.shows_info:
997 if not self.mode.is_intro:
1003 def pick_selectable(task_name):
1005 i = int(self.input_)
1006 if i < 0 or i >= len(self.selectables):
1007 self.log_msg('? invalid index, aborted')
1009 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1011 self.log_msg('? invalid index, aborted')
1013 self.switch_mode('play')
1015 action_descriptions = {
1017 'flatten': 'flatten surroundings',
1018 'teleport': 'teleport',
1019 'take_thing': 'pick up thing',
1020 'drop_thing': 'drop thing',
1021 'toggle_map_mode': 'toggle map view',
1022 'toggle_tile_draw': 'toggle protection character drawing',
1023 'install': '(un-)install',
1024 'wear': '(un-)wear',
1025 'door': 'open/close',
1026 'consume': 'consume',
1030 'flatten': 'FLATTEN_SURROUNDINGS',
1031 'take_thing': 'PICK_UP',
1032 'drop_thing': 'DROP',
1034 'install': 'INSTALL',
1037 'command': 'COMMAND',
1038 'consume': 'INTOXICATE',
1041 curses.curs_set(False) # hide cursor
1042 curses.start_color()
1043 self.set_default_colors()
1044 curses.init_pair(1, 1, 2)
1047 self.explorer = YX(0, 0)
1050 interval = datetime.timedelta(seconds=5)
1051 last_ping = datetime.datetime.now() - interval
1053 if self.disconnected and self.force_instant_connect:
1054 self.force_instant_connect = False
1056 now = datetime.datetime.now()
1057 if now - last_ping > interval:
1058 if self.disconnected:
1068 self.do_refresh = False
1071 msg = self.queue.get(block=False)
1076 key = stdscr.getkey()
1077 self.do_refresh = True
1078 except curses.error:
1080 self.show_help = False
1081 if key == 'KEY_RESIZE':
1083 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1084 self.input_ = self.input_[:-1]
1085 elif self.mode.has_input_prompt and key == '\n' and self.input_ == ''\
1086 and self.mode.name in {'chat', 'command_thing', 'take_thing',
1087 'drop_thing', 'admin_enter'}:
1088 if self.mode.name != 'chat':
1089 self.log_msg('@ aborted')
1090 self.switch_mode('play')
1091 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1092 self.show_help = True
1094 self.restore_input_values()
1095 elif self.mode.has_input_prompt and key != '\n': # Return key
1097 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1098 if len(self.input_) > max_length:
1099 self.input_ = self.input_[:max_length]
1100 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1101 self.show_help = True
1102 elif self.mode.name == 'login' and key == '\n':
1103 self.login_name = self.input_
1104 self.send('LOGIN ' + quote(self.input_))
1106 elif self.mode.name == 'enter_face' and key == '\n':
1107 if len(self.input_) != 18:
1108 self.log_msg('? wrong input length, aborting')
1110 self.send('PLAYER_FACE %s' % quote(self.input_))
1112 self.switch_mode('edit')
1113 elif self.mode.name == 'take_thing' and key == '\n':
1114 pick_selectable('PICK_UP')
1115 elif self.mode.name == 'drop_thing' and key == '\n':
1116 pick_selectable('DROP')
1117 elif self.mode.name == 'command_thing' and key == '\n':
1118 self.send('TASK:COMMAND ' + quote(self.input_))
1120 elif self.mode.name == 'control_pw_pw' and key == '\n':
1121 if self.input_ == '':
1122 self.log_msg('@ aborted')
1124 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1125 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1126 self.switch_mode('admin')
1127 elif self.mode.name == 'password' and key == '\n':
1128 if self.input_ == '':
1130 self.password = self.input_
1131 self.switch_mode('edit')
1132 elif self.mode.name == 'admin_enter' and key == '\n':
1133 self.send('BECOME_ADMIN ' + quote(self.input_))
1134 self.switch_mode('play')
1135 elif self.mode.name == 'control_pw_type' and key == '\n':
1136 if len(self.input_) != 1:
1137 self.log_msg('@ entered non-single-char, therefore aborted')
1138 self.switch_mode('admin')
1140 self.tile_control_char = self.input_
1141 self.switch_mode('control_pw_pw')
1142 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1143 if len(self.input_) != 1:
1144 self.log_msg('@ entered non-single-char, therefore aborted')
1146 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1147 quote(self.input_)))
1148 self.log_msg('@ sent new protection character for thing')
1149 self.switch_mode('admin')
1150 elif self.mode.name == 'control_tile_type' and key == '\n':
1151 if len(self.input_) != 1:
1152 self.log_msg('@ entered non-single-char, therefore aborted')
1153 self.switch_mode('admin')
1155 self.tile_control_char = self.input_
1156 self.switch_mode('control_tile_draw')
1157 elif self.mode.name == 'chat' and key == '\n':
1158 if self.input_ == '':
1160 if self.input_[0] == '/':
1161 if self.input_.startswith('/nick'):
1162 tokens = self.input_.split(maxsplit=1)
1163 if len(tokens) == 2:
1164 self.send('NICK ' + quote(tokens[1]))
1166 self.log_msg('? need login name')
1168 self.log_msg('? unknown command')
1170 self.send('ALL ' + quote(self.input_))
1172 elif self.mode.name == 'name_thing' and key == '\n':
1173 if self.input_ == '':
1175 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1177 quote(self.password)))
1178 self.switch_mode('edit')
1179 elif self.mode.name == 'annotate' and key == '\n':
1180 if self.input_ == '':
1182 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1183 quote(self.password)))
1184 self.switch_mode('edit')
1185 elif self.mode.name == 'portal' and key == '\n':
1186 if self.input_ == '':
1188 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1189 quote(self.password)))
1190 self.switch_mode('edit')
1191 elif self.mode.name == 'study':
1192 if self.mode.mode_switch_on_key(self, key):
1194 elif key == self.keys['toggle_map_mode']:
1195 self.toggle_map_mode()
1196 elif key in self.movement_keys:
1197 move_explorer(self.movement_keys[key])
1198 elif self.mode.name == 'play':
1199 if self.mode.mode_switch_on_key(self, key):
1201 elif key == self.keys['door'] and task_action_on('door'):
1202 self.send('TASK:DOOR')
1203 elif key == self.keys['consume'] and task_action_on('consume'):
1204 self.send('TASK:INTOXICATE')
1205 elif key == self.keys['install'] and task_action_on('install'):
1206 self.send('TASK:INSTALL')
1207 elif key == self.keys['wear'] and task_action_on('wear'):
1208 self.send('TASK:WEAR')
1209 elif key == self.keys['teleport']:
1210 player = self.game.get_thing(self.game.player_id)
1211 if player.position in self.game.portals:
1212 self.host = self.game.portals[player.position]
1216 self.log_msg('? not standing on portal')
1217 elif key in self.movement_keys and task_action_on('move'):
1218 self.send('TASK:MOVE ' + self.movement_keys[key])
1219 elif self.mode.name == 'write':
1220 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1221 self.switch_mode('edit')
1222 elif self.mode.name == 'control_tile_draw':
1223 if self.mode.mode_switch_on_key(self, key):
1225 elif key in self.movement_keys:
1226 move_explorer(self.movement_keys[key])
1227 elif key == self.keys['toggle_tile_draw']:
1228 self.tile_draw = False if self.tile_draw else True
1229 elif self.mode.name == 'admin':
1230 if self.mode.mode_switch_on_key(self, key):
1232 elif key in self.movement_keys and task_action_on('move'):
1233 self.send('TASK:MOVE ' + self.movement_keys[key])
1234 elif self.mode.name == 'edit':
1235 if self.mode.mode_switch_on_key(self, key):
1237 elif key == self.keys['flatten'] and task_action_on('flatten'):
1238 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1239 elif key == self.keys['toggle_map_mode']:
1240 self.toggle_map_mode()
1241 elif key in self.movement_keys and task_action_on('move'):
1242 self.send('TASK:MOVE ' + self.movement_keys[key])
1244 if len(sys.argv) != 2:
1245 raise ArgError('wrong number of arguments, need game host')