World Engine Tutorial Example 3

Foreshadowed

On this page you will find the code for the "Foreshadowed" world. Here is a brief description of this world:

After leaving the orphanage with Anna, you both decide to stay a while in Rivercross, a small and peaceful village. But soon, Anna becomes sick, and you begin to worry. Things get even more troubling when you have a strange, but eerily lifeike nightmare. You can't remember what it was about, but it leaves you wondering if something more sinister isn't waiting just around the corner."

import random

general_data = {
    "stage": 1,
    "num_steps": 0,
    "last_story_text_len": 0,
    "general_lore_text": "This is a fantasy story set in the the Empire of Aeloria, which is a vast, diverse land with towering mountains, lush valleys, and sprawling cities. The human population is divided into distinct provinces, each with its own customs, traditions, and governance, but united under a powerful empress (Empress Seraphina the Enlightened). Adding to this complex tapestry are vast stretches of desolate, ash-covered plains that starkly contrast with the rest of the country. These plains are shrouded in mystery, holding a dark secret that remains unknown to the inhabitants of the empire.",
    "first_scene_text": "The main character is from a small village called Rivercross. The main character grew up in an orphanage but now lives together with Anna (who also grew up in the same orphanage). They are barely scraping by and Anna got sick. Introduce the village name and Anna in the next paragraph."
}

talents = [
    "You have a natural talent for physical activities, characterized by excellent reflexes, agility, and strength.",
    "You possess a charming personality and a knack for persuasion, coupled with an ability to move quietly and unobtrusively.",
    "You are naturally curious and well-read, often absorbing information from books, which makes you knowledgeable in various subjects."
]

abilities = {
    "Frenzy": {
        "max_cooldown": 25,
        "cur_cooldown": 0,
        "description": 'You\'ve had enough and you just "lose it" for a few seconds. You attack everyone and everything in your vicininty. Usually it\'s the bad guys, but sometimes things get out of control.',
        "command": "/frenzy",
        "guideline_text": "Make the main protagonist go into a frenzy for a few seconds and attack everyone in their vicinity with an extreme intensity.",
    },
    "Charm": {
        "max_cooldown": 30,
        "cur_cooldown": 0,
        "description": "You suddenly know all the right words to say, in order to make someone do exactly what you want.",
        "command": "/charm",
        "guideline_text": "The main protagonist suddenly finds all the right words to say, in order to make someone do exactly what they want. Make the protagonist completely charm the person they're currently interacting with."
    },
    "Transform": {
        "max_cooldown": 40,
        "cur_cooldown": 0,
        "command": "/transform",
        "description": "There's an ancient power within you that allows you to transform into a black raven - that part comes to you naturally. However, transforming back into a human is a little more difficult.",
        "guideline_text": "Make the protagonist use their magic power to transform into a black raven right now."
    }
}

character_data = {
    "name": "",
    "description": "",
    "chosen_talent": 0,
    "skills": {
        "melee combat": {"lvl": random.randint(1, 3), "exp": 0},
        "ranged combat": {"lvl": random.randint(1, 3), "exp": 0},
        "running": {"lvl": random.randint(1, 3), "exp": 0},
        "crafting": {"lvl": random.randint(1, 3), "exp": 0},
        "hunting": {"lvl": random.randint(1, 3), "exp": 0},
        "cooking": {"lvl": random.randint(1, 3), "exp": 0},
        "diplomacy": {"lvl": random.randint(1, 3), "exp": 0},
        "sneaking": {"lvl": random.randint(1, 3), "exp": 0},
        "pickpocketing": {"lvl": random.randint(1, 3), "exp": 0},
        "healing": {"lvl": random.randint(1, 3), "exp": 0},
        "alchemy": {"lvl": random.randint(1, 3), "exp": 0},
        "magic": {"lvl": random.randint(1, 3), "exp": 0},
    },
    "abilities": [],
    "is_dead": False
}


