import argparse from os import environ, path from typing import List, Tuple from dotenv import load_dotenv from packit.utils import logger_with_colors from taleweave.context import ( get_dungeon_master, get_game_config, get_game_systems, set_game_config, set_game_systems, subscribe, ) from taleweave.engine import load_or_generate_world, load_or_initialize_system_data from taleweave.game_system import GameSystem from taleweave.generate import ( generate_character, generate_item, generate_portals, generate_room, link_rooms, ) from taleweave.main import get_world_prompt, load_prompt_library from taleweave.models.base import dump_model from taleweave.models.config import DEFAULT_CONFIG, Config from taleweave.models.entity import World, WorldState from taleweave.models.event import GenerateEvent from taleweave.plugins import load_plugin from taleweave.utils.file import load_yaml, save_yaml from taleweave.utils.search import ( find_character, find_item, find_portal, find_room, list_characters, list_items, list_portals, list_rooms, ) from taleweave.utils.world import describe_entity ENTITY_TYPES = ["room", "portal", "item", "character"] logger = logger_with_colors(__name__) # load environment variables before anything else load_dotenv(environ.get("TALEWEAVE_ENV", ".env"), override=True) def generate_listener(event: GenerateEvent): if event.entity: logger.info(f"Generating {event.entity.type} named {event.entity.name}") else: logger.info(event.name) subscribe(GenerateEvent, generate_listener) def parse_args(): parser = argparse.ArgumentParser(description="TaleWeave AI Editor") parser.add_argument("--config", type=str, help="Configuration file to load") parser.add_argument("--prompts", type=str, nargs="*", help="Prompt files to load") parser.add_argument("--state", type=str, help="State file to edit") parser.add_argument("--world", type=str, help="World file to edit") parser.add_argument("--systems", type=str, nargs="*", help="Game systems to load") subparsers = parser.add_subparsers(dest="command", help="Command to execute") subparsers.required = True # Set up the 'new' command new_parser = subparsers.add_parser("new", help="Create a new world") new_parser.add_argument("name", type=str, help="Name of the new world") new_parser.add_argument("--rooms", type=int, help="Number of rooms to generate") new_parser.add_argument( "--world-flavor", type=str, default="", help="Some additional flavor text for the generated world", ) new_parser.add_argument( "--world-template", type=str, help="The template file to load the world prompt from", ) new_parser.add_argument( "--world-theme", type=str, default="fantasy", help="The theme of the generated world", ) # Set up the 'list' command list_parser = subparsers.add_parser("list", help="List entities of a specific type") list_parser.add_argument( "type", help="Type of entity to list", choices=ENTITY_TYPES ) # Set up the 'describe' command describe_parser = subparsers.add_parser("describe", help="Describe an entity") describe_parser.add_argument( "type", help="Type of entity to describe", choices=ENTITY_TYPES ) describe_parser.add_argument("entity", type=str, help="Entity to describe") # Set up the 'create' command create_parser = subparsers.add_parser("create", help="Create an entity") create_parser.add_argument( "type", help="Type of entity to create", choices=ENTITY_TYPES ) create_parser.add_argument("name", type=str, help="Name of the entity to create") create_parser.add_argument("--room", type=str, help="Room the entity is in") # Set up the 'generate' command generate_parser = subparsers.add_parser("generate", help="Generate an entity") generate_parser.add_argument( "type", help="Type of entity to generate", choices=ENTITY_TYPES ) generate_parser.add_argument( "prompt", type=str, help="Prompt to generate the entity" ) generate_parser.add_argument("--room", type=str, help="Room the entity is in") generate_parser.add_argument( "--dest-room", type=str, help="Destination room for portals" ) # Set up the 'delete' command delete_parser = subparsers.add_parser("delete", help="Delete an entity") delete_parser.add_argument( "type", help="Type of entity to delete", choices=ENTITY_TYPES ) delete_parser.add_argument("entity", type=str, help="Entity to delete") # Set up the 'update' command update_parser = subparsers.add_parser("update", help="Update an entity") update_parser.add_argument( "type", help="Type of entity to update", choices=ENTITY_TYPES ) update_parser.add_argument("entity", type=str, help="Entity to update") update_parser.add_argument("--backstory", type=str, help="Backstory of the entity") update_parser.add_argument( "--description", type=str, help="Description of the entity" ) # Set up the 'link' command link_parser = subparsers.add_parser("link", help="Link rooms") link_parser.add_argument( "rooms", type=str, nargs="*", help="Rooms to link. Leave blank to link all rooms.", ) return parser.parse_args() def load_world(state_file, world_file) -> Tuple[World, WorldState | None]: systems = get_game_systems() if state_file and path.exists(state_file): with open(state_file, "r") as f: state = WorldState(**load_yaml(f)) load_or_initialize_system_data(world_file, systems, state.world) return (state.world, state) if world_file and path.exists(world_file): with open(world_file, "r") as f: world = World(**load_yaml(f)) load_or_initialize_system_data(world_file, systems, world) return (world, None) raise ValueError("No state or world file found") def save_world(state_file, world_file, world: World, state: WorldState | None): """ Save the world to the given files. This is intentionally a noop stub until the editor is more stable. """ if state: logger.warning(f"Saving world {world.name} to {state_file}") return with open(state_file, "w") as f: save_yaml(f, dump_model(WorldState, state)) else: logger.warning(f"Saving world {world.name} to {world_file}") return with open(world_file, "w") as f: save_yaml(f, dump_model(World, world)) def command_new(args): logger.info(f"Creating new world {args.name}") config = get_game_config() systems = get_game_systems() prompt = get_world_prompt(args) world, state_file, _ = load_or_generate_world( args.name, args.state, config, [], systems, prompt, room_count=args.rooms ) save_world(state_file, args.name, world, None) def command_list(args): world, _ = load_world(args.state, args.world) logger.info(f"Listing {args.type}s from world {world.name}") if args.type == "room": for room in list_rooms(world): logger.info(room.name) if args.type == "portal": for portal in list_portals(world): logger.info(portal.name) if args.type == "item": for item in list_items( world, include_character_inventory=True, include_item_inventory=True ): logger.info(item.name) if args.type == "character": for character in list_characters(world): logger.info(character.name) def command_describe(args): world, _ = load_world(args.state, args.world) logger.info(f"Describing {args.entity} from world {world.name}") if args.type == "room": room = find_room(world, args.entity) if not room: logger.error(f"Room {args.entity} not found") else: logger.info(describe_entity(room)) if args.type == "portal": portal = find_portal(world, args.entity) if not portal: logger.error(f"Portal {args.entity} not found") else: logger.info(describe_entity(portal)) if args.type == "item": item = find_item( world, args.entity, include_character_inventory=True, include_item_inventory=True, ) if not item: logger.error(f"Item {args.entity} not found") else: logger.info(describe_entity(item)) if args.type == "character": character = find_character(world, args.entity) if not character: logger.error(f"Character {args.entity} not found") else: logger.info(describe_entity(character)) def command_create(args): world, state = load_world(args.state, args.world) logger.info(f"Create {args.type} named {args.name} in world {world.name}") # TODO: Create the entity save_world(args.state, args.world, world, state) def command_generate(args): world, state = load_world(args.state, args.world) logger.info( f"Generating {args.type} for world {world.name} using prompt: {args.prompt}" ) dungeon_master = get_dungeon_master() systems = get_game_systems() if args.type == "room": room = generate_room(dungeon_master, world, systems) world.rooms.append(room) if args.type == "portal": source_room = find_room(world, args.room) if not source_room: logger.error(f"Room {args.room} not found") return destination_room = find_room(world, args.dest_room) if not destination_room: logger.error(f"Room {args.dest_room} not found") return outgoing_portal, incoming_portal = generate_portals( dungeon_master, world, source_room, destination_room, systems ) source_room.portals.append(outgoing_portal) destination_room.portals.append(incoming_portal) if args.type == "item": # TODO: add item to character or container inventory room = find_room(world, args.room) if not room: logger.error(f"Room {args.room} not found") return item = generate_item(dungeon_master, world, systems) room.items.append(item) if args.type == "character": room = find_room(world, args.room) if not room: logger.error(f"Room {args.room} not found") return character = generate_character( dungeon_master, world, systems, room, args.prompt ) room.characters.append(character) save_world(args.state, args.world, world, state) def command_delete(args): world, state = load_world(args.state, args.world) logger.info(f"Delete {args.entity} from world {world.name}") # TODO: Delete the entity save_world(args.state, args.world, world, state) def command_update(args): world, state = load_world(args.state, args.world) logger.info(f"Update {args.entity} in world {world.name}") if args.type == "room": room = find_room(world, args.entity) if not room: logger.error(f"Room {args.entity} not found") else: logger.info(describe_entity(room)) if args.type == "portal": portal = find_portal(world, args.entity) if not portal: logger.error(f"Portal {args.entity} not found") else: logger.info(describe_entity(portal)) if args.type == "item": item = find_item( world, args.entity, include_character_inventory=True, include_item_inventory=True, ) if not item: logger.error(f"Item {args.entity} not found") else: logger.info(describe_entity(item)) if args.type == "character": character = find_character(world, args.entity) if not character: logger.error(f"Character {args.entity} not found") else: if args.backstory: character.backstory = args.backstory if args.description: character.description = args.description logger.info(describe_entity(character)) save_world(args.state, args.world, world, state) def command_link(args): world, state = load_world(args.state, args.world) logger.info(f"Linking rooms {args.rooms} in world {world.name}") dungeon_master = get_dungeon_master() systems = get_game_systems() link_rooms(dungeon_master, world, systems) save_world(args.state, args.world, world, state) COMMAND_TABLE = { "new": command_new, "list": command_list, "describe": command_describe, "create": command_create, "generate": command_generate, "delete": command_delete, "update": command_update, "link": command_link, } def main(): args = parse_args() logger.debug(f"running with args: {args}") load_prompt_library(args) if args.config: with open(args.config, "r") as f: config = Config(**load_yaml(f)) else: config = DEFAULT_CONFIG set_game_config(config) # load game systems before executing commands systems: List[GameSystem] = [] for system_name in args.systems or []: logger.info(f"loading extra systems from {system_name}") module_systems = load_plugin(system_name) logger.info(f"loaded extra systems: {module_systems}") systems.extend(module_systems) set_game_systems(systems) command = COMMAND_TABLE[args.command] command(args) if __name__ == "__main__": main()