From a2a288ba91d65f6c95703bf6a9ecbecc5d32d402 Mon Sep 17 00:00:00 2001 From: holmes Date: Tue, 8 Oct 2024 23:42:46 -0400 Subject: [PATCH] added crafter example --- examples/crafter/README.md | 6 + examples/crafter/baseline.py | 124 ++++ examples/crafter/build_graph_new.py | 581 ++++++++++++++++++ examples/crafter/cache/ctxt.pkl | Bin 0 -> 4682 bytes examples/crafter/compose_prompt.py | 433 +++++++++++++ examples/crafter/crafter/__init__.py | 17 + examples/crafter/crafter/assets/1.png | Bin 0 -> 1827 bytes examples/crafter/crafter/assets/2.png | Bin 0 -> 1501 bytes examples/crafter/crafter/assets/3.png | Bin 0 -> 1559 bytes examples/crafter/crafter/assets/4.png | Bin 0 -> 1533 bytes examples/crafter/crafter/assets/5.png | Bin 0 -> 1518 bytes examples/crafter/crafter/assets/6.png | Bin 0 -> 1567 bytes examples/crafter/crafter/assets/7.png | Bin 0 -> 1538 bytes examples/crafter/crafter/assets/8.png | Bin 0 -> 1506 bytes examples/crafter/crafter/assets/9.png | Bin 0 -> 1535 bytes .../crafter/crafter/assets/arrow-down.png | Bin 0 -> 1825 bytes .../crafter/crafter/assets/arrow-left.png | Bin 0 -> 1731 bytes .../crafter/crafter/assets/arrow-right.png | Bin 0 -> 1752 bytes examples/crafter/crafter/assets/arrow-up.png | Bin 0 -> 1833 bytes examples/crafter/crafter/assets/coal.png | Bin 0 -> 866 bytes examples/crafter/crafter/assets/cow.png | Bin 0 -> 1983 bytes examples/crafter/crafter/assets/debug-2.png | Bin 0 -> 5735 bytes examples/crafter/crafter/assets/debug-3.png | Bin 0 -> 5762 bytes examples/crafter/crafter/assets/debug.png | Bin 0 -> 5761 bytes examples/crafter/crafter/assets/diamond.png | Bin 0 -> 839 bytes examples/crafter/crafter/assets/drink.png | Bin 0 -> 2492 bytes examples/crafter/crafter/assets/energy.png | Bin 0 -> 2213 bytes examples/crafter/crafter/assets/fence.png | Bin 0 -> 2449 bytes examples/crafter/crafter/assets/food.png | Bin 0 -> 2292 bytes examples/crafter/crafter/assets/furnace.png | Bin 0 -> 910 bytes examples/crafter/crafter/assets/grass.png | Bin 0 -> 691 bytes examples/crafter/crafter/assets/health.png | Bin 0 -> 2721 bytes examples/crafter/crafter/assets/iron.png | Bin 0 -> 1048 bytes .../crafter/crafter/assets/iron_pickaxe.png | Bin 0 -> 2354 bytes .../crafter/crafter/assets/iron_sword.png | Bin 0 -> 2390 bytes examples/crafter/crafter/assets/lava.png | Bin 0 -> 2130 bytes examples/crafter/crafter/assets/leaves.png | Bin 0 -> 755 bytes examples/crafter/crafter/assets/log.png | Bin 0 -> 745 bytes examples/crafter/crafter/assets/path.png | Bin 0 -> 7488 bytes .../crafter/crafter/assets/plant-ripe.png | Bin 0 -> 2154 bytes .../crafter/crafter/assets/plant-young.png | Bin 0 -> 2511 bytes examples/crafter/crafter/assets/plant.png | Bin 0 -> 2080 bytes .../crafter/crafter/assets/player-down.png | Bin 0 -> 6186 bytes .../crafter/crafter/assets/player-left.png | Bin 0 -> 6195 bytes .../crafter/crafter/assets/player-right.png | Bin 0 -> 6212 bytes .../crafter/crafter/assets/player-sleep.png | Bin 0 -> 6590 bytes examples/crafter/crafter/assets/player-up.png | Bin 0 -> 6144 bytes examples/crafter/crafter/assets/player.png | Bin 0 -> 813 bytes examples/crafter/crafter/assets/sand.png | Bin 0 -> 729 bytes examples/crafter/crafter/assets/sapling.png | Bin 0 -> 1817 bytes examples/crafter/crafter/assets/skeleton.png | Bin 0 -> 5993 bytes examples/crafter/crafter/assets/stone.png | Bin 0 -> 7242 bytes .../crafter/crafter/assets/stone_pickaxe.png | Bin 0 -> 2345 bytes .../crafter/crafter/assets/stone_sword.png | Bin 0 -> 2429 bytes examples/crafter/crafter/assets/table.png | Bin 0 -> 885 bytes examples/crafter/crafter/assets/tree.png | Bin 0 -> 7782 bytes examples/crafter/crafter/assets/unknown.png | Bin 0 -> 2127 bytes examples/crafter/crafter/assets/water.png | Bin 0 -> 2198 bytes examples/crafter/crafter/assets/wood.png | Bin 0 -> 1577 bytes .../crafter/crafter/assets/wood_pickaxe.png | Bin 0 -> 2045 bytes .../crafter/crafter/assets/wood_sword.png | Bin 0 -> 2453 bytes examples/crafter/crafter/assets/zombie.png | Bin 0 -> 2089 bytes examples/crafter/crafter/constants.py | 7 + examples/crafter/crafter/data.yaml | 102 +++ examples/crafter/crafter/engine.py | 284 +++++++++ examples/crafter/crafter/env.py | 187 ++++++ examples/crafter/crafter/objects.py | 424 +++++++++++++ examples/crafter/crafter/recorder.py | 185 ++++++ examples/crafter/crafter/run_gui.py | 150 +++++ examples/crafter/crafter/run_random.py | 48 ++ examples/crafter/crafter/run_terrain.py | 43 ++ examples/crafter/crafter/worldgen.py | 91 +++ examples/crafter/crafter_description.py | 192 ++++++ examples/crafter/crafter_initial_QA.pkl | Bin 0 -> 15686 bytes examples/crafter/main.py | 534 ++++++++++++++++ examples/crafter/post_processing.py | 257 ++++++++ examples/crafter/prompts.py | 0 examples/crafter/tests.py | 86 +++ examples/crafter/utils.py | 137 +++++ 79 files changed, 3888 insertions(+) create mode 100644 examples/crafter/README.md create mode 100644 examples/crafter/baseline.py create mode 100644 examples/crafter/build_graph_new.py create mode 100644 examples/crafter/cache/ctxt.pkl create mode 100644 examples/crafter/compose_prompt.py create mode 100644 examples/crafter/crafter/__init__.py create mode 100644 examples/crafter/crafter/assets/1.png create mode 100644 examples/crafter/crafter/assets/2.png create mode 100644 examples/crafter/crafter/assets/3.png create mode 100644 examples/crafter/crafter/assets/4.png create mode 100644 examples/crafter/crafter/assets/5.png create mode 100644 examples/crafter/crafter/assets/6.png create mode 100644 examples/crafter/crafter/assets/7.png create mode 100644 examples/crafter/crafter/assets/8.png create mode 100644 examples/crafter/crafter/assets/9.png create mode 100644 examples/crafter/crafter/assets/arrow-down.png create mode 100644 examples/crafter/crafter/assets/arrow-left.png create mode 100644 examples/crafter/crafter/assets/arrow-right.png create mode 100644 examples/crafter/crafter/assets/arrow-up.png create mode 100644 examples/crafter/crafter/assets/coal.png create mode 100644 examples/crafter/crafter/assets/cow.png create mode 100644 examples/crafter/crafter/assets/debug-2.png create mode 100644 examples/crafter/crafter/assets/debug-3.png create mode 100644 examples/crafter/crafter/assets/debug.png create mode 100644 examples/crafter/crafter/assets/diamond.png create mode 100644 examples/crafter/crafter/assets/drink.png create mode 100644 examples/crafter/crafter/assets/energy.png create mode 100644 examples/crafter/crafter/assets/fence.png create mode 100644 examples/crafter/crafter/assets/food.png create mode 100644 examples/crafter/crafter/assets/furnace.png create mode 100644 examples/crafter/crafter/assets/grass.png create mode 100644 examples/crafter/crafter/assets/health.png create mode 100644 examples/crafter/crafter/assets/iron.png create mode 100644 examples/crafter/crafter/assets/iron_pickaxe.png create mode 100644 examples/crafter/crafter/assets/iron_sword.png create mode 100644 examples/crafter/crafter/assets/lava.png create mode 100644 examples/crafter/crafter/assets/leaves.png create mode 100644 examples/crafter/crafter/assets/log.png create mode 100644 examples/crafter/crafter/assets/path.png create mode 100644 examples/crafter/crafter/assets/plant-ripe.png create mode 100644 examples/crafter/crafter/assets/plant-young.png create mode 100644 examples/crafter/crafter/assets/plant.png create mode 100644 examples/crafter/crafter/assets/player-down.png create mode 100644 examples/crafter/crafter/assets/player-left.png create mode 100644 examples/crafter/crafter/assets/player-right.png create mode 100644 examples/crafter/crafter/assets/player-sleep.png create mode 100644 examples/crafter/crafter/assets/player-up.png create mode 100644 examples/crafter/crafter/assets/player.png create mode 100644 examples/crafter/crafter/assets/sand.png create mode 100644 examples/crafter/crafter/assets/sapling.png create mode 100644 examples/crafter/crafter/assets/skeleton.png create mode 100644 examples/crafter/crafter/assets/stone.png create mode 100644 examples/crafter/crafter/assets/stone_pickaxe.png create mode 100644 examples/crafter/crafter/assets/stone_sword.png create mode 100644 examples/crafter/crafter/assets/table.png create mode 100644 examples/crafter/crafter/assets/tree.png create mode 100644 examples/crafter/crafter/assets/unknown.png create mode 100644 examples/crafter/crafter/assets/water.png create mode 100644 examples/crafter/crafter/assets/wood.png create mode 100644 examples/crafter/crafter/assets/wood_pickaxe.png create mode 100644 examples/crafter/crafter/assets/wood_sword.png create mode 100644 examples/crafter/crafter/assets/zombie.png create mode 100644 examples/crafter/crafter/constants.py create mode 100644 examples/crafter/crafter/data.yaml create mode 100644 examples/crafter/crafter/engine.py create mode 100644 examples/crafter/crafter/env.py create mode 100644 examples/crafter/crafter/objects.py create mode 100644 examples/crafter/crafter/recorder.py create mode 100644 examples/crafter/crafter/run_gui.py create mode 100644 examples/crafter/crafter/run_random.py create mode 100644 examples/crafter/crafter/run_terrain.py create mode 100644 examples/crafter/crafter/worldgen.py create mode 100644 examples/crafter/crafter_description.py create mode 100644 examples/crafter/crafter_initial_QA.pkl create mode 100644 examples/crafter/main.py create mode 100644 examples/crafter/post_processing.py create mode 100644 examples/crafter/prompts.py create mode 100644 examples/crafter/tests.py create mode 100644 examples/crafter/utils.py diff --git a/examples/crafter/README.md b/examples/crafter/README.md new file mode 100644 index 0000000..0e37c39 --- /dev/null +++ b/examples/crafter/README.md @@ -0,0 +1,6 @@ +# Using AgentKit for Crafter + +Run +``` +python main.py +``` \ No newline at end of file diff --git a/examples/crafter/baseline.py b/examples/crafter/baseline.py new file mode 100644 index 0000000..39087f9 --- /dev/null +++ b/examples/crafter/baseline.py @@ -0,0 +1,124 @@ +import os +os.environ["MINEDOJO_HEADLESS"]="1" +import argparse +import numpy as np +from tqdm import tqdm +import gym +import crafter +from crafter_description import describe_frame, action_list, match_act +from functools import partial +from utils import get_ctxt, describe_achievements +MANUAL = get_ctxt() + +parser = argparse.ArgumentParser() +parser.add_argument('--llm_name', type=str, default='yintat-all-gpt-4', help='Name of the LLM') + +args = parser.parse_args() + +LLM_name = args.llm_name + +env = crafter.Env(area=(256, 256)) +action_space = env.action_space + +# Replace with your own LLM API. +# Note: query_model takes two arguments: 1) message in openai chat completion form (list of dictionaries), +# 2) an index to indicate where the message should be truncated if the length exceeds LLM context length. +from llm_api import get_query +query_model = partial(get_query(LLM_name), max_gen=2048) + +def compose_ingame_prompt(info, question, past_qa=[]): + messages = [ + {"role": "system", "content" : "You’re a player trying to play the game."} + ] + + if len(info['manual'])>0: + messages.append({"role": "system", "content": info['manual']}) + + messages.append({"role": "system", "content": "{}".format(info['obs'])}) + + if len(past_qa)>0: + for q,a in past_qa: + messages.append({"role": "user", "content": q}) + messages.append({"role": "assistant", "content": a}) + + messages.append({"role": "user", "content": question}) + + return messages, 1 # This is the index of the history, we will truncate the history if it is too long for LLM + +questions=[ + "What is the best action to take? Let's think step by step, ", + "Choose the best executable action from the list of all actions. Write the exact chosen action." + ] + +def run(): + env = crafter.Env(area=(256, 256)) + env_steps = 1000000 + num_iter = 2 + + rewards = [] + progresses = [] + for eps in tqdm(range(num_iter), desc="Evaluating LLM {}".format(LLM_name)): + import wandb + wandb.init(project="Crafter_baseline", config={"LLM": LLM_name, "eps": eps, "num_iter": num_iter, "env_steps": env_steps}) + step = 0 + trajectories = [] + qa_history = [] + progress = [0] + reward = 0 + rewards = [] + done=False + + columns=["Context", "Step", "OBS", "Score", "Reward", "Total Reward"] + questions + ["Action"] + wandb_table = wandb.Table(columns=columns) + + env.reset() + a = action_list.index("noop") + obs, reward, done, info = env.step(a) + + while step < env_steps: + last_act_desc, desc = describe_frame(info, 1) + if len(trajectories)>0: + trajectories[-1][1] = last_act_desc + trajectories.append([step, None, desc]) + text_obs = "\n\n".join(["== Gamestep {}{} ==\n\n".format(i, "" if i!=trajectories[-1][0] else " (current)",) + "{}{}".format(d, "\n\nAction:\n{}".format(a) if a is not None else "") for i, a, d in trajectories[-2:]]) + info['obs'] = text_obs + info['manual'] = describe_achievements(info, MANUAL) + info['reward'] = reward + info['score'] = sum(rewards) + new_row = [info['manual'], step, info['obs'], info['score'], reward, sum(rewards)] + wandb.log({"metric/total_reward".format(eps): sum(rewards), + "metric/score".format(eps): info['score'], + "metric/reward".format(eps): reward, + }) + + if done: + break + + qa_history = [] + for question in questions: + prompt = compose_ingame_prompt(info, question, qa_history) + answer, _ = query_model(*prompt) + qa_history.append((question, answer)) + new_row.append(answer) + answer_act = answer + + a, _, _ = match_act(answer_act) + if a is None: + a = action_list.index("noop") + new_row.append(action_list[a]) + obs, reward, done, info = env.step(a) + rewards.append(reward) + + step += 1 + wandb_table.add_data(*new_row) + + progresses.append(np.max(progress)) + wandb.log({"rollout/rollout-{}".format(eps): wandb_table, + "final/total_reward":sum(rewards), + "final/episodic_step":step, + "final/eps":eps, + }) + del wandb_table + wandb.finish() + +run() \ No newline at end of file diff --git a/examples/crafter/build_graph_new.py b/examples/crafter/build_graph_new.py new file mode 100644 index 0000000..98cc6c7 --- /dev/null +++ b/examples/crafter/build_graph_new.py @@ -0,0 +1,581 @@ +from agentkit import Graph, SimpleDBNode +from compose_prompt import * +from post_processing import * + +query_fast = 'query_fast' +query_fast_accurate = 'query_fast_accurate' +query_reason = 'query_reason' +query_plan_accurate = 'query_plan_accurate' +query_spatial = 'query_spatial' + +prompts = { + +'obs_obj':{ +'prompt':""" +First, describe all objects the player faces or around the player. Describe the object type, the direction, the distance, the coordinates, and the requirements to interact with the object from the instruction manual. +Be precise and accurate with the direction, coordinates. +Output a Json list in the following format: +[ +{"object":$type, "direction":$precise_direction, "distance":$distance$, "facing":$[yes/no], 'coordinate':"precise coordinates", "requirements":"requirements to interact from instruction manual, put 'NA' if not applicable"}, +... +] + +Second, in one sentence, describe what the player sees further to the front. +""", +'dep':[], +'shorthand': "Current observation/surroundings", +'compose': ComposeObservationPrompt(), +'query': query_fast_accurate, +}, + +'obs_inv':{ +'prompt':"""Describe the player's inventory as a json dictionary, with the item_names as key and item_amount as value. Write '{}' if inventory is empty.""", +'dep':[], +'shorthand': "Current inventory", +'compose': ComposeObservationPrompt(), +'query': query_fast, +}, + +'obs_vit':{ +'prompt':"""Describe the player's vitals as a json dictionary, in the format of {vital_name: "value/max"}.""", +'dep':[], +'shorthand': "Current vitals", +'compose': ComposeObservationPrompt(), +'query': query_fast, +}, + +'obs_chg':{ +'prompt':"""Using a list, document any changes to the player observation, inventory, and status in the most recent step. +Be precise about changes to numbers and details about the vitals, inventory, and the distance to objects around the player. +Be concise and only list the changes. Output 'NA' if there is no change""", +'dep':[], +'shorthand': "Changes to the observation in the current step", +'compose': ComposeObservationReasoningPrompt(), +'query': query_reason, +}, + +'obs_last_act':{ +'prompt':"""First, consider how the most recent action should have changed the player's observation/status/inventory, be specific with the action and the target. +Then, deduce if the last action succeeded. +If yes, end the answer here. +If not, identify potential cause for the failure, by focusing on the player's surrounding/observation, inventory, instruction manual, and missing information. +Be concise with the language, but precise with the details.""", +'dep':['obs_chg',], +'compose': ComposeActionReflectionPrompt(), +'query': query_spatial, +}, + + +'s-obs-obs':{ +'prompt':"""In two sentences, describe the observation and surroundings, including all object types and what the player is facing. +Make sure to include the location details of objects relevant to the current plan '$db.action_summary.plan-sketch$' and target '$db.action_summary.target$'.""", +'dep':['obs_obj'], +'shorthand': "Player observation", +'compose': ComposeSummaryPrompt(), +'query': query_fast, +}, + +'s-obs-vitals':{ +'prompt':"In one sentence, describe all inventory items and the current vitals.", +'dep':['obs_inv', 'obs_vit'], +'shorthand': "Player vitals", +'compose': ComposeSummaryPrompt(), +'query': query_fast, +}, + +'s-action':{ +'prompt':"""Output a Json dictionary of the following format: +``` +{ +"action": $action, # The most recent action, including the direction if it's a movement action +"repeats": $repeats # The number of times the action was repeated +"target": $target # The target of the action. Write 'NA' if the action is a movement/sleep action. +"success": $success # [yes/no] If the action succeeded +"causes_of_failure": $causes_of_failure # In one sentence, write the cause(s) of the failure. Write 'NA' if the action succeeded. +} +``` +""", +'dep':['obs_last_act',], +'shorthand': "Most recent action and result", +'compose': ComposeSummaryPrompt(), +'query': query_fast, +'after_query': SummaryAfterQuery(), +}, + +'obs_current_actions':{ +'prompt':"""For each action in the numbered list of all actions, reason with the current observation/surroundings and identify if the action is allowed at the current gamestep according to the requirements. +Then predict the target the action will interact with. +Format the output as a Json dictionary of the following format: +{ +"$action": {"requirements": $requirements of the action, "related observation": $what in the observation may affect/block the action?, "reasoning": $precise but very concise reason of why the action is blocked, "allowed": $yes/no, "target": $inferred target, "unlock new achievement": $yes/no, will the success of this action unlock an unaccomplished achievement?}, # do not output white spaces +... +} +""", +'dep':['s-action', 'obs_obj'], +'shorthand': "List of all actions", +'compose': ComposeObservationActionReasoningPrompt(), +'query': query_plan_accurate, +'after_query': ListActionAfterQuery(), +}, + +'reflect':{ +'prompt':"""Consider how the player reached the current state, concisely summarize the player's high-level trajectory in the gameplay history. Focus on the present and the past and do not make future predictions. +Has the player been making effective progress towards the top subgoal '$db.subgoals.subgoal$'? Has the player been effectively addressing all the mistakes/obstacles encountered? +Output 'NA' if there is no history.""", +'dep': ['obs_obj', 'obs_inv', 'obs_vit', 'obs_chg', 'obs_last_act'], +'after': ['s-obs-obs', 's-obs-vitals', 's-action',], +'shorthand': "Gameplay history", +'compose': ComposePlannerPrompt(), +'query': query_reason, +}, + +'planner_unexpected':{ +'prompt':"""Did the player encounter any obstacles or unexpected scenarios? Also, did player status change unexpectedly? +Write \"NA\" and end answer here if no expectancy. +Otherwise, should the player modify the subgoals to react?""", +'dep':['s-obs-obs', 's-obs-vitals', 'obs_obj', 'obs_inv', 'obs_chg', 'obs_last_act'], +'after': ['s-action',], +'short': "Past Unexpected Scenarios", +'compose': ComposePlannerReflectionPrompt(), +'query': query_reason, +}, + +'planner_mistake':{ +'prompt':"""Did the player make any mistakes approaching the top subgoal '$db.subgoals.subgoal$'? Write \"NA\" and stop the answer here if there's no mistake. +Otherwise, identify the potential causes of the mistake and find the most probable cause by analyzing the current situation. +Then, suggest a precise and executable modification to the subgoals to address the mistake.""", +'dep':['s-obs-obs', 's-obs-vitals', 'obs_chg', 'obs_current_actions'], +'after': ['s-action',], +'short': "Past Mistakes", +'compose': ComposePlannerReflectionPrompt(), +'query': query_reason, +}, + +'challenge':{ +'prompt':"""Identify three high level challenges. +Based on the current situation, score their urgency out of 5 (5 is the highest urgency) and score their convenience out of 5 (5 being most convenient). +Then choose a challenge to address based on the urgency, convenience, and overall objective of the game. +Be concise.""", +'dep':['reflect', 'planner_mistake', 'obs_obj', 'obs_inv', 'obs_vit'], +'after': ['s-action',], +'compose': ComposePlannerPrompt(), +'query': query_reason, +}, + +'achievements':{ +'prompt':"""Reason with the current situation and the requirements of the *current unaccomplished achievements*. +Identify a list of unaccomplished achievements that are easily unlockable and relevant to the current situation. +For each achievement, write a one-sentence explanation of how the achievement may be easily unlocked, and how it may benefit.""", +'dep':['reflect', 'obs_obj', 'obs_inv', 'obs_vit', 'obs_current_actions'], +'shorthand': "Relevant unaccomplished achievements", +'after': ['s-action',], +'compose': ComposePlannerPrompt(), +'query': query_reason, +}, + +'gate-plan_sketch':{ +'prompt':"""Reason with the instruction manual and knowledge base. Explain concisely how the choosen challenge may be approached given the player's current situation, with actions permitted by the game. +Then, +1. Confirm if the subgoal '$db.subgoals.subgoal$' is still accuracte for the choosen challenge. +2. Confirm if the subgoal '$db.subgoals.subgoal$' is incomplete and up-to-date according to the completion criteria '$db.subgoals.completion_criteria$'. + +If yes to both, end the answer here. +Then, sketch an updated high-level plan for addressing the choosen challenge. Do not plan to go back to something if you cannot provide it's location. +If situation allows, the plan-sketch should aim for achieving relevant unaccomplished achievements in the order of difficulty, without impacting player safety.""", +'dep':['reflect', 'planner_unexpected', 'planner_mistake', 'obs_obj', 'obs_inv', 'obs_vit', 'obs_current_actions', 'achievements', 'challenge'], +'compose': ComposePlannerPrompt(), +'query': query_reason, +}, + +'gate':{ +'prompt':"""Reason with the previous conversation and context, and output a Json dictionary with answers to the following questions: +``` +{ +"unexpected_encounters": $ANSWER, # [yes/no] Did the player encounter any unexpected scenarios or obstacles? +"mistake": $ANSWER, # [yes/no] Did the player make any mistakes? +"correction_planned": $ANSWER, # [yes/no] Was correction planned at current step for the mistake? +"confused": $ANSWER, # [yes/no] Does the player seem confused? +"top_subgoal_completed": $ANSWER, # [yes/no] Is the most recent top subgoal complete according to the completion criteria? +"top_subgoal_changed": $ANSWER, # [yes/no] Has the most recent top subgoal been changed? +"replan": $ANSWER # [yes/no] Was there a re-plan/change for the plan sketch? +} +``` +""", +'dep':['reflect', 'planner_unexpected', 'planner_mistake', 'gate-plan_sketch'], +'compose': ComposePlannerPrompt(), +'query': query_reason, +'after_query': ReflectionAfterQuery(), +}, + +'subgoals':{ +'prompt':"""List the top 3 subgoals for the player according to the plan sketch. The subgoals should be quantifiable and actionable with in-game actions. Put subgoals of highest priority first. +For each subgoal, specify how the completion may be precisely quantified in at most one sentence (include the numbers and details). +Do not include additional information other than the subgoals and quantification.""", +'dep':['reflect', 'obs_obj', 'obs_inv', 'obs_vit', 'gate-plan_sketch'], +'after': ['gate'], +'shorthand': "Current subgoals (decreasing priority)", +'compose': ComposePlannerPrompt(), +'query': query_reason, +}, + +'top-subgoal':{ +'prompt':"""Write the highest priority subgoal and completion criteria as a Json dictionary of the following format: +``` +{ +"subgoal": $subgoal, # The highest priority subgoal +"completion_criteria": $completion_criteria # The completion criteria for the subgoal +"guide": $guide # A brief high-level guide for completing the subgoal +} +``` +""", +'dep':['reflect', 's-action', 'obs_current_actions', 'subgoals'], +'shorthand': "Current top subgoal and completion criteria", +'compose': ComposePlannerPrompt(), +'query': query_reason, +'after_query': SubgoalAfterQuery(), +}, + +'subgoal_analysis':{ +'prompt':"""Check the instruction manual for requirements to complete the top subgoal. +Identify a list of unknown/missing information/details and do not make any assumptions. +Pay close attention to the details such as the numbers and specifications.""", +'dep':['reflect', 'obs_current_actions', 'top-subgoal'], +'shorthand': "Requirement and Missing Info Analysis for the Top Subgoal", +'compose': ComposePlannerPrompt(), +'query': query_reason, +}, + +'skill':{ +'prompt':"""First, try to find an existing skill from the 'skill library' that could be used to represent the current top subgoal. + +If none of the existing skills could be applied, create a new skill. + +The skill, parameters, and guide should be general enough to be reusable for tasks of the same class. + +Format the choosen skill as a Json object of the following format: +``` +{$skill_name: [$1_line_skill_desciption, $supported_parameters, $skill_guide]} +``` +""", +'dep':['top-subgoal'], +'compose': ComposeSkillPrompt(), +'query': query_reason, +'after_query': SkillAfterQuery(), +}, + +'planner-adaptive':{ +'prompt':"""List at most 3 questions to help guide the agent on the subgoal: '$db.subgoals.subgoal$'. The questions should prompt the player to think concretely and precisely. +Example questions could ask the player to: + - identify goal-relevant details from the observation, history, and instruction manual + - recall related historical encounters + - potential obstacles and how to overcome them + - specify concrete ways to improve efficiency + +Output only the list of questions and nothing else. Write \"NA\" if there's no question to ask.""", +'dep':['reflect', 'planner_unexpected', 'planner_mistake', 'top-subgoal', 'subgoal_analysis'], +'compose': ComposePlannerPrompt(), +'query': query_reason, +'after_query': AdaptiveAfterQuery(), +}, + +'kb-add':{ +'prompt':"""Rewrite the unknown information and details list into a Json dictionary. +Give each item a concise but precise name as the key, and a dictionary of answers to the following inquiries as the value: +``` +"item_name":{ +"discovered": $ANSWER, # Can this unknown information be precisely uncovered or determined at the current gamestep? [yes/no] +"discovery": $ANSWER, # In concise but precise terms, write what has been uncovered [If the information is not uncovered, write 'NA'.] +"discovery_short": $ANSWER, # Condensed version of 'discovery', only containing precisely the discovered info [If the information is not uncovered, write "NA".] +"general": $ANSWER, # Confirm that this uncovered information remain unchanged in subsequent steps of this game. [yes/no] +"unknown": $ANSWER, # Confirm that this uncovered information is missing from instruction manual. [yes/no] +"concrete_and_precise": $ANSWER, # Is the uncovered information concrete and precise enough to add to instruction manual? [yes/no] +"solid": $ANSWER, # Confirm that this information is not speculative or deduced based on assumption. [yes/no] +} +``` + +Include answers with a one-sentence justification or detail. Do not repeat the inquiries. +Write '{}' if there is nothing in the list of 'unknown information and details'.""", +'dep':['planner_unexpected', 'planner_mistake', 'top-subgoal', 'subgoal_analysis', 'obs_obj', 'obs_inv', 'obs_vit', 'obs_chg', 'obs_last_act'], +'shorthand': 'discovered information and details', +'compose': ComposeKBAddPrompt(), +'query': query_reason, +'after_query': KBAddAfterQuery(), +}, + +'unknown':{ +'prompt':"""Merge the 'unknown/missing information' in the previous answer and the 'Previous unknown information and details' into a single Json dictionary. +Give each item a concise but precise name as the key, and a dictionary of answers to the following inquiries as the value: +``` +"item_name": { # if applicable, use the same item name as in the previous answer or knowledge base +"info": $ANSWER, # In concise but precise language, describe what exactly is missing. [If the information is not missing, write 'NA'.] +"knowledge": $ANSWER, # What do you already know about the current requested info? [If nothing is known, write 'NA'.] +"unknown": $ANSWER, # Confirm that this requested info is missing from the instruction manual. [yes/no] +"novel": $ANSWER, # Confirm that the knowledge base does not already contain precise answer to this requested info. [yes/no] +"general": $ANSWER, # Is the requested info expected to remain unchanged in future game steps? [yes/no] +"relevant": $ANSWER, # Is this requested info helpful for succeeding the game? [yes/no] +"correct": $ANSWER # Confirm that this request does not disagree with the instruction manual. [yes/no] followed by a one-sentence justification. +} +``` +Only include the answers, not the inquires. Remove duplicates and arrange multi-target inquiries into separate items.""", +'dep':['subgoal_analysis'], +'after': ['kb-add'], +'shorthand': 'Unknown information and details', +'compose': ComposeKBReasonPrompt(), +'query': query_reason, +'after_query': KBReasonAfterQuery(), +}, + +'actor-reflect':{ +'prompt':"""First, identify up to 2 types of information other than observation and action (from the gameplay history) that may be important for the subgoal '$db.subgoals.subgoal$', and output them in a concise list. + +Secondly, summarize the gameplay history using a table. +Use 'TBD' for the action corresponding to the current step, and include the observations, action (including number of steps and result), and the identified information in the table. + +Finally, analyze and evaluate each action in '$db.allowed_actions$'. Utilize spatial and temporal reasoning with the table to determine if the action is safe, if it leads to an unexplored state. +Format the analysis as a Json dictionary of the following format, and write "NA" for not applicable fields: +``` +{ +"$action": { # The action name +"spatial_relation_with_current_obs": $answer, # How does this action spatially relate to objects in the observation? Focus on important objects (1 sentence) +"alignment_with_history":{ +"temporal": $answer, # How does this action relate with the most recent historical trajectory temporally? Explain your reasoning by connecting with the history table. (1 sentence) +"spatial": $answer, # How does this action relate to the most recent historical path spatially? Explain your reasoning by connecting with the history table. (1 sentence) +}, +"risk": $answer, # list potential risks or hazards associated with the action as concisely as possible +"benefit": $answer, # list potential benefits or advantages associated with the action as concisely as possible +}, +... +} +""", +'dep':['top-subgoal', 'subgoal_analysis', 'obs_current_actions', 'obs_obj', 'obs_inv', 'obs_vit'], +'after': ['unknown',], +'shorthand': "Trajectory summary and action analysis", +'compose': ComposeActorEfficiencyPrompt(), +'query': query_spatial, +}, + +'actor-plan-sketch':{ +'prompt':"""First, describe the current observation/surroundings in with a focus on things related to the subgoal '$db.subgoals.subgoal$' and answer the following questions (no more than 1 sentence per-answer): +Out of the goal-relevant objects, which ones are easily reachable and which ones are harder to reach? +Are there direct obstacles in the way? +Are there risks in the way? Are they addressable? + +Then, determine if the previous plan '$db.action_summary.plan-sketch$' still applies for the subgoal based on the criteiras and the expiration condition. +Relevance criteria: $db.action_summary.relevance-crieria$ +Expiration condition: $db.action_summary.expiration-condition$ + +If the plan still applies, examine the current observation for any obstacles, hazarads, or unexpected scenarios and reason spatially how they may be addressed. +If necessary, update only the 'details' of the previous plan to achieve the target '$db.action_summary.target$' of the plan. + +If the plan does not apply, explain your reasoning, and write an new plan-sketch. +Reason spatially and temporally with the current observation, the gameplay history, and the action analysis. + +Finally, output the updated plan-sketch. The plan-sketch details should concretely describe a procedure or a specific direction to follow, and must be a Json dictionary of the following format: +``` +{ +"plan-sketch": $plan-sketch, # A 1-line summary of the plan-sketch +"detials", # Concrete description of what procedure or a specific direction to follow. Do not offer multiple options or possibilities. +"target", # Concisely write the target of the plan +"relevance-crieria": $relevance_criteria, # The criteria that the plan is relevant to the current situation +"expiration-condition": $expiration_condition, # The condition that may cause the plan to expire, like specific changes in the observation or the inventory, or after a certain number of steps. +"notes": $notes # Anything about the current situation or reasoning that may be meaningful to remember for the future. +} +``` +""", +'dep':['planner_unexpected', 'planner_mistake', 'top-subgoal', 'subgoal_analysis', 'obs_obj', 'obs_inv', 'obs_vit', 's-action', 'obs_current_actions', 'actor-reflect'], +'shorthand': "Plan-sketch", +'compose': ComposeActorPlannerPrompt(), +'query': query_spatial, +'after_query': ActionSummaryAfterQuery(), +}, + +'actor-actions':{ +'prompt':"""Given the target: $db.action_summary.target$ +First, describe the current observation/surroundings and identify any hazards. +Examine the observation/surroundings any obstacles that may interfere with achieving the target. + +- Plan-sketch: $db.action_summary.plan-sketch$ +- Plan details: $db.action_summary.details$ + +Discuss how to spatially address or evade the hazards and obstacles based on analysis of the observation, target, and the plan-sketch. + +Then, reason with the instruction manual and the current observation, and identify a sequence of actions to achieve the target. + +The sequence of identified actions should start from current step and only include actions for up to the first 6 steps, without skipping any steps. +Think step by step and explain your reasoning before writing down the sequence of actions. + +Finally, group repeated actions together to speed up the game, and explicitly state the number of repeats. + +Note: The player's reach is 1 step to the front. +""", +'dep':['obs_inv', 'obs_vit', 's-action', 'actor-reflect', 'obs_current_actions', 'obs_obj'], +'after':['actor-plan-sketch'], +'shorthand': "Actor plan and reasoning", +'compose': ComposeActorReasoningPrompt(), +'query': query_spatial, +}, + +'actor-final':{ +'prompt':"""Examine the actor plan and reasoning and identify the first action (only the exact action name from the list of all actions in the manual) in the plan. +Then, if the identified action is *explicitly stated as a repeat*, write the number of times this action should be repeated. +Finally, identify if there are any observed/nearby hazards or obstacles, no matter if they interfere with the plan. +Format the output as a Json dictionary of the following format: +``` +{ +"action": $action, # Write only the exact action name from the list of all actions in the manual. +"repeats": $repeats # The number of times this action should be repeated, write 1 if the action is not stated as repeated. +"obstacles": $hazard # [yes/no] Presence of obstacles in the course of the action. +"hazards": $hazard # [yes/no] Presence of hazards in the observation/surroundings, no matter if they interfere with the plan. +} +``` +""", +'dep':['obs_obj', 'actor-actions'], +'compose': ComposeActorBarePrompt(), +'query': query_reason, +'after_query': ActionAfterQuery(), +}, + +'s-plan':{ +'prompt':"In one sentence, describe the high-level subgoals and the reasoning behind the subgoals.", +'dep':['reflect', 'planner_unexpected', 'planner_mistake', 'top-subgoal', 'subgoal_analysis', 'actor-reflect'], +'shorthand': "Subgoals", +'compose': ComposeSummaryPrompt(), +'query': query_fast, +}, + +'s-mistakes':{ +'prompt':"In one sentence, concisely explain any unexpected situations or mistakes encountered recently. Output \"NA\" and end the answer if there's nothing unexpected and no mistakes.", +'dep':['reflect', 'planner_unexpected', 'planner_mistake', 'top-subgoal', 'subgoal_analysis'], +'shorthand': "Mistakes", +'compose': ComposeSummaryPrompt(), +'query': query_fast, +}, + +} + + +gamestep_questions = { + prompts['s-obs-obs']['prompt']:"Observation and inventory", + prompts['s-obs-vitals']['prompt']:"Vitals", + prompts['s-action']['prompt']:"Action", +} +delayed_gamestep_questions = { + prompts['s-action']['prompt'] +} + +strategy_questions_desc = { + prompts['subgoals']['prompt']: "Most recent top 3 subgoals", + prompts['top-subgoal']['prompt']: "Most recent top subgoal and completion criteria", + # strategy_questions_list[12]: "Unknown information and details", +} +attention_question = prompts['gate']['prompt'] +adaptive_strategy_questions = [ + prompts['gate-plan_sketch']['prompt'], + prompts['top-subgoal']['prompt'], +] +goal_question = prompts['top-subgoal']['prompt'] +adaptive_question = prompts['planner-adaptive']['prompt'] +reflection_skip_questions = [ + 'subgoals', 'top-subgoal', 'subgoal_analysis', 'skill', 'planner-adaptive', 'kb-add', 'unknown', 's-mistakes', +] +reflection_skip_questions = [prompts[k]['prompt'] for k in reflection_skip_questions] + +kb_questions_desc = { + prompts['unknown']['prompt']: "Unknown information and details", +} +kb_question = prompts['kb-add']['prompt'] +adaptive_kb_questions = [ +] + +q_act = prompts['actor-final']['prompt'] +adaptive_actor_questions = [prompts['actor-reflect']['prompt'], prompts['actor-plan-sketch']['prompt'], prompts['actor-actions']['prompt']] +adaptive_dependencies = ['obs_obj', 'obs_inv', 'obs_vit', 'obs_chg', 'obs_last_act'] +adaptive_dependencies = [prompts[k]['prompt'] for k in adaptive_dependencies] + + + +gameplan_questions = { + prompts['s-mistakes']['prompt']:"Mistakes and Unexpectancies", + prompts['s-plan']['prompt']:"Subgoals", +} + +feedback_questions = [ + # "Concisely make a list of historical observations or findings that could benefit the player. Only include the most important 5 items and do not include anything you already know.", +"""Concisely list up to a total number of 3 significant mistakes or dangerous situations encountered on the current skill. +If there are less than 3, list all of them. If there are no significant encounters, write 'NA'. + +For each encounter, offer a brief one-sentence advice to address it using information from the instruction manual. +You advice should be precise and actionable, but should retain generality to be applicable to similar situations/problems in the future. + +Do not include anything unrelated to the current skill.""", +] + +feedback_questions = [ + # (feedback_questions[0], gamestep_questions, "Past findings", False), + (feedback_questions[0], gameplan_questions, "Past Notes", True), +] + +gameplay_questions = { + # "Summarize learnings from the gameplay into a list. Do not include anything you already know.": ["How did the player die at the final step?",], + "Base your answers on the gameplay, create a list of up to 5 most important aspects the player should pay more attention to next time. For each aspect, write a 1-line specification on how the aspected may be addressed/quantified.": ["What did the player accomplish?", "How did the player die at the final step?", "What did the player fail to accomplish?", "What mistakes did the player make?", ], +} +gameplay_shorthands = { + # "Summarize learnings from the gameplay into a list. Do not include anything you already know.": "Learnings", + "Base your answers on the gameplay, create a list of up to 5 most important aspects the player should pay more attention to next time. For each aspect, write a 1-line specification on how the aspected may be addressed/quantified.": "Attention", +} + +def build_graph(llm_functions, database={}): + + database['shorthands'] = {} + + # Create graph + graph = Graph() + edge_list = [] + order_list = [] + + for _, node_info in prompts.items(): + key = node_prompt = node_info['prompt'] + node = SimpleDBNode(key, node_prompt, graph, llm_functions[node_info['query']]['query_model'], node_info['compose'], database, after_query=node_info['after_query'] if 'after_query' in node_info.keys() else None, verbose=True, token_counter=llm_functions[node_info['query']]['token_counter']) + graph.add_node(node) + + if 'shorthand' in node_info.keys() and node_info['shorthand'] is not None: + database['shorthands'][key] = node_info['shorthand'] + + for dependency in node_info['dep']: + dependency_name = prompts[dependency]['prompt'] + edge_list.append((dependency_name, key)) + + if 'after' in node_info.keys(): + for dependency in node_info['after']: + dependency_name = prompts[dependency]['prompt'] + order_list.append((dependency_name, key)) + + + for edge in edge_list: + graph.add_edge(*edge) + for order in order_list: + graph.add_order(*order) + + database['prompts'] = { + 'gamestep_questions': gamestep_questions, + 'delayed_gamestep_questions': delayed_gamestep_questions, + 'strategy_questions_desc': strategy_questions_desc, + 'attention_question': attention_question, + 'adaptive_strategy_questions': adaptive_strategy_questions, + 'goal_question': goal_question, + 'adaptive_question': adaptive_question, + 'kb_questions_desc': kb_questions_desc, + 'kb_question': kb_question, + 'adaptive_kb_questions': adaptive_kb_questions, + 'q_act': q_act, + 'adaptive_actor_questions': adaptive_actor_questions, + 'gameplan_questions': gameplan_questions, + 'feedback_questions': feedback_questions, + 'gameplay_questions': gameplay_questions, + 'gameplay_shorthands': gameplay_shorthands, + 'adaptive_dependencies': adaptive_dependencies, + 'reflection_skip_questions': reflection_skip_questions, + # 'action_summary': action_summary, + } + + return graph \ No newline at end of file diff --git a/examples/crafter/cache/ctxt.pkl b/examples/crafter/cache/ctxt.pkl new file mode 100644 index 0000000000000000000000000000000000000000..c948738cea59cd50e2f8e2f0a3ccb175fe7cdb52 GIT binary patch literal 4682 zcmbVQO^@3)5bdESANvaiIU8!z&vbJr+H3+WkZgiDizL0#5^b?7i7H9iaZmjb?f>nY zp(N|eY1$wNHsa&VkTdh8&Z`&YR8D@>WzMvtR5!x#1@5413B$&<5KO( zH)&4F)6v>&O+MI4udy{Fx6!3Op%dCWI;R6=-S!9 z4o)!db+MzJvn7?-AP5zxGW$Fvt8Wsz^LlQVjbwVK5kYT5?UjLn-b}Lkw)4GpCfnl} z5~%f_3U!bF@&TMVAgE;Z9d5+zDKu53aRwlL<7#K%{TvtYwh6ve4rk4EnXJA~=vASr zKwi~_srJ!m8yZ7s8Wx+@6^tWyxJj+D6Ov_dK}}s3r)2d*LQio6PwbJQ$U(5c=2{gY zPgal4>A7~Mx*uXfZX8m(`cZ;P*+C4BUBV>SL}%yWR|&nqEdcYDy}vT)z52jcO=+9{ z4W}6efr=O+H%IF-#_=g(^;cVNjm8x|M6O$)Y+bT>gL(lHc^x6AHaGXW&~VRIkwu(B zPV=mB75@b1zC<}uYYxflYW9g$#cUl}>ehJ&ua|Q6N-Xq&azRKaaEmm=5GRVfNj0ni4KqWUkdGFdOI#t^H><^Oul zEEYWqZFZw0L!N130hB9HYHE)Q2KEk|w{wjN3yYbsoeT;k{5`kr3$1G|HQcw_;kvoV z=CCfWL2ZkL<;avv$&4W!643OoWO<0q(b;&6 z<*5Jhe4TIEMrLuif6RCjxKB~9OIcv!xwFkarz5n-cvAMgH+SK+zV9>urzlm2CHwKd zkhm&{p7CUNpFUgXZaJAkQ#cyNb6Z`^P9L4Y)6$M~rRXCiqqwN&0dza%Nb}`(hAN8u zH3!7~7kGhBr?||kh0^OycUhSmKk}Gp>iI$!bx$~}v)U9z8JQY$z)yAMGl&eK`W~_t z#YyOD45{B6@{ddwdM#23z_>v2{j035G2dGGgS1!1t7FU+=qXwFaS*hW}SCE)L`@Y zSmpKGxTOc?&e62mPn2GCy_sM~Hbm(0%}xUxge?-U4XzVB3*jLR5{CHeWY{3aiC-jS z0H0ps1dQCYGHAguvJsNhG!7|UZjQy`aitz!a{mJ z*E{sZvjQau$ZLOO`)f z+avNj3}UMbezToJ;5VfOMx{J@QU|)`XCDX>k9F;gDjrjgIS93lN48fEVi+%Ic~x#$ z%UsdZO1u3D=R0V;^F2i$rTSpX2G7sFjN68d1%~lzssnDpI=m-)K55BPxM~dPAB=rB zEE!@l8MOEnZIlovkMn`Q{=GOR#apc$t9oYa$5B&H_I1z4QJ0{1XB*q!KurC1kIHd7 z4Y#lPMa2p_2C&A+=3X-VCRF1?bj&1&XuD}>YWWC@1U27w;Q-w0: + kb_desc = "\n\n".join(["{}:\n\n{}".format(d, db['kb']['unknowns'][q]) for q,d in db['prompts']['kb_questions_desc'].items()]) + messages.append({"role": "system", "content": "{}".format(kb_desc)}) + + messages.append({"role": "system", "content": "Most recent two steps of the player's in-game observation:\n\n{}".format(db['environment']['observation_2step_with_action'])}) + return messages + +class ComposePlannerPrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Assist the player to make the best plan for the game by analyzing current observation, current inventory, current status, the manual, the gameplay history, and game guidance. The game does not contain bugs or glitches, and does not offer additional cues or patterns. +Pay close attention to the details such as the numbers and requirement specifications. +Be vigilant for information or details missing from the manual, and do not make use of or assume anything not in the game, the manual, or the knowledge base. +Finally, make sure that each planned items is both quantifiable and achievable with the game actions, and the requirements are well-addressed.""" + self.shrink_idx = 2 + + def before_dependencies(self, messages, db): + skill_feedback = db['feedback']['skill_feedback'][db['skills']['skill']] if db['skills']['skill'] in db['feedback']['skill_feedback'].keys() else "" + qa_history = db['history']['qa_history_stream'][-db['history']['qa_history_planner_length']:] + step_offset = db['environment']['step'] - len(qa_history) + + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + + if len(db['kb']['knowledge_base'])>0: + messages.append({"role": "system", "content": "Knowledge base:\n\n{}".format(json.dumps(db['kb']['knowledge_base'], indent=0))}) + + text_desc = self.compose_gamestep_prompt(qa_history, db, step_offset, skip_last=False) + + messages.append({"role": "system", "content": "Gameplay history for the previous {} steps:\n\n{}".format(len(qa_history), text_desc)}) + + if len(db['feedback']['feedback']) > 0: + messages.append({"role": "system", "content": "General guidance:\n\n{}".format(db['feedback']['feedback'])}) + + if len(db['history']['qa_history'])>0: + strategy_desc = "\n\n".join(["## {}\n{}".format(d, db['history']['qa_history'][-1][q]) for q,d in db['prompts']['strategy_questions_desc'].items()]) + kb_desc = "\n\n".join(["## {}\n{}".format(d, db['kb']['unknowns'][q]) for q,d in db['prompts']['kb_questions_desc'].items()]) + else: + strategy_desc = "Most recent subgoals: None available. Re-plan necessary." + kb_desc = "" + + messages.append({"role": "system", "content": "{}".format(strategy_desc)}) + + if len(skill_feedback) > 0: + messages.append({"role": "system", "content": "Subgoal guidance:\n\n{}".format(skill_feedback)}) + + return messages + +class ComposePlannerReflectionPrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Analyze the observation, inventory, status, the manual, the gameplay history. The game does not contain bugs or glitches, and does not offer additional cues or patterns. +Be vigilant for information or details missing from the manual.""" + self.shrink_idx = 2 + + def before_dependencies(self, messages, db): + qa_history = db['history']['qa_history_stream'][-db['history']['qa_history_planner_reflection_length']:] + step_offset = db['environment']['step'] - len(qa_history) + + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + + text_desc = self.compose_gamestep_prompt(qa_history, db, step_offset, skip_last=False) + + messages.append({"role": "system", "content": "Gameplay history for the previous {} steps:\n\n{}".format(len(qa_history), text_desc)}) + + if len(db["history"]["qa_history"])>0: + strategy_desc = "\n\n".join(["## {}\n{}".format(d, db["history"]["qa_history"][-1][q]) for q,d in db['prompts']['strategy_questions_desc'].items()]) + kb_desc = "\n\n".join(["## {}\n{}".format(d, db['kb']['unknowns'][q]) for q,d in db['prompts']['kb_questions_desc'].items()]) + else: + strategy_desc = "Most recent subgoals: NA" + kb_desc = "" + + messages.append({"role": "system", "content": "{}".format(strategy_desc)}) + + return messages + +def print_skill_library(skill_library): + output = { + k: v['skill_desc'] for k,v in skill_library.items() + } + return json.dumps(output, indent=0) + +class ComposeSkillPrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Build a skill library to help the player ace the game. The skills should exmplifies high simplicity, granularity, and reusability. Pay close attetion to actions allowed by the game. All skills must be actionable and executable.""" + + def before_dependencies(self, messages, db): + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + return messages + + def after_dependencies(self, messages, db): + messages.append({"role": "system", "content": "Skill library\n\n```{}```".format(print_skill_library(db['skills']['skill_library']))}) + return messages + +class ComposeKBAddPrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Reason with the instruction manual, the observation, the gameplay history. +Gather accuracte information about the 'unknown information and details'. Pay attention to past failures and make sure to separate assumptions from facts. +Reason carefully and precisely.""" + + def before_dependencies(self, messages, db): + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + return messages + + def after_dependencies(self, messages, db): + if 'unknowns' in db['kb'] and len(db['kb']['unknowns'])>0: + kb_desc = "\n\n".join(["{}:\n\n{}".format(d, db['kb']['unknowns'][q]) for q,d in db['prompts']['kb_questions_desc'].items()]) + else: + kb_desc = "Unknown information and details: NA" + + messages.append({"role": "system", "content": "{}".format(kb_desc)}) + return messages + +class ComposeRewritePrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Rewrite information accurately and precisely according to the user instructions. Pay close attention to the numbers, the details, and the format of the output.""" + +class ComposeKBReasonPrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Uncover previous unknown information from the player's gameplay and observation. Be vigilant for information or details missing from the manual. Focus on the details such as the numbers and requirement specifications.""" + self.shrink_idx = 2 + + def before_dependencies(self, messages, db): + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + + if len(db['kb']['knowledge_base'])>0: + messages.append({"role": "system", "content": "Knowledge base:\n\n{}".format(json.dumps(db['kb']['knowledge_base'], indent=0))}) + + if 'unknowns' in db['kb'] and len(db['kb']['unknowns'])>0: + kb_desc = "\n\n".join(["{}:\n\n{}".format(d, db['kb']['unknowns'][q]) for q,d in db['prompts']['kb_questions_desc'].items()]) + else: + kb_desc = "Unknown information and details: NA" + + messages.append({"role": "system", "content": "{}".format(kb_desc)}) + return messages + +class ComposeActorPlannerPrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Your task is to assist the player to accomplish the current top subgoal for a game. Find the best action by reasoning with the previous plan, the manual, the gameplay history, and guidances.""" + + def before_dependencies(self, messages, db): + skill_feedback = db['feedback']['skill_feedback'][db['skills']['skill']] if db['skills']['skill'] in db['feedback']['skill_feedback'].keys() else "" + feedback = db['feedback']['feedback'] + knowledge_base = db['kb']['knowledge_base'] + + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + if len(feedback) > 0: + messages.append({"role": "system", "content": "General guidance:\n\n{}".format(feedback)}) + + if len(skill_feedback) > 0: + messages.append({"role": "system", "content": "Past mistakes:\n\n{}".format(skill_feedback)}) + + if len(knowledge_base)>0: + messages.append({"role": "system", "content": "Knowledge base:\n\n{}".format(json.dumps(knowledge_base, indent=0))}) + + messages.append({"role": "system", "content": "Most recent two steps of in-game observation\n\n{}".format(db['environment']['observation_2step_with_action'])}) + self.shrink_idx = len(messages) + return messages + + def after_dependencies(self, messages, db): + if db['skills']['skill'] is not None and db['skills']['skill'] == db['skills']['skill_old']: + messages.append({"role": "system", "content": "Previous plan:\n\n{}".format(json.dumps(db['action_summary'], indent=0))}) + messages.append({"role": "system", "content": "Notes from the last step:\n\n{}".format(db['action_notes'])}) + else: + messages.append({"role": "system", "content": "Previous plan: Not available, please re-plan."}) + messages.append({"role": "system", "content": "Notes from the last step: NA"}) + db['action_summary'] = { + "plan-sketch": "NA", + "details": "NA", + "target": "NA", + "relevance-criteria": "NA", + "expiration-condition": "NA", + } + db["action_notes"] = "NA" + return messages + +class ComposeActorEfficiencyPrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Your task is to assist the player to accomplish the current top subgoal for a game. +Base your answer on the gameplay history, instruction manual, and knowledge base. +Think spatially and temporally, and keep track relative/absolute positions and timestep. +Do not make use of or assume anything not in the game, the manual, or the knowledge base. +The game does not contain bugs or glitches, and does not offer additional cues or patterns.""" + self.shrink_idx = 2 + + def before_dependencies(self, messages, db): + qa_history = db['history']['qa_history_stream'][-db['history']['qa_history_actor_length']:] + step_offset = db['environment']['step'] - len(qa_history) + + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + + text_desc = self.compose_gamestep_prompt(qa_history, db, step_offset, skip_last=False) + + messages.append({"role": "system", "content": "Gameplay history for the previous {} steps:\n\n{}".format(len(qa_history), text_desc)}) + + if len(db['kb']['knowledge_base'])>0: + messages.append({"role": "system", "content": "Knowledge base:\n\n{}".format(json.dumps(db['kb']['knowledge_base'], indent=0))}) + + return messages + +class ComposeActorReasoningPrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Your task is to assist the player to accomplish the current top subgoal for a game. Start with the current state, and identify what actions to take by reasoning with the previous plan, the manual, the gameplay history, and guidance. Do not make use of or assume anything not in the game, the manual, or the knowledge base. The game does not contain bugs or glitches, and does not offer additional cues or patterns.""" + self.shrink_idx = 2 + + def before_dependencies(self, messages, db): + knowledge_base = db['kb']['knowledge_base'] + + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + + if len(knowledge_base)>0: + messages.append({"role": "system", "content": "Knowledge base:\n\n{}".format(json.dumps(knowledge_base, indent=0))}) + + messages.append({"role": "system", "content": "Most recent two steps of in-game observation\n\n{}".format(db['environment']['observation_2step_with_action'])}) + return messages + +class ComposeActorBarePrompt(ComposeBasePrompt): + + def __init__(self): + super().__init__() + self.system_prompt = """Precisely answer the following questions based on the context provided.""" + + def before_dependencies(self, messages, db): + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(db['environment']['manual'])}) + return messages + + +def compose_filtered_gamestep_prompt(qa_history, attention_rounds, game_questions, plan_questions=None, db=None, step_offset=0, skip_last=False): + text_desc = "" + for i,step in enumerate(qa_history): + if (skip_last and i==len(qa_history)-1) or (i not in attention_rounds): + continue + text_desc+=describe_gamestep(db, qa_history, i, step, step_offset) + text_desc += "\n\n" + + if len(text_desc.strip())==0: + text_desc = "NA" + return text_desc.strip() + +def compose_feedback_prompt(CTXT_dict, qa_history, current_objective, questions): + + CTXT = CTXT_dict['CTXT'] + db = CTXT_dict['db'] + attention_rounds = CTXT_dict['attention_rounds'] + step_offset = CTXT_dict['step_offset'] - len(CTXT_dict['qa_history']) + + messages_feedback = [ + {"role": "system", "content" : "Provide concrete feedback for the player using concise language. The feedback must be on the current skill."} + ] + + messages_feedback.append({"role": "system", "content": "Instruction manual:\n\n{}".format(CTXT)}) + + messages_feedback.append({"role": "system", "content": ""}) + # messages_feedback.append({"role": "system", "content": "Your gameplay:\n\n{}".format(text_desc)}) + + messages_feedback.append({"role": "system", "content": "Current skill:\n{}".format(current_objective)}) + + messages_feedback.append({"role": "user", "content": ""}) + + messages = {} + for question, dependencies, shorthand, filtered in questions: + if filtered: + text_desc = compose_filtered_gamestep_prompt(qa_history, attention_rounds, dependencies, plan_questions=None, db=db) + else: + raise Exception("Not implemented") + # text_desc = compose_gamestep_prompt(qa_history, db, step_offset) + msg = copy.deepcopy(messages_feedback) + msg[2]['content'] = "Gameplay history:\n\n{}".format(text_desc) + msg[-1]['content'] = question + messages[shorthand]=msg + + return messages, 2 + + + +def compose_gameplay_prompt(CTXT_dict, qa_history, Q_CTXT, question): + CTXT = CTXT_dict['CTXT'] + db = CTXT_dict['db'] + attention_rounds = CTXT_dict['attention_rounds'] + + messages = [ + {"role": "system", "content" : "The player just finished playing the game of crafter. Provide concrete feedback for the player's gameplay using very concise language."} + ] + + messages.append({"role": "system", "content": "You already know:\n\n{}".format(CTXT)}) + + # text_desc = "\n\n".join(["Step {}:\n{}".format((i+1)*int(granularity), "\n".join(["{}: {}".format(h, step[q]) for q,h in gamestep_questions.items()])) for i,step in enumerate(qa_history)]) + text_desc = compose_filtered_gamestep_prompt(qa_history, attention_rounds, db['prompts']['gamestep_questions'], plan_questions=db['prompts']['gameplan_questions'], db=db) + + messages.append({"role": "system", "content": "Gameplay:\n\n{}".format(text_desc)}) + + if len(Q_CTXT)>0: + for q,a in Q_CTXT: + messages.append({"role": "user", "content": q}) + messages.append({"role": "assistant", "content": a}) + + messages.append({"role": "user", "content": question}) + + return messages, 2 \ No newline at end of file diff --git a/examples/crafter/crafter/__init__.py b/examples/crafter/crafter/__init__.py new file mode 100644 index 0000000..0fd476b --- /dev/null +++ b/examples/crafter/crafter/__init__.py @@ -0,0 +1,17 @@ +from .env import Env +from .recorder import Recorder + +# try: +# import gym +# gym.register( +# id='CrafterReward-v1', +# entry_point='crafter:Env', +# max_episode_steps=10000, +# kwargs={'reward': True}) +# gym.register( +# id='CrafterNoReward-v1', +# entry_point='crafter:Env', +# max_episode_steps=10000, +# kwargs={'reward': False}) +# except ImportError: +# pass diff --git a/examples/crafter/crafter/assets/1.png b/examples/crafter/crafter/assets/1.png new file mode 100644 index 0000000000000000000000000000000000000000..a659963dff1bae56d5b71b77cd495bc5c15ff257 GIT binary patch literal 1827 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%0D*ul}$fuy^mTVa;s+O0L&xp;l?00 zdTav`>MN*m2Kd&V6LNtSAdEB2h65JA9V)g+_uP^RTCgu-JZg-z9`6rD0k$KYApruP zNFpWj%B(~PqybPNZ!Cc~5TF>j!buK-(o;qez_i0VN^>tqKIHgHf<%-g3N~2^V1*Eo zKN<`|Hra+QiYuBmFZnrms)kV-AI z=vr#ptmRfZa;-}bJ@(YKThG0m)E3nb?G0+QsPQB<_UTCtQq^O0Ghd)&Fat3T1mZdf zAfb6M^F|@?AUBx#%&3aUa54@ym3*PZ!AWSuvD3-!PVOykE`^_RiF5-C$XanM@+q-`mm?4;SXO$Hg}u6GfMlo;A z_s65Vs^EV`|Fxp`ze0bkM6R96mq|C8*#1Ku{;JD~Jg zYXATNglR)VP)S2WAaHVTW@&6?004NLeUUv#!$2IxU(=R~R2=Le#UVrOWI}qKlOHzogJ2#)IR2yu0_f zdj|-uD$~rsIH2j4kx3=RTz*vyy&{Mx0*E6dGqXM?NojbFuY36TdY9l?-sk=t18Tu! zfKMczWp%?M-XNadwCbGqi6g8mtHkHTV+LK2_>t?1%Ws@Z4huXpVq`P(#1Ue#)Wb>- zv$CNPPZ7sdO{aVz=d#Lqi?dd3u+BaC3&RC{WtrKyC{ul*9yFj~X+uz5w-97>Q z&%l-5_19X!%qQv1t`<822DgEW>#nBk0hc?#@KaqjWJmJT6pBUQ{fxdT4-DM`{cCP- zoqe1>09ooPeFGdE0`U@MuY0_^ueY~<&vg3x0W{WfjwOk1*Z=?k24YJ`L;(K){{a7> zy{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2jvAC1q2)mzeBnJ000?uMObu0 zZ*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0000~NklH(IA#IeBVAQ}M0FcCtgiF$3`gQ~K){l_8)$VRNNtR=~fnGQmPB?WChV6~7 jUg4t#OXHD$@VSEm4sa48z<`xO00000NkvXXu0mjfZS>Aa literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/5.png b/examples/crafter/crafter/assets/5.png new file mode 100644 index 0000000000000000000000000000000000000000..355950bc2c8198b26e61a7a7310f7976105ad69f GIT binary patch literal 1518 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 RPTc?i002ovPDHLkV1kRl-^&01 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/7.png b/examples/crafter/crafter/assets/7.png new file mode 100644 index 0000000000000000000000000000000000000000..32c67f0ecf225873546a686411b251b0fc60e2ba GIT binary patch literal 1538 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/arrow-down.png b/examples/crafter/crafter/assets/arrow-down.png new file mode 100644 index 0000000000000000000000000000000000000000..426ebf6987d025c1815ff714483280425af3bf9c GIT binary patch literal 1825 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 Z3;;c|N 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 XQ^Zft231;700000NkvXXu0mjf8CYf^ literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/coal.png b/examples/crafter/crafter/assets/coal.png new file mode 100644 index 0000000000000000000000000000000000000000..467a44d4c10dcefaa3644740ff73089a7643f549 GIT binary patch literal 866 zcmV-o1D*VdP)EX>4Tx04R}tkv&MmKpe$iQ?)7;K`V$jWT;LSL`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4RLxj8AiNQwVT3N2ziIPS;0dyl(!fKV+m&1xG5G~G56 zv8b3zuZn?J_|b&{bf8aWmN6$uakLy?_we!cF2b`~pZjz4s2Pg^K9P8q8KzCVK|H-_ z8=UuvLo6?=#OK6gCS8#Dk?V@bZ=7>33p_JqCKL0-A!0Gt#7Yyhyr~gS5rx%5yK*uAVGwJG72cdMub+K6blL3kGAj+xqgXU3b_hk z+6mx)2Cnpmzf=WgK1r`M zwD1woyA51iH#B7rxZD8-pA6ZQUCB>V$Yz1}Gy0}9(0>bbuX(-I_i_3FB&n zjOHkNy~Vpbn|u5BOufG!`xYc++!y*!?G+9RBJ_R4Xrf* z!0Yu2pqW8cBN-7L49pDA=M&5vK(~G00RYS#Co@Nq$uo%vwrx8B9Sk78=+wRNl(B(c;9!-^9(aP5@d=w@P5BTL||rMW^CK`rOM<~#~5D{=E+ml06R+n zxb)uNLczKT{s$prTeqa{vGU07*qoM6N<$f)*NWyZ`_I literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/cow.png b/examples/crafter/crafter/assets/cow.png new file mode 100644 index 0000000000000000000000000000000000000000..5c346ac784f46a3b99be00f20ea09efe3104bef0 GIT binary patch literal 1983 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 P4M8jl@IK%b_0hipH~mY( literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/debug-3.png b/examples/crafter/crafter/assets/debug-3.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d00a2a3432135d9a9f6a626b2e88487132ab5c GIT binary patch literal 5762 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/diamond.png b/examples/crafter/crafter/assets/diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..fe74e39f4c6c999100280a88c41152512023fe19 GIT binary patch literal 839 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/energy.png b/examples/crafter/crafter/assets/energy.png new file mode 100644 index 0000000000000000000000000000000000000000..a77aaf5b71ef472ea77a2a3fb2cec80a3c564246 GIT binary patch literal 2213 zcmV;W2wL}vP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O=0%mgFc5{O1&N1hjzQIGE+VxxpNNg2h=`)z#It z%$S!3%UB8&k|16G=kMYE!o}>P(_ks)7(Fh{nz>+*{JzS%lh<`W5B?s!lDo$XhDgxM zeoXPmJLHV{eB|}$DUgRJMY*3B*XtMMDB^Ss+!l`^p>4poqo^E3!pE}i=j&LGIob0P z+P#8wBx(QLyr~$+5XlsGWp5TU#)-We{MIH=v1R{Z=3sfkrea+ zcab@d&`(LkT_rC6dI!n?0h;siZTAmvzVgk3mG>|AnPr`Dct!VPbi%;>^5RVx#D3)R zhQ0Xcm)yO5LE(M67pG5%Odp2%6(ZK@si!Nw{KjZ$jJ{V_G-Ao|Eb3kfxjDkbYX-sk zETS2H0b^S&qa~zE1=Y=0f1y>%#GrVPQlmn{QD1{f6&RH}8+zsn)~76ii_BFf={g$? zHp$W?AuuXJMIhxd+`&T2ZeI2(C0K5RsZ3Bt_~ehxeP+@}ZH$>qMDFAaZGw4*Jzz}c z<{i_(2#w>^upE51*8zEm6;K$Kn-dmT{IykFfnH)u7SV!v0sYP~vbMjUiWIg3z>t6f zA4vj5^2TgLaAX2Og?wUBzQ6#5zzrZ-MIyLC62P>>H%4nN2i|g2NiY$Rlm>3H6p*DF z5I?2~F;s#C4I#u}A%z@Dl&B6F7Nf;kZi|Vj1&d~uEL%yEG=&tCrId0inX{l8hm3U0 zDd$`YY!+xP(G|=ukX(GprB}G(WmmfLRVu=#Y7I43tEuK%8aHXk;})B>)N(7GIzp*y z53a{Ly7z@x7wi&@ux3qUee0kS(?ptAed|pl#4N-G7kv}ws zhnukhUR#J5pl&C6u82(fO|ZJ}=ldPBy=7hWTgA#bMQ!^{Dm%Smi73>YiMi<}-s6^~ zr(f!*`&p%aQiO0h+XS|ZsMR0#Ypv}@6QuS#xDwwpaRt?Uk*C_r?A@e&-M zb9?MywZsDld#U|KoAW>4WRD`?st1Q4(>cOeYwj?7uID#H}0004mX+uL$Nkc;*aB^>EX>4Tx z0C=2zkv&MmKpe$iTT4ai2iQSGBtvzwAS&XhRVYG*P%E_RU~=gfG-*guTpR`0f`cE6 zRRU5saWpZjxkD>;(^K7n|a>4rtTK|H-_>74h8BdjPX z#OK6g23?T&k?XR{Z=4Gb`*~)>NT=qBBg8_Xjpa6GMMEW?B91DmM*04X%L?Z$&T6^J zn)l={4Cl0!Wv+@x?E2)@|%#|RMG1sXNm{yw(t#t9I32ClT0zfuQgK1r{&wCEAgyA51i zw={VVxZD8-o^;8O94SE4pU(sDXY@^3p#K)=T624A?&I_UNK;qI8{ps&7%foty2raa z+k5->OtZfqUpsQ2u*y9t00006VoOIv00000008+zyMF)x010qNS#tmY3ljhU3ljkV znw%H_000McNlirueSad^gZEa<4bO1wgWnpw>WFU8GbZ8() zNlj2!fese{00BKoL_t(I%e9lSPQq{yhrj>7rQkvuS&(NjzJdA%2E(9lpqqocadc$x z6`W*qRwrg11WExZw52q(oD(EOjID{^bh+Wn-Q{=i1NBy24(|7bU(nl{jaEZ?ZWq@b z@F&4w+Q;#Fc)pikaP)8}ACnG4e~5F{SGfeIPbV^*xeOwga2b<@4MDI7@#Y?38lg&( zNw6OA^5sHaooCwJHd8;s(2Ck_m0G!mU9>4KOPGrS&Ew|ATIQ-4%}0FtV}faf1VUm6 zeZ$3%rxe7(j7{|X`_0#a_t6{9$RQmsVJ5Jnh|Pp}7C;Pyf<*H33fr)6Y)NMVz917J zxbsz}9IV(YSJ8%oM3A{;57LTCu|i1|kQBmZpmGV$AJ4>04OFZNZhe(2$^e$O(22%K nbpBreD2j!VSahy?ivQ#b4n~lFy|v))00000NkvXXu0mjflG6|3 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/fence.png b/examples/crafter/crafter/assets/fence.png new file mode 100644 index 0000000000000000000000000000000000000000..4814616da1ede03186af884e69311b925a66d8fc GIT binary patch literal 2449 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/grass.png b/examples/crafter/crafter/assets/grass.png new file mode 100644 index 0000000000000000000000000000000000000000..08360bb865f22706d95f94c1913606c9668b8c9f GIT binary patch literal 691 zcmV;k0!;mhP)EX>4Tx04R}tkv&MmKpe$iQ?)7;K`V$jWT;LSL`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4RLxj8AiNQwVT3N2ziIPS;0dyl(!fKV+m&1xG5G~G56 zv8b3zuZn?J_|b&{bf8aWmN6$uakLy?_we!cF2b`~pZjz4s2Pg^K9P8q8KzCVK|H-_ z8=UuvLo6?=#OK6gCS8#Dk?V@bZ=7>33p_JqCKL0-A!0Gt#7Yyhyr~gS5rx%5yK*uAVGwJG72cdMub+K6blL3kGAj+xqgXU3b_hk z+6mx)2Cnpmzf=WgK1r`M zwD1woyA51iH#B7rxZD8-pA6ZQUCB>V$Yz1}Gy0}9(0>bbuX(-I_i_3FB&n zjOHkNy~Vpbn|u5BOufG!`xJNR4C7tQacXAAPjxAP?QnTiGgeMy1h@Y!N3+Vq7Ep7E94rjQ`m$e3VMujk9k$Nold`-Dbmq}?`=ig$yNu}N}^pDa?6wJj*eU~Bh Z=ufLsNKQ8G*Np%G002ovPDHLkV1n5!91;Kk literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/health.png b/examples/crafter/crafter/assets/health.png new file mode 100644 index 0000000000000000000000000000000000000000..88a8597daad67b6c412b04a28f3ce2ce6ed1303f GIT binary patch literal 2721 zcmV;S3SRYzP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+ND=nk}M|-{O1%t0uo|74r_#O;Nv?1Q9V7c{g!ao zbTtJkDW#MIW5z#!PxBW(?2;JqF@+q0#YeJaE*K>%=<5j^3a9)zED&VCpB;_sv$?mfbFWNs3ON|SW-G2nnHUsL66#cF8X9}hr~#w!;D}%52G-YH0vDOvrd-xVlcUS7 zyM$zDNEL@!p3^86+II7{*C@eqCx|jZ8R3;*Ugmd%es;~dYKh2b&Cns3SK0%{Vs70b z21aOVQ2h?@z1|a2iFKgR?=UAgSZqBis;5_OxlOc#oX78}F^c`XzZ3~CAuil#}H#oF~^e7i<3_w#gtM`C3AM@#y+DQ zbILiF0-FVzOE3lU1v8bat5SUpHP%#fEe+|@r1=(FY^mi|I(OGm$367eQ_sB&8c3xf z!AYuyzJ?f-`HZ~c zM3+8>^V$`Mn!=GTpJ2xRf3mBe}CzM8)=-6>#3(upI-A zC1_!{RIbpIF!x2A{PT4hBTbv&uQulnbadELxVuf(swK@=M$N}J*#ZGGh^srv@@RYT`h4_qd zCPPKsRLEKs*u2J5c=+$=MpmJ}*uJTWDSN%ReQmXoO#~omh8YRrCSGe+dw|E*G%hw9 z8}^E02<^RpMJf+c1E`wM45H)|<94Y~3$G)Y7S{R9zZ{&3+58$E!Rp(d8nv zcP1;;lA3x1a_#N}X3ttEI_3dTTD?%d?1te5;PwdnRnR<=r9Do)1Tx=7hW$-rxhm_u zu+XxA=*hkt;IvxasbZRW?25gof=|4j3^4E8>_^F8Y1#)Lv#WCC@a#~kQ*IkTCm-{s z|A_6&DF`Pdxm&(@PA;F1Ngs|lpEa5M3js7c-6#?7CuOm9oD-7ZBrwzN**{6NPl@vbH3GyXaG7dmEWbNk834FCWEglR)VP)S2WAaHVTW@&6? z004NLeUUv#!%!53Pg6@pDh_t2qL86FSr8R*)G8FALZ}s5bufA9A2ex5Qd}Gb*MfsT zi&X~~XI&j!1wrr!#Ldk~(M3wUuPL;M@xkSNocGS*zWV^7US^urH4bRHZKe`&F_T>t zgRcl6LN$P4nOVl1BqiawzV6}U`(2D@dEfhU^{P3G0X~sY^GE5#9?Bw(7{Rvv!baHPZ38|O{aVzy{D4^000SaNLh0L01FcU01FcV z0GgZ_00007bV*G`2jvAA1tSOX7Vx+L000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2H zM@dakSAh-}0005ZNkl=i5m&VZ0`-~c)G8OXT{7jfbX zkRk|hMz-wEzW@23ZU+w|EQ}?~a!D1mS}K*gtKc7DUJml`pqdgj1l}DT{jw`}cPh@2 zyN<)tQ}aqd?(db9kPwguaSH>O7O$mU**beQ{x)1?1*NaSpskLqJNQ z){3gq930R98Y6X{iG9!X_{gx?@Zsvpn0kAwSc~a87)LZlR24M_6~UAOIiucFoFfba zleM(F9s6NmrNe+}TXegHt^@BETTO!zff&&cQ0EZmDAPphdwkzBP7^D&7PS_$-D0eT zX#x?U8YhkxrIF67Ks6wqs&eRymDG^h`$H*K4a|pPU z7Psm0QrT=UyB!PzgaEaowIZdEOCjXUSPE^<*qqs>#Fwr!&u@(9=ZdwlL)Y1ueF2#r63|C z5oT2epjG8#jONWK|HSvhL!}f{ganLF0%xU|KX&0Iu2(BnRWdNEawcN_x@q54 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/iron.png b/examples/crafter/crafter/assets/iron.png new file mode 100644 index 0000000000000000000000000000000000000000..eafc7fef8b3137364c6d97475982307a34276837 GIT binary patch literal 1048 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/leaves.png b/examples/crafter/crafter/assets/leaves.png new file mode 100644 index 0000000000000000000000000000000000000000..97a8cef5c767be849bf3e3f8a72209986cae5a57 GIT binary patch literal 755 zcmVEX>4Tx04R}tkv&MmKpe$iQ?)7;K`V$jWT;LSL`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4RLxj8AiNQwVT3N2ziIPS;0dyl(!fKV+m&1xG5G~G56 zv8b3zuZn?J_|b&{bf8aWmN6$uakLy?_we!cF2b`~pZjz4s2Pg^K9P8q8KzCVK|H-_ z8=UuvLo6?=#OK6gCS8#Dk?V@bZ=7>33p_JqCKL0-A!0Gt#7Yyhyr~gS5rx%5yK*uAVGwJG72cdMub+K6blL3kGAj+xqgXU3b_hk z+6mx)2Cnpmzf=WgK1r`M zwD1woyA51iH#B7rxZD8-pA6ZQUCB>V$Yz1}Gy0}9(0>bbuX(-I_i_3FB&n zjOHkNy~Vpbn|u5BOufG!`xU6)_DU*Ew9l6Ku&wHSjvakUmpN~b>0wSe*pBr?e@l liBwxFd%^jDgXb<8?h7pBstVa>_ap!S002ovPDHLkV1l@RKsf*a literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/log.png b/examples/crafter/crafter/assets/log.png new file mode 100644 index 0000000000000000000000000000000000000000..6ce29850fef297ec9b4167cf33629eaf04505f2e GIT binary patch literal 745 zcmVEX>4Tx04R}tkv&MmKpe$iQ?)7;K`V$jWT;LSL`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4RLxj8AiNQwVT3N2ziIPS;0dyl(!fKV+m&1xG5G~G56 zv8b3zuZn?J_|b&{bf8aWmN6$uakLy?_we!cF2b`~pZjz4s2Pg^K9P8q8KzCVK|H-_ z8=UuvLo6?=#OK6gCS8#Dk?V@bZ=7>33p_JqCKL0-A!0Gt#7Yyhyr~gS5rx%5yK*uAVGwJG72cdMub+K6blL3kGAj+xqgXU3b_hk z+6mx)2Cnpmzf=WgK1r`M zwD1woyA51iH#B7rxZD8-pA6ZQUCB>V$Yz1}Gy0}9(0>bbuX(-I_i_3FB&n zjOHkNy~Vpbn|u5BOufG!`x4*|B}@_ z?za~Jtj`@oUr<^D0PM#JMrqCMHH;TD=4As(8M-d5U3=5VdMOLys>U?q&=&{NlHw$q z#_mI)AOK8b_oaq-v}>8Ke=Jd{W!2>wEe(CS$Y{qYpggsqepMy0s|zt}_8^iw1?oL| zj&hZlgs!A)jY^IEk7xEPBS|w`t_!uSSxe_-^A>6aiY=`UQ~cliak}3ar&O_q*d88d?+^_G;aRTQFKyGT@) z(qgH!FIsG+q(vm!UX|Z7LsD;V@7HmB-|w#+$IN){`@GI;x$pbD?&C@In&+yaW}pUx z!89mt&fd_kmh_{l41F8*4bovSrGa=~n%J8j3l|9aY>*4U#c=`v4oE;Y3?{i>=pUKU zHADU5Ra2p&v6fN&nT7rx%A}$7uOsINo)>hMfdUB*)$J~VdlP3NP}r;|5z#q?sjuV~bHohEhJiEe(-c=S5{wD8&+Qg>SG!=1~` zOO(|uEgvzeCZtBSKGMZ|oqXqVV0!iD_+H!C{1fii`RC30$6yCzdK54U1yi%fXZP5j zDQY`mrrPJR;tV;?pn1%zg$U3dC}8t2T>11H+x?xHSC`F zu)W^U*ltBNe5uEzc`hI0Rvf(AwrN*er+!CV_sI%99WCkslOFZ_59%{QmsuaG*Iv82 z=mEKCSG&on595TZdOn}MzF&JqR{i|l4r``V+$r~QP2Ir1LbQz4!q>Z>4=!IZv6!eC zPfRwLWAdP|@1;GUp0H>~*73s$`D$3rv%WJFce%W^o7COL`+Vi$*1eu7=Qpny|Ga&F za3&^aOLexA=&$-|MZx)ea9-jbVP=+N0fI#!FonUruV#?j|0b%*cVJYgN2t1leE(_eTAk4 zn--KSOSa$g6(P$S8}H(c7p2@rSJNN2n+5k~n1y_p<$p3TIU&C5hR@ZL{r8(5{GrTc z?B3w2A6>eoS!3^sN66TUzL4(t<0^rR&*~V(-PpM8^{u^=8#8b7>-oiVE*hZrJgG{c zo!i%N&+%HRPEOu(%v!~|J{BzT$%P!$U2_SRK7 zV0vp_W?1{0!E_Q~|A1Y#XnXf;zFo1^6Wcozn3ZE^?y=oIP=T|!+~>c2>mJTVs=YEV`;&>(7# zOpQ1^w=E?Ce6@C;i!rS})ZmicxS&`clRageo%xdj+w>;~ALvs2Y>YNn2zx;S&zBml zJ{3Ospm|7CzQ<8GE&#PdrpeD=S?%Ay=i!_c$}AK$XyBR?6WV#6_+uo?^Ns zN%izw^LqaAs~MbW%9qO~JBOAW*wREZIXJ6$_E-|~C6*rY7yLrufd`8_gZXH3!|C22 za^(G#{oPrnpsG;@)zF|}@AenK(o&`PoSVVm$(j0U<3p)Pf56Ook;Nd*`-pm)TuCLX zR;hL6`|QKhHXUL-JQS=LxqZ2_(~+#4Ga%t*Q*miRP?*ax4Ash77?kd}u`nk4pl^la z`$aRusJR#Z63~oVkG*A`4f@o*z+utkmYE7!R3~n!;b1{`?)>Fd`fGy%kI6$N@|$}< z7(2Ce0njkx4CgM{v7&e;{_*Vd>un^5-2D`M61b1m(pLE>#rrH-0@Cz3N$*yUKe}mY zo^Ss1g&j{Wx*?4PH}rOHjyb5$rfi$gTWEN0=bsrS35WVRwB5C=PW7gS^gVHv__ai} zI1hFlTAZKS85)}u{x&6j4!XYh@Sbf8GN+J@M&ezOOENbLvg2a zXj%NXtk)LTvCJCKa7uZSByagCgEGEK_Ad7(!)B#6&nMx>jBfYs>kdqbT76OTreg{9 zkF`&4b%a%INfR^4Tji2pJW*RD7Zq~^(V6LK5?}Puk#JCazah=*5hg5h*Y*LeDh{^G zs#Yy#-JPa!{ZBj$>Z~xnXzn^MrHXTRUFzj3<+ax+zCXJ^W1eR!`2N7r0soKWQ_Hr@ zZM50LZQ(Ux`T=5gZteJ$@2Kl43KkyqPbq zOLwKGPl5NWaWPs^=e_dHQ}m*4&C=cBxNm%5G&=5?5e{_o4xXfRCvk;cx}VB~@Q}&D zN)E47YA#=C zqq6bR#})0phMgG=@;&?yp|oN9(An4LH~suz23* zvu+My0Z+)cRlR97+QunfUam@hcQgH!$fkN&@z(xc&B~>z!D{8R7CxrgYZ$QZXnt0y zK3bOMvoUn9oA%QsQF-g;!%N4O8m#SG(nT!PfxlM|em42f!ZDW;GFqQEMekXAWtR&O#FJ&T8vCjS0;+D>q zB`xKj7Vph((_FPiSfu&cTnPr#ZU>>ZOry>vGx?DgbQYfhSV$rTP}_#VtnDNMIx_+g z!x=z0$g@H8R8}M4Aj<~fkEf!k0w;h2y2T3tpZIyc%=ic)qwwrX%|A&X7+cAhg# z0X^9uIAXDYj6%i6##+Q$TJVM8C=7{2LZPuJEEWkNkfJ!Am@Yx`MAM`cLmbY4h$#dG zVvx^+OF8Kbeze#Ifq>@W!{pE`(?tRe4m*%O%jiX77K#Er;GldU01Asn6Om{v5=%mj zwufe^)Ddf*Xt;_{Jy8<60EMwYqaq`}u@H$}V!rzOp@qm7x*egs0TDl1$OK$s0G@c- zXr}^hv}m-?Xb~VClEuwsu~AS^GS8!JTq#tq5gTb4!@)>_%tDGDjbt%LaDr$dSB7CR zQ2-Z+gaQ#k%$RTR0uEoy7jgK45wmZS8kzoENRZXhPaz1M3_5poY*H!~LBo4y@kR&=_g=AxKYyyJ;5J^axN6>tsFQ3mHW|am!LPmuY zgGCq9|3)X}B7DaMg$=UfehQT;1KfJ}%#-KxnRKZ(428^+O%4f%BRw27ghl-f{5K{a z4nLOnpYeQ!j<7fg#j$*0gs0Gxu?S#_f6ntG@CcJPB-tXdFplysNqvX2hL(g%CA&ek zd|})u|2{y}(9=+PxS&iy;c%Ijk?Bln6>Shq+0wIQs|VG2XcGLvIs1{Mk$n@I~VmW~HB>dxM99rt(p79U4iRdxF zcbPFcbY3_BHN;^x{8`3qD>j_d>C!O-Jf2|1L=rJXNP(FcCXzt}pu^9AW*8(in}EmsG+kGyIl-k;AX`;P|Nd=B zu~;%5QkE~8Fr1?`O4|0n>|SeWWBhJ^TT2@!nIe@#pJ;)A3j)G#_x%?g_y^o5dzp;z z|J8gnY{=S)FNlL$2}kT1%loPO9{`6K=7LOsC;F-HZy?rR75_`~a)5fF^w^6=r4PyA zx5wUg=q~qt>$QUV8IE8@Leg0*3=)UOF_9!3)PGq_sAIAL9Gk(g99jx4I7}~Fm@oX+ zsDFuP$avIs4q_P^gRY6ttrInJ?HrC55|01n-?04sFI~Xlzn%P2eSeqhce#G40>1?Q zy}N#w>z69%UemwXZL(03LKXkA+@K9pC({5PGFGM&K48g2Be>NI!D0+`Ng< zpo*A6by4Y1P*WhN90o8iU@-YWinD_+vB4#ysbmXL%Vo2Ls$AVdqk78c>D3$m%r@GZ zn^dDBy5s^M^V)vpbsv9!|A=?@gFg0sYKpOKQN_rk=OKaKRgGy;xwH*=CAy|cj`jl> zMkzz+xmr=9UV-vFRUM@J!Z6hu1^_4E=-LvFQ z_{Lt@+hkkyCgobMguiZOYUAAQ3uPBKSX~}7RahBndWha+b^q|0Wc#flFq4b3!@9S@ zis$UgRNOwuS#R`dcA#15f~r+2fu^v`)o!Na{A?A4L5P~cJNiKg%b;DE4_y1wFEw7e zxxyoQ<{VA?=!;(NT94dQwmL^F#Z^PDwX{PXn^re1Z+er~m`0Z~Rk}WV_eHQ$c_S@l zEud6r`TW4nZR}EAPUoBwU*_t%fs`ZDHwJm_Vdu>dR&Fac_1LdFbh9WAj;hNsxz-*R(r@xowA_|iS9a}KpIEy%LvdB_ienK_ z1f|}qeHtE-iVqt`=P-?v(7RgtS@-5mJTo=2w;SFPb9w&Y$|bX|s=}Sul?G6A_U^uC zc>Kj1)r=d{-8Wx4N{k10uCr#*P)>2V z#qVLubemRZ4xk1<-`SpZ_O5o#xaaqx@7~g4)Noc(b!ObrX4L*? z8k)Pkir>HzK7C9*79sGke;|1?Ej{*qK~uoWSAPhDc4~X5n!zFx=r41O iDJMegpw*=(bZSPQ(u)Jbt2RjAuu@#+IUjahnD~D*cSH*S literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/plant-ripe.png b/examples/crafter/crafter/assets/plant-ripe.png new file mode 100644 index 0000000000000000000000000000000000000000..ed7394ef64ac9d80a00a20b69a4535689f77ec30 GIT binary patch literal 2154 zcmV-w2$lDVP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+SONCw(KSh{AU$e0s?_xIgICI2U&iK!EU#Ectetp znA-*mSrV!e_^S5TpI!Zd%NSq_%Zn&*8#=wt9CSLcL*$fCFF zG59TSkQ?ZmEicG}W%@rI-JjK=Y7moYmaLw4UQycdBb(5B z+#!b^*@!VuiJO`*sQ?*5faSPz+w_^ur`(*VyuZ}XENj!@3GQ>WX`o&vZZz<1%k2$4 zfA&+Z?i1wR!#y#*L?nDVHct_88kxFW;qmLehThv+U1Eopg{I#){S^xETnVa%V2tp}51aZKrEjhGQA*9X34Qj0tttr6kvk_1z5}u z{1Fb|P;%C>bIv<)!9|z6^{h}~-isgGZ4xUiPP~W&iIRd199-~0LI^RWXrje#>>|oB zh8SZ?@R?vad6l4_Aju}%%-Ln1C5IeyDxgo{;)*X)LWw0+Q*A{ZS6`)u8f$9QK&8gb zspgxs&|*s+T#O{IIFX5&jNq?f>tFI_ohoxkQWYcaCv5q-99k zva{A*=<=J^)G@HePepGn2sRa~v)Iv(`MNE2!(ErdYKe0`2339(Ky!YF%iWicVX0d< zPwT$)&tcxe8)2y#*Xbw`JNyYwJ->$I3uyJOFH^BJKJB|S-{f%|-Gebz+YQOW~=+#|N^YR`L$oFMECro`L z&PQt9DAmHQwcI51Os86toskx}hxW~4Ft2v+Y;$ZR8 z@Gpa@OWjaycYaIcrHidXuty(q^`e*-n_T1AC`LQwmg1FL-S0G&j7^?KI@C2EHf^ew zgtOya$;8^W8=T`Q=tl*qMdi5H&atq>x`Ssg#p_$S-FpHJ(5vx+EjXfLIT96cI^MP8 zPJJrU_>yP0xs;S{+0tG*ngQtK;UGRb4PH8mwAO1~7?+Lvu zSbZ&|U+C4_3Vhj0M; zpBoZ<@J01E^k^bgf;{-u0004nX+uL$Nkc;*aB^>EX>4Tx0C=2zkv&MmKpe$iQ?(+s z4i@~tAwzYtAS&XhRVYG*P%E_RU~=h)(4-+rad8w}3l4rPRvlcNb#-tR1i=pwH#a9m z7b)?7Nufoo2gm(*ckglc4iKtErde%cfTr7KG9DAtnH4eciVlR(hXI6SW*M`Rlt9bz zbq^n3@1i`b^|?Psx0%>!=w!wLyILr#NN__`OLDw&lOCi?=7&+!qf(F_3ga5(rZmrzJxR(@+0qrl2 z^Dztrc7a;OalVfor*;DPpMfjA;V)Hz=}*!t4J~p6^lSqc*9}eC11@)f{wG5=Wmodk z6mmJ>{fxdT1N7bkU8`Pi^?jT^04eG!aRVG20wa0KUTg90&gS0!JyY-R2RXuWkTYgu z4FCWD24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2jvD1 z7cMq97K=gv000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0003INkl)y0zU|={I)p+CtiJ#R0aqL;2sP$(|Guvo-1fscy!) zXbjEVa1WpPYKpeUJr`$mq1f6Ym7t}@4hN7mdp$wmAJxwwT!lBcQscpRUjixdXo!P( zQzUQfapj?WK&nNa~ZaR*~YeRnfrZYl&3nMO_@`#D|-2|SCOu2ZK^!Kx#i`;qk$=J_e gs@sL=UfiG64{4i=1GL29p#T5?07*qoM6N<$g5`7uxc~qF literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/plant-young.png b/examples/crafter/crafter/assets/plant-young.png new file mode 100644 index 0000000000000000000000000000000000000000..4e7fda20e9219cb2cd9c26de51fceb4333d2be05 GIT binary patch literal 2511 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/player-right.png b/examples/crafter/crafter/assets/player-right.png new file mode 100644 index 0000000000000000000000000000000000000000..6c3efdcd912681ae5dc8d5b6131ea4957d825caa GIT binary patch literal 6212 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 N?d0irVDYNN{{q)L`XvAW literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/player-up.png b/examples/crafter/crafter/assets/player-up.png new file mode 100644 index 0000000000000000000000000000000000000000..b4c971bc6b0f69b922c509069462991590fd2288 GIT binary patch literal 6144 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/sand.png b/examples/crafter/crafter/assets/sand.png new file mode 100644 index 0000000000000000000000000000000000000000..8948044eac0be2d088de4527ba2e46a5a236cce5 GIT binary patch literal 729 zcmV;~0w(>5P)EX>4Tx04R}tkv&MmKpe$iQ?)7;K`V$jWT;LSL`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4RLxj8AiNQwVT3N2ziIPS;0dyl(!fKV+m&1xG5G~G56 zv8b3zuZn?J_|b&{bf8aWmN6$uakLy?_we!cF2b`~pZjz4s2Pg^K9P8q8KzCVK|H-_ z8=UuvLo6?=#OK6gCS8#Dk?V@bZ=7>33p_JqCKL0-A!0Gt#7Yyhyr~gS5rx%5yK*uAVGwJG72cdMub+K6blL3kGAj+xqgXU3b_hk z+6mx)2Cnpmzf=WgK1r`M zwD1woyA51iH#B7rxZD8-pA6ZQUCB>V$Yz1}Gy0}9(0>bbuX(-I_i_3FB&n zjOHkNy~Vpbn|u5BOufG!`x1?0xf6VdKAx5ivmZ*vDyx=|jBE1{{RpMXXn}>5Ny(>*>ut z443XBv+11A_a{1IW6Y-aHhlOH9Rl^(HLf1Jpi53ZL^(t>Z0qvKh7q)7&4wAq3@JMI zH2`|#mrViF!EA3wLMtC)$*CvThj_oV`F7XY@J&qA;k`gyzo@?dD;AXbL}U;B00000 LNkvXXu0mjf(5^JQ literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/sapling.png b/examples/crafter/crafter/assets/sapling.png new file mode 100644 index 0000000000000000000000000000000000000000..f7122163dedeacfb70c19f495d23fde8411dc9b9 GIT binary patch literal 1817 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/stone.png b/examples/crafter/crafter/assets/stone.png new file mode 100644 index 0000000000000000000000000000000000000000..d5d37d4b8b1c173f714c170f98e17a257e7bd30c GIT binary patch literal 7242 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 o{pHJ-k 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 LNkvXXu0mjfj8=AQ18g z$9#N$G&Gww*f~=&aoeiu;8i!CPKq21UR;$Fu@XA-ppZx)s?9cdbZV1s)Q~Lea|@%{ zshyFQ$vXM&-Am#uQuQ9U=FfE>hKe=&c@cftR*RqcKYDLnu)lZmhl`CdTumR_k15a8 z5;VdMk1k$krL8w~%<#j{eOARWJ=By;qwUXR+e=wwX&8}ceX1rTljK=Wa`3jy>yXDATsc+x=C$=yt||EiLp`L zBi~Bjd|+`t_sp5Ha;W(u+2sCdt7S&2fbjNUJ^1OT7TeR;a|CuBg=-7*=Af@_(n~u1 zcB?P4n)w9H-?(?_&!yOp(T8KIN`Tq=CHKP}8d}ctlPEWn!;}WvXVo1jQbtVgpXNS^ z^Ex*rH8!cvcz=h!Xr`7|p5^gF8k5$lHS9ACd+K7m{nmrIL1_;qX6^^>rre*1Qg&Y6 zVk}YeZC1VS)}uUkVRWpfLG`wn%x#PW{m3c!nwdV^7A5R(Gq{wVb|5FQM6N!=D8VHo z&5Y>3{tEt(xn-xvm6?1mw?!5y}EjK>s z9Ji*`(7divCrq`b-XAfEnM7Pvw_){lbRFNA>WDPeolPk?dr8N%@8XKgF7d#nNAtTm zF@w{gKH2r%j*lyz24wd7Tvxhy54tk&O@E>xCS$dw}6B3*;|P!e;qmf{M$x3 zs@dYOY409+&f`i?S|e@tmDlR=pR=qPj}K)!GdCpMtfx%bdqYR}g2|jZZBBw|fsiC4 zCBB#SdB*r8;a{t26r&!W-rz5rkFHA%M+cv%)sQYN{tZvSGwak=2 zyDf}W>%v8x*t0tO&Hj5g?=-);?U&%(Grwp4)C^^pxDCjSrVAqX9I^;2@j9ecre6AE zXQ9>{kTk2+_oZaB-DQ#!r6uFSiaVW`Q`ZGH=EYxO>pC{wv_9k0VjUn@lx*&x^5%hj zim}wS<=ksurRt{4Xi$2xzyTXpMGlO7E<>xN1-<&Ob%+BNahQ~!-9uxPk(7Fz#-Uw`0x zhFw_`dSd~}M;O>1!4vyy?AS+*=A zZex&7!?pH#-mh!Oo2K!GJn{5^Q|&dmP>m=>O**&Zb8mIYiS6y$OPzMO*b zzqw|g`Zk|Fj~l@!{n(GBeybx3nK^o`@mIccJ$CBUI>_Vw!X@WcnBJ$fc&sav=2kb^ z@D7EnBgcm?aSWMwi=>}5OLK#KEdd#qY7%^mB23W}S-JVK6ku36mGfQd8 ze$@r>F4r5lo`(&sFE`R7BFdAx&0kBRQiVnkq0(f=P2A&>_{M}o7U8!HQ@UrG7 z)rzy^$=dBg57TKA*YQ8H)YQ6^Q?dk6^B;z`PkduL!Dscg?+;I?-+Q2O_3A#gXOAag ziq>SEQ(fDI*lyxSFm&#zpQ)kJGL(GYOttT^vfoi5bR#my2BG_WVb}zHedUXewd;E2 zGVTWG^B!LRz=SPU;^Kx6@&^cn_BSjM7|!w_i@9kq(-j$}Y$5J|$2FgUcMoEJ(!*=QiG#cU4E!@+5k z0=}c80;EzA4U3h@WEdF{1BtmCj=qa0owRiL;Zrk60v$s^p>)A` zr6PWaWW3H038+{awwur9U}2+%HIJ8Zc4fMc$tc3eC#_03hUoa3dV8f?xBP16WW&3WbO!k*QQP$pUUv{uYWZf(I#hk_C=M zp#WbR{onOiku^jbkusWu{{xR%7XAPc{-9YLswJ94@dwdVD(p6%jHloL9GgO5asFwK z$JL4dfyX3@C6!3U;n8G(%z=A|1sP2xQ*p4{WHN#5j|Vt7{8v48hC4A*Q3Z#OGFQ_O7A}onUwVN;c)*vzue3f~tha_=hpCHPd?g6PWNpQzgviR#ffrS!u1rVO zCo1zbXOkjIyJQH2va_p$otNp&5D%uC_KKNwsNi6|`xJp^$h^Z5rY+7@I?3u&4;#){ z+|;6>U5XJ^IrR@JX_sb}tZ%jVof}u<&e3M?CF^Be4tbA$J9V9)=fYO4FN*N<$s=cIi=$ zZ{DuqC-ir!iC4{ke_n4XAy(&ptl@)8CHwBIs7>R0Y*5+QaV4I~dMt>vty?CmR=Q42 z02nHZXQJ}w%^~}XXYZe@w)Y{`;LRbi3!@? z!B8-GS0HNjzUXP7x$(`d)>j&O&eIUHeY};D*Yq88Q`OR$dL_lpWo(H+>GMB-tTN&q2YLTlFiRN_eI{dMJwYBeV(WKTrewZjGBe$*6K7tG%yX! z5!I@dV1S(WjXYS)A;-8ptRYKaUf9>a1ASOSP`b?>t&C?P_di_eYub&-{6n zDNCj1+QGd{hEn5X%lFMiMiP?)4q;@YyLUN=8H>n literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/unknown.png b/examples/crafter/crafter/assets/unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..6df29cd2bc66a71063642f201ce256d9934df322 GIT binary patch literal 2127 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 literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/wood_pickaxe.png b/examples/crafter/crafter/assets/wood_pickaxe.png new file mode 100644 index 0000000000000000000000000000000000000000..69e936295651b5a46a063ea663fe0676341f4cff GIT binary patch literal 2045 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 TLgLf;00000NkvXXu0mjfS2>ig literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/assets/zombie.png b/examples/crafter/crafter/assets/zombie.png new file mode 100644 index 0000000000000000000000000000000000000000..f154bed4253233587b674828a20e9b5581a4953a GIT binary patch literal 2089 zcmV+^2-f$BP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+QnB{k}D|;{O1&N1SA21<4`OS?*?=HNfx_ZUfS+{ zA5(-25CREh3d*Ma`=^_K@UXBo&T9-Q1dGQdmpG#l?RxTjlXdgGJND8z$<1|uArjQG zx-o2dgPftyTVD1$8FGKKP-=VNI1l8#5XWucVsRT1S_W+03+26#@V4w$|Jas8ih8?+ zdf7qTlDK|myqqWpA4nbEj;(4)Xh*hA5w|)V6`l64hVJ|3qRQ}zn~>P~cFRi{Kaz}E z@Fod-kzC|F<7*8dg9}(W?btSZjpivfS5%%q=96u0I-KCXwl)pS!@wO4Lfdk=LoZzG zDL3~6gI`Gzus%?y{+A47qI29H+9(wnbCscF-S0{1r){4 zqph3!r9k=Z#z3t*l+r4dFMZ>Rwg z>N}`@2KZ9WgxtUiAoMfL5gW|*SW6t9zPKe5Xn~$by>g7SuK8mj2iqRbkN^T3NIV6y z%B(~PWCTE&Y-DkK00HuzE1YB}NIhjF0ZbdrQJT8!dC74~0tqic6xbxm0LwWK{-}%K zQ1;@T_da;_(I=mS1ZR+8A!r!KZB|jIUW2MeO`4-b7kvy-V~i>0#7S@(`-F5%F{P9< zY-X&SgUQg(kQ{u-;SPU8J9iJNKu z2Dc}u)p2c?iP*}*5mPJ3K8&}RIp%STW^-wLTlai~*=#||Ve^z*G}lTIhm}F3hWK#U zXOlIvMHyf$^$%#~_*=YlZI9!_94YBM}O<#&)QptYLLi$iEyH_Q34x0Va?bP(JYLc$L4Lq0g z<7;l!3cimPGT)u752Mx3mQM**a~vL_7m(bM=ZX%Y$cwZ`JAetk6E;FEAZF!dihM); z6Tfm^S|TYv88B<)Q|Ao)y!J`3Zzk?23Gh!yN=>RSb^9a^XZ^w*EHdB zI&-9%bAaA3hH$hlel$(xq+-$?d2YZv#o*;uw-NJ#88;Mq5tJR1%U;dhWruq3T`#gZ=?HTxC0L?GIs~&3ew+p;oNIE`5 z6aE_f2eye|$Ua$gJ^%m#glR)VP)S2WAaHVTW@&6?004NLeUUv#!%!53PgA8L6%jiW zamY}eEQpFYY88r5A=C=3I+(ol51KS2DK3tJYr(;v#j1mgv#t)Vf*|+<;^yY0=prTF zmlRsWc;WFr&b#OE-hF`3s4&gy8V5ApHq*(3n9Z$%FHt6Bq;^o`nso1 zs=GMP^6&ez`qaF|fPhFm%M8;d-XNadv<=St#1U4KRpN8vF_SJx{K$31<2TMlmj#{~ zF*E6T;s~)=>|mvXS;^Fhr-);!rc=I<^;qS+#aXM=SnHnrh2gxuvdnc_!$@KgOOPN! zK@DY8U?WbuPKt#zohN+!L#|&UmqM;G7&#VDg$CL6ga5(r*;<9k2`?#}0J>is=VJs2 z?E=lZ<9r`GPV)o^J_A>J+h1(}GoPf_+gj`h=-&n|uG^Zj2VCv|(I-PTWmgK)5()+2 z{fxdT2MpcVJ)Lv=x2H9~AMT5Caw26c!~g&Q z24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2jv410Tly^ zCbuF0000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}00039Nklw*FhQZA4-O)9DsD~cRol@p*;DJZyu zDOQdqXhK-+YwWS-^JbncFoLYkEdUf%iLPmJyIck5C-DMBRbmSAKf#ljZ`{Y6&i_*{HIeVwsc-7TfwHEq9FQGiq@IPCWDW+%}d*1rj8 zWjApZ1^vo_sv^0IR40SrR43?~7C*;tk?LfS%=6Dhjw3 z%u5wq0Hx`M$N2$(N2^lWbPqjbH@F-72Sju8Y|~cc#A7rg_S*?2A>OEL7`w*<&XRZ| Tk1)=O00000NkvXXu0mjfB)-Tq literal 0 HcmV?d00001 diff --git a/examples/crafter/crafter/constants.py b/examples/crafter/crafter/constants.py new file mode 100644 index 0000000..868446b --- /dev/null +++ b/examples/crafter/crafter/constants.py @@ -0,0 +1,7 @@ +import pathlib + +import ruamel.yaml as yaml + +root = pathlib.Path(__file__).parent +for key, value in yaml.safe_load((root / 'data.yaml').read_text()).items(): + globals()[key] = value diff --git a/examples/crafter/crafter/data.yaml b/examples/crafter/crafter/data.yaml new file mode 100644 index 0000000..4bcd365 --- /dev/null +++ b/examples/crafter/crafter/data.yaml @@ -0,0 +1,102 @@ +actions: + - noop + - move_left + - move_right + - move_up + - move_down + - do + - sleep + - place_stone + - place_table + - place_furnace + - place_plant + - make_wood_pickaxe + - make_stone_pickaxe + - make_iron_pickaxe + - make_wood_sword + - make_stone_sword + - make_iron_sword + +materials: + - water + - grass + - stone + - path + - sand + - tree + - lava + - coal + - iron + - diamond + - table + - furnace + +walkable: + - grass + - path + - sand + +items: + health: {max: 9, initial: 9} + food: {max: 9, initial: 9} + drink: {max: 9, initial: 9} + energy: {max: 9, initial: 9} + sapling: {max: 9, initial: 0} + wood: {max: 9, initial: 0} + stone: {max: 9, initial: 0} + coal: {max: 9, initial: 0} + iron: {max: 9, initial: 0} + diamond: {max: 9, initial: 0} + wood_pickaxe: {max: 9, initial: 0} + stone_pickaxe: {max: 9, initial: 0} + iron_pickaxe: {max: 9, initial: 0} + wood_sword: {max: 9, initial: 0} + stone_sword: {max: 9, initial: 0} + iron_sword: {max: 9, initial: 0} + +collect: + tree: {require: {}, receive: {wood: 1}, leaves: grass} + stone: {require: {wood_pickaxe: 1}, receive: {stone: 1}, leaves: path} + coal: {require: {wood_pickaxe: 1}, receive: {coal: 1}, leaves: path} + iron: {require: {stone_pickaxe: 1}, receive: {iron: 1}, leaves: path} + diamond: {require: {iron_pickaxe: 1}, receive: {diamond: 1}, leaves: path} + water: {require: {}, receive: {drink: 1}, leaves: water} + grass: {require: {}, receive: {sapling: 1}, probability: 0.1, leaves: grass} + +place: + stone: {uses: {stone: 1}, where: [grass, sand, path, water, lava], type: material} + table: {uses: {wood: 2}, where: [grass, sand, path], type: material} + furnace: {uses: {stone: 4}, where: [grass, sand, path], type: material} + plant: {uses: {sapling: 1}, where: [grass], type: object} + +make: + wood_pickaxe: {uses: {wood: 1}, nearby: [table], gives: 1} + stone_pickaxe: {uses: {wood: 1, stone: 1}, nearby: [table], gives: 1} + iron_pickaxe: {uses: {wood: 1, coal: 1, iron: 1}, nearby: [table, furnace], gives: 1} + wood_sword: {uses: {wood: 1}, nearby: [table], gives: 1} + stone_sword: {uses: {wood: 1, stone: 1}, nearby: [table], gives: 1} + iron_sword: {uses: {wood: 1, coal: 1, iron: 1}, nearby: [table, furnace], gives: 1} + +achievements: + - collect_coal + - collect_diamond + - collect_drink + - collect_iron + - collect_sapling + - collect_stone + - collect_wood + - defeat_skeleton + - defeat_zombie + - eat_cow + - eat_plant + - make_iron_pickaxe + - make_iron_sword + - make_stone_pickaxe + - make_stone_sword + - make_wood_pickaxe + - make_wood_sword + - place_furnace + - place_plant + - place_stone + - place_table + - wake_up diff --git a/examples/crafter/crafter/engine.py b/examples/crafter/crafter/engine.py new file mode 100644 index 0000000..2ba960a --- /dev/null +++ b/examples/crafter/crafter/engine.py @@ -0,0 +1,284 @@ +import collections +import functools +import pathlib + +import imageio +import numpy as np +from PIL import Image, ImageEnhance + + +class AttrDict(dict): + + __getattr__ = dict.__getitem__ + + +class staticproperty: + + def __init__(self, function): + self.function = function + + def __get__(self, instance, owner=None): + return self.function() + + +class World: + + def __init__(self, area, materials, chunk_size): + self.area = area + self._chunk_size = chunk_size + self._mat_names = {i: x for i, x in enumerate([None] + materials)} + self._mat_ids = {x: i for i, x in enumerate([None] + materials)} + self.reset() + + def reset(self, seed=None): + self.random = np.random.RandomState(seed) + self.daylight = 0.0 + self._chunks = collections.defaultdict(set) + self._objects = [None] + self._mat_map = np.zeros(self.area, np.uint8) + self._obj_map = np.zeros(self.area, np.uint32) + + @property + def objects(self): + # Return a new list so the objects cannot change while being iterated over. + return [obj for obj in self._objects if obj] + + @property + def chunks(self): + return self._chunks.copy() + + def add(self, obj): + assert hasattr(obj, 'pos') + obj.pos = np.array(obj.pos) + assert self._obj_map[tuple(obj.pos)] == 0 + index = len(self._objects) + self._objects.append(obj) + self._obj_map[tuple(obj.pos)] = index + self._chunks[self.chunk_key(obj.pos)].add(obj) + + def remove(self, obj): + if obj.removed: + return + self._objects[self._obj_map[tuple(obj.pos)]] = None + self._obj_map[tuple(obj.pos)] = 0 + self._chunks[self.chunk_key(obj.pos)].remove(obj) + obj.removed = True + + def move(self, obj, pos): + if obj.removed: + return + pos = np.array(pos) + assert self._obj_map[tuple(pos)] == 0 + index = self._obj_map[tuple(obj.pos)] + self._obj_map[tuple(pos)] = index + self._obj_map[tuple(obj.pos)] = 0 + old_chunk = self.chunk_key(obj.pos) + new_chunk = self.chunk_key(pos) + if old_chunk != new_chunk: + self._chunks[old_chunk].remove(obj) + self._chunks[new_chunk].add(obj) + obj.pos = pos + + def __setitem__(self, pos, material): + if material not in self._mat_ids: + id_ = len(self._mat_ids) + self._mat_ids[material] = id_ + self._mat_map[tuple(pos)] = self._mat_ids[material] + + def __getitem__(self, pos): + if not _inside((0, 0), pos, self.area): + return None, None + material = self._mat_names[self._mat_map[tuple(pos)]] + obj = self._objects[self._obj_map[tuple(pos)]] + return material, obj + + def nearby(self, pos, distance): + (x, y), d = pos, distance + ids = set(self._mat_map[ + x - d: x + d + 1, y - d: y + d + 1].flatten().tolist()) + materials = tuple(self._mat_names[x] for x in ids) + indices = self._obj_map[ + x - d: x + d + 1, y - d: y + d + 1].flatten().tolist() + objs = {self._objects[i] for i in indices if i > 0} + return materials, objs + + def mask(self, xmin, xmax, ymin, ymax, material): + region = self._mat_map[xmin: xmax, ymin: ymax] + return (region == self._mat_ids[material]) + + def count(self, material): + return (self._mat_map == self._mat_ids[material]).sum() + + def chunk_key(self, pos): + (x, y), (csx, csy) = pos, self._chunk_size + xmin, ymin = (x // csx) * csx, (y // csy) * csy + xmax = min(xmin + csx, self.area[0]) + ymax = min(ymin + csy, self.area[1]) + return (xmin, xmax, ymin, ymax) + + +class Textures: + + def __init__(self, directory): + self._originals = {} + self._textures = {} + for filename in pathlib.Path(directory).glob('*.png'): + image = imageio.imread(filename.read_bytes()) + image = image.transpose((1, 0) + tuple(range(2, len(image.shape)))) + self._originals[filename.stem] = image + self._textures[(filename.stem, image.shape[:2])] = image + + def get(self, name, size): + if name is None: + name = 'unknown' + size = int(size[0]), int(size[1]) + key = name, size + if key not in self._textures: + image = self._originals[name] + image = Image.fromarray(image) + image = image.resize(size[::-1], resample=Image.NEAREST) + image = np.array(image) + self._textures[key] = image + return self._textures[key] + + +class GlobalView: + + pass + + +class UncoverView: + + pass + + +class LocalView: + + def __init__(self, world, textures, grid): + self._world = world + self._textures = textures + self._grid = np.array(grid) + self._offset = self._grid // 2 + self._area = np.array(self._world.area) + self._center = None + + def __call__(self, player, unit): + self._unit = np.array(unit) + self._center = np.array(player.pos) + canvas = np.zeros(tuple(self._grid * unit) + (3,), np.uint8) + 127 + for x in range(self._grid[0]): + for y in range(self._grid[1]): + pos = self._center + np.array([x, y]) - self._offset + if not _inside((0, 0), pos, self._area): + continue + texture = self._textures.get(self._world[pos][0], unit) + _draw(canvas, np.array([x, y]) * unit, texture) + for obj in self._world.objects: + pos = obj.pos - self._center + self._offset + if not _inside((0, 0), pos, self._grid): + continue + texture = self._textures.get(obj.texture, unit) + _draw_alpha(canvas, pos * unit, texture) + canvas = self._light(canvas, self._world.daylight) + if player.sleeping: + canvas = self._sleep(canvas) + # if player.health < 1: + # canvas = self._tint(canvas, (128, 0, 0), 0.6) + return canvas + + def _light(self, canvas, daylight): + night = canvas + if daylight < 0.5: + night = self._noise(night, 2 * (0.5 - daylight), 0.5) + night = np.array(ImageEnhance.Color( + Image.fromarray(night.astype(np.uint8))).enhance(0.4)) + night = self._tint(night, (0, 16, 64), 0.5) + return daylight * canvas + (1 - daylight) * night + + def _sleep(self, canvas): + canvas = np.array(ImageEnhance.Color( + Image.fromarray(canvas.astype(np.uint8))).enhance(0.0)) + canvas = self._tint(canvas, (0, 0, 16), 0.5) + return canvas + + def _tint(self, canvas, color, amount): + color = np.array(color) + return (1 - amount) * canvas + amount * color + + def _noise(self, canvas, amount, stddev): + noise = self._world.random.uniform(32, 127, canvas.shape[:2])[..., None] + mask = amount * self._vignette(canvas.shape, stddev)[..., None] + return (1 - mask) * canvas + mask * noise + + @functools.lru_cache(10) + def _vignette(self, shape, stddev): + xs, ys = np.meshgrid( + np.linspace(-1, 1, shape[0]), + np.linspace(-1, 1, shape[1])) + return 1 - np.exp(-0.5 * (xs ** 2 + ys ** 2) / (stddev ** 2)).T + + +class ItemView: + + def __init__(self, textures, grid): + self._textures = textures + self._grid = np.array(grid) + + def __call__(self, inventory, unit): + unit = np.array(unit) + canvas = np.zeros(tuple(self._grid * unit) + (3,), np.uint8) + for index, (item, amount) in enumerate(inventory.items()): + if amount < 1: + continue + self._item(canvas, index, item, unit) + self._amount(canvas, index, amount, unit) + return canvas + + def _item(self, canvas, index, item, unit): + pos = index % self._grid[0], index // self._grid[0] + pos = (pos * unit + 0.1 * unit).astype(np.int32) + texture = self._textures.get(item, 0.8 * unit) + _draw_alpha(canvas, pos, texture) + + def _amount(self, canvas, index, amount, unit): + pos = index % self._grid[0], index // self._grid[0] + pos = (pos * unit + 0.4 * unit).astype(np.int32) + text = str(amount) if amount in list(range(10)) else 'unknown' + texture = self._textures.get(text, 0.6 * unit) + _draw_alpha(canvas, pos, texture) + + +class SemanticView: + + def __init__(self, world, obj_types): + self._world = world + self._mat_ids = world._mat_ids.copy() + self._obj_ids = { + c: len(self._mat_ids) + i + for i, c in enumerate(obj_types)} + + def __call__(self): + canvas = self._world._mat_map.copy() + for obj in self._world.objects: + canvas[tuple(obj.pos)] = self._obj_ids[type(obj)] + return canvas + + +def _inside(lhs, mid, rhs): + return (lhs[0] <= mid[0] < rhs[0]) and (lhs[1] <= mid[1] < rhs[1]) + +def _draw(canvas, pos, texture): + (x, y), (w, h) = pos, texture.shape[:2] + if texture.shape[-1] == 4: + texture = texture[..., :3] + canvas[x: x + w, y: y + h] = texture + +def _draw_alpha(canvas, pos, texture): + (x, y), (w, h) = pos, texture.shape[:2] + if texture.shape[-1] == 4: + alpha = texture[..., 3:].astype(np.float32) / 255 + texture = texture[..., :3].astype(np.float32) / 255 + current = canvas[x: x + w, y: y + h].astype(np.float32) / 255 + blended = alpha * texture + (1 - alpha) * current + texture = (255 * blended).astype(np.uint8) + canvas[x: x + w, y: y + h] = texture diff --git a/examples/crafter/crafter/env.py b/examples/crafter/crafter/env.py new file mode 100644 index 0000000..62522f2 --- /dev/null +++ b/examples/crafter/crafter/env.py @@ -0,0 +1,187 @@ +import collections + +import numpy as np + +from . import constants +from . import engine +from . import objects +from . import worldgen + + +# Gym is an optional dependency. +try: + import gym + DiscreteSpace = gym.spaces.Discrete + BoxSpace = gym.spaces.Box + DictSpace = gym.spaces.Dict + BaseClass = gym.Env +except ImportError: + DiscreteSpace = collections.namedtuple('DiscreteSpace', 'n') + BoxSpace = collections.namedtuple('BoxSpace', 'low, high, shape, dtype') + DictSpace = collections.namedtuple('DictSpace', 'spaces') + BaseClass = object + + +class Env(BaseClass): + + def __init__( + self, area=(64, 64), view=(9, 9), size=(64, 64), + reward=True, length=10000, seed=None): + view = np.array(view if hasattr(view, '__len__') else (view, view)) + size = np.array(size if hasattr(size, '__len__') else (size, size)) + seed = np.random.randint(0, 2**31 - 1) if seed is None else seed + self._area = area + self._view = view + self._size = size + self._reward = reward + self._length = length + self._seed = seed + self._episode = 0 + self._world = engine.World(area, constants.materials, (12, 12)) + self._textures = engine.Textures(constants.root / 'assets') + item_rows = int(np.ceil(len(constants.items) / view[0])) + self._local_view = engine.LocalView( + self._world, self._textures, [view[0], view[1] - item_rows]) + self._item_view = engine.ItemView( + self._textures, [view[0], item_rows]) + self._sem_view = engine.SemanticView(self._world, [ + objects.Player, objects.Cow, objects.Zombie, + objects.Skeleton, objects.Arrow, objects.Plant]) + self._step = None + self._player = None + self._last_health = None + self._unlocked = None + # Some libraries expect these attributes to be set. + self.reward_range = None + self.metadata = None + + @property + def observation_space(self): + return BoxSpace(0, 255, tuple(self._size) + (3,), np.uint8) + + @property + def action_space(self): + return DiscreteSpace(len(constants.actions)) + + @property + def action_names(self): + return constants.actions + + def reset(self): + center = (self._world.area[0] // 2, self._world.area[1] // 2) + self._episode += 1 + self._step = 0 + self._world.reset(seed=hash((self._seed, self._episode)) % (2 ** 31 - 1)) + self._update_time() + self._player = objects.Player(self._world, center) + self._last_health = self._player.health + self._world.add(self._player) + self._unlocked = set() + worldgen.generate_world(self._world, self._player) + return self._obs() + + def step(self, action): + if self._player.sleeping: + action = constants.actions.index('noop') + self._step += 1 + self._update_time() + self._player.action = constants.actions[action] + for obj in self._world.objects: + if self._player.distance(obj) < 2 * max(self._view): + obj.update() + if self._step % 10 == 0: + for chunk, objs in self._world.chunks.items(): + # xmin, xmax, ymin, ymax = chunk + # center = (xmax - xmin) // 2, (ymax - ymin) // 2 + # if self._player.distance(center) < 4 * max(self._view): + self._balance_chunk(chunk, objs) + obs = self._obs() + reward = (self._player.health - self._last_health) / 10 + self._last_health = self._player.health + unlocked = { + name for name, count in self._player.achievements.items() + if count > 0 and name not in self._unlocked} + if unlocked: + self._unlocked |= unlocked + reward += 1.0 + dead = self._player.health <= 0 + over = self._length and self._step >= self._length + done = dead # or over + info = { + 'inventory': self._player.inventory.copy(), + 'achievements': self._player.achievements.copy(), + 'sleeping': self._player.sleeping, + 'discount': 1 - float(dead), + 'semantic': self._sem_view(), + 'player_pos': self._player.pos, + 'player_facing': self._player.facing, + 'reward': reward, + 'dead': dead, + 'unlocked': unlocked, + 'action': self._player.action, + 'view': self._view, + } + if not self._reward: + reward = 0.0 + return obs, reward, done, info + + def render(self, size=None): + size = size or self._size + unit = size // self._view + canvas = np.zeros(tuple(size) + (3,), np.uint8) + local_view = self._local_view(self._player, unit) + item_view = self._item_view(self._player.inventory, unit) + view = np.concatenate([local_view, item_view], 1) + border = (size - (size // self._view) * self._view) // 2 + (x, y), (w, h) = border, view.shape[:2] + canvas[x: x + w, y: y + h] = view + return canvas.transpose((1, 0, 2)) + + def _obs(self): + return self.render() + + def _update_time(self): + # https://www.desmos.com/calculator/grfbc6rs3h + progress = (self._step / 300) % 1 + 0.3 + daylight = 1 - np.abs(np.cos(np.pi * progress)) ** 3 + self._world.daylight = daylight + + def _balance_chunk(self, chunk, objs): + light = self._world.daylight + self._balance_object( + chunk, objs, objects.Zombie, 'grass', 6, 0, 0.3, 0.4, + lambda pos: objects.Zombie(self._world, pos, self._player), + lambda num, space: ( + 0 if space < 50 else 3.5 - 3 * light, 3.5 - 3 * light)) + self._balance_object( + chunk, objs, objects.Skeleton, 'path', 7, 7, 0.1, 0.1, + lambda pos: objects.Skeleton(self._world, pos, self._player), + lambda num, space: (0 if space < 6 else 1, 2)) + self._balance_object( + chunk, objs, objects.Cow, 'grass', 5, 5, 0.01, 0.1, + lambda pos: objects.Cow(self._world, pos), + lambda num, space: (0 if space < 30 else 1, 1.5 + light)) + + def _balance_object( + self, chunk, objs, cls, material, span_dist, despan_dist, + spawn_prob, despawn_prob, ctor, target_fn): + xmin, xmax, ymin, ymax = chunk + random = self._world.random + creatures = [obj for obj in objs if isinstance(obj, cls)] + mask = self._world.mask(*chunk, material) + target_min, target_max = target_fn(len(creatures), mask.sum()) + if len(creatures) < int(target_min) and random.uniform() < spawn_prob: + xs = np.tile(np.arange(xmin, xmax)[:, None], [1, ymax - ymin]) + ys = np.tile(np.arange(ymin, ymax)[None, :], [xmax - xmin, 1]) + xs, ys = xs[mask], ys[mask] + i = random.randint(0, len(xs)) + pos = np.array((xs[i], ys[i])) + empty = self._world[pos][1] is None + away = self._player.distance(pos) >= span_dist + if empty and away: + self._world.add(ctor(pos)) + elif len(creatures) > int(target_max) and random.uniform() < despawn_prob: + obj = creatures[random.randint(0, len(creatures))] + away = self._player.distance(obj.pos) >= despan_dist + if away: + self._world.remove(obj) diff --git a/examples/crafter/crafter/objects.py b/examples/crafter/crafter/objects.py new file mode 100644 index 0000000..fe0699e --- /dev/null +++ b/examples/crafter/crafter/objects.py @@ -0,0 +1,424 @@ +import numpy as np + +from . import constants +from . import engine + + +class Object: + + def __init__(self, world, pos): + self.world = world + self.pos = np.array(pos) + self.random = world.random + self.inventory = {'health': 0} + self.removed = False + + @property + def texture(self): + raise 'unknown' + + @property + def walkable(self): + return constants.walkable + + @property + def health(self): + return self.inventory['health'] + + @health.setter + def health(self, value): + self.inventory['health'] = max(0, value) + + @property + def all_dirs(self): + return ((-1, 0), (+1, 0), (0, -1), (0, +1)) + + def move(self, direction): + direction = np.array(direction) + target = self.pos + direction + if self.is_free(target): + self.world.move(self, target) + return True + return False + + def is_free(self, target, materials=None): + materials = self.walkable if materials is None else materials + material, obj = self.world[target] + return obj is None and material in materials + + def distance(self, target): + if hasattr(target, 'pos'): + target = target.pos + return np.abs(target - self.pos).sum() + + def toward(self, target, long_axis=True): + if hasattr(target, 'pos'): + target = target.pos + offset = target - self.pos + dists = np.abs(offset) + if (dists[0] > dists[1] if long_axis else dists[0] <= dists[1]): + return np.array((np.sign(offset[0]), 0)) + else: + return np.array((0, np.sign(offset[1]))) + + def random_dir(self): + return self.all_dirs[self.random.randint(0, 4)] + + +class Player(Object): + + def __init__(self, world, pos): + super().__init__(world, pos) + self.facing = (0, 1) + self.inventory = { + name: info['initial'] for name, info in constants.items.items()} + self.achievements = {name: 0 for name in constants.achievements} + self.action = 'noop' + self.sleeping = False + self._last_health = self.health + self._hunger = 0 + self._thirst = 0 + self._fatigue = 0 + self._recover = 0 + + @property + def texture(self): + if self.sleeping: + return 'player-sleep' + return { + (-1, 0): 'player-left', + (+1, 0): 'player-right', + (0, -1): 'player-up', + (0, +1): 'player-down', + }[tuple(self.facing)] + + @property + def walkable(self): + return constants.walkable + ['lava'] + + def update(self): + target = (self.pos[0] + self.facing[0], self.pos[1] + self.facing[1]) + material, obj = self.world[target] + action = self.action + if self.sleeping: + if self.inventory['energy'] < constants.items['energy']['max']: + action = 'sleep' + else: + self.sleeping = False + self.achievements['wake_up'] += 1 + if action == 'noop': + pass + elif action.startswith('move_'): + self._move(action[len('move_'):]) + elif action == 'do' and obj: + self._do_object(obj) + elif action == 'do': + self._do_material(target, material) + elif action == 'sleep': + if self.inventory['energy'] < constants.items['energy']['max']: + self.sleeping = True + elif action.startswith('place_'): + self._place(action[len('place_'):], target, material) + elif action.startswith('make_'): + self._make(action[len('make_'):]) + self._update_life_stats() + self._degen_or_regen_health() + for name, amount in self.inventory.items(): + maxmium = constants.items[name]['max'] + self.inventory[name] = max(0, min(amount, maxmium)) + # This needs to happen after the inventory states are clamped + # because it involves the health water inventory count. + self._wake_up_when_hurt() + + def _update_life_stats(self): + self._hunger += 0.5 if self.sleeping else 1 + if self._hunger > 25: + self._hunger = 0 + self.inventory['food'] -= 1 + self._thirst += 0.5 if self.sleeping else 1 + if self._thirst > 20: + self._thirst = 0 + self.inventory['drink'] -= 1 + if self.sleeping: + self._fatigue = min(self._fatigue - 1, 0) + else: + self._fatigue += 1 + if self._fatigue < -10: + self._fatigue = 0 + self.inventory['energy'] += 1 + if self._fatigue > 30: + self._fatigue = 0 + self.inventory['energy'] -= 1 + + def _degen_or_regen_health(self): + necessities = ( + self.inventory['food'] > 0, + self.inventory['drink'] > 0, + self.inventory['energy'] > 0 or self.sleeping) + if all(necessities): + self._recover += 2 if self.sleeping else 1 + else: + self._recover -= 0.5 if self.sleeping else 1 + if self._recover > 25: + self._recover = 0 + self.health += 1 + if self._recover < -15: + self._recover = 0 + self.health -= 1 + + def _wake_up_when_hurt(self): + if self.health < self._last_health: + self.sleeping = False + self._last_health = self.health + + def _move(self, direction): + directions = dict(left=(-1, 0), right=(+1, 0), up=(0, -1), down=(0, +1)) + self.facing = directions[direction] + self.move(self.facing) + if self.world[self.pos][0] == 'lava': + self.health = 0 + + def _do_object(self, obj): + damage = max([ + 1, + self.inventory['wood_sword'] and 2, + self.inventory['stone_sword'] and 3, + self.inventory['iron_sword'] and 5, + ]) + if isinstance(obj, Plant): + if obj.ripe: + obj.grown = 0 + self.inventory['food'] += 4 + self.achievements['eat_plant'] += 1 + if isinstance(obj, Fence): + self.world.remove(obj) + self.inventory['fence'] += 1 + self.achievements['collect_fence'] += 1 + if isinstance(obj, Zombie): + obj.health -= damage + if obj.health <= 0: + self.achievements['defeat_zombie'] += 1 + if isinstance(obj, Skeleton): + obj.health -= damage + if obj.health <= 0: + self.achievements['defeat_skeleton'] += 1 + if isinstance(obj, Cow): + obj.health -= damage + if obj.health <= 0: + self.inventory['food'] += 6 + self.achievements['eat_cow'] += 1 + # TODO: Keep track of previous inventory state to do this in a more + # general way. + self._hunger = 0 + + def _do_material(self, target, material): + if material == 'water': + # TODO: Keep track of previous inventory state to do this in a more + # general way. + self._thirst = 0 + info = constants.collect.get(material) + if not info: + return + for name, amount in info['require'].items(): + if self.inventory[name] < amount: + return + self.world[target] = info['leaves'] + if self.random.uniform() <= info.get('probability', 1): + for name, amount in info['receive'].items(): + self.inventory[name] += amount + self.achievements[f'collect_{name}'] += 1 + + def _place(self, name, target, material): + if self.world[target][1]: + return + info = constants.place[name] + if material not in info['where']: + return + if any(self.inventory[k] < v for k, v in info['uses'].items()): + return + for item, amount in info['uses'].items(): + self.inventory[item] -= amount + if info['type'] == 'material': + self.world[target] = name + elif info['type'] == 'object': + cls = { + 'fence': Fence, + 'plant': Plant, + }[name] + self.world.add(cls(self.world, target)) + self.achievements[f'place_{name}'] += 1 + + def _make(self, name): + nearby, _ = self.world.nearby(self.pos, 1) + info = constants.make[name] + if not all(util in nearby for util in info['nearby']): + return + if any(self.inventory[k] < v for k, v in info['uses'].items()): + return + for item, amount in info['uses'].items(): + self.inventory[item] -= amount + self.inventory[name] += info['gives'] + self.achievements[f'make_{name}'] += 1 + + +class Cow(Object): + + def __init__(self, world, pos): + super().__init__(world, pos) + self.health = 3 + + @property + def texture(self): + return 'cow' + + def update(self): + if self.health <= 0: + self.world.remove(self) + if self.random.uniform() < 0.5: + direction = self.random_dir() + self.move(direction) + + +class Zombie(Object): + + def __init__(self, world, pos, player): + super().__init__(world, pos) + self.player = player + self.health = 5 + self.cooldown = 0 + + @property + def texture(self): + return 'zombie' + + def update(self): + if self.health <= 0: + self.world.remove(self) + dist = self.distance(self.player) + if dist <= 8 and self.random.uniform() < 0.9: + self.move(self.toward(self.player, self.random.uniform() < 0.8)) + else: + self.move(self.random_dir()) + dist = self.distance(self.player) + if dist <= 1: + if self.cooldown: + self.cooldown -= 1 + else: + if self.player.sleeping: + damage = 7 + else: + damage = 2 + self.player.health -= damage + self.cooldown = 5 + + +class Skeleton(Object): + + def __init__(self, world, pos, player): + super().__init__(world, pos) + self.player = player + self.health = 3 + self.reload = 0 + + @property + def texture(self): + return 'skeleton' + + def update(self): + if self.health <= 0: + self.world.remove(self) + self.reload = max(0, self.reload - 1) + dist = self.distance(self.player.pos) + if dist <= 3: + moved = self.move(-self.toward(self.player, self.random.uniform() < 0.6)) + if moved: + return + if dist <= 5 and self.random.uniform() < 0.5: + self._shoot(self.toward(self.player)) + elif dist <= 8 and self.random.uniform() < 0.3: + self.move(self.toward(self.player, self.random.uniform() < 0.6)) + elif self.random.uniform() < 0.2: + self.move(self.random_dir()) + + def _shoot(self, direction): + if self.reload > 0: + return + if direction[0] == 0 and direction[1] == 0: + return + pos = self.pos + direction + if self.is_free(pos, Arrow.walkable): + self.world.add(Arrow(self.world, pos, direction)) + self.reload = 4 + + +class Arrow(Object): + + def __init__(self, world, pos, facing): + super().__init__(world, pos) + self.facing = facing + + @property + def texture(self): + return { + (-1, 0): 'arrow-left', + (+1, 0): 'arrow-right', + (0, -1): 'arrow-up', + (0, +1): 'arrow-down', + }[tuple(self.facing)] + + @engine.staticproperty + def walkable(): + return constants.walkable + ['water', 'lava'] + + def update(self): + target = self.pos + self.facing + material, obj = self.world[target] + if obj: + obj.health -= 2 + self.world.remove(self) + elif material not in self.walkable: + self.world.remove(self) + if material in ['table', 'furnace']: + self.world[target] = 'path' + else: + self.move(self.facing) + + +class Plant(Object): + + def __init__(self, world, pos): + super().__init__(world, pos) + self.health = 1 + self.grown = 0 + + @property + def texture(self): + if self.ripe: + return 'plant-ripe' + else: + return 'plant' + + @property + def ripe(self): + return self.grown > 300 + + def update(self): + self.grown += 1 + objs = [self.world[self.pos + dir_][1] for dir_ in self.all_dirs] + if any(isinstance(obj, (Zombie, Skeleton, Cow)) for obj in objs): + self.health -= 1 + if self.health <= 0: + self.world.remove(self) + + +class Fence(Object): + + def __init__(self, world, pos): + super().__init__(world, pos) + + @property + def texture(self): + return 'fence' + + def update(self): + pass diff --git a/examples/crafter/crafter/recorder.py b/examples/crafter/crafter/recorder.py new file mode 100644 index 0000000..76dc6b0 --- /dev/null +++ b/examples/crafter/crafter/recorder.py @@ -0,0 +1,185 @@ +import datetime +import json +import pathlib + +import imageio +import numpy as np + + +class Recorder: + + def __init__( + self, env, directory, save_stats=True, save_video=True, + save_episode=True, video_size=(512, 512)): + if directory and save_stats: + env = StatsRecorder(env, directory) + if directory and save_video: + env = VideoRecorder(env, directory, video_size) + if directory and save_episode: + env = EpisodeRecorder(env, directory) + self._env = env + + def __getattr__(self, name): + if name.startswith('__'): + raise AttributeError(name) + return getattr(self._env, name) + + +class StatsRecorder: + + def __init__(self, env, directory): + self._env = env + self._directory = pathlib.Path(directory).expanduser() + self._directory.mkdir(exist_ok=True, parents=True) + self._file = (self._directory / 'stats.jsonl').open('a') + self._length = None + self._reward = None + self._unlocked = None + self._stats = None + + def __getattr__(self, name): + if name.startswith('__'): + raise AttributeError(name) + return getattr(self._env, name) + + def reset(self): + obs = self._env.reset() + self._length = 0 + self._reward = 0 + self._unlocked = None + self._stats = None + return obs + + def step(self, action): + obs, reward, done, info = self._env.step(action) + self._length += 1 + self._reward += info['reward'] + if done: + self._stats = {'length': self._length, 'reward': round(self._reward, 1)} + for key, value in info['achievements'].items(): + self._stats[f'achievement_{key}'] = value + self._save() + return obs, reward, done, info + + def _save(self): + self._file.write(json.dumps(self._stats) + '\n') + self._file.flush() + + +class VideoRecorder: + + def __init__(self, env, directory, size=(512, 512)): + if not hasattr(env, 'episode_name'): + env = EpisodeName(env) + self._env = env + self._directory = pathlib.Path(directory).expanduser() + self._directory.mkdir(exist_ok=True, parents=True) + self._size = size + self._frames = None + + def __getattr__(self, name): + if name.startswith('__'): + raise AttributeError(name) + return getattr(self._env, name) + + def reset(self): + obs = self._env.reset() + self._frames = [self._env.render(self._size)] + return obs + + def step(self, action): + obs, reward, done, info = self._env.step(action) + self._frames.append(self._env.render(self._size)) + if done: + self._save() + return obs, reward, done, info + + def _save(self): + filename = str(self._directory / (self._env.episode_name + '.mp4')) + imageio.mimsave(filename, self._frames) + + +class EpisodeRecorder: + + def __init__(self, env, directory): + if not hasattr(env, 'episode_name'): + env = EpisodeName(env) + self._env = env + self._directory = pathlib.Path(directory).expanduser() + self._directory.mkdir(exist_ok=True, parents=True) + self._episode = None + + def __getattr__(self, name): + if name.startswith('__'): + raise AttributeError(name) + return getattr(self._env, name) + + def reset(self): + obs = self._env.reset() + self._episode = [{'image': obs}] + return obs + + def step(self, action): + # Transitions are defined from the environment perspective, meaning that a + # transition contains the action and the resulting reward and next + # observation produced by the environment in response to said action. + obs, reward, done, info = self._env.step(action) + transition = { + 'action': action, 'image': obs, 'reward': reward, 'done': done, + } + for key, value in info.items(): + if key in ('inventory', 'achievements'): + continue + transition[key] = value + for key, value in info['achievements'].items(): + transition[f'achievement_{key}'] = value + for key, value in info['inventory'].items(): + transition[f'ainventory_{key}'] = value + self._episode.append(transition) + if done: + self._save() + return obs, reward, done, info + + def _save(self): + filename = str(self._directory / (self._env.episode_name + '.npz')) + # Fill in zeros for keys missing at the first time step. + for key, value in self._episode[1].items(): + if key not in self._episode[0]: + self._episode[0][key] = np.zeros_like(value) + episode = { + k: np.array([step[k] for step in self._episode]) + for k in self._episode[0]} + np.savez_compressed(filename, **episode) + + +class EpisodeName: + + def __init__(self, env): + self._env = env + self._timestamp = None + self._unlocked = None + self._length = None + + def __getattr__(self, name): + if name.startswith('__'): + raise AttributeError(name) + return getattr(self._env, name) + + def reset(self): + obs = self._env.reset() + self._timestamp = None + self._unlocked = None + self._length = 0 + return obs + + def step(self, action): + obs, reward, done, info = self._env.step(action) + self._length += 1 + if done: + self._timestamp = datetime.datetime.now().strftime('%Y%m%dT%H%M%S') + self._unlocked = sum(int(v >= 1) for v in info['achievements'].values()) + return obs, reward, done, info + + @property + def episode_name(self): + return f'{self._timestamp}-ach{self._unlocked}-len{self._length}' diff --git a/examples/crafter/crafter/run_gui.py b/examples/crafter/crafter/run_gui.py new file mode 100644 index 0000000..06885f9 --- /dev/null +++ b/examples/crafter/crafter/run_gui.py @@ -0,0 +1,150 @@ +import argparse + +import numpy as np +try: + import pygame +except ImportError: + print('Please install the pygame package to use the GUI.') + raise +from PIL import Image + +import crafter + + +def main(): + boolean = lambda x: bool(['False', 'True'].index(x)) + parser = argparse.ArgumentParser() + parser.add_argument('--seed', type=int, default=None) + parser.add_argument('--area', nargs=2, type=int, default=(64, 64)) + parser.add_argument('--view', type=int, nargs=2, default=(9, 9)) + parser.add_argument('--length', type=int, default=None) + parser.add_argument('--health', type=int, default=9) + parser.add_argument('--window', type=int, nargs=2, default=(600, 600)) + parser.add_argument('--size', type=int, nargs=2, default=(0, 0)) + parser.add_argument('--record', type=str, default=None) + parser.add_argument('--fps', type=int, default=5) + parser.add_argument('--wait', type=boolean, default=False) + parser.add_argument('--death', type=str, default='reset', choices=[ + 'continue', 'reset', 'quit']) + args = parser.parse_args() + + keymap = { + pygame.K_a: 'move_left', + pygame.K_d: 'move_right', + pygame.K_w: 'move_up', + pygame.K_s: 'move_down', + pygame.K_SPACE: 'do', + pygame.K_TAB: 'sleep', + + pygame.K_r: 'place_stone', + pygame.K_t: 'place_table', + pygame.K_f: 'place_furnace', + pygame.K_p: 'place_plant', + + pygame.K_1: 'make_wood_pickaxe', + pygame.K_2: 'make_stone_pickaxe', + pygame.K_3: 'make_iron_pickaxe', + pygame.K_4: 'make_wood_sword', + pygame.K_5: 'make_stone_sword', + pygame.K_6: 'make_iron_sword', + } + print('Actions:') + for key, action in keymap.items(): + print(f' {pygame.key.name(key)}: {action}') + + crafter.constants.items['health']['max'] = args.health + crafter.constants.items['health']['initial'] = args.health + + size = list(args.size) + size[0] = size[0] or args.window[0] + size[1] = size[1] or args.window[1] + + env = crafter.Env( + area=args.area, view=args.view, length=args.length, seed=args.seed) + env = crafter.Recorder(env, args.record) + env.reset() + achievements = set() + duration = 0 + return_ = 0 + was_done = False + print('Diamonds exist:', env._world.count('diamond')) + + pygame.init() + screen = pygame.display.set_mode(args.window) + clock = pygame.time.Clock() + running = True + while running: + + # Rendering. + image = env.render(size) + if size != args.window: + image = Image.fromarray(image) + image = image.resize(args.window, resample=Image.NEAREST) + image = np.array(image) + surface = pygame.surfarray.make_surface(image.transpose((1, 0, 2))) + screen.blit(surface, (0, 0)) + pygame.display.flip() + clock.tick(args.fps) + + # Keyboard input. + action = None + pygame.event.pump() + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: + running = False + elif event.type == pygame.KEYDOWN and event.key in keymap.keys(): + action = keymap[event.key] + if action is None: + pressed = pygame.key.get_pressed() + for key, action in keymap.items(): + if pressed[key]: + break + else: + if args.wait and not env._player.sleeping: + continue + else: + action = 'noop' + + # Environment step. + _, reward, done, _ = env.step(env.action_names.index(action)) + duration += 1 + + # Achievements. + unlocked = { + name for name, count in env._player.achievements.items() + if count > 0 and name not in achievements} + for name in unlocked: + achievements |= unlocked + total = len(env._player.achievements.keys()) + print(f'Achievement ({len(achievements)}/{total}): {name}') + if env._step > 0 and env._step % 100 == 0: + print(f'Time step: {env._step}') + if reward: + print(f'Reward: {reward}') + return_ += reward + + # Episode end. + if done and not was_done: + was_done = True + print('Episode done!') + print('Duration:', duration) + print('Return:', return_) + if args.death == 'quit': + running = False + if args.death == 'reset': + print('\nStarting a new episode.') + env.reset() + achievements = set() + was_done = False + duration = 0 + return_ = 0 + if args.death == 'continue': + pass + + pygame.quit() + + +if __name__ == '__main__': + main() diff --git a/examples/crafter/crafter/run_random.py b/examples/crafter/crafter/run_random.py new file mode 100644 index 0000000..8ab1367 --- /dev/null +++ b/examples/crafter/crafter/run_random.py @@ -0,0 +1,48 @@ +import argparse +import pathlib +import time + +import numpy as np + +import crafter + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--seed', type=int, default=None) + parser.add_argument('--area', nargs=2, type=int, default=(64, 64)) + parser.add_argument('--length', type=int, default=10000) + parser.add_argument('--health', type=int, default=9) + parser.add_argument('--record', type=pathlib.Path, default=None) + parser.add_argument('--episodes', type=int, default=1) + args = parser.parse_args() + + random = np.random.RandomState(args.seed) + crafter.constants.items['health']['max'] = args.health + crafter.constants.items['health']['initial'] = args.health + env = crafter.Env(area=args.area, length=args.length, seed=args.seed) + env = crafter.Recorder(env, args.record) + + for _ in range(args.episodes): + + start = time.time() + obs = env.reset() + print('') + print(f'Reset time: {1000*(time.time()-start):.2f}ms') + print('Coal exist: ', env._world.count('coal')) + print('Iron exist: ', env._world.count('iron')) + print('Diamonds exist:', env._world.count('diamond')) + + start = time.time() + done = False + while not done: + action = random.randint(0, env.action_space.n) + obs, reward, done, info = env.step(action) + duration = time.time() - start + step = env._step + print(f'Step time: {1000*duration/step:.2f}ms ({int(step/duration)} FPS)') + print('Episode length:', step) + + +if __name__ == '__main__': + main() diff --git a/examples/crafter/crafter/run_terrain.py b/examples/crafter/crafter/run_terrain.py new file mode 100644 index 0000000..009955b --- /dev/null +++ b/examples/crafter/crafter/run_terrain.py @@ -0,0 +1,43 @@ +import argparse + +import imageio +import numpy as np + +import crafter + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--seed', type=int, default=None) + parser.add_argument('--amount', type=int, default=4) + parser.add_argument('--cols', type=int, default=4) + parser.add_argument('--area', nargs=2, type=int, default=(64, 64)) + parser.add_argument('--size', type=int, default=1024) + parser.add_argument('--filename', type=str, default='terrain.png') + args = parser.parse_args() + + env = crafter.Env(args.area, args.area, args.size, seed=args.seed) + images = [] + for index in range(args.amount): + images.append(env.reset()) + diamonds = env._world.count('diamond') + print(f'Map: {index:>2}, diamonds: {diamonds:>2}') + + rows = len(images) // args.cols + strips = [] + for row in range(rows): + strip = [] + for col in range(args.cols): + try: + strip.append(images[row * args.cols + col]) + except IndexError: + strip.append(np.zeros_like(strip[-1])) + strips.append(np.concatenate(strip, 1)) + grid = np.concatenate(strips, 0) + + imageio.imsave(args.filename, grid) + print('Saved', args.filename) + + +if __name__ == '__main__': + main() diff --git a/examples/crafter/crafter/worldgen.py b/examples/crafter/crafter/worldgen.py new file mode 100644 index 0000000..257d5e7 --- /dev/null +++ b/examples/crafter/crafter/worldgen.py @@ -0,0 +1,91 @@ +import functools + +import numpy as np +import opensimplex + +from . import constants +from . import objects + + +def generate_world(world, player): + simplex = opensimplex.OpenSimplex(seed=world.random.randint(0, 2 ** 31 - 1)) + tunnels = np.zeros(world.area, bool) + for x in range(world.area[0]): + for y in range(world.area[1]): + _set_material(world, (x, y), player, tunnels, simplex) + for x in range(world.area[0]): + for y in range(world.area[1]): + _set_object(world, (x, y), player, tunnels) + + +def _set_material(world, pos, player, tunnels, simplex): + x, y = pos + simplex = functools.partial(_simplex, simplex) + uniform = world.random.uniform + start = 4 - np.sqrt((x - player.pos[0]) ** 2 + (y - player.pos[1]) ** 2) + start += 2 * simplex(x, y, 8, 3) + start = 1 / (1 + np.exp(-start)) + water = simplex(x, y, 3, {15: 1, 5: 0.15}, False) + 0.1 + water -= 2 * start + mountain = simplex(x, y, 0, {15: 1, 5: 0.3}) + mountain -= 4 * start + 0.3 * water + if start > 0.5: + world[x, y] = 'grass' + elif mountain > 0.15: + if (simplex(x, y, 6, 7) > 0.15 and mountain > 0.3): # cave + world[x, y] = 'path' + elif simplex(2 * x, y / 5, 7, 3) > 0.4: # horizonal tunnle + world[x, y] = 'path' + tunnels[x, y] = True + elif simplex(x / 5, 2 * y, 7, 3) > 0.4: # vertical tunnle + world[x, y] = 'path' + tunnels[x, y] = True + elif simplex(x, y, 1, 8) > 0 and uniform() > 0.85: + world[x, y] = 'coal' + elif simplex(x, y, 2, 6) > 0.4 and uniform() > 0.75: + world[x, y] = 'iron' + elif mountain > 0.18 and uniform() > 0.994: + world[x, y] = 'diamond' + elif mountain > 0.3 and simplex(x, y, 6, 5) > 0.35: + world[x, y] = 'lava' + else: + world[x, y] = 'stone' + elif 0.25 < water <= 0.35 and simplex(x, y, 4, 9) > -0.2: + world[x, y] = 'sand' + elif 0.3 < water: + world[x, y] = 'water' + else: # grassland + if simplex(x, y, 5, 7) > 0 and uniform() > 0.8: + world[x, y] = 'tree' + else: + world[x, y] = 'grass' + + +def _set_object(world, pos, player, tunnels): + x, y = pos + uniform = world.random.uniform + dist = np.sqrt((x - player.pos[0]) ** 2 + (y - player.pos[1]) ** 2) + material, _ = world[x, y] + if material not in constants.walkable: + pass + elif dist > 3 and material == 'grass' and uniform() > 0.985: + world.add(objects.Cow(world, (x, y))) + elif dist > 10 and uniform() > 0.993: + world.add(objects.Zombie(world, (x, y), player)) + elif material == 'path' and tunnels[x, y] and uniform() > 0.95: + world.add(objects.Skeleton(world, (x, y), player)) + + +def _simplex(simplex, x, y, z, sizes, normalize=True): + if not isinstance(sizes, dict): + sizes = {sizes: 1} + value = 0 + for size, weight in sizes.items(): + if hasattr(simplex, 'noise3d'): + noise = simplex.noise3d(x / size, y / size, z) + else: + noise = simplex.noise3(x / size, y / size, z) + value += weight * noise + if normalize: + value /= sum(sizes.values()) + return value diff --git a/examples/crafter/crafter_description.py b/examples/crafter/crafter_description.py new file mode 100644 index 0000000..ab4c71c --- /dev/null +++ b/examples/crafter/crafter_description.py @@ -0,0 +1,192 @@ +import numpy as np +import crafter + +env = crafter.Env(size=(224, 224)) +action_space = env.action_space + +vitals = ["health","food","drink","energy",] + +rot = np.array([[0,-1],[1,0]]) +directions = ['front', 'right', 'back', 'left'] + +id_to_item = [0]*19 +import itertools +import difflib +for name, ind in itertools.chain(env._world._mat_ids.items(), env._sem_view._obj_ids.items()): + name = str(name)[str(name).find('objects.')+len('objects.'):-2].lower() if 'objects.' in str(name) else str(name) + id_to_item[ind] = name +player_idx = id_to_item.index('player') +print(id_to_item) + +def describe_inventory(info): + result = "" + + status_str = "* Vitals:\n{}".format("\n".join([" - {}: {}/9".format(v, info['inventory'][v]) for v in vitals])) + result += status_str + "\n\n" + + inventory_str = "\n".join([" - {}: {}".format(i, num) for i,num in info['inventory'].items() if i not in vitals and num!=0]) + inventory_str = "* Inventory:\n{}".format(inventory_str) if inventory_str else "Inventory: empty" + result += inventory_str + + return result.strip() + + +REF = np.array([0, 1]) + +def rotation_matrix(v1, v2): + dot = np.dot(v1,v2) + cross = np.cross(v1,v2) + rotation_matrix = np.array([[dot, -cross],[cross, dot]]) + return rotation_matrix + +def describe_dir(ref, P): + desc = [] + desc_detailed = [] + if ref[1] > P[1]: + desc.append("north") + desc_detailed.append("{}N".format(abs(ref[1]-P[1]))) + elif ref[1] < P[1]: + desc.append("south") + desc_detailed.append("{}S".format(abs(ref[1]-P[1]))) + if ref[0] > P[0]: + desc.append("west") + desc_detailed.append("{}W".format(abs(ref[0]-P[0]))) + elif ref[0] < P[0]: + desc.append("east") + desc_detailed.append("{}E".format(abs(ref[0]-P[0]))) + + result = "-".join(desc) + + return result, " ".join(desc_detailed) + +def describe_loc(ref, P, target_facing): + direction, desc_detailed = describe_dir(ref, P) + + if P[0]==target_facing[0] and P[1]==target_facing[1]: + direction += ", {} (facing)".format(desc_detailed) + else: + direction += ", {}".format(desc_detailed) + + return direction + + +def describe_env(info): + assert(info['semantic'][info['player_pos'][0],info['player_pos'][1]] == player_idx) + semantic = info['semantic'][info['player_pos'][0]-info['view'][0]//2:info['player_pos'][0]+info['view'][0]//2+1, info['player_pos'][1]-info['view'][1]//2+1:info['player_pos'][1]+info['view'][1]//2] + center = np.array([info['view'][0]//2,info['view'][1]//2-1]) + result = "" + x = np.arange(semantic.shape[1]) + y = np.arange(semantic.shape[0]) + x1, y1 = np.meshgrid(x,y) + loc = np.stack((y1, x1),axis=-1) + dist = np.absolute(center-loc).sum(axis=-1) + obj_info_list = [] + grass_idx = id_to_item.index('grass') + + facing = info['player_facing'] + target_facing = (center[0] + facing[0], center[1] + facing[1]) + target = id_to_item[semantic[target_facing[0],target_facing[1]]] + around = {} + + obs = "* Observation (1-step):\n" + for d in [[center[0]-1,center[1]],[center[0]+1,center[1]],[center[0],center[1]-1],[center[0],center[1]+1]]: + around[describe_loc(np.array([0,0]), np.array(d) - center, facing)] = id_to_item[semantic[d[0], d[1]]] + + obs = "* Observation (1-step):\n"+"\n".join([" - {}: {}".format(o,d) for d,o in around.items()]) + + for idx in np.unique(semantic): + if idx == player_idx or idx == grass_idx: + continue + + distances = np.where(semantic == idx, dist, np.inf) + smallest_indices = np.unravel_index(np.argsort(distances, axis=None), distances.shape) + smallest_indices = [(smallest_indices[0][i], smallest_indices[1][i]) for i in range(min(2, np.count_nonzero(semantic == idx)))] + + for i in range(len(smallest_indices)): + smallest = smallest_indices[i] + obj_info_list.append((id_to_item[idx], dist[smallest], describe_loc(np.array([0, 0]), smallest - center, facing))) + + if len(obj_info_list)>0: + status_str = "* Near-by objects (7x9 grid):\n{}".format("\n".join([" - {} {} steps to {}".format(name.replace("arrow", "flying-arrow"), dist, loc) for name, dist, loc in obj_info_list])) + else: + status_str = "* Near-by objects (7x9 grid): nothing other than grass" + + # get the player direction and filter semantic to only the front half of the player's facing direction + if facing[0] == 1: + front = semantic[center[0]+1:, :] + elif facing[0] == -1: + front = semantic[:center[0], :] + elif facing[1] == 1: + front = semantic[:, center[1]+1:] + else: + front = semantic[:, :center[1]] + + # get a list of counts for each item in the front + counts = np.bincount(front.flatten()) + # get a sorted named list of counts for each item in the front + counts = sorted([(id_to_item[i], counts[i]) for i in range(len(counts)) if id_to_item[i] not in {'player', 'None'} and counts[i]>0], key=lambda x: x[1], reverse=True) + # find the top 3 items in the front with non-zero counts + counts = counts[:3] + + general_desc = ", ".join(["{} {}(s)".format(count, name) for name, count in counts]) + + general_obs = "* Further to the {}: {}.".format(describe_dir(np.array([0,0]), facing)[0], general_desc) + + result += obs.strip() + "\n\n" + status_str + "\n\n" + general_obs + + return result.strip(), target in {'path', 'grass'} + + +def describe_act(info, repeats): + result = "" + + action_str = info['action'].replace('do_', 'interact_') + if 'move' in action_str: + action_str = action_str.replace('move_up', 'move_north {} step(s)'.format(repeats)) + action_str = action_str.replace('move_down', 'move_south {} step(s)'.format(repeats)) + action_str = action_str.replace('move_left', 'move_west {} step(s)'.format(repeats)) + action_str = action_str.replace('move_right', 'move_east {} step(s)'.format(repeats)) + else: + action_str = action_str + " {} time(s)".format(repeats) + + return action_str.strip() + + +def describe_status(info): + + if info['sleeping']: + return "Player is sleeping, and will not be able take actions until energy is full.\n\n" + elif info['dead']: + return "Player died.\n\n" + else: + return "" + + +def describe_frame(info, repeats): + result = "" + + result+=describe_status(info) + + env_description, front_unblocked = describe_env(info) + + result+=env_description + + result+="\n\n" + + result+=describe_inventory(info) + + return describe_act(info, repeats).strip(), result.strip(), front_unblocked + +action_list = ["Noop", "Move West", "Move East", "Move North", "Move South", "Do", \ + "Sleep", "Place Stone", "Place Table", "Place Furnace", "Place Plant", \ + "Make Wood Pickaxe", "Make Stone Pickaxe", "Make Iron Pickaxe", "Make Wood Sword", \ + "Make Stone Sword", "Make Iron Sword"] +action_list = [a.lower() for a in action_list] + +def match_act(string): + matches = difflib.get_close_matches(string.lower(), action_list, n=1, cutoff=0.85) + if matches: + print("Action matched \"{}\" to \"{}\"".format(string, matches[0])) + return action_list.index(matches[0]), matches[0], "" + else: + return None, None, "'{}' does not seem to match any elemet in the list: {}".format(string, action_list) diff --git a/examples/crafter/crafter_initial_QA.pkl b/examples/crafter/crafter_initial_QA.pkl new file mode 100644 index 0000000000000000000000000000000000000000..bc2b4d13d0aeed4db2f7afd66e14699d87bc12aa GIT binary patch literal 15686 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: + trajectories[-1][1] = last_act_desc + trajectories.append([step, None, desc]) + text_obs_no_act = "\n\n".join(["== Gamestep {}{} ==\n\n".format(i, "" if i!=trajectories[-1][0] else " (current)",) + "{}".format(d) for i, _, d in trajectories[-2:]]) + text_obs = "\n\n".join(["== Gamestep {}{} ==\n\n".format(i, "" if i!=trajectories[-1][0] else " (current)",) + "{}{}".format(d, "\n\nAction:\n{}".format(a) if a is not None else "") for i, a, d in trajectories[-2:]]) + qprint(text_obs) + + database['environment'] = { + 'manual': describe_achievements(info, MANUAL), + 'observation_2step': text_obs_no_act, + 'observation_2step_with_action': text_obs, + 'observation_current': desc, + 'step': step, + } + qa_history_stream = copy.copy(qa_history) + qa_history_stream.append(graph.get_streaming_history()) + database['history'] = { + 'qa_history': qa_history, + 'qa_history_stream': qa_history_stream, + 'qa_history_actor_length': min(args.actor_reflection_granularity, max(3, skill_length)), + 'qa_history_planner_length': args.planner_reflection_granularity, + 'qa_history_planner_reflection_length': 3, + } + + # Printing + qprint("\n" + Fore.BLACK + Back.GREEN + "--"*10 + " Actor Plans " + "--"*10 + Style.RESET_ALL) + if 'action_summary' in database.keys(): + qprint(Style.DIM+json.dumps(database['action_summary'], indent=2) + Style.RESET_ALL) + qprint("Skill: {} -> {}".format(database['skills']['skill_old'], database['skills']['skill'])) + qprint("Past actions:", " -> ".join(past_actions)) + qprint("\n" + Fore.BLACK + Back.GREEN + "--"*10 + " Subgoal " + "--"*10 + Style.RESET_ALL) + if len(qa_history)>0: + strategy_desc = "\n\n".join(["## {}\n{}".format(d, qa_history[-1][q]) for q,d in database['prompts']['strategy_questions_desc'].items()]) + qprint(Style.DIM+ strategy_desc + Style.RESET_ALL) + qprint("\n" + Fore.BLACK + Back.GREEN + "--"*10 + " Knowledge Base " + "--"*10 + Style.RESET_ALL) + qprint(Style.DIM+json.dumps(database['kb']['knowledge_base'], indent=2) + Style.RESET_ALL) + if 'unknowns_json' in database['kb'].keys(): + qprint("\n" + Fore.BLACK + Back.GREEN + "--"*10 + " Unknowns " + "--"*10 + Style.RESET_ALL) + qprint(Style.DIM+json.dumps(list(database['kb']['unknowns_json'].values())[0], indent=2) + Style.RESET_ALL) + qprint("\n" + Fore.BLACK + Back.GREEN + "--"*10 + " Skills " + "--"*10 + Style.RESET_ALL) + qprint(Style.DIM+json.dumps({k: v['skill_desc'] for k,v in database['skills']['skill_library'].items()}, indent=2) + Style.RESET_ALL) + qprint("\n" + Fore.BLACK + Back.GREEN + "--"*10 + " Skill Feedback " + "--"*10 + Style.RESET_ALL) + qprint(Style.DIM + json.dumps(database['feedback']['skill_feedback'], indent=2) + Style.RESET_ALL) + # qprint("\n" + Fore.BLACK + Back.GREEN + "--"*10 + " Achievements " + "--"*10 + Style.RESET_ALL) + # qprint(Style.DIM + describe_achievements(info, MANUAL) + Style.RESET_ALL) + + # Reasoning + qprint("\n" + Fore.BLACK + Back.GREEN + "--"*10 + " Reasoning " + "--"*10 + Style.RESET_ALL) + database['skills']['skill_old'] = database['skills']['skill'] + qa_results = graph.evaluate() + qa_history.append(qa_results) + + skill = database['skills']['skill'] + skill_old = database['skills']['skill_old'] + attention_rounds = database['reflection'] + + if skill is not None: + if skill not in skill_history.keys(): + skill_history[skill] = [] + skill_history[skill].append(step) + if (len(set(skill_history[skill]) & set(attention_rounds['mistake']).union(set(attention_rounds["confusion"])))+1) % args.feedback_granularity == 0: + CTXT_dict = { + "CTXT": describe_achievements(info, MANUAL), + "attention_rounds": list(set(skill_history[skill]) & set(attention_rounds["mistake"]).union(set(attention_rounds["confusion"]))), + 'step_offset': step, + 'qa_history': qa_history, + 'db': database, + } + messages, shrink_idx = compose_feedback_prompt(CTXT_dict, qa_history, "{}:{}".format(skill, database['skills']['skill_library'][skill]['skill_guide']), database['prompts']['feedback_questions']) + database['feedback']['skill_feedback'][skill] = "" + for shorthand, msg in messages.items(): + answer, _ = llm_functions['query_reason']['query_model'](msg, shrink_idx) + # database['feedback']['skill_feedback'][skill] += "{}:\n{}\n\n".format(shorthand, answer) + database['feedback']['skill_feedback'][skill] = "{}".format(answer) + database['feedback']['skill_feedback'][skill] = database['feedback']['skill_feedback'][skill].strip() + qprint(Fore.MAGENTA + "Feedback for {}:".format(skill) + Style.RESET_ALL) + qprint(Style.DIM+database['feedback']['skill_feedback'][skill] + Style.RESET_ALL) + qprint() + + reward = 0 + if info['sleeping']: + qprint(Fore.RED + "Player is sleeping. We manually take noop until the player's awake to save LLM calls:" + Style.RESET_ALL) + a = action_list.index("noop") + database['action'] = a + rep = 0 + while info['sleeping']: + obs, rr, done, info = env.step(a) + qprint(Style.DIM + "====Sleeping: {} Reward: {}====".format(rep+1, rr) + Style.RESET_ALL) + qprint(Style.DIM + describe_frame(info, 1)[1]) + reward += rr + env_step += 1 + rep += 1 + database['action_repeats'] = rep + else: + a = database['action'] + for _ in range(database['action_repeats']): + obs, rr, done, info = env.step(a) + reward += rr + env_step += 1 + + # if the player is blocked, we stop repeating the action + if 'move' in action_list[a] and not describe_frame(info, 1)[-1]: + break + + new_row.append(action_list[database['action']]) + new_row.append(database['action_repeats']) + new_row.append(json.dumps(database['skills']['skill_library'], indent=2)) + new_row.append(json.dumps(database['kb']['knowledge_base'], indent=2)) + new_row.append(env_step) + + for q in table_questions: + new_row.append(qa_results[q]) + + R += reward + OBS.append(obs.copy()) + + step += 1 + if skill_old != skill: + past_actions = [] + skill_length = 1 + else: + past_actions.append(action_list[a]) + skill_length += 1 + achievement_table.add_data(*[info['achievements'][k] for k in achievements]) + + rollout_history.append(new_row) + + + # Knowledge Base Refinement + if step % args.kb_refine_granularity == 0 and len(database['kb']['knowledge_base']) > 0: + messages = [ + {"role": "system", "content" : "Improve the knowledge base. Note that items in the knowledge base should augment the instruction manual, not duplicate it or contradict it. In addition, the knowledge base should not contain duplicate items."} + ] + messages.append({"role": "system", "content": "Instruction manual:\n\n{}".format(MANUAL)}) + messages.append({"role": "system", "content": "Knowledge base:\n\n{}".format(json.dumps(database['kb']['knowledge_base'], indent=0))}) + + messages.append({"role": "user", "content": """ +For each item in the knowledge base, provide a 1-sentence summary of the related manual information if applicable, and determine whether the item should be included or removed from the knowledge base. +Format the output as a JSON dictionary in the following format: +``` +{ +"item_key": { + "item_value": $ANSWER, + "duplicate": $ANSWER, # Is this item a duplicate? [yes/no] + "manual_summary": $ANSWER, # 1-sentence summary of related manual information. Write "NA" if there's no related manual information. + "addition": $ANSWER, # Does this item offer additional information to the manual? [yes/no] + "contradiction": $ANSWER, # Does this item directly contradict the manual_summary? [yes/no] + } +} +``` +""".strip()}) + for _ in range(10): + result, _ = llm_functions['query_reason']['query_model'](messages, 1) + parsed_answer, error_msg = utils.extract_json_objects(result) + if parsed_answer is None or type(parsed_answer[-1]) != dict: + messages.append({"role": "assistant", "content": result}) + messages.append({"role": "user", "content": "Invalid Type: Expecting the last Json object to be dictionary"}) + continue + problem = False + for k, v in parsed_answer[-1].items(): + if len(v) != 5 or type(v) != dict: + messages.append({"role": "assistant", "content": result}) + messages.append({"role": "user", "content": "Invalid Type: Expecting each value to be a dictionary with 5 keys"}) + problem = True + break + if problem: + continue + qprint(Fore.MAGENTA + "Refining Knowledge Base:" + Style.RESET_ALL) + qprint(Style.DIM+json.dumps(parsed_answer[-1], indent=2) + Style.RESET_ALL) + for k, v in parsed_answer[-1].items(): + if "no" in [v['addition'].strip(), ] or 'yes' in [v['duplicate'].strip(), v['contradiction'].strip()]: + del database['kb']['knowledge_base'][k] + break + + + + qprint() + if step % args.wandb_log_interval == 0 or done: + if root_span is not None: + root_span._span.end_time_ms = round(datetime.datetime.now().timestamp() * 1000) + root_span.log(name="eps-{}-trace".format(eps)) + root_span = None + graph.set_wandb_root_span(root_span) + for skill in skill_history.keys(): + feedback_table.add_data(*([skill, "NA", len(set(skill_history[skill]) & set(attention_rounds["mistake"]).union(set(attention_rounds["confusion"])))] if skill not in database['feedback']['skill_feedback'].keys() else [skill, database['feedback']['skill_feedback'][skill], len(set(skill_history[skill]) & set(attention_rounds["mistake"]))])) + rollouts = wandb.Table(columns=columns, data=copy.deepcopy(rollout_history)) + wandb.log({"eps-{}-rollout/rollout {}~{}".format(eps, last_log, step-1): rollouts, + "eps-{}-achievements/achievements {}~{}".format(eps, last_log, step-1): achievement_table, + "eps-{}-feedback/feedback {}~{}".format(eps, last_log, step-1): feedback_table, + "eps-{}-feedback/feedback-current".format(eps): feedback_table, + "eps-{}-current/rollout-current".format(eps): rollouts, + "eps-{}-current/achievements-current".format(eps): achievement_table, + }) + achievement_table = wandb.Table(columns=achievements) + feedback_table = wandb.Table(columns=["Skill", "feedback", "attention_rounds"]) + last_log = step + + with open("saves/{}.pkl".format(wandb.run.id), 'wb') as f: + pickle.dump({ + 'eps': eps, + 'done': done, + 'step': step, + 'env_step': env_step, + 'env': env, + 'trajectories': trajectories, + 'qa_history': qa_history, + 'gameplay_history': gameplay_history, + 'R': R, + 'OBS': OBS, + 'a': a, + 'obs': obs, + 'reward': reward, + 'info': info, + 'last_log': last_log, + 'rollout_history': rollout_history, + 'skill_length': skill_length, + 'skill_history': skill_history, + 'achievement_table': achievement_table, + 'feedback_table': feedback_table, + 'adaptive_answers': adaptive_answers, + 'database': database, + 'past_actions': past_actions, + }, f) + + if done: + with open("saves/{}_eps{}.pkl".format(wandb.run.id, eps), 'wb') as f: + pickle.dump({ + 'eps': eps, + 'done': done, + 'step': step, + 'env_step': env_step, + 'env': env, + 'trajectories': trajectories, + 'qa_history': qa_history, + 'gameplay_history': gameplay_history, + 'R': R, + 'OBS': OBS, + 'a': a, + 'obs': obs, + 'reward': reward, + 'info': info, + 'last_log': last_log, + 'rollout_history': rollout_history, + 'skill_length': skill_length, + 'skill_history': skill_history, + 'achievement_table': achievement_table, + 'feedback_table': feedback_table, + 'adaptive_answers': adaptive_answers, + 'database': database, + 'past_actions': past_actions, + }, f) + break + + wandb.log({"eps-{}-achievements/achievements {}~{}".format(eps, last_log, step-1): achievement_table, + "eps-{}-feedback/feedback {}~{}".format(eps, last_log, step-1): feedback_table, + "eps-{}-ALL/rollout-ALL".format(eps): wandb.Table(columns=columns, data=rollout_history), + "eps-{}-current/achievements-current".format(eps): achievement_table, + "eps-{}-feedback/feedback-current".format(eps): feedback_table, + }) + achievement_table = wandb.Table(columns=achievements) + last_log = step + + # Developer: This part does not seem to help the agent learn better. It's commented out for now. + # I wrote this part to collect feedback at the end of each round of game, but it seems to be unnecessary. + # + # + # CTXT_dict = { + # "CTXT": MANUAL, + # "db": database, + # "attention_rounds": database['reflection'], + # } + # qa_results = topological_traverse(CTXT_dict, qa_history, database['prompts']['gameplay_questions'], compose_gameplay_prompt, max_gen=1024) + # end_of_round_table = wandb.Table(columns=[s for s in database['prompts']['gameplay_shorthands'].values()]) + # row = [] + # database['feedback']['feedback'] = "" + # for q,s in database['prompts']['gameplay_shorthands'].items(): + # database['feedback']['feedback'] += "{}:\n{}\n\n".format(s, qa_results[q].strip()) + # row.append(qa_results[q].strip()) + # end_of_round_table.add_data(*row) + # qprint(Fore.YELLOW + "End of Round FEEDBACK:" + Style.RESET_ALL) + # qprint(Fore.YELLOW + Style.DIM + database['feedback']['feedback'] + Style.RESET_ALL) + # wandb.log({"eps-{}-end-feedback/feedback-end-of-round".format(eps): end_of_round_table,}) + + + eps+=1 + + + +wandb.finish() \ No newline at end of file diff --git a/examples/crafter/post_processing.py b/examples/crafter/post_processing.py new file mode 100644 index 0000000..4803c2e --- /dev/null +++ b/examples/crafter/post_processing.py @@ -0,0 +1,257 @@ +import json +from utils import parse_tuple +from agentkit import exceptions as ex +from agentkit import SimpleDBNode +from compose_prompt import ComposePlannerPrompt +from colorama import Fore +import traceback +from crafter_description import match_act +from agentkit import after_query as aq +import agentkit.utils as utils + +class SubgoalAfterQuery(aq.JsonAfterQuery): + + def __init__(self): + super().__init__() + self.type = dict + self.required_keys = ['subgoal', 'completion_criteria', 'guide'] + self.length = 3 + + def post_process(self): + parsed_answer = self.parse_json() + self.node.db['subgoals']['subgoal'] = parsed_answer[-1]['subgoal'] + self.node.db['subgoals']['completion_criteria'] = parsed_answer[-1]['completion_criteria'] + self.node.db['subgoals']['guide'] = parsed_answer[-1]['guide'] + +class SkillAfterQuery(aq.BaseAfterQuery): + + def post_process(self): + parsed_answer, error_msg = utils.extract_json_objects(self.node.result) + + error = None + if parsed_answer is None: + error = ex.AfterQueryError("Failed to parse answer", error_msg) + elif parsed_answer[-1] is None or len(parsed_answer[-1])==0: + error = ex.AfterQueryError("No answer", "Invalid Json: It seems that the last Json object in the output above is either invalid or empty.") + elif type(parsed_answer[-1]) != dict: + error = ex.AfterQueryError("Invalid answer", "Invalid Type: Expecting the last Json object to be dictionary, got length {} instead.".format(type(parsed_answer[-1]))) + elif len(parsed_answer[-1]) != 1: + error = ex.AfterQueryError("Invalid answer", "Invalid Length: Expecting only one identified skill in the dictionary, got {} instead.".format(len(parsed_answer[-1]))) + elif list(parsed_answer[-1].values())[0] is None or len(list(parsed_answer[-1].values())[0])!=3: + error = ex.AfterQueryError("Invalid answer", "Invalid Value: Expecting the value in the last Json dictionary to be `[description, supported parameters tuple, usage_guide]`, got length {} instead.".format(list(parsed_answer[-1].values())[0])) + + if error is not None: + raise error + + skill_type = list(parsed_answer[-1].keys())[0] + skill_desc, skill_param, skill_guide = parsed_answer[-1][skill_type] + self.node.result_raw = self.node.result + self.node.result = "[{},{},{}]".format(skill_type, skill_desc, skill_param, skill_guide) + self.node.db['skills']['skill_library'][skill_type] = { + 'skill_desc': skill_desc, + 'skill_param': skill_param, + 'skill_guide': skill_guide, + } + self.node.db['skills']['skill'] = skill_type + +class AdaptiveAfterQuery(aq.JsonAfterQuery): + + def post_process(self): + + if self.node.result.strip() == "N/A": + self.node.result = "N/A" + self.node.db['adaptive_questions'] = None + return + + questions = """Answer the current questions based on the observation, gameplay history, knowledge base, and instruction manual. +In your answer, explicitly state 'missing' if something is missing from the instruction manual and the knowledge base. Do not make assumptions. + +Questions: +{}""".format(self.node.result) + + self.node.graph.add_temporary_node(SimpleDBNode(questions, questions, self.node.graph, self.node.query_llm, ComposePlannerPrompt(), self.node.db)) + for node in self.node.db['prompts']['adaptive_dependencies']: + self.node.graph.add_edge_temporary(node, questions) + + for node in self.node.db['prompts']['adaptive_actor_questions']: + self.node.graph.add_edge_temporary(questions, node, prepend=True) + + self.node.db['adaptive_questions'] = questions + +class KBAddAfterQuery(aq.JsonAfterQuery): + + def __init__(self): + super().__init__() + self.type = dict + self.required_keys = [] + self.length = None + + def post_process(self): + parsed_answer = self.parse_json() + json_dict = parsed_answer[-1] + new_knowledge = {} + try: + features = ['discovered', 'general', 'unknown', 'concrete_and_precise', 'solid'] + for k, v in json_dict.items(): + if False not in ['yes' in v[f].lower() for f in features]: + new_knowledge[k] = v['discovery_short'] + except Exception as e: + raise ex.AfterQueryError("Invalid answer", "{}: {}".format(e, traceback.format_exc())) + self.node.result_raw = self.node.result + self.node.result = json.dumps(json_dict, sort_keys=True, indent=0) + self.node.db['kb']['knowledge_base'].update(new_knowledge) + + +class KBReasonAfterQuery(aq.JsonAfterQuery): + + def __init__(self): + super().__init__() + self.type = dict + self.required_keys = [] + self.length = None + + def post_process(self): + parsed_answer = self.parse_json() + json_dict = parsed_answer[-1] + unknowns = {} + try: + features = ['unknown', 'novel', 'general', 'relevant', 'correct'] + for k, v in json_dict.items(): + if False not in ['yes' in v[f].lower() for f in features]: + unknowns[k] = v['info'] + except Exception as e: + raise ex.AfterQueryError("Invalid answer", "{}: {}".format(e, traceback.format_exc())) + self.node.db['kb']['unknowns']={self.node.prompt: json.dumps(unknowns, sort_keys=True, indent=0)} + self.node.db['kb']['unknowns_json']={self.node.prompt: unknowns} + +class ReflectionAfterQuery(aq.JsonAfterQuery): + + def __init__(self): + super().__init__() + self.type = dict + self.required_keys = [] + self.length = 7 + + def post_process(self): + parsed_answer = self.parse_json() + json_dict = parsed_answer[-1] + if 'yes' in json_dict['unexpected_encounters'].lower(): + self.node.db["reflection"]["unexpected"].append(self.node.db["environment"]["step"]) + if 'yes' in json_dict['mistake'].lower(): + self.node.db["reflection"]["mistake"].append(self.node.db["environment"]["step"]) + if 'yes' in json_dict['correction_planned'].lower(): + self.node.db["reflection"]["correction"].append(self.node.db["environment"]["step"]) + if 'yes' in json_dict['confused'].lower(): + self.node.db["reflection"]["confusion"].append(self.node.db["environment"]["step"]) + if True in ['yes' in json_dict[k].lower() for k in ['unexpected_encounters', 'mistake', 'correction_planned', 'confused']]: + self.node.db["reflection"]["all"].append(self.node.db["environment"]["step"]) + if True not in ['yes' in v.lower() for v in json_dict.values()] and len(self.node.db["history"]["qa_history"]) > 0: + print(Fore.BLUE + "Skipping a bunch of reflection questions..." + Fore.RESET) + self.node.graph.skip_nodes_temporary(self.node.db["prompts"]["reflection_skip_questions"]) + +class ListActionAfterQuery(aq.JsonAfterQuery): + + def __init__(self): + super().__init__() + self.type = dict + self.required_keys = [] + self.length = None + + def post_process(self): + parsed_answer = self.parse_json() + filtered_result = {} + try: + self.node.db['allowed_actions'] = [] + keys_to_keep_yes = ['target', 'allowed', 'unlock new achievement'] + keys_to_keep_no = ['target', 'allowed', 'reasoning'] + for action,v in parsed_answer[-1].items(): + if action.strip().lower() == "noop": # Skip noop. This doesn't change the behavior of the LLM experimentally but saves quite a bit of tokens. + continue + if "yes" in v['allowed'].lower(): + self.node.db['allowed_actions'].append(action) + filtered_result[action] = {k:v[k] for k in keys_to_keep_yes} + else: + filtered_result[action] = {k:v[k] for k in keys_to_keep_no} + except Exception as e: + raise ex.AfterQueryError("Invalid answer", "{}: {}".format(e, traceback.format_exc())) + self.node.result = json.dumps({k:str(i) for i,k in enumerate(filtered_result.keys())}, indent=0).strip() + for i, v in enumerate(filtered_result.values()): + self.node.result = self.node.result.replace('"{}"'.format(i), json.dumps(v)) + + # Adaptive Questions + if 'adaptive_questions' not in self.node.db or self.node.db['adaptive_questions'] is None: + return + + questions = self.node.db['adaptive_questions'] + + self.node.graph.add_temporary_node(SimpleDBNode(questions, questions, self.node.graph, self.node.query_llm, ComposePlannerPrompt(), self.node.db, verbose=self.node.verbose)) + for node in self.node.db['prompts']['adaptive_dependencies']: + self.node.graph.add_edge_temporary(node, questions) + + for node in self.node.db['prompts']['adaptive_strategy_questions']: + self.node.graph.add_edge_temporary(questions, node, prepend=True) + +class ActionSummaryAfterQuery(aq.JsonAfterQuery): + + def __init__(self): + super().__init__() + self.type = dict + self.required_keys = ['plan-sketch', 'details', 'target', 'relevance-criteria', 'expiration-condition', 'notes'] + # self.length = 6 + + def post_process(self): + parsed_answer = self.parse_json() + self.node.db['action_summary'] = parsed_answer[-1] + self.node.db['action_notes'] = parsed_answer[-1]['notes'] + del self.node.db['action_summary']['notes'] + +class ActionAfterQuery(aq.JsonAfterQuery): + + def __init__(self): + super().__init__() + self.type = dict + self.required_keys = ['action', 'repeats', 'hazards', 'obstacles'] + # self.length = 3 + + def post_process(self): + parsed_answer = self.parse_json() + act, action_name, error_msg = match_act(parsed_answer[-1]['action'].replace("(","").replace("_"," ")) + if act is None: + raise ex.AfterQueryError("Invalid answer", "Invalid action: {}".format(parsed_answer[-1]['action'], error_msg)) + if type(parsed_answer[-1]['repeats']) == str and not (parsed_answer[-1]['repeats']).isnumeric(): + raise ex.AfterQueryError("Invalid answer", "Invalid repeats: '{}'. Expecting an integer.".format(parsed_answer[-1]['repeats'])) + self.node.db['action'] = act + if "move" in action_name.lower() and "yes" not in parsed_answer[-1]['hazards'].lower()and "yes" not in parsed_answer[-1]['obstacles'].lower(): + self.node.db['action_repeats'] = min(4,int(parsed_answer[-1]['repeats'])) + elif "do" in action_name.lower(): + self.node.db['action_repeats'] = min(3,int(parsed_answer[-1]['repeats'])) + else: + self.node.db['action_repeats'] = 1 + + self.node.result = json.dumps({ + 'action': action_name, + 'repeats': self.node.db['action_repeats'] + }, indent=2) + +class SummaryAfterQuery(aq.JsonAfterQuery): + + def __init__(self): + super().__init__() + self.type = dict + self.required_keys = ['action', 'repeats', 'target', 'success', 'causes_of_failure'] + # self.length = 5 + + def post_process(self): + parsed_answer = self.parse_json() + + action_desc = "" + if 'move' in parsed_answer[-1]['action'].lower(): + action_desc += "{}, {} steps".format(parsed_answer[-1]['action'], parsed_answer[-1]['repeats']) + else: + action_desc += "{}, {} steps, target: {}".format(parsed_answer[-1]['action'], parsed_answer[-1]['repeats'], parsed_answer[-1]['target']) + if 'no' in parsed_answer[-1]['success'].lower(): + action_desc += " (failed, causes of failure: {})".format(parsed_answer[-1]['causes_of_failure']) + else: + action_desc += " (succeeded)" + + self.node.result = action_desc \ No newline at end of file diff --git a/examples/crafter/prompts.py b/examples/crafter/prompts.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/crafter/tests.py b/examples/crafter/tests.py new file mode 100644 index 0000000..784ba1d --- /dev/null +++ b/examples/crafter/tests.py @@ -0,0 +1,86 @@ +import json + +prompts = [ + str(i) for i in range(11) +] + +edges = { + 2: [1], + 3: [1, 2], + 4: [1, 3], + 5: [2, 4], + 6: [1, 4], + 7: [6], + 8: [6], + 9: [2, 6], + 10: [3, 4, 5, 7, 8, 9], +} + +database = { + "A": "a", + "B": "b", + "C": "c", +} + +# level 1 feature: +# allow user to specify a query_model function for each prompt. +# Run query_model on all '''prompts''' according to topological order defined by '''edges''' +# expected return value: a dictionary mapping prompt to result. + +# level 2 feature: +# allow user to specify a compose_prompt function for each prompt. +# Run compose_prompt before query_model on each prompt. +# +# compose_prompt takes the following arguments: +# dependencies: list of (prompt, result) pairs. We want to query LLM for all the dependencies before querying LLM for the current prompt. +# prompt: the current prompt we want to query LLM for. +# database: a database that supports the following operations (can be accessed by the user as well): +# database.get(key) -> value +# database.put(key, value) +# database.delete(key) +# database.clear() +# database.keys() -> list of keys +# database.values() -> list of values +# database.items() -> list of (key, value) pairs +# +# return value: a tuple (prompt, idx) directly passed to query_model. + +# level 3 feature: +# add a history argument to compose_prompt. +# +# history stores a list of dictionary (see expected return value of level 1). +# history.clear() clears the history. +# adding to history should be automatic. +# +# This allows compose_prompt to access the results of previous queries. + +# level 4 feature: +# allow user to specify a after_query function for each prompt. +# Run after_query after query_model on each prompt. +# +# after_query takes the following arguments: +# prompt: the current prompt we queried LLM for. +# result: the result returned by LLM. +# database: see level 2 +# history: see level 3 +# +# after_query can modify the database, and change how the graph is traversed. +# For example, one can add a new node to the graph, or remove nodes from the graph. (This should be made as easy as possible) +# +# return value: processed_result (can be None) +# +# Catch exceptions in after_query and re-attempt query_model with the exception message added to the prompt. +# If the re-attempt fails k times, return the exception message as the processed_result. + + +def compose_test_prompt(node): + dependencies = node.get_dependencies() # list of nodes + prompt = "" + for dep in dependencies: + prompt += json.dumps(dep.represent()) + "\n" + prompt += "Question: {}".format(node.get_prompt()) + return prompt + + +def query_model_test(msg, shrink_idx, max_gen=512, temp=0.): + return "Answer to:\n\n{}".format(msg) \ No newline at end of file diff --git a/examples/crafter/utils.py b/examples/crafter/utils.py new file mode 100644 index 0000000..a987522 --- /dev/null +++ b/examples/crafter/utils.py @@ -0,0 +1,137 @@ +import os,pickle +import json +import traceback + +def parse_tuple(answer): + try: + start = answer.index("(") + end = answer.rindex(")") + result = eval(answer[start:end+1]) + return result, None + except: + pass + try: + # extract the tuple from the string + start_index = answer.index("(") + # print(start_index, answer[start_index:]) + tup = [] + c = 1 + tracking = None + start = start_index+1 + for i in range(start_index+1, len(answer)): + if answer[i] in {'(','{'}: + c+=1 + elif answer[i] in {')','}'}: + c-=1 + elif answer[i] in {"'",'"'} and tracking == None: + c+=1 + tracking = answer[i] + elif answer[i] == tracking: + c-=1 + tracking = None + elif c==1 and answer[i] == ',': + item = answer[start:i].replace("\"", "").replace("'","").strip() + if item == "False": + item = False + elif item == "True": + item = True + tup.append(item) + start = i+1 + if c==0: + item = answer[start:i].replace("\"", "").replace("'","").strip() + if item == "False": + item = False + elif item == "True": + item = True + tup.append(item) + end_index = i + break + if len(tup)==0: + return None, "Error: could not evaluate answer as a tuple" + return tuple(tup), None + except Exception: + return None, "Error: {}".format(traceback.format_exc()) + +def get_ctxt(): + import pickle + with open("./crafter_initial_QA.pkl", "rb") as f: + QA_data = pickle.load(f) + + QA_data.keys() + + choosen_idx = { + "gameplay":[1, 3,], + "objective":[1,], + "actions":[1,], + } + + + for k, v in QA_data.items(): + print("=="*10) + print(k) + print() + print("\n".join(["{}{}. {}".format("-> " if i in choosen_idx[k] else " ", i, x) for i,x in enumerate(v['questions'])])) + + if os.path.exists("cache/ctxt.pkl"): + with open("cache/ctxt.pkl", 'rb') as f: + CTXT = pickle.load(f) + else: + import itertools + from llm_api import get_query + query_model = get_query("gpt-3.5-turbo-1106") + def get_list(L, idx): + if L==[]: + return [] + if type(L[0]) == str: + return [L[idx]] + else: + return list(itertools.chain.from_iterable([get_list(ll, idx) for ll in L])) + + CTXT = "" + for k, ll in choosen_idx.items(): + for idx in ll: + ans_list = get_list(QA_data[k]['answers'], idx) + CTXT+= QA_data[k]["questions"][idx] + "\n" + prompt = "Question: {}\n".format(QA_data[k]["questions"][idx]) + "\n".join(ans_list) + "\n\nRemove duplicate items. New Answer:\n" + answer = query_model(prompt, 0) + CTXT+= answer + CTXT+= "\n\n" + CTXT = CTXT.strip() + with open("cache/ctxt.pkl", 'wb') as f: + pickle.dump(CTXT, f) + CTXT = CTXT.replace("DO NOT answer in LaTeX.", "") + + CTXT = CTXT.replace("Move Up: Flat ground above the agent.", "Move North: Flat ground to the north of the agent.") + CTXT = CTXT.replace("Move Down: Flat ground below the agent.", "Move South: Flat ground to the south of the agent.") + CTXT = CTXT.replace("Move Left: Flat ground left to the agent.", "Move West: Flat ground to the west of the agent.") + CTXT = CTXT.replace("Move Right: Flat ground right to the agent.", "Move East: Flat ground to the east of the agent.") + # CTXT = CTXT.replace("8. Place Table: Wood in inventory.", "8. Place Table: 2 Wood in inventory.") + # CTXT = CTXT.replace("9. Place Furnace: Stone in inventory.", "9. Place Furnace: 4 Stone in inventory.") + CTXT += "\n\nHealth restores automatically over time, independent from food and hydration." + notes = [ + "Diagonal actions are not supported, only use the four cardinal directions.", + "The game world is infinitely large and procedurally generated from a fixed random seed.", + "If you are within close proximity to a zombie, it will chase you. You must kill the zombie to survive.", + "When sleeping, the player will not be able to take any actions until energy is full and will take triple damage from zombies. Therefore, do not sleep when threats are nearby.", + ] + CTXT += "\n\nNotes:\n" + '\n'.join([" - " + x for x in notes]) + + CTXT = CTXT.replace("In plain text. List all objects I need to interact/avoid to survive in the game. Use \"I would like to X object Y\" in each step. Replace Y by the actual object, X by the actual interaction.", "List of desired interactions:") + CTXT = CTXT.replace("I would like to ", " - ") + + CTXT = CTXT.replace("Write all information helpful for the game in a numbered list.", "List of helpful information:") + CTXT = CTXT.replace("Write all game objectives numbered list. For each objective, list its requirements.", "List of game achievements and their requirements:") + CTXT = CTXT.replace("Write all actions as a numbered list. For each action, list its requirements.", "List of all actions and their requirements:") + + print(CTXT) + return CTXT + +def describe_achievements(info, CTXT): + new_CTXT = CTXT + new_CTXT += "\n\nGame Objective: Survive and accomplish as many of the achievements as possible, and always be prepared for threats in the game." + unaccomplished_list = [k.replace("_", " ") for k,v in info['achievements'].items() if v<1] + accomplished_list = [k.replace("_", " ") for k,v in info['achievements'].items() if v>0] + # print(unaccomplished_list) + new_CTXT += "\nCurrent *accomplished* achievements: " + ", ".join(accomplished_list) + new_CTXT += "\nCurrent *unaccomplished* achievements: " + ", ".join(unaccomplished_list) + return new_CTXT \ No newline at end of file