def setup():
    ai.perform_generation = False
    if general_data["stage"] == 1:
        if not ui.action:
            ui.display("Please choose a name for your character before proceeding.")
            return
        
        character_data["name"] = ui.action
        general_data["stage"] = 2
        choices_str = ""
        for i in range(1, len(talents)):
            choices_str += f'"{i}", '
        choices_str += f'or "{len(talents)}"'
        ui.display(f"[STEP 2/3]  Please choose a talent by writing {choices_str} in the action input box below. Here are the descriptions of talents:")
        for i, talent in enumerate(talents):
            ui.display(f"{i + 1}. {talent}")

    elif general_data["stage"] == 2:
        if not ui.action:
            ui.display("Please choose a talent before proceeding.")

        try:
            chosen_talent = int(ui.action)
            if chosen_talent not in range(1, len(talents) + 1):
                raise Exception
            chosen_talent -=1
            character_data["chosen_talent"] = chosen_talent

            if chosen_talent == 0:
                character_data["skills"]["melee combat"]["lvl"] = 9
                character_data["skills"]["ranged combat"]["lvl"] = 8
                character_data["skills"]["running"]["lvl"] = 8
                character_data["abilities"] = ["Frenzy"]
            elif chosen_talent == 1:
                character_data["skills"]["diplomacy"]["lvl"] = 9
                character_data["skills"]["sneaking"]["lvl"] = 8
                character_data["skills"]["pickpocketing"]["lvl"] = 8
                character_data["abilities"] = ["Charm"]
            elif chosen_talent == 2:
                character_data["skills"]["diplomacy"]["lvl"] = 7
                character_data["skills"]["alchemy"]["lvl"] = 7
                character_data["skills"]["healing"]["lvl"] = 7
                character_data["skills"]["magic"]["lvl"] = 5
                character_data["abilities"] = ["Transform"]

            general_data["stage"] = 3
            ui.display(f"[STEP 3/3]  Please provide a short description (500 characters max) of your character in the action input box below.")
        except:
            ui.display(f"Please provide a number between 1 and {len(talents)}")

    elif general_data["stage"] == 3:
        if not ui.action:
            ui.display("Please provide a description before proceeding.")
        elif len(ui.action) > 500:
            ui.display("Please provide a shorter description.")
        else:
            character_data["description"] = ui.action
            
            register_has_died_check()
            register_impossible_action_checks_and_gets()
            register_action_success_checks_and_gets()
            update_story_info()
            
            ai.perform_generation = False
            ui.display('You are ready to begin your adventure! During your adventures, you may access information about your character, and their abilities and skills, by pressing the "?" button, which is located next to the "Send" button. To begin your adventure, simply press the "Send" button without specifying any action.')
            general_data["stage"] = 4

    elif general_data["stage"] == 4:
        if ui.action:
            ('Please press "send" without specifying an action.')
        else:
            
            ai.perform_generation = True
            ai.enforce(general_data["general_lore_text"])
            ai.enforce(f'The main protagonist\'s name is "{character_data["name"]}". Here is a description of them them: "' + character_data["description"] + '"')
            ai.enforce(general_data["first_scene_text"])
            general_data["stage"] = 5

def no_action_provided():
    if not ui.action:
        ai.perform_generation = False
        ui.display("Please specify an action.")
        return True
    return False


# HANDLE IMPOSSIBLE ACTIONS
#################################################################################
impossible_action_checks = [
    ("This action involves interacting with an object invented after the year 1800.", "action"),
    ("This action defies the laws of physics or is otherwise impossible.", "action"),
    ("If this were a game in the form of an interactive story, this action would be considered cheating.", "action")
]

impossible_action_gets = [
    (("Rate how plausible this action is on a scale of 1 to 5. A score of 1 means this action makes no sense or is highly implausible based on the context, while a score of 5 means this action is highly plausible.", "int", "action"), 2),
]

def register_impossible_action_checks_and_gets():
    for check in impossible_action_checks:
        ai.future_check(*check)
    for get in impossible_action_gets:
        ai.future_get(*get[0])

def is_doing_impossible_action():
    for check in impossible_action_checks:
        if ai.check(*check):
            ai.perform_generation = False
            ui.display("Attempting an impossible action. Please choose a different action.")
            return True
    
    for get in impossible_action_gets:
        score = ai.get(*get[0])
        if type(score) is int and score <= get[1]:
            ai.perform_generation = False
            ui.display("Attempting an impossible action. Please choose a different action.")
            return True
    return False


# HANDLE ABILITIES
#################################################################################
def is_using_ability():
    for ability_name, ability in abilities.items():
        if ui.action != ability["command"]:
            if abilities[ability_name]["cur_cooldown"] > 0:
                abilities[ability_name]["cur_cooldown"] -= 1
            continue
        if ability_name not in character_data["abilities"]:
            ai.perform_generation = False
            ui.display("You cannot use this ability.")
            return True
        if ability["cur_cooldown"] > 0:
            ai.perform_generation = False
            ui.display("You cannot use this ability yet.") 
            return True

        ai.perform_action = False
        ai.enforce(ability["guideline_text"])
        ability["cur_cooldown"] = ability["max_cooldown"]
        return True
    
    return False
    

