def test_state_after_making_choice(): first_node = DialogNode("START", "Hello!", [DialogChoice("Good bye!", "END")], NodeGraphics(image_ids=["::image::"])) second_node = DialogNode("END", "It's over.", [], NodeGraphics(image_ids=["::image::"])) graph = DialogGraph("START", [first_node, second_node]) graph.make_choice(0) assert graph.current_node() == second_node
def test_reject_missing_root(): with pytest.raises(ValueError) as excinfo: DialogGraph("MISSING", [ DialogNode("::id::", "::text::", [], NodeGraphics(image_ids=["::image::"])) ]) assert "No node found with ID: MISSING" in str(excinfo.value)
def test_reject_duplicate_ids(): with pytest.raises(ValueError) as excinfo: DialogGraph("DUPLICATE", [ DialogNode("DUPLICATE", "::text::", [], NodeGraphics(image_ids=["::image::"])), DialogNode("DUPLICATE", "::text::", [], NodeGraphics(image_ids=["::image::"])) ]) assert "Duplicate node ID found: DUPLICATE" in str(excinfo.value)
def test_reject_missing_child(): with pytest.raises(ValueError) as excinfo: DialogGraph("ROOT", [ DialogNode( "ROOT", "::text::", [DialogChoice("::text::", "MISSING_CHILD")], NodeGraphics(image_ids=["::image::"]), ) ]) assert "Dialog choice leading to missing node: MISSING_CHILD" in str( excinfo.value)
def generate_graphviz(graph_name: str, dialog_graph: DialogGraph) -> Digraph: graph = Digraph( name=graph_name, comment=f"generated with Graphviz from {graph_name}", node_attr={"shape": "box", "style": "filled", "color": "#BBCCFF"}, edge_attr={"fontsize": "11"} ) for node in dialog_graph.nodes(): node_label = _add_newlines(node.text, 30) graph.node(node.node_id, node_label) for choice in node.choices: edge_label = _add_newlines(choice.text, 20) graph.edge(node.node_id, choice.leads_to_id, label=edge_label) return graph
def test_simple_graphviz(): dialog_graph = DialogGraph( root_node_id="ROOT_NODE", nodes=[ DialogNode("ROOT_NODE", "Start text", [DialogChoice("Choice text", "OTHER_NODE")], NodeGraphics(image_ids=["some_image.png"])), DialogNode("OTHER_NODE", "Other text", [], NodeGraphics(image_ids=["other_image.png"])) ]) graph = generate_graphviz("Some name", dialog_graph) assert graph.name == "Some name" assert set(graph.body) == { '\tROOT_NODE [label="Start text"]', '\tROOT_NODE -> OTHER_NODE [label="Choice text"]', '\tOTHER_NODE [label="Other text"]' }
def _validate_inputs(dialog_graph: DialogGraph, images: Dict[str, Surface], sound_player: SoundPlayer): for node in dialog_graph.nodes(): if node.graphics.image_ids: for image_id in node.graphics.image_ids: if image_id not in images: raise ValueError( f"Invalid config! Graph node '{node.node_id}' refers to missing image: '{image_id}'" ) if node.sound_id: if not sound_player.has_sound(node.sound_id): raise ValueError( f"Invalid config! Graph node '{node.node_id}' refers to missing sound: '{node.sound_id}" ) background_id = dialog_graph.background_image_id if background_id and background_id not in images: raise ValueError( f"Invalid config! Graph refers to missing background image: '{background_id}'" )
def test_initial_state(): node = DialogNode("START", "Hello!", [DialogChoice("Good bye!", "START")], NodeGraphics(image_ids=["::image::"])) graph = DialogGraph("START", [node]) assert graph.current_node() == node
def main(): dialog_graph = DialogGraph( root_node_id="START", nodes=[ DialogNode( node_id="START", text="You are in a dimly lit room. There are two doors leading out of the room, one to the west and " "another one to the east.", choices=[DialogChoice("Exit west", "WEST"), DialogChoice("Exit east", "EAST")]), DialogNode( node_id="WEST", text="You are in a library. There seems to be nothing here of interest.", choices=[DialogChoice("Leave the library", "START")]), DialogNode( node_id="EAST", text="You are in a narrow and straight corridor. On the east end of it, there's a hole in the floor!", choices=[DialogChoice("Leave corridor to the west", "START"), DialogChoice("Jump down in the hole", "BASEMENT")]), DialogNode( node_id="BASEMENT", text="You hurt yourself quite badly in the fall, and find yourself in a dark cellar. You can't see " "anything.", choices=[DialogChoice("Sit down and wait for better days", "SUNLIGHT"), DialogChoice("Feel your way through the room", "SPEAR")]), DialogNode( node_id="SUNLIGHT", text="Eventually the room gets brighter. The sun is shining in through a window and you can see a " "door at the other end of the cellar leading to the outside. The walls are full of mounted spears " "and other nasty things, and you think to yourself that it was a good thing you didn't try to " "navigate here in the dark.", choices=[DialogChoice("Leave through the door", "VICTORY")]), DialogNode( node_id="SPEAR", text="You accidentally walk into spear that's mounted to the wall. You are dead.", choices=[DialogChoice("Retry", "START"), DialogChoice("Exit game", "EXIT")]), DialogNode( node_id="VICTORY", text="Hooray, you escaped!", choices=[DialogChoice("Start from beginning", "START"), DialogChoice("Exit game", "EXIT")]), DialogNode( node_id="EXIT", text="", choices=[]), ], ) while True: node = dialog_graph.current_node() if node.node_id == "EXIT": break print("") print_in_box(node.text, 50) print("") time.sleep(0.5) print("Select one of these choices:") for i, choice in enumerate(node.choices): time.sleep(0.15) print(f"{i} : {choice.text}") choice = -1 valid_choices = range(len(node.choices)) while choice not in valid_choices: text_input = input("> ") try: choice = int(text_input) if choice not in valid_choices: print("Invalid choice. Select one of the listed numbers!") except ValueError: print("Invalid input. Type a number!") print(f"\"{node.choices[choice].text.upper()}\"") time.sleep(0.5) dialog_graph.make_choice(choice)
def main(): pygame.init() directory = Path(__file__).parent font = Font(str(directory.joinpath("demo_font.dfont")), 13) screen_size = (500, 500) dialog_margin = 30 dialog_padding = 5 outer_dialog_size = (screen_size[0] - dialog_margin * 2, 330) dialog_size = (outer_dialog_size[0] - dialog_padding * 2, outer_dialog_size[1] - dialog_padding * 2) picture_component_size = (dialog_size[0], 200) screen = pygame.display.set_mode(screen_size) dialog_surface = Surface(dialog_size) dialog_pos = (dialog_margin + dialog_padding, dialog_margin + dialog_padding) dialog_rect = Rect(dialog_pos, dialog_size) images = { "demo1_background": filled_surface(picture_component_size, (0, 50, 35)) } animations = {"demo1_animation": create_animation(picture_component_size)} text_blip_sound = load_sound("blip.ogg") select_blip_sound = load_sound("blip_2.ogg") select_blip_sound_id = "blip" sound_player = SoundPlayer( sounds={select_blip_sound_id: select_blip_sound}, text_blip_sound=text_blip_sound) dialog_closed_node_id = "DIALOG_CLOSED" dialog_graph = DialogGraph( root_node_id="ROOT", nodes=[ DialogNode( node_id="ROOT", text= "This is a minimal demo app. Let this text slowly appear or click any key to skip it. " "Use the UP/DOWN keys to switch between your dialog choices, and click RETURN to go " "for that choice. Or you could just use the mouse!", choices=[ DialogChoice("See this dialog again", "ROOT"), DialogChoice("Close dialog", dialog_closed_node_id) ], graphics=NodeGraphics(animation_id="demo1_animation")), DialogNode(node_id=dialog_closed_node_id, text="", choices=[], graphics=NodeGraphics(animation_id="demo1_animation")), ], title="DEMO 1", background_image_id="demo1_background") pygame.display.set_caption(dialog_graph.title) dialog_component = DialogComponent( surface=dialog_surface, dialog_font=font, choice_font=font, images=images, animations=animations, sound_player=sound_player, dialog_graph=dialog_graph, picture_size=picture_component_size, select_blip_sound_id=select_blip_sound_id) clock = Clock() is_dialog_shown = True while True: elapsed_time = Millis(clock.tick()) for event in pygame.event.get(): if event.type == pygame.QUIT: _exit_game() elif event.type == pygame.KEYDOWN: if is_dialog_shown: dialog_component.skip_text() if event.key == pygame.K_RETURN: dialog_component.commit_selected_choice() if dialog_component.current_node_id( ) == dialog_closed_node_id: is_dialog_shown = False elif event.key == pygame.K_DOWN: dialog_component.move_choice_selection(1) elif event.key == pygame.K_UP: dialog_component.move_choice_selection(-1) elif event.type == pygame.MOUSEBUTTONDOWN: ui_coordinates = translate_screen_to_ui_coordinates( dialog_rect, pygame.mouse.get_pos()) if ui_coordinates: dialog_component.commit_choice_at_position(ui_coordinates) if dialog_component.current_node_id( ) == dialog_closed_node_id: is_dialog_shown = False elif event.type == pygame.MOUSEMOTION: ui_coordinates = translate_screen_to_ui_coordinates( dialog_rect, pygame.mouse.get_pos()) if ui_coordinates: dialog_component.select_choice_at_position(ui_coordinates) if is_dialog_shown: dialog_component.update(elapsed_time) dialog_component.redraw() screen.fill(BLACK) if is_dialog_shown: screen.blit(dialog_component.surface, dialog_pos) pygame.draw.rect(screen, (255, 100, 100), Rect((dialog_margin, dialog_margin), outer_dialog_size), width=1) fps_string = str(int(clock.get_fps())).rjust(3, ' ') rendered_fps = font.render(f"FPS: {fps_string}", True, WHITE, (0, 0, 0)) screen.blit(rendered_fps, (5, 5)) screen.blit( font.render( "The dialog library is confined to the red rectangle above.", True, WHITE), (15, 400)) screen.blit( font.render("This text is handled separately from the dialog.", True, WHITE), (15, 430)) if not is_dialog_shown: screen.blit( font.render( "Oops, you closed the dialog. Restart app to see it again.", True, (255, 150, 150)), (15, 460)) pygame.display.update()