# HANDLE ACTIONS AND SKILLS
##########################################################################################
action_success_get = ("On a scale of 1 to 100 (representing percentage), what are the chances of the character performing this action successfully", "int", "action")
skill_involved_get = (f"Which of the following skills does this action involve: {list(character_data['skills'].keys())}. Please specify only one skill. If the action does not involve any of those skills, return `None`.", "str", "action")
def register_action_success_checks_and_gets():
    ai.future_get(*action_success_get)
    ai.future_get(*skill_involved_get)


def determine_action_success():
    action_success_chance = ai.get(*action_success_get)
    skill_involved = ai.get(*skill_involved_get)
    if skill_involved is not None:
        if skill_involved in character_data["skills"]:
            chance_modifier = (character_data["skills"][skill_involved]["lvl"] - 10) * 5
            action_success_chance += chance_modifier
            if action_success_chance < 0:
                action_success_chance = 0
    if random.randint(1, 100) < action_success_chance:
        ai.enforce("Make the character successfully perform the specified action.")
        action_succeeded = True
    else:
        ai.enforce("Make the character fail at performing the specified action.")
        action_succeeded = False
    return action_succeeded, action_success_chance


def update_skills(action_succeeded, action_success_chance):
    skill_involved = ai.get(*skill_involved_get)
    if skill_involved is None:
        return
    if skill_involved not in character_data["skills"]:
        return
    exp_gained = (100 - action_success_chance) + random.randint(-5, 5)
    if not action_succeeded:
        exp_gained = int(exp_gained * 0.1)
    if exp_gained <= 0:
        return
    character_data["skills"][skill_involved]["exp"] += exp_gained
    level_boundry = 100 + (character_data["skills"][skill_involved]["lvl"] * 20)
    if character_data["skills"][skill_involved]["exp"] >= level_boundry:
        character_data["skills"][skill_involved]["exp"] -= level_boundry
        character_data["skills"][skill_involved]["lvl"] += 1
        ui.display(f"You improved the following skill: {skill_involved}!")
            

# HANDLE FORESHADOWING
##########################################################################################
def foreshadow():
    if general_data["num_steps"] in (100, 300, 900):
        ui.display("You suddenly feel as if something dark is lurking somewhere in the corner.")
    if general_data["num_steps"] in (102, 302, 902):
        ai.enforce("Make servants of a dark force appear and attack the main protagonist.")


# HANDLE ALIVE STATUS
##########################################################################################
has_died_check = ("The main protagonist has died.", "story")
def register_has_died_check():
    ai.future_check(*has_died_check)


def character_has_died_pre():
    if character_data["is_dead"]:
        ai.perform_generation = False
        ui.display("Your character has died. Please start a new story.")
        return True
    return False


def character_has_died_post():
    if ai.check(*has_died_check):
        character_data["is_dead"] = True
        return True
    return False


# HANDLE STORY INFO
##########################################################################################
def get_cooldown_text(x): return f"cooldown: {x} turns" if x > 0 else "ready to use"

def update_story_info():
    character_abilities = {ability_name: ability for ability_name, ability in abilities.items() if ability_name in character_data["abilities"]}
    ui.story_info = [
        "Character",
        f"Name: {character_data['name']}",
        f"Description: {character_data['description']}",
        "",
        "Abilities",
    ]

    character_abilities = [f"{ability_name} ({get_cooldown_text(ability['cur_cooldown'])}): {ability['description']} To use this ability, type in the following command in the action input box: \"{ability['command']}\"." for ability_name, ability in character_abilities.items()]
    ui.story_info += character_abilities
    ui.story_info += ["", "Skills"] 
    ui.story_info += [f"{skill_name.capitalize()}: {character_data['skills'][skill_name]['lvl']} ({character_data['skills'][skill_name]['exp']}/{100 + (character_data['skills'][skill_name]['lvl'] * 20)} exp)" for skill_name in character_data["skills"]]

def pre_generate():
    if general_data["stage"] <= 4:
        setup()
    else:
        general_data["num_steps"] += 1

        if character_has_died_pre():
            return
        if no_action_provided():
            return
        if is_doing_impossible_action():
            return
        
        if general_data["num_steps"] <= 100:
            ai.enforce(general_data["general_lore_text"])
            if general_data["num_steps"] <= 30:
                ai.enforce(f'The main protagonist\'s name is "{character_data["name"]}". Here is a description of them them: "' + character_data["description"] + '"')
        
        if is_using_ability():
            return

        action_succeeded, action_success_chance = determine_action_success()
        if len(ui.story_text) != general_data["last_story_text_len"]:
            update_skills(action_succeeded, action_success_chance)
        foreshadow()


def post_generate():
    if character_has_died_post():
        return
    update_story_info()