├── .gitignore ├── LICENSE ├── README.md ├── analysis ├── common.py ├── plot_counts.py ├── plot_reward.py ├── plot_scores.py ├── plot_spectrum.py ├── print_reward.py ├── print_scores.py ├── print_spectrum.py └── read_metrics.py ├── crafter ├── __init__.py ├── assets │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── arrow-down.png │ ├── arrow-left.png │ ├── arrow-right.png │ ├── arrow-up.png │ ├── coal.png │ ├── cow.png │ ├── debug-2.png │ ├── debug-3.png │ ├── debug.png │ ├── diamond.png │ ├── drink.png │ ├── energy.png │ ├── fence.png │ ├── food.png │ ├── furnace.png │ ├── grass.png │ ├── health.png │ ├── iron.png │ ├── iron_pickaxe.png │ ├── iron_sword.png │ ├── lava.png │ ├── leaves.png │ ├── log.png │ ├── path.png │ ├── plant-ripe.png │ ├── plant-young.png │ ├── plant.png │ ├── player-down.png │ ├── player-left.png │ ├── player-right.png │ ├── player-sleep.png │ ├── player-up.png │ ├── player.png │ ├── sand.png │ ├── sapling.png │ ├── skeleton.png │ ├── stone.png │ ├── stone_pickaxe.png │ ├── stone_sword.png │ ├── table.png │ ├── tree.png │ ├── unknown.png │ ├── water.png │ ├── wood.png │ ├── wood_pickaxe.png │ ├── wood_sword.png │ └── zombie.png ├── constants.py ├── data.yaml ├── engine.py ├── env.py ├── objects.py ├── recorder.py ├── run_gui.py ├── run_random.py ├── run_terrain.py └── worldgen.py ├── examples ├── run_ppo.py └── run_random.py ├── media ├── scores.png ├── terrain.png ├── video.gif └── video.mp4 ├── plots ├── counts-dreamerv2.pdf ├── counts-ppo.pdf ├── counts-rainbow.pdf ├── counts-random.pdf ├── counts-unsup_plan2explore.pdf ├── counts-unsup_rnd.pdf ├── reward.pdf ├── scores-agents.pdf ├── scores-human.pdf ├── spectrum-noreward.pdf └── spectrum-reward.pdf ├── scores ├── crafter_noreward-random.json ├── crafter_noreward-unsup_plan2explore.json ├── crafter_noreward-unsup_rnd.json ├── crafter_reward-dreamerv2.json ├── crafter_reward-human.json ├── crafter_reward-ppo.json └── crafter_reward-rainbow.json └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *.egg-info 4 | /dist 5 | /*.png 6 | /*.mp4 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Danijar Hafner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Status:** Stable release 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/crafter.svg)](https://pypi.python.org/pypi/crafter/#history) 4 | 5 | # Crafter 6 | 7 | Open world survival game for evaluating a wide range of agent abilities within 8 | a single environment. 9 | 10 | ![Crafter Terrain](https://github.com/danijar/crafter/raw/main/media/terrain.png) 11 | 12 | ## Overview 13 | 14 | Crafter features randomly generated 2D worlds where the player needs to forage 15 | for food and water, find shelter to sleep, defend against monsters, collect 16 | materials, and build tools. Crafter aims to be a fruitful benchmark for 17 | reinforcement learning by focusing on the following design goals: 18 | 19 | - **Research challenges:** Crafter poses substantial challenges to current 20 | methods, evaluating strong generalization, wide and deep exploration, 21 | representation learning, and long-term reasoning and credit assignment. 22 | 23 | - **Meaningful evaluation:** Agents are evaluated by semantically meaningful 24 | achievements that can be unlocked in each episode, offering insights into the 25 | ability spectrum of both reward agents and unsupervised agents. 26 | 27 | - **Iteration speed:** Crafter evaluates many agent abilities within a single 28 | env, vastly reducing the computational requirements over benchmarks suites 29 | that require training on many separate envs from scratch. 30 | 31 | See the research paper to find out more: [Benchmarking the Spectrum of Agent 32 | Capabilities](https://arxiv.org/pdf/2109.06780.pdf) 33 | 34 | ``` 35 | @article{hafner2021crafter, 36 | title={Benchmarking the Spectrum of Agent Capabilities}, 37 | author={Danijar Hafner}, 38 | year={2021}, 39 | journal={arXiv preprint arXiv:2109.06780}, 40 | } 41 | ``` 42 | 43 | ## Play Yourself 44 | 45 | ```sh 46 | python3 -m pip install crafter # Install Crafter 47 | python3 -m pip install pygame # Needed for human interface 48 | python3 -m crafter.run_gui # Start the game 49 | ``` 50 | 51 |
52 | Keyboard mapping (click to expand) 53 | 54 | | Key | Action | 55 | | :-: | :----- | 56 | | WASD | Move around | 57 | | SPACE| Collect material, drink from lake, hit creature | 58 | | TAB | Sleep | 59 | | T | Place a table | 60 | | R | Place a rock | 61 | | F | Place a furnace | 62 | | P | Place a plant | 63 | | 1 | Craft a wood pickaxe | 64 | | 2 | Craft a stone pickaxe | 65 | | 3 | Craft an iron pickaxe | 66 | | 4 | Craft a wood sword | 67 | | 5 | Craft a stone sword | 68 | | 6 | Craft an iron sword | 69 | 70 |
71 | 72 | ![Crafter Video](https://github.com/danijar/crafter/raw/main/media/video.gif) 73 | 74 | ## Interface 75 | 76 | To install Crafter, run `pip3 install crafter`. The environment follows the 77 | [OpenAI Gym][gym] interface. Observations are images of size (64, 64, 3) and 78 | outputs are one of 17 categorical actions. 79 | 80 | ```py 81 | import gym 82 | import crafter 83 | 84 | env = gym.make('CrafterReward-v1') # Or CrafterNoReward-v1 85 | env = crafter.Recorder( 86 | env, './path/to/logdir', 87 | save_stats=True, 88 | save_video=False, 89 | save_episode=False, 90 | ) 91 | 92 | obs = env.reset() 93 | done = False 94 | while not done: 95 | action = env.action_space.sample() 96 | obs, reward, done, info = env.step(action) 97 | ``` 98 | 99 | [gym]: https://github.com/openai/gym 100 | 101 | ## Evaluation 102 | 103 | Agents are allowed a budget of 1M environmnent steps and are evaluated by their 104 | success rates of the 22 achievements and by their geometric mean score. Example 105 | scripts for computing these are included in the `analysis` directory of the 106 | repository. 107 | 108 | - **Reward:** The sparse reward is `+1` for unlocking an achievement during 109 | the episode and `-0.1` or `+0.1` for lost or regenerated health points. 110 | Results should be reported not as reward but as success rates and score. 111 | 112 | - **Success rates:** The success rates of the 22 achievemnts are computed 113 | as the percentage across all training episodes in which the achievement was 114 | unlocked, allowing insights into the ability spectrum of an agent. 115 | 116 | - **Crafter score:** The score is the geometric mean of success rates, so that 117 | improvements on difficult achievements contribute more than improvements on 118 | achievements with already high success rates. 119 | 120 | ## Scoreboards 121 | 122 | Please create a pull request if you would like to add your or another algorithm 123 | to the scoreboards. For the reinforcement learning and unsupervised agents 124 | categories, the interaction budget is 1M. The external knowledge category is 125 | defined more broadly. 126 | 127 | ### Reinforcement Learning 128 | 129 | | Algorithm | Score (%) | Reward | Open Source | 130 | |:----------|----------:|-------:|:-----------:| 131 | | [Curious Replay](https://arxiv.org/pdf/2306.15934.pdf) | 19.4±1.6 | - | [AutonomousAgentsLab/cr-dv3](https://github.com/AutonomousAgentsLab/cr-dv3) | 132 | | [PPO (ResNet)](https://arxiv.org/pdf/2307.03486.pdf)| 15.6±1.6 | 10.3±0.5 | [snu-mllab/Achievement-Distillation](https://github.com/snu-mllab/Achievement-Distillation) 133 | | [DreamerV3](https://arxiv.org/pdf/2301.04104v1.pdf) | 14.5±1.6 | 11.7±1.9 | [danijar/dreamerv3](https://github.com/danijar/dreamerv3) | 134 | | [LSTM-SPCNN](https://arxiv.org/pdf/2208.03374.pdf) | 12.1±0.8 | — | [astanic/crafter-ood](https://github.com/astanic/crafter-ood) | 135 | | [EDE](https://openreview.net/pdf?id=GZDsKahGY-2) | 11.7±1.0 | — | [yidingjiang/ede](https://github.com/yidingjiang/ede) | 136 | | [OC-SA](https://arxiv.org/pdf/2208.03374.pdf) | 11.1±0.7 | — | [astanic/crafter-ood](https://github.com/astanic/crafter-ood) | 137 | | [DreamerV2](https://arxiv.org/pdf/2010.02193.pdf) | 10.0±1.2 | 9.0±1.7 | [danijar/dreamerv2](https://github.com/danijar/dreamerv2) | 138 | | [PPO](https://arxiv.org/pdf/1710.02298.pdf) | 4.6±0.3 | 4.2±1.2 | [DLR-RM/stable-baselines3](https://github.com/DLR-RM/stable-baselines3) | 139 | | [Rainbow](https://arxiv.org/pdf/1710.02298.pdf) | 4.3±0.2 | 6.0±1.3 | [Kaixhin/Rainbow](https://github.com/Kaixhin/Rainbow) | 140 | 141 | ### Unsupervised Agents 142 | 143 | | Algorithm | Score (%) | Reward | Open Source | 144 | |:----------|----------:|-------:|:-----------:| 145 | | [Plan2Explore](https://arxiv.org/pdf/2010.02193.pdf) | 2.1±0.1 | 2.1±1.5 | [danijar/dreamerv2](https://github.com/danijar/dreamerv2) | 146 | | [RND](https://arxiv.org/pdf/1810.12894.pdf) | 2.0±0.1 | 0.7±1.3 | [alirezakazemipour/PPO-RND](https://github.com/alirezakazemipour/PPO-RND) | 147 | | Random | 1.6±0.0 | 2.1±1.3 | — | 148 | 149 | ### External Knowledge 150 | 151 | | Algorithm | Score (%) | Reward | Uses | Interaction | Open Source | 152 | |:----------|----------:|-------:|:-----|:-----------:|:-----------:| 153 | | [Human](https://en.wikipedia.org/wiki/Human) | 50.5±6.8 | 14.3±2.3 | Life experience | 0 | [crafter_human_dataset](https://archive.org/details/crafter_human_dataset) | 154 | | [SPRING](https://arxiv.org/pdf/2305.15486.pdf) | 27.3±1.2 | 12.3±0.7 | LLM, scene description, Crafter paper | 0 | ❌ | 155 | | [Achievement Distillation](https://arxiv.org/pdf/2307.03486.pdf) | 21.8±1.4 | 12.6±0.3 | Reward structure | 1M | [snu-mllab/Achievement-Distillation](https://github.com/snu-mllab/Achievement-Distillation) | 156 | | [ELLM](https://arxiv.org/pdf/2302.06692.pdf) | — | 6.0±0.4 | LLM, scene description | 5M | ❌ | 157 | 158 | ## Baselines 159 | 160 | Baseline scores of various agents are available for Crafter, both with and 161 | without rewards. The scores are available in JSON format in the `scores` 162 | directory of the repository. For comparison, the score of human expert players 163 | is 50.5\%. The [baseline 164 | implementations](https://github.com/danijar/crafter-baselines) are available as 165 | a separate repository. 166 | 167 | 168 | 169 | ## Questions 170 | 171 | Please [open an issue][issues] on Github. 172 | 173 | [issues]: https://github.com/danijar/crafter/issues 174 | -------------------------------------------------------------------------------- /analysis/common.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | import warnings 4 | 5 | import numpy as np 6 | 7 | 8 | def load_runs(filenames, budget=1e6, verbose=True): 9 | verbose and print('') 10 | runs = [] 11 | for filename in filenames: 12 | loaded = json.loads(pathlib.Path(filename).read_text()) 13 | for run in [loaded] if isinstance(loaded, dict) else loaded: 14 | message = f'Loading {run["method"]} seed {run["seed"]}' 15 | verbose and print(message, flush=True) 16 | if run['xs'][-1] < budget - 1e4: 17 | verbose and print(f' Contains only {run["xs"][-1]} steps!') 18 | runs.append(run) 19 | verbose and print('') 20 | return runs 21 | 22 | 23 | def compute_success_rates(runs, budget=1e6, sortby=None): 24 | methods = sorted(set(run['method'] for run in runs)) 25 | seeds = sorted(set(run['seed'] for run in runs)) 26 | tasks = sorted(key for key in runs[0] if key.startswith('achievement_')) 27 | percents = np.empty((len(methods), len(seeds), len(tasks))) 28 | percents[:] = np.nan 29 | for run in runs: 30 | episodes = (np.array(run['xs']) <= budget).sum() 31 | i = methods.index(run['method']) 32 | j = seeds.index(run['seed']) 33 | for key, values in run.items(): 34 | if key in tasks: 35 | k = tasks.index(key) 36 | percent = 100 * (np.array(values[:episodes]) >= 1).mean() 37 | percents[i, j, k] = percent 38 | if isinstance(sortby, (str, int)): 39 | if isinstance(sortby, str): 40 | sortby = methods.index(sortby) 41 | order = np.argsort(-np.nanmean(percents[sortby], 0), -1) 42 | percents = percents[:, :, order] 43 | tasks = np.array(tasks)[order].tolist() 44 | return percents, methods, seeds, tasks 45 | 46 | 47 | def compute_scores(percents): 48 | # Geometric mean with an offset of 1%. 49 | assert (0 <= percents).all() and (percents <= 100).all() 50 | if (percents <= 1.0).all(): 51 | print('Warning: The input may not be in the right range.') 52 | with warnings.catch_warnings(): # Empty seeds become NaN. 53 | warnings.simplefilter('ignore', category=RuntimeWarning) 54 | scores = np.exp(np.nanmean(np.log(1 + percents), -1)) - 1 55 | return scores 56 | 57 | 58 | def binning(xs, ys, borders, reducer=np.nanmean, fill='nan'): 59 | xs, ys = np.array(xs), np.array(ys) 60 | order = np.argsort(xs) 61 | xs, ys = xs[order], ys[order] 62 | binned = [] 63 | with warnings.catch_warnings(): # Empty buckets become NaN. 64 | warnings.simplefilter('ignore', category=RuntimeWarning) 65 | for start, stop in zip(borders[:-1], borders[1:]): 66 | left = (xs <= start).sum() 67 | right = (xs <= stop).sum() 68 | if left < right: 69 | value = reducer(ys[left:right]) 70 | elif binned: 71 | value = {'nan': np.nan, 'last': binned[-1]}[fill] 72 | else: 73 | value = np.nan 74 | binned.append(value) 75 | return borders[1:], np.array(binned) 76 | -------------------------------------------------------------------------------- /analysis/plot_counts.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import matplotlib.ticker as ticker 6 | 7 | import common 8 | 9 | 10 | def plot_counts( 11 | inpath, outpath, color, budget=1e6, cols=4, size=(2, 1.8)): 12 | runs = common.load_runs([inpath], budget) 13 | percents, methods, seeds, tasks = common.compute_success_rates(runs, budget) 14 | borders = np.arange(0, budget, 1e4) 15 | keys = ['reward', 'length'] + tasks 16 | 17 | rows = len(keys) // cols 18 | fig, axes = plt.subplots( 19 | nrows=rows, ncols=cols, figsize=(size[0] * cols, size[1] * rows)) 20 | 21 | for ax, key in zip(axes.flatten(), keys): 22 | ax.set_title(key.replace('achievement_', '').replace('_', ' ').title()) 23 | xs = np.concatenate([run['xs'] for run in runs]) 24 | ys = np.concatenate([run[key] for run in runs]) 25 | 26 | binxs, binys = common.binning(xs, ys, borders, np.nanmean) 27 | ax.plot(binxs, binys, color=color) 28 | 29 | mins = common.binning(xs, ys, borders, np.nanmin)[1] 30 | maxs = common.binning(xs, ys, borders, np.nanmax)[1] 31 | ax.fill_between(binxs, mins, maxs, linewidths=0, alpha=0.2, color=color) 32 | 33 | ax.ticklabel_format(axis='x', style='sci', scilimits=(0, 0)) 34 | ax.xaxis.set_major_locator(ticker.MaxNLocator(4, steps=[1, 2, 2.5, 5, 10])) 35 | ax.yaxis.set_major_locator(ticker.MaxNLocator(5, steps=[1, 2, 2.5, 5, 10])) 36 | 37 | if maxs.max() == 0: 38 | ax.set_ylim(-0.05, 1.05) 39 | 40 | fig.tight_layout() 41 | pathlib.Path(outpath).parent.mkdir(exist_ok=True, parents=True) 42 | fig.savefig(outpath) 43 | print(f'Saved {outpath}') 44 | 45 | 46 | plot_counts( 47 | 'scores/crafter_reward-dreamerv2.json', 48 | 'plots/counts-dreamerv2.pdf', '#377eb8') 49 | 50 | plot_counts( 51 | 'scores/crafter_reward-ppo.json', 52 | 'plots/counts-ppo.pdf', '#5fc35d') 53 | 54 | plot_counts( 55 | 'scores/crafter_reward-rainbow.json', 56 | 'plots/counts-rainbow.pdf', '#984ea3') 57 | 58 | plot_counts( 59 | 'scores/crafter_noreward-unsup_plan2explore.json', 60 | 'plots/counts-unsup_plan2explore.pdf', '#bf3217') 61 | 62 | plot_counts( 63 | 'scores/crafter_noreward-unsup_rnd.json', 64 | 'plots/counts-unsup_rnd.pdf', '#de9f42') 65 | 66 | plot_counts( 67 | 'scores/crafter_noreward-random.json', 68 | 'plots/counts-random.pdf', '#6a554d') 69 | -------------------------------------------------------------------------------- /analysis/plot_reward.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import matplotlib.ticker as ticker 6 | 7 | import common 8 | 9 | 10 | def plot_reward(inpaths, outpath, legend, colors, cols=4, budget=1e6): 11 | runs = common.load_runs(inpaths, budget) 12 | percents, methods, seeds, tasks = common.compute_success_rates(runs, budget) 13 | if not legend: 14 | methods = sorted(set(run['method'] for run in runs)) 15 | legend = {x: x.replace('_', ' ').title() for x in methods} 16 | borders = np.arange(0, budget, 1e4) 17 | 18 | fig, ax = plt.subplots(figsize=(4.5, 2.3)) 19 | for j, (method, label) in enumerate(legend.items()): 20 | relevant = [run for run in runs if run['method'] == method] 21 | if not relevant: 22 | print(f'No runs found for method {method}.') 23 | # Average within each time bucket. 24 | binned_xs, binned_ys = [], [] 25 | for run in relevant: 26 | xs, ys = common.binning(run['xs'], run['reward'], borders, np.nanmean) 27 | binned_xs.append(xs) 28 | binned_ys.append(ys) 29 | xs = np.concatenate(binned_xs) 30 | ys = np.concatenate(binned_ys) 31 | # Compute mean and stddev over seeds. 32 | means = common.binning(xs, ys, borders, np.nanmean)[1] 33 | stds = common.binning(xs, ys, borders, np.nanstd)[1] 34 | # Plot line and shaded area. 35 | kwargs = dict(alpha=0.2, linewidths=0, color=colors[j], zorder=10 - j) 36 | ax.fill_between(borders[1:], means - stds, means + stds, **kwargs) 37 | ax.plot(borders[1:], means, label=label, color=colors[j], zorder=100 - j) 38 | 39 | ax.axhline(y=22, c='#888888', ls='--', lw=1) 40 | ax.text(6.2e5, 18, 'Optimal', c='#888888') 41 | 42 | ax.set_title('Crafter Reward') 43 | ax.set_xlim(0, budget) 44 | ax.ticklabel_format(axis='x', style='sci', scilimits=(0, 0)) 45 | ax.grid(alpha=0.3) 46 | ax.xaxis.set_major_locator(ticker.MaxNLocator(5, steps=[1, 2, 2.5, 5, 10])) 47 | ax.yaxis.set_major_locator(ticker.MaxNLocator(6, steps=[1, 2, 2.5, 5, 10])) 48 | 49 | fig.tight_layout(rect=(0, 0, 0.55, 1)) 50 | fig.legend(bbox_to_anchor=(0.52, 0.54), loc='center left', frameon=False) 51 | 52 | pathlib.Path(outpath).parent.mkdir(exist_ok=True, parents=True) 53 | fig.savefig(outpath) 54 | print(f'Saved {outpath}') 55 | 56 | 57 | inpaths = [ 58 | 'scores/crafter_reward-dreamerv2.json', 59 | 'scores/crafter_reward-rainbow.json', 60 | 'scores/crafter_reward-ppo.json', 61 | 'scores/crafter_noreward-random.json', 62 | ] 63 | legend = { 64 | 'dreamerv2': 'DreamerV2', 65 | 'rainbow': 'Rainbow', 66 | 'ppo': 'PPO', 67 | 'random': 'Random', 68 | } 69 | colors = ['#377eb8', '#4daf4a', '#984ea3', '#6a554d'] 70 | plot_reward(inpaths, 'plots/reward.pdf', legend, colors) 71 | -------------------------------------------------------------------------------- /analysis/plot_scores.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | import common 7 | 8 | 9 | def plot_scores(inpaths, outpath, legend, colors, budget=1e6, ylim=None): 10 | runs = common.load_runs(inpaths, budget) 11 | percents, methods, seeds, tasks = common.compute_success_rates(runs, budget) 12 | scores = common.compute_scores(percents) 13 | if not legend: 14 | methods = sorted(set(run['method'] for run in runs)) 15 | legend = {x: x.replace('_', ' ').title() for x in methods} 16 | legend = dict(reversed(legend.items())) 17 | 18 | scores = scores[np.array([methods.index(m) for m in legend.keys()])] 19 | mean = np.nanmean(scores, -1) 20 | std = np.nanstd(scores, -1) 21 | 22 | fig, ax = plt.subplots(figsize=(4, 3)) 23 | centers = np.arange(len(legend)) 24 | width = 0.7 25 | colors = list(reversed(colors[:len(legend)])) 26 | error_kw = dict(capsize=5, c='#000') 27 | ax.bar(centers, mean, yerr=std, color=colors, error_kw=error_kw) 28 | 29 | ax.spines['top'].set_visible(False) 30 | ax.spines['right'].set_visible(False) 31 | ax.spines['bottom'].set_visible(False) 32 | ax.tick_params( 33 | axis='x', which='both', width=50, length=0.8, direction='inout') 34 | ax.set_xlim(centers[0] - 2 * (1 - width), centers[-1] + 2 * (1 - width)) 35 | ax.set_xticks(centers + 0.0) 36 | ax.set_xticklabels( 37 | list(legend.values()), rotation=45, ha='right', rotation_mode='anchor') 38 | 39 | ax.set_ylabel('Crafter Score (%)') 40 | if ylim: 41 | ax.set_ylim(0, ylim) 42 | 43 | fig.tight_layout() 44 | pathlib.Path(outpath).parent.mkdir(exist_ok=True, parents=True) 45 | fig.savefig(outpath) 46 | print(f'Saved {outpath}') 47 | 48 | 49 | inpaths = [ 50 | 'scores/crafter_reward-human.json', 51 | 'scores/crafter_reward-dreamerv2.json', 52 | 'scores/crafter_reward-ppo.json', 53 | 'scores/crafter_reward-rainbow.json', 54 | 'scores/crafter_noreward-unsup_plan2explore.json', 55 | 'scores/crafter_noreward-unsup_rnd.json', 56 | 'scores/crafter_noreward-random.json', 57 | ] 58 | legend = { 59 | 'human': 'Human Experts', 60 | 'dreamerv2': 'DreamerV2', 61 | 'ppo': 'PPO', 62 | 'rainbow': 'Rainbow', 63 | 'unsup_plan2explore': 'Plan2Explore\n(Unsup)', 64 | 'unsup_rnd': 'RND\n(Unsup)', 65 | 'random': 'Random', 66 | } 67 | colors = [ 68 | '#cccccc', 69 | '#377eb8', '#5fc35d', '#984ea3', 70 | '#bf3217', '#de9f42', '#6a554d', 71 | ] 72 | plot_scores(inpaths, 'plots/scores-human.pdf', legend, colors, ylim=100) 73 | 74 | inpaths = [ 75 | 'scores/crafter_reward-dreamerv2.json', 76 | 'scores/crafter_reward-ppo.json', 77 | 'scores/crafter_reward-rainbow.json', 78 | 'scores/crafter_noreward-unsup_plan2explore.json', 79 | 'scores/crafter_noreward-unsup_rnd.json', 80 | 'scores/crafter_noreward-random.json', 81 | ] 82 | legend = { 83 | 'dreamerv2': 'DreamerV2', 84 | 'ppo': 'PPO', 85 | 'rainbow': 'Rainbow', 86 | 'unsup_plan2explore': 'Plan2Explore\n(Unsup)', 87 | 'unsup_rnd': 'RND\n(Unsup)', 88 | 'random': 'Random', 89 | } 90 | colors = [ 91 | '#377eb8', '#5fc35d', '#984ea3', 92 | '#bf3217', '#de9f42', '#6a554d', 93 | ] 94 | plot_scores(inpaths, 'plots/scores-agents.pdf', legend, colors, ylim=12) 95 | -------------------------------------------------------------------------------- /analysis/plot_spectrum.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | import common 7 | 8 | 9 | def plot_spectrum(inpaths, outpath, legend, colors, budget=1e6, sort=False): 10 | runs = common.load_runs(inpaths, budget) 11 | percents, methods, seeds, tasks = common.compute_success_rates( 12 | runs, budget, sortby=sort and legend and list(legend.keys())[0]) 13 | if not legend: 14 | methods = sorted(set(run['method'] for run in runs)) 15 | legend = {x: x.replace('_', ' ').title() for x in methods} 16 | 17 | fig, ax = plt.subplots(figsize=(7, 3)) 18 | centers = np.arange(len(tasks)) 19 | width = 0.7 20 | for index, (method, label) in enumerate(legend.items()): 21 | heights = np.nanmean(percents[methods.index(method)], 0) 22 | pos = centers + width * (0.5 / len(methods) + index / len(methods) - 0.5) 23 | color = colors[index] 24 | ax.bar(pos, heights, width / len(methods), label=label, color=color) 25 | 26 | names = [x[len('achievement_'):].replace('_', ' ').title() for x in tasks] 27 | ax.spines['top'].set_visible(False) 28 | ax.spines['right'].set_visible(False) 29 | ax.spines['bottom'].set_visible(False) 30 | ax.tick_params( 31 | axis='x', which='both', width=14, length=0.8, direction='inout') 32 | ax.set_xlim(centers[0] - 2 * (1 - width), centers[-1] + 2 * (1 - width)) 33 | ax.set_xticks(centers + 0.0) 34 | ax.set_xticklabels(names, rotation=45, ha='right', rotation_mode='anchor') 35 | 36 | ax.set_ylabel('Success Rate (%)') 37 | ax.set_yscale('log') 38 | ax.set_ylim(0.01, 100) 39 | ax.set_yticks([0.01, 0.1, 1, 10, 100]) 40 | ax.set_yticklabels('0.01 0.1 1 10 100'.split()) 41 | 42 | fig.tight_layout(rect=(0, 0, 1, 0.95)) 43 | fig.legend( 44 | loc='upper center', ncol=10, frameon=False, borderpad=0, borderaxespad=0) 45 | 46 | pathlib.Path(outpath).parent.mkdir(exist_ok=True, parents=True) 47 | fig.savefig(outpath) 48 | print(f'Saved {outpath}') 49 | 50 | 51 | inpaths = [ 52 | 'scores/crafter_reward-dreamerv2.json', 53 | 'scores/crafter_reward-ppo.json', 54 | 'scores/crafter_reward-rainbow.json', 55 | ] 56 | legend = { 57 | 'dreamerv2': 'DreamerV2', 58 | 'ppo': 'PPO', 59 | 'rainbow': 'Rainbow', 60 | } 61 | colors = ['#377eb8', '#5fc35d', '#984ea3'] 62 | plot_spectrum(inpaths, 'plots/spectrum-reward.pdf', legend, colors) 63 | 64 | inpaths = [ 65 | 'scores/crafter_noreward-unsup_plan2explore.json', 66 | 'scores/crafter_noreward-unsup_rnd.json', 67 | 'scores/crafter_noreward-random.json', 68 | ] 69 | legend = { 70 | 'unsup_plan2explore': 'Plan2Explore', 71 | 'unsup_rnd': 'RND', 72 | 'random': 'Random', 73 | } 74 | colors = ['#bf3217', '#de9f42', '#6a554d'] 75 | plot_spectrum(inpaths, 'plots/spectrum-noreward.pdf', legend, colors) 76 | -------------------------------------------------------------------------------- /analysis/print_reward.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import common 4 | 5 | 6 | def print_reward(inpaths, legend, budget=1e6, last=1e5, sort=False): 7 | runs = common.load_runs(inpaths, budget) 8 | if not legend: 9 | methods = sorted(set(run['method'] for run in runs)) 10 | legend = {x: x.replace('_', ' ').title() for x in methods} 11 | seeds = sorted({x['seed'] for x in runs}) 12 | rewards = np.empty((len(legend), len(seeds))) 13 | rewards[:] = np.nan 14 | for i, (method, label) in enumerate(legend.items()): 15 | relevant = [run for run in runs if run['method'] == method] 16 | if not relevant: 17 | print(f'No runs found for method {method}.') 18 | for run in relevant: 19 | j = seeds.index(run['seed']) 20 | xs = np.array(run['xs']) 21 | ys = np.array(run['reward']) 22 | rewards[i][j] = ys[-(xs >= (xs.max() - last)).sum()] 23 | means = np.nanmean(rewards, -1) 24 | stds = np.nanstd(rewards, -1) 25 | print('') 26 | print(r'\textbf{Method} & \textbf{Reward} \\') 27 | print('') 28 | for method, mean, std in zip(legend.values(), means, stds): 29 | mean = f'{mean:.1f}' 30 | mean = (r'\o' if len(mean) < 4 else ' ') + mean 31 | print(rf'{method:<25} & ${mean} \pm {std:4.1f}$ \\') 32 | print('') 33 | 34 | 35 | inpaths = [ 36 | 'scores/crafter_reward-dreamerv2.json', 37 | 'scores/crafter_reward-ppo.json', 38 | 'scores/crafter_reward-rainbow.json', 39 | 'scores/crafter_noreward-unsup_plan2explore.json', 40 | 'scores/crafter_noreward-unsup_rnd.json', 41 | 'scores/crafter_noreward-random.json', 42 | 'scores/crafter_reward-human.json', 43 | ] 44 | legend = { 45 | 'human': 'Human Experts', 46 | 'dreamerv2': 'DreamerV2', 47 | 'ppo': 'PPO', 48 | 'rainbow': 'Rainbow', 49 | 'unsup_plan2explore': 'Plan2Explore (Unsup)', 50 | 'unsup_rnd': 'RND (Unsup)', 51 | 'random': 'Random', 52 | } 53 | print_reward(inpaths, legend) 54 | -------------------------------------------------------------------------------- /analysis/print_scores.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import common 4 | 5 | 6 | def print_scores(inpaths, legend, budget=1e6, sort=False): 7 | runs = common.load_runs(inpaths, budget) 8 | percents, methods, seeds, tasks = common.compute_success_rates(runs, budget) 9 | scores = common.compute_scores(percents) 10 | if not legend: 11 | methods = sorted(set(run['method'] for run in runs)) 12 | legend = {x: x.replace('_', ' ').title() for x in methods} 13 | 14 | scores = scores[np.array([methods.index(m) for m in legend.keys()])] 15 | means = np.nanmean(scores, -1) 16 | stds = np.nanstd(scores, -1) 17 | 18 | print('') 19 | print(r'\textbf{Method} & \textbf{Score} \\') 20 | print('') 21 | for method, mean, std in zip(legend.values(), means, stds): 22 | mean = f'{mean:.1f}' 23 | mean = (r'\o' if len(mean) < 4 else ' ') + mean 24 | print(rf'{method:<25} & ${mean} \pm {std:4.1f}\%$ \\') 25 | print('') 26 | 27 | 28 | inpaths = [ 29 | 'scores/crafter_reward-dreamerv2.json', 30 | 'scores/crafter_reward-ppo.json', 31 | 'scores/crafter_reward-rainbow.json', 32 | 'scores/crafter_noreward-unsup_plan2explore.json', 33 | 'scores/crafter_noreward-unsup_rnd.json', 34 | 'scores/crafter_noreward-random.json', 35 | 'scores/crafter_reward-human.json', 36 | ] 37 | legend = { 38 | 'human': 'Human Experts', 39 | 'dreamerv2': 'DreamerV2', 40 | 'ppo': 'PPO', 41 | 'rainbow': 'Rainbow', 42 | 'unsup_plan2explore': 'Plan2Explore (Unsup)', 43 | 'unsup_rnd': 'RND (Unsup)', 44 | 'random': 'Random', 45 | } 46 | print_scores(inpaths, legend) 47 | -------------------------------------------------------------------------------- /analysis/print_spectrum.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import numpy as np 4 | 5 | import common 6 | 7 | 8 | def print_spectrum(inpaths, legend, budget=1e6, sort=False): 9 | runs = common.load_runs(inpaths, budget) 10 | percents, methods, seeds, tasks = common.compute_success_rates(runs, budget) 11 | scores = common.compute_scores(percents) 12 | if not legend: 13 | methods = sorted(set(run['method'] for run in runs)) 14 | legend = {x: x.replace('_', ' ').title() for x in methods} 15 | 16 | scores = np.nanmean(scores, 1) 17 | percents = np.nanmean(percents, 1) 18 | 19 | if sort: 20 | first = next(iter(legend.keys())) 21 | tasks = sorted(tasks, key=lambda task: -np.nanmean(percents[first, task])) 22 | legend = dict(reversed(legend.items())) 23 | 24 | cols = ''.join(rf' & \textbf{{{k}}}' for k in legend.values()) 25 | print(r'\newcommand{\o}{\hphantom{0}}') 26 | print(r'\newcommand{\b}[1]{\textbf{#1}}') 27 | print('') 28 | print(f'{"Achievement":<20}' + cols + r' \\') 29 | print('') 30 | wins = collections.defaultdict(int) 31 | for task in tasks: 32 | k = tasks.index(task) 33 | if task.startswith('achievement_'): 34 | name = task[len('achievement_'):].replace('_', ' ').title() 35 | else: 36 | name = task.replace('_', ' ').title() 37 | print(f'{name:<20}', end='') 38 | best = max(percents[methods.index(m), k] for m in legend.keys()) 39 | for method in legend.keys(): 40 | i = methods.index(method) 41 | value = percents[i][k] 42 | winner = value >= 0.95 * best and value > 0 43 | fmt = rf'{value:.1f}\%' 44 | fmt = (r'\o' if len(fmt) < 6 else ' ') + fmt 45 | fmt = rf'\b{{{fmt}}}' if winner else f' {fmt} ' 46 | if winner: 47 | wins[method] += 1 48 | print(rf' & ${fmt}$', end='') 49 | print(r' \\') 50 | print('') 51 | 52 | print(f'{"Score":<20}', end='') 53 | best = max(scores[methods.index(m)] for m in legend.keys()) 54 | for method in legend.keys(): 55 | value = scores[methods.index(method)] 56 | bold = value >= 0.95 * best and value > 0 57 | fmt = rf'{value:.1f}\%' 58 | fmt = (r'\o' if len(fmt) < 6 else ' ') + fmt 59 | fmt = rf'\b{{{fmt}}}' if bold else f' {fmt} ' 60 | print(rf' & ${fmt}$', end='') 61 | print(r' \\') 62 | 63 | 64 | inpaths = [ 65 | 'scores/crafter_reward-human.json', 66 | ] 67 | legend = { 68 | 'human': 'Human Experts', 69 | } 70 | print_spectrum(inpaths, legend) 71 | 72 | inpaths = [ 73 | 'scores/crafter_reward-dreamerv2.json', 74 | 'scores/crafter_reward-ppo.json', 75 | 'scores/crafter_reward-rainbow.json', 76 | ] 77 | legend = { 78 | 'dreamerv2': 'DreamerV2', 79 | 'ppo': 'PPO', 80 | 'rainbow': 'Rainbow', 81 | } 82 | print_spectrum(inpaths, legend) 83 | 84 | 85 | inpaths = [ 86 | 'scores/crafter_noreward-unsup_plan2explore.json', 87 | 'scores/crafter_noreward-unsup_rnd.json', 88 | 'scores/crafter_noreward-random.json', 89 | ] 90 | legend = { 91 | 'unsup_plan2explore': 'Plan2Explore', 92 | 'unsup_rnd': 'RND', 93 | 'random': 'Random', 94 | } 95 | print_spectrum(inpaths, legend) 96 | -------------------------------------------------------------------------------- /analysis/read_metrics.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import json 3 | import pathlib 4 | 5 | import numpy as np 6 | 7 | import common 8 | 9 | 10 | def read_stats(indir, outdir, task, method, budget=int(1e6), verbose=False): 11 | indir = pathlib.Path(indir) 12 | outdir = pathlib.Path(outdir) 13 | runs = [] 14 | print(f'Loading {indir.name}...') 15 | filenames = sorted(list(indir.glob('**/stats.jsonl'))) 16 | for index, filename in enumerate(filenames): 17 | if not filename.is_file(): 18 | continue 19 | rewards, lengths, achievements = load_stats(filename, budget) 20 | if sum(lengths) < budget - 1e4: 21 | message = f'Skipping incomplete run ({sum(lengths)} < {budget} steps): ' 22 | message += f'{filename.relative_to(indir.parent)}' 23 | print(f'==> {message}') 24 | continue 25 | runs.append(dict( 26 | task=task, 27 | method=method, 28 | seed=str(index), 29 | xs=np.cumsum(lengths).tolist(), 30 | reward=rewards, 31 | length=lengths, 32 | **achievements, 33 | )) 34 | if not runs: 35 | print('No completed runs.\n') 36 | return 37 | print_summary(runs, budget, verbose) 38 | outdir.mkdir(exist_ok=True, parents=True) 39 | filename = (outdir / f'{task}-{method}.json') 40 | filename.write_text(json.dumps(runs)) 41 | print('Wrote', filename) 42 | print('') 43 | 44 | 45 | def load_stats(filename, budget): 46 | steps = 0 47 | rewards = [] 48 | lengths = [] 49 | achievements = collections.defaultdict(list) 50 | for line in filename.read_text().split('\n'): 51 | if not line.strip(): 52 | continue 53 | episode = json.loads(line) 54 | steps += episode['length'] 55 | if steps > budget: 56 | break 57 | lengths.append(episode['length']) 58 | for key, value in episode.items(): 59 | if key.startswith('achievement_'): 60 | achievements[key].append(value) 61 | unlocks = int(np.sum([(v[-1] >= 1) for v in achievements.values()])) 62 | health = -0.9 63 | rewards.append(unlocks + health) 64 | return rewards, lengths, achievements 65 | 66 | 67 | def print_summary(runs, budget, verbose): 68 | episodes = np.array([len(x['length']) for x in runs]) 69 | rewards = np.array([np.mean(x['reward']) for x in runs]) 70 | lengths = np.array([np.mean(x['length']) for x in runs]) 71 | percents, methods, seeds, tasks = common.compute_success_rates( 72 | runs, budget, sortby=0) 73 | scores = np.squeeze(common.compute_scores(percents)) 74 | print(f'Score: {np.mean(scores):10.2f} ± {np.std(scores):.2f}') 75 | print(f'Reward: {np.mean(rewards):10.2f} ± {np.std(rewards):.2f}') 76 | print(f'Length: {np.mean(lengths):10.2f} ± {np.std(lengths):.2f}') 77 | print(f'Episodes: {np.mean(episodes):10.2f} ± {np.std(episodes):.2f}') 78 | if verbose: 79 | for task, percent in sorted(tasks, np.squeeze(percents).T): 80 | name = task[len('achievement_'):].replace('_', ' ').title() 81 | print(f'{name:<20} {np.mean(percent):6.2f}%') 82 | 83 | 84 | read_stats( 85 | 'logdir/crafter_reward-dreamerv2', 86 | 'scores', 'crafter_reward', 'dreamerv2') 87 | 88 | read_stats( 89 | 'logdir/crafter_reward-ppo', 90 | 'scores', 'crafter_reward', 'ppo') 91 | 92 | read_stats( 93 | 'logdir/crafter_reward-rainbow', 94 | 'scores', 'crafter_reward', 'rainbow') 95 | 96 | read_stats( 97 | 'logdir/crafter_noreward-unsup_plan2explore', 98 | 'scores', 'crafter_noreward', 'unsup_plan2explore') 99 | 100 | read_stats( 101 | 'logdir/crafter_noreward-unsup_rnd', 102 | 'scores', 'crafter_noreward', 'unsup_rnd') 103 | 104 | read_stats( 105 | 'logdir/crafter_noreward-random', 106 | 'scores', 'crafter_noreward', 'random') 107 | -------------------------------------------------------------------------------- /crafter/__init__.py: -------------------------------------------------------------------------------- 1 | from .env import Env 2 | from .recorder import Recorder 3 | 4 | try: 5 | import gym 6 | gym.register( 7 | id='CrafterReward-v1', 8 | entry_point='crafter:Env', 9 | max_episode_steps=10000, 10 | kwargs={'reward': True}) 11 | gym.register( 12 | id='CrafterNoReward-v1', 13 | entry_point='crafter:Env', 14 | max_episode_steps=10000, 15 | kwargs={'reward': False}) 16 | except ImportError: 17 | pass 18 | -------------------------------------------------------------------------------- /crafter/assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/1.png -------------------------------------------------------------------------------- /crafter/assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/2.png -------------------------------------------------------------------------------- /crafter/assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/3.png -------------------------------------------------------------------------------- /crafter/assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/4.png -------------------------------------------------------------------------------- /crafter/assets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/5.png -------------------------------------------------------------------------------- /crafter/assets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/6.png -------------------------------------------------------------------------------- /crafter/assets/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/7.png -------------------------------------------------------------------------------- /crafter/assets/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/8.png -------------------------------------------------------------------------------- /crafter/assets/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/9.png -------------------------------------------------------------------------------- /crafter/assets/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/arrow-down.png -------------------------------------------------------------------------------- /crafter/assets/arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/arrow-left.png -------------------------------------------------------------------------------- /crafter/assets/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/arrow-right.png -------------------------------------------------------------------------------- /crafter/assets/arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/arrow-up.png -------------------------------------------------------------------------------- /crafter/assets/coal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/coal.png -------------------------------------------------------------------------------- /crafter/assets/cow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/cow.png -------------------------------------------------------------------------------- /crafter/assets/debug-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/debug-2.png -------------------------------------------------------------------------------- /crafter/assets/debug-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/debug-3.png -------------------------------------------------------------------------------- /crafter/assets/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/debug.png -------------------------------------------------------------------------------- /crafter/assets/diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/diamond.png -------------------------------------------------------------------------------- /crafter/assets/drink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/drink.png -------------------------------------------------------------------------------- /crafter/assets/energy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/energy.png -------------------------------------------------------------------------------- /crafter/assets/fence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/fence.png -------------------------------------------------------------------------------- /crafter/assets/food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/food.png -------------------------------------------------------------------------------- /crafter/assets/furnace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/furnace.png -------------------------------------------------------------------------------- /crafter/assets/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/grass.png -------------------------------------------------------------------------------- /crafter/assets/health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/health.png -------------------------------------------------------------------------------- /crafter/assets/iron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/iron.png -------------------------------------------------------------------------------- /crafter/assets/iron_pickaxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/iron_pickaxe.png -------------------------------------------------------------------------------- /crafter/assets/iron_sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/iron_sword.png -------------------------------------------------------------------------------- /crafter/assets/lava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/lava.png -------------------------------------------------------------------------------- /crafter/assets/leaves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/leaves.png -------------------------------------------------------------------------------- /crafter/assets/log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/log.png -------------------------------------------------------------------------------- /crafter/assets/path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/path.png -------------------------------------------------------------------------------- /crafter/assets/plant-ripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/plant-ripe.png -------------------------------------------------------------------------------- /crafter/assets/plant-young.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/plant-young.png -------------------------------------------------------------------------------- /crafter/assets/plant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/plant.png -------------------------------------------------------------------------------- /crafter/assets/player-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/player-down.png -------------------------------------------------------------------------------- /crafter/assets/player-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/player-left.png -------------------------------------------------------------------------------- /crafter/assets/player-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/player-right.png -------------------------------------------------------------------------------- /crafter/assets/player-sleep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/player-sleep.png -------------------------------------------------------------------------------- /crafter/assets/player-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/player-up.png -------------------------------------------------------------------------------- /crafter/assets/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/player.png -------------------------------------------------------------------------------- /crafter/assets/sand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/sand.png -------------------------------------------------------------------------------- /crafter/assets/sapling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/sapling.png -------------------------------------------------------------------------------- /crafter/assets/skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/skeleton.png -------------------------------------------------------------------------------- /crafter/assets/stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/stone.png -------------------------------------------------------------------------------- /crafter/assets/stone_pickaxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/stone_pickaxe.png -------------------------------------------------------------------------------- /crafter/assets/stone_sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/stone_sword.png -------------------------------------------------------------------------------- /crafter/assets/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/table.png -------------------------------------------------------------------------------- /crafter/assets/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/tree.png -------------------------------------------------------------------------------- /crafter/assets/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/unknown.png -------------------------------------------------------------------------------- /crafter/assets/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/water.png -------------------------------------------------------------------------------- /crafter/assets/wood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/wood.png -------------------------------------------------------------------------------- /crafter/assets/wood_pickaxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/wood_pickaxe.png -------------------------------------------------------------------------------- /crafter/assets/wood_sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/wood_sword.png -------------------------------------------------------------------------------- /crafter/assets/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/crafter/assets/zombie.png -------------------------------------------------------------------------------- /crafter/constants.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import ruamel.yaml 4 | 5 | root = pathlib.Path(__file__).parent 6 | yaml = ruamel.yaml.YAML(typ='safe', pure=True) 7 | for key, value in yaml.load((root / 'data.yaml').read_text()).items(): 8 | globals()[key] = value 9 | -------------------------------------------------------------------------------- /crafter/data.yaml: -------------------------------------------------------------------------------- 1 | actions: 2 | - noop 3 | - move_left 4 | - move_right 5 | - move_up 6 | - move_down 7 | - do 8 | - sleep 9 | - place_stone 10 | - place_table 11 | - place_furnace 12 | - place_plant 13 | - make_wood_pickaxe 14 | - make_stone_pickaxe 15 | - make_iron_pickaxe 16 | - make_wood_sword 17 | - make_stone_sword 18 | - make_iron_sword 19 | 20 | materials: 21 | - water 22 | - grass 23 | - stone 24 | - path 25 | - sand 26 | - tree 27 | - lava 28 | - coal 29 | - iron 30 | - diamond 31 | - table 32 | - furnace 33 | 34 | walkable: 35 | - grass 36 | - path 37 | - sand 38 | 39 | items: 40 | health: {max: 9, initial: 9} 41 | food: {max: 9, initial: 9} 42 | drink: {max: 9, initial: 9} 43 | energy: {max: 9, initial: 9} 44 | sapling: {max: 9, initial: 0} 45 | wood: {max: 9, initial: 0} 46 | stone: {max: 9, initial: 0} 47 | coal: {max: 9, initial: 0} 48 | iron: {max: 9, initial: 0} 49 | diamond: {max: 9, initial: 0} 50 | wood_pickaxe: {max: 9, initial: 0} 51 | stone_pickaxe: {max: 9, initial: 0} 52 | iron_pickaxe: {max: 9, initial: 0} 53 | wood_sword: {max: 9, initial: 0} 54 | stone_sword: {max: 9, initial: 0} 55 | iron_sword: {max: 9, initial: 0} 56 | 57 | collect: 58 | tree: {require: {}, receive: {wood: 1}, leaves: grass} 59 | stone: {require: {wood_pickaxe: 1}, receive: {stone: 1}, leaves: path} 60 | coal: {require: {wood_pickaxe: 1}, receive: {coal: 1}, leaves: path} 61 | iron: {require: {stone_pickaxe: 1}, receive: {iron: 1}, leaves: path} 62 | diamond: {require: {iron_pickaxe: 1}, receive: {diamond: 1}, leaves: path} 63 | water: {require: {}, receive: {drink: 1}, leaves: water} 64 | grass: {require: {}, receive: {sapling: 1}, probability: 0.1, leaves: grass} 65 | 66 | place: 67 | stone: {uses: {stone: 1}, where: [grass, sand, path, water, lava], type: material} 68 | table: {uses: {wood: 2}, where: [grass, sand, path], type: material} 69 | furnace: {uses: {stone: 4}, where: [grass, sand, path], type: material} 70 | plant: {uses: {sapling: 1}, where: [grass], type: object} 71 | 72 | make: 73 | wood_pickaxe: {uses: {wood: 1}, nearby: [table], gives: 1} 74 | stone_pickaxe: {uses: {wood: 1, stone: 1}, nearby: [table], gives: 1} 75 | iron_pickaxe: {uses: {wood: 1, coal: 1, iron: 1}, nearby: [table, furnace], gives: 1} 76 | wood_sword: {uses: {wood: 1}, nearby: [table], gives: 1} 77 | stone_sword: {uses: {wood: 1, stone: 1}, nearby: [table], gives: 1} 78 | iron_sword: {uses: {wood: 1, coal: 1, iron: 1}, nearby: [table, furnace], gives: 1} 79 | 80 | achievements: 81 | - collect_coal 82 | - collect_diamond 83 | - collect_drink 84 | - collect_iron 85 | - collect_sapling 86 | - collect_stone 87 | - collect_wood 88 | - defeat_skeleton 89 | - defeat_zombie 90 | - eat_cow 91 | - eat_plant 92 | - make_iron_pickaxe 93 | - make_iron_sword 94 | - make_stone_pickaxe 95 | - make_stone_sword 96 | - make_wood_pickaxe 97 | - make_wood_sword 98 | - place_furnace 99 | - place_plant 100 | - place_stone 101 | - place_table 102 | - wake_up 103 | -------------------------------------------------------------------------------- /crafter/engine.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import functools 3 | import pathlib 4 | 5 | import imageio.v3 as imageio 6 | import numpy as np 7 | from PIL import Image, ImageEnhance 8 | 9 | 10 | class AttrDict(dict): 11 | 12 | __getattr__ = dict.__getitem__ 13 | 14 | 15 | class staticproperty: 16 | 17 | def __init__(self, function): 18 | self.function = function 19 | 20 | def __get__(self, instance, owner=None): 21 | return self.function() 22 | 23 | 24 | class World: 25 | 26 | def __init__(self, area, materials, chunk_size): 27 | self.area = area 28 | self._chunk_size = chunk_size 29 | self._mat_names = {i: x for i, x in enumerate([None] + materials)} 30 | self._mat_ids = {x: i for i, x in enumerate([None] + materials)} 31 | self.reset() 32 | 33 | def reset(self, seed=None): 34 | self.random = np.random.RandomState(seed) 35 | self.daylight = 0.0 36 | self._chunks = collections.defaultdict(set) 37 | self._objects = [None] 38 | self._mat_map = np.zeros(self.area, np.uint8) 39 | self._obj_map = np.zeros(self.area, np.uint32) 40 | 41 | @property 42 | def objects(self): 43 | # Return a new list so the objects cannot change while being iterated over. 44 | return [obj for obj in self._objects if obj] 45 | 46 | @property 47 | def chunks(self): 48 | return self._chunks.copy() 49 | 50 | def add(self, obj): 51 | assert hasattr(obj, 'pos') 52 | obj.pos = np.array(obj.pos) 53 | assert self._obj_map[tuple(obj.pos)] == 0 54 | index = len(self._objects) 55 | self._objects.append(obj) 56 | self._obj_map[tuple(obj.pos)] = index 57 | self._chunks[self.chunk_key(obj.pos)].add(obj) 58 | 59 | def remove(self, obj): 60 | if obj.removed: 61 | return 62 | self._objects[self._obj_map[tuple(obj.pos)]] = None 63 | self._obj_map[tuple(obj.pos)] = 0 64 | self._chunks[self.chunk_key(obj.pos)].remove(obj) 65 | obj.removed = True 66 | 67 | def move(self, obj, pos): 68 | if obj.removed: 69 | return 70 | pos = np.array(pos) 71 | assert self._obj_map[tuple(pos)] == 0 72 | index = self._obj_map[tuple(obj.pos)] 73 | self._obj_map[tuple(pos)] = index 74 | self._obj_map[tuple(obj.pos)] = 0 75 | old_chunk = self.chunk_key(obj.pos) 76 | new_chunk = self.chunk_key(pos) 77 | if old_chunk != new_chunk: 78 | self._chunks[old_chunk].remove(obj) 79 | self._chunks[new_chunk].add(obj) 80 | obj.pos = pos 81 | 82 | def __setitem__(self, pos, material): 83 | if material not in self._mat_ids: 84 | id_ = len(self._mat_ids) 85 | self._mat_ids[material] = id_ 86 | self._mat_map[tuple(pos)] = self._mat_ids[material] 87 | 88 | def __getitem__(self, pos): 89 | if not _inside((0, 0), pos, self.area): 90 | return None, None 91 | material = self._mat_names[self._mat_map[tuple(pos)]] 92 | obj = self._objects[self._obj_map[tuple(pos)]] 93 | return material, obj 94 | 95 | def nearby(self, pos, distance): 96 | (x, y), d = pos, distance 97 | ids = set(self._mat_map[ 98 | x - d: x + d + 1, y - d: y + d + 1].flatten().tolist()) 99 | materials = tuple(self._mat_names[x] for x in ids) 100 | indices = self._obj_map[ 101 | x - d: x + d + 1, y - d: y + d + 1].flatten().tolist() 102 | objs = {self._objects[i] for i in indices if i > 0} 103 | return materials, objs 104 | 105 | def mask(self, xmin, xmax, ymin, ymax, material): 106 | region = self._mat_map[xmin: xmax, ymin: ymax] 107 | return (region == self._mat_ids[material]) 108 | 109 | def count(self, material): 110 | return (self._mat_map == self._mat_ids[material]).sum() 111 | 112 | def chunk_key(self, pos): 113 | (x, y), (csx, csy) = pos, self._chunk_size 114 | xmin, ymin = (x // csx) * csx, (y // csy) * csy 115 | xmax = min(xmin + csx, self.area[0]) 116 | ymax = min(ymin + csy, self.area[1]) 117 | return (xmin, xmax, ymin, ymax) 118 | 119 | 120 | class Textures: 121 | 122 | def __init__(self, directory): 123 | self._originals = {} 124 | self._textures = {} 125 | for filename in pathlib.Path(directory).glob('*.png'): 126 | image = imageio.imread(filename.read_bytes()) 127 | image = image.transpose((1, 0) + tuple(range(2, len(image.shape)))) 128 | self._originals[filename.stem] = image 129 | self._textures[(filename.stem, image.shape[:2])] = image 130 | 131 | def get(self, name, size): 132 | if name is None: 133 | name = 'unknown' 134 | size = int(size[0]), int(size[1]) 135 | key = name, size 136 | if key not in self._textures: 137 | image = self._originals[name] 138 | image = Image.fromarray(image) 139 | image = image.resize(size[::-1], resample=Image.NEAREST) 140 | image = np.array(image) 141 | self._textures[key] = image 142 | return self._textures[key] 143 | 144 | 145 | class GlobalView: 146 | 147 | pass 148 | 149 | 150 | class UncoverView: 151 | 152 | pass 153 | 154 | 155 | class LocalView: 156 | 157 | def __init__(self, world, textures, grid): 158 | self._world = world 159 | self._textures = textures 160 | self._grid = np.array(grid) 161 | self._offset = self._grid // 2 162 | self._area = np.array(self._world.area) 163 | self._center = None 164 | 165 | def __call__(self, player, unit): 166 | self._unit = np.array(unit) 167 | self._center = np.array(player.pos) 168 | canvas = np.zeros(tuple(self._grid * unit) + (3,), np.uint8) + 127 169 | for x in range(self._grid[0]): 170 | for y in range(self._grid[1]): 171 | pos = self._center + np.array([x, y]) - self._offset 172 | if not _inside((0, 0), pos, self._area): 173 | continue 174 | texture = self._textures.get(self._world[pos][0], unit) 175 | _draw(canvas, np.array([x, y]) * unit, texture) 176 | for obj in self._world.objects: 177 | pos = obj.pos - self._center + self._offset 178 | if not _inside((0, 0), pos, self._grid): 179 | continue 180 | texture = self._textures.get(obj.texture, unit) 181 | _draw_alpha(canvas, pos * unit, texture) 182 | canvas = self._light(canvas, self._world.daylight) 183 | if player.sleeping: 184 | canvas = self._sleep(canvas) 185 | # if player.health < 1: 186 | # canvas = self._tint(canvas, (128, 0, 0), 0.6) 187 | return canvas 188 | 189 | def _light(self, canvas, daylight): 190 | night = canvas 191 | if daylight < 0.5: 192 | night = self._noise(night, 2 * (0.5 - daylight), 0.5) 193 | night = np.array(ImageEnhance.Color( 194 | Image.fromarray(night.astype(np.uint8))).enhance(0.4)) 195 | night = self._tint(night, (0, 16, 64), 0.5) 196 | return daylight * canvas + (1 - daylight) * night 197 | 198 | def _sleep(self, canvas): 199 | canvas = np.array(ImageEnhance.Color( 200 | Image.fromarray(canvas.astype(np.uint8))).enhance(0.0)) 201 | canvas = self._tint(canvas, (0, 0, 16), 0.5) 202 | return canvas 203 | 204 | def _tint(self, canvas, color, amount): 205 | color = np.array(color) 206 | return (1 - amount) * canvas + amount * color 207 | 208 | def _noise(self, canvas, amount, stddev): 209 | noise = self._world.random.uniform(32, 127, canvas.shape[:2])[..., None] 210 | mask = amount * self._vignette(canvas.shape, stddev)[..., None] 211 | return (1 - mask) * canvas + mask * noise 212 | 213 | @functools.lru_cache(10) 214 | def _vignette(self, shape, stddev): 215 | xs, ys = np.meshgrid( 216 | np.linspace(-1, 1, shape[0]), 217 | np.linspace(-1, 1, shape[1])) 218 | return 1 - np.exp(-0.5 * (xs ** 2 + ys ** 2) / (stddev ** 2)).T 219 | 220 | 221 | class ItemView: 222 | 223 | def __init__(self, textures, grid): 224 | self._textures = textures 225 | self._grid = np.array(grid) 226 | 227 | def __call__(self, inventory, unit): 228 | unit = np.array(unit) 229 | canvas = np.zeros(tuple(self._grid * unit) + (3,), np.uint8) 230 | for index, (item, amount) in enumerate(inventory.items()): 231 | if amount < 1: 232 | continue 233 | self._item(canvas, index, item, unit) 234 | self._amount(canvas, index, amount, unit) 235 | return canvas 236 | 237 | def _item(self, canvas, index, item, unit): 238 | pos = index % self._grid[0], index // self._grid[0] 239 | pos = (pos * unit + 0.1 * unit).astype(np.int32) 240 | texture = self._textures.get(item, 0.8 * unit) 241 | _draw_alpha(canvas, pos, texture) 242 | 243 | def _amount(self, canvas, index, amount, unit): 244 | pos = index % self._grid[0], index // self._grid[0] 245 | pos = (pos * unit + 0.4 * unit).astype(np.int32) 246 | text = str(amount) if amount in list(range(10)) else 'unknown' 247 | texture = self._textures.get(text, 0.6 * unit) 248 | _draw_alpha(canvas, pos, texture) 249 | 250 | 251 | class SemanticView: 252 | 253 | def __init__(self, world, obj_types): 254 | self._world = world 255 | self._mat_ids = world._mat_ids.copy() 256 | self._obj_ids = { 257 | c: len(self._mat_ids) + i 258 | for i, c in enumerate(obj_types)} 259 | 260 | def __call__(self): 261 | canvas = self._world._mat_map.copy() 262 | for obj in self._world.objects: 263 | canvas[tuple(obj.pos)] = self._obj_ids[type(obj)] 264 | return canvas 265 | 266 | 267 | def _inside(lhs, mid, rhs): 268 | return (lhs[0] <= mid[0] < rhs[0]) and (lhs[1] <= mid[1] < rhs[1]) 269 | 270 | def _draw(canvas, pos, texture): 271 | (x, y), (w, h) = pos, texture.shape[:2] 272 | if texture.shape[-1] == 4: 273 | texture = texture[..., :3] 274 | canvas[x: x + w, y: y + h] = texture 275 | 276 | def _draw_alpha(canvas, pos, texture): 277 | (x, y), (w, h) = pos, texture.shape[:2] 278 | if texture.shape[-1] == 4: 279 | alpha = texture[..., 3:].astype(np.float32) / 255 280 | texture = texture[..., :3].astype(np.float32) / 255 281 | current = canvas[x: x + w, y: y + h].astype(np.float32) / 255 282 | blended = alpha * texture + (1 - alpha) * current 283 | texture = (255 * blended).astype(np.uint8) 284 | canvas[x: x + w, y: y + h] = texture 285 | -------------------------------------------------------------------------------- /crafter/env.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import numpy as np 4 | 5 | from . import constants 6 | from . import engine 7 | from . import objects 8 | from . import worldgen 9 | 10 | 11 | # Gym is an optional dependency. 12 | try: 13 | import gym 14 | DiscreteSpace = gym.spaces.Discrete 15 | BoxSpace = gym.spaces.Box 16 | DictSpace = gym.spaces.Dict 17 | BaseClass = gym.Env 18 | except ImportError: 19 | DiscreteSpace = collections.namedtuple('DiscreteSpace', 'n') 20 | BoxSpace = collections.namedtuple('BoxSpace', 'low, high, shape, dtype') 21 | DictSpace = collections.namedtuple('DictSpace', 'spaces') 22 | BaseClass = object 23 | 24 | 25 | class Env(BaseClass): 26 | 27 | def __init__( 28 | self, area=(64, 64), view=(9, 9), size=(64, 64), 29 | reward=True, length=10000, seed=None): 30 | view = np.array(view if hasattr(view, '__len__') else (view, view)) 31 | size = np.array(size if hasattr(size, '__len__') else (size, size)) 32 | seed = np.random.randint(0, 2**31 - 1) if seed is None else seed 33 | self._area = area 34 | self._view = view 35 | self._size = size 36 | self._reward = reward 37 | self._length = length 38 | self._seed = seed 39 | self._episode = 0 40 | self._world = engine.World(area, constants.materials, (12, 12)) 41 | self._textures = engine.Textures(constants.root / 'assets') 42 | item_rows = int(np.ceil(len(constants.items) / view[0])) 43 | self._local_view = engine.LocalView( 44 | self._world, self._textures, [view[0], view[1] - item_rows]) 45 | self._item_view = engine.ItemView( 46 | self._textures, [view[0], item_rows]) 47 | self._sem_view = engine.SemanticView(self._world, [ 48 | objects.Player, objects.Cow, objects.Zombie, 49 | objects.Skeleton, objects.Arrow, objects.Plant]) 50 | self._step = None 51 | self._player = None 52 | self._last_health = None 53 | self._unlocked = None 54 | # Some libraries expect these attributes to be set. 55 | self.reward_range = None 56 | self.metadata = None 57 | 58 | @property 59 | def observation_space(self): 60 | return BoxSpace(0, 255, tuple(self._size) + (3,), np.uint8) 61 | 62 | @property 63 | def action_space(self): 64 | return DiscreteSpace(len(constants.actions)) 65 | 66 | @property 67 | def action_names(self): 68 | return constants.actions 69 | 70 | def reset(self): 71 | center = (self._world.area[0] // 2, self._world.area[1] // 2) 72 | self._episode += 1 73 | self._step = 0 74 | self._world.reset(seed=hash((self._seed, self._episode)) % (2 ** 31 - 1)) 75 | self._update_time() 76 | self._player = objects.Player(self._world, center) 77 | self._last_health = self._player.health 78 | self._world.add(self._player) 79 | self._unlocked = set() 80 | worldgen.generate_world(self._world, self._player) 81 | return self._obs() 82 | 83 | def step(self, action): 84 | self._step += 1 85 | self._update_time() 86 | self._player.action = constants.actions[action] 87 | for obj in self._world.objects: 88 | if self._player.distance(obj) < 2 * max(self._view): 89 | obj.update() 90 | if self._step % 10 == 0: 91 | for chunk, objs in self._world.chunks.items(): 92 | # xmin, xmax, ymin, ymax = chunk 93 | # center = (xmax - xmin) // 2, (ymax - ymin) // 2 94 | # if self._player.distance(center) < 4 * max(self._view): 95 | self._balance_chunk(chunk, objs) 96 | obs = self._obs() 97 | reward = (self._player.health - self._last_health) / 10 98 | self._last_health = self._player.health 99 | unlocked = { 100 | name for name, count in self._player.achievements.items() 101 | if count > 0 and name not in self._unlocked} 102 | if unlocked: 103 | self._unlocked |= unlocked 104 | reward += 1.0 105 | dead = self._player.health <= 0 106 | over = self._length and self._step >= self._length 107 | done = dead or over 108 | info = { 109 | 'inventory': self._player.inventory.copy(), 110 | 'achievements': self._player.achievements.copy(), 111 | 'discount': 1 - float(dead), 112 | 'semantic': self._sem_view(), 113 | 'player_pos': self._player.pos, 114 | 'reward': reward, 115 | } 116 | if not self._reward: 117 | reward = 0.0 118 | return obs, reward, done, info 119 | 120 | def render(self, size=None): 121 | size = size or self._size 122 | unit = size // self._view 123 | canvas = np.zeros(tuple(size) + (3,), np.uint8) 124 | local_view = self._local_view(self._player, unit) 125 | item_view = self._item_view(self._player.inventory, unit) 126 | view = np.concatenate([local_view, item_view], 1) 127 | border = (size - (size // self._view) * self._view) // 2 128 | (x, y), (w, h) = border, view.shape[:2] 129 | canvas[x: x + w, y: y + h] = view 130 | return canvas.transpose((1, 0, 2)) 131 | 132 | def _obs(self): 133 | return self.render() 134 | 135 | def _update_time(self): 136 | # https://www.desmos.com/calculator/grfbc6rs3h 137 | progress = (self._step / 300) % 1 + 0.3 138 | daylight = 1 - np.abs(np.cos(np.pi * progress)) ** 3 139 | self._world.daylight = daylight 140 | 141 | def _balance_chunk(self, chunk, objs): 142 | light = self._world.daylight 143 | self._balance_object( 144 | chunk, objs, objects.Zombie, 'grass', 6, 0, 0.3, 0.4, 145 | lambda pos: objects.Zombie(self._world, pos, self._player), 146 | lambda num, space: ( 147 | 0 if space < 50 else 3.5 - 3 * light, 3.5 - 3 * light)) 148 | self._balance_object( 149 | chunk, objs, objects.Skeleton, 'path', 7, 7, 0.1, 0.1, 150 | lambda pos: objects.Skeleton(self._world, pos, self._player), 151 | lambda num, space: (0 if space < 6 else 1, 2)) 152 | self._balance_object( 153 | chunk, objs, objects.Cow, 'grass', 5, 5, 0.01, 0.1, 154 | lambda pos: objects.Cow(self._world, pos), 155 | lambda num, space: (0 if space < 30 else 1, 1.5 + light)) 156 | 157 | def _balance_object( 158 | self, chunk, objs, cls, material, span_dist, despan_dist, 159 | spawn_prob, despawn_prob, ctor, target_fn): 160 | xmin, xmax, ymin, ymax = chunk 161 | random = self._world.random 162 | creatures = [obj for obj in objs if isinstance(obj, cls)] 163 | mask = self._world.mask(*chunk, material) 164 | target_min, target_max = target_fn(len(creatures), mask.sum()) 165 | if len(creatures) < int(target_min) and random.uniform() < spawn_prob: 166 | xs = np.tile(np.arange(xmin, xmax)[:, None], [1, ymax - ymin]) 167 | ys = np.tile(np.arange(ymin, ymax)[None, :], [xmax - xmin, 1]) 168 | xs, ys = xs[mask], ys[mask] 169 | i = random.randint(0, len(xs)) 170 | pos = np.array((xs[i], ys[i])) 171 | empty = self._world[pos][1] is None 172 | away = self._player.distance(pos) >= span_dist 173 | if empty and away: 174 | self._world.add(ctor(pos)) 175 | elif len(creatures) > int(target_max) and random.uniform() < despawn_prob: 176 | obj = creatures[random.randint(0, len(creatures))] 177 | away = self._player.distance(obj.pos) >= despan_dist 178 | if away: 179 | self._world.remove(obj) 180 | -------------------------------------------------------------------------------- /crafter/objects.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from . import constants 4 | from . import engine 5 | 6 | 7 | class Object: 8 | 9 | def __init__(self, world, pos): 10 | self.world = world 11 | self.pos = np.array(pos) 12 | self.random = world.random 13 | self.inventory = {'health': 0} 14 | self.removed = False 15 | 16 | @property 17 | def texture(self): 18 | raise 'unknown' 19 | 20 | @property 21 | def walkable(self): 22 | return constants.walkable 23 | 24 | @property 25 | def health(self): 26 | return self.inventory['health'] 27 | 28 | @health.setter 29 | def health(self, value): 30 | self.inventory['health'] = max(0, value) 31 | 32 | @property 33 | def all_dirs(self): 34 | return ((-1, 0), (+1, 0), (0, -1), (0, +1)) 35 | 36 | def move(self, direction): 37 | direction = np.array(direction) 38 | target = self.pos + direction 39 | if self.is_free(target): 40 | self.world.move(self, target) 41 | return True 42 | return False 43 | 44 | def is_free(self, target, materials=None): 45 | materials = self.walkable if materials is None else materials 46 | material, obj = self.world[target] 47 | return obj is None and material in materials 48 | 49 | def distance(self, target): 50 | if hasattr(target, 'pos'): 51 | target = target.pos 52 | return np.abs(target - self.pos).sum() 53 | 54 | def toward(self, target, long_axis=True): 55 | if hasattr(target, 'pos'): 56 | target = target.pos 57 | offset = target - self.pos 58 | dists = np.abs(offset) 59 | if (dists[0] > dists[1] if long_axis else dists[0] <= dists[1]): 60 | return np.array((np.sign(offset[0]), 0)) 61 | else: 62 | return np.array((0, np.sign(offset[1]))) 63 | 64 | def random_dir(self): 65 | return self.all_dirs[self.random.randint(0, 4)] 66 | 67 | 68 | class Player(Object): 69 | 70 | def __init__(self, world, pos): 71 | super().__init__(world, pos) 72 | self.facing = (0, 1) 73 | self.inventory = { 74 | name: info['initial'] for name, info in constants.items.items()} 75 | self.achievements = {name: 0 for name in constants.achievements} 76 | self.action = 'noop' 77 | self.sleeping = False 78 | self._last_health = self.health 79 | self._hunger = 0 80 | self._thirst = 0 81 | self._fatigue = 0 82 | self._recover = 0 83 | 84 | @property 85 | def texture(self): 86 | if self.sleeping: 87 | return 'player-sleep' 88 | return { 89 | (-1, 0): 'player-left', 90 | (+1, 0): 'player-right', 91 | (0, -1): 'player-up', 92 | (0, +1): 'player-down', 93 | }[tuple(self.facing)] 94 | 95 | @property 96 | def walkable(self): 97 | return constants.walkable + ['lava'] 98 | 99 | def update(self): 100 | target = (self.pos[0] + self.facing[0], self.pos[1] + self.facing[1]) 101 | material, obj = self.world[target] 102 | action = self.action 103 | if self.sleeping: 104 | if self.inventory['energy'] < constants.items['energy']['max']: 105 | action = 'sleep' 106 | else: 107 | self.sleeping = False 108 | self.achievements['wake_up'] += 1 109 | if action == 'noop': 110 | pass 111 | elif action.startswith('move_'): 112 | self._move(action[len('move_'):]) 113 | elif action == 'do' and obj: 114 | self._do_object(obj) 115 | elif action == 'do': 116 | self._do_material(target, material) 117 | elif action == 'sleep': 118 | if self.inventory['energy'] < constants.items['energy']['max']: 119 | self.sleeping = True 120 | elif action.startswith('place_'): 121 | self._place(action[len('place_'):], target, material) 122 | elif action.startswith('make_'): 123 | self._make(action[len('make_'):]) 124 | self._update_life_stats() 125 | self._degen_or_regen_health() 126 | for name, amount in self.inventory.items(): 127 | maxmium = constants.items[name]['max'] 128 | self.inventory[name] = max(0, min(amount, maxmium)) 129 | # This needs to happen after the inventory states are clamped 130 | # because it involves the health water inventory count. 131 | self._wake_up_when_hurt() 132 | 133 | def _update_life_stats(self): 134 | self._hunger += 0.5 if self.sleeping else 1 135 | if self._hunger > 25: 136 | self._hunger = 0 137 | self.inventory['food'] -= 1 138 | self._thirst += 0.5 if self.sleeping else 1 139 | if self._thirst > 20: 140 | self._thirst = 0 141 | self.inventory['drink'] -= 1 142 | if self.sleeping: 143 | self._fatigue = min(self._fatigue - 1, 0) 144 | else: 145 | self._fatigue += 1 146 | if self._fatigue < -10: 147 | self._fatigue = 0 148 | self.inventory['energy'] += 1 149 | if self._fatigue > 30: 150 | self._fatigue = 0 151 | self.inventory['energy'] -= 1 152 | 153 | def _degen_or_regen_health(self): 154 | necessities = ( 155 | self.inventory['food'] > 0, 156 | self.inventory['drink'] > 0, 157 | self.inventory['energy'] > 0 or self.sleeping) 158 | if all(necessities): 159 | self._recover += 2 if self.sleeping else 1 160 | else: 161 | self._recover -= 0.5 if self.sleeping else 1 162 | if self._recover > 25: 163 | self._recover = 0 164 | self.health += 1 165 | if self._recover < -15: 166 | self._recover = 0 167 | self.health -= 1 168 | 169 | def _wake_up_when_hurt(self): 170 | if self.health < self._last_health: 171 | self.sleeping = False 172 | self._last_health = self.health 173 | 174 | def _move(self, direction): 175 | directions = dict(left=(-1, 0), right=(+1, 0), up=(0, -1), down=(0, +1)) 176 | self.facing = directions[direction] 177 | self.move(self.facing) 178 | if self.world[self.pos][0] == 'lava': 179 | self.health = 0 180 | 181 | def _do_object(self, obj): 182 | damage = max([ 183 | 1, 184 | self.inventory['wood_sword'] and 2, 185 | self.inventory['stone_sword'] and 3, 186 | self.inventory['iron_sword'] and 5, 187 | ]) 188 | if isinstance(obj, Plant): 189 | if obj.ripe: 190 | obj.grown = 0 191 | self.inventory['food'] += 4 192 | self.achievements['eat_plant'] += 1 193 | if isinstance(obj, Fence): 194 | self.world.remove(obj) 195 | self.inventory['fence'] += 1 196 | self.achievements['collect_fence'] += 1 197 | if isinstance(obj, Zombie): 198 | obj.health -= damage 199 | if obj.health <= 0: 200 | self.achievements['defeat_zombie'] += 1 201 | if isinstance(obj, Skeleton): 202 | obj.health -= damage 203 | if obj.health <= 0: 204 | self.achievements['defeat_skeleton'] += 1 205 | if isinstance(obj, Cow): 206 | obj.health -= damage 207 | if obj.health <= 0: 208 | self.inventory['food'] += 6 209 | self.achievements['eat_cow'] += 1 210 | # TODO: Keep track of previous inventory state to do this in a more 211 | # general way. 212 | self._hunger = 0 213 | 214 | def _do_material(self, target, material): 215 | if material == 'water': 216 | # TODO: Keep track of previous inventory state to do this in a more 217 | # general way. 218 | self._thirst = 0 219 | info = constants.collect.get(material) 220 | if not info: 221 | return 222 | for name, amount in info['require'].items(): 223 | if self.inventory[name] < amount: 224 | return 225 | self.world[target] = info['leaves'] 226 | if self.random.uniform() <= info.get('probability', 1): 227 | for name, amount in info['receive'].items(): 228 | self.inventory[name] += amount 229 | self.achievements[f'collect_{name}'] += 1 230 | 231 | def _place(self, name, target, material): 232 | if self.world[target][1]: 233 | return 234 | info = constants.place[name] 235 | if material not in info['where']: 236 | return 237 | if any(self.inventory[k] < v for k, v in info['uses'].items()): 238 | return 239 | for item, amount in info['uses'].items(): 240 | self.inventory[item] -= amount 241 | if info['type'] == 'material': 242 | self.world[target] = name 243 | elif info['type'] == 'object': 244 | cls = { 245 | 'fence': Fence, 246 | 'plant': Plant, 247 | }[name] 248 | self.world.add(cls(self.world, target)) 249 | self.achievements[f'place_{name}'] += 1 250 | 251 | def _make(self, name): 252 | nearby, _ = self.world.nearby(self.pos, 1) 253 | info = constants.make[name] 254 | if not all(util in nearby for util in info['nearby']): 255 | return 256 | if any(self.inventory[k] < v for k, v in info['uses'].items()): 257 | return 258 | for item, amount in info['uses'].items(): 259 | self.inventory[item] -= amount 260 | self.inventory[name] += info['gives'] 261 | self.achievements[f'make_{name}'] += 1 262 | 263 | 264 | class Cow(Object): 265 | 266 | def __init__(self, world, pos): 267 | super().__init__(world, pos) 268 | self.health = 3 269 | 270 | @property 271 | def texture(self): 272 | return 'cow' 273 | 274 | def update(self): 275 | if self.health <= 0: 276 | self.world.remove(self) 277 | if self.random.uniform() < 0.5: 278 | direction = self.random_dir() 279 | self.move(direction) 280 | 281 | 282 | class Zombie(Object): 283 | 284 | def __init__(self, world, pos, player): 285 | super().__init__(world, pos) 286 | self.player = player 287 | self.health = 5 288 | self.cooldown = 0 289 | 290 | @property 291 | def texture(self): 292 | return 'zombie' 293 | 294 | def update(self): 295 | if self.health <= 0: 296 | self.world.remove(self) 297 | dist = self.distance(self.player) 298 | if dist <= 8 and self.random.uniform() < 0.9: 299 | self.move(self.toward(self.player, self.random.uniform() < 0.8)) 300 | else: 301 | self.move(self.random_dir()) 302 | dist = self.distance(self.player) 303 | if dist <= 1: 304 | if self.cooldown: 305 | self.cooldown -= 1 306 | else: 307 | if self.player.sleeping: 308 | damage = 7 309 | else: 310 | damage = 2 311 | self.player.health -= damage 312 | self.cooldown = 5 313 | 314 | 315 | class Skeleton(Object): 316 | 317 | def __init__(self, world, pos, player): 318 | super().__init__(world, pos) 319 | self.player = player 320 | self.health = 3 321 | self.reload = 0 322 | 323 | @property 324 | def texture(self): 325 | return 'skeleton' 326 | 327 | def update(self): 328 | if self.health <= 0: 329 | self.world.remove(self) 330 | self.reload = max(0, self.reload - 1) 331 | dist = self.distance(self.player.pos) 332 | if dist <= 3: 333 | moved = self.move(-self.toward(self.player, self.random.uniform() < 0.6)) 334 | if moved: 335 | return 336 | if dist <= 5 and self.random.uniform() < 0.5: 337 | self._shoot(self.toward(self.player)) 338 | elif dist <= 8 and self.random.uniform() < 0.3: 339 | self.move(self.toward(self.player, self.random.uniform() < 0.6)) 340 | elif self.random.uniform() < 0.2: 341 | self.move(self.random_dir()) 342 | 343 | def _shoot(self, direction): 344 | if self.reload > 0: 345 | return 346 | if direction[0] == 0 and direction[1] == 0: 347 | return 348 | pos = self.pos + direction 349 | if self.is_free(pos, Arrow.walkable): 350 | self.world.add(Arrow(self.world, pos, direction)) 351 | self.reload = 4 352 | 353 | 354 | class Arrow(Object): 355 | 356 | def __init__(self, world, pos, facing): 357 | super().__init__(world, pos) 358 | self.facing = facing 359 | 360 | @property 361 | def texture(self): 362 | return { 363 | (-1, 0): 'arrow-left', 364 | (+1, 0): 'arrow-right', 365 | (0, -1): 'arrow-up', 366 | (0, +1): 'arrow-down', 367 | }[tuple(self.facing)] 368 | 369 | @engine.staticproperty 370 | def walkable(): 371 | return constants.walkable + ['water', 'lava'] 372 | 373 | def update(self): 374 | target = self.pos + self.facing 375 | material, obj = self.world[target] 376 | if obj: 377 | obj.health -= 2 378 | self.world.remove(self) 379 | elif material not in self.walkable: 380 | self.world.remove(self) 381 | if material in ['table', 'furnace']: 382 | self.world[target] = 'path' 383 | else: 384 | self.move(self.facing) 385 | 386 | 387 | class Plant(Object): 388 | 389 | def __init__(self, world, pos): 390 | super().__init__(world, pos) 391 | self.health = 1 392 | self.grown = 0 393 | 394 | @property 395 | def texture(self): 396 | if self.ripe: 397 | return 'plant-ripe' 398 | else: 399 | return 'plant' 400 | 401 | @property 402 | def ripe(self): 403 | return self.grown > 300 404 | 405 | def update(self): 406 | self.grown += 1 407 | objs = [self.world[self.pos + dir_][1] for dir_ in self.all_dirs] 408 | if any(isinstance(obj, (Zombie, Skeleton, Cow)) for obj in objs): 409 | self.health -= 1 410 | if self.health <= 0: 411 | self.world.remove(self) 412 | 413 | 414 | class Fence(Object): 415 | 416 | def __init__(self, world, pos): 417 | super().__init__(world, pos) 418 | 419 | @property 420 | def texture(self): 421 | return 'fence' 422 | 423 | def update(self): 424 | pass 425 | -------------------------------------------------------------------------------- /crafter/recorder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import pathlib 4 | 5 | import imageio 6 | import numpy as np 7 | 8 | 9 | class Recorder: 10 | 11 | def __init__( 12 | self, env, directory, save_stats=True, save_video=True, 13 | save_episode=True, video_size=(512, 512)): 14 | if directory and save_stats: 15 | env = StatsRecorder(env, directory) 16 | if directory and save_video: 17 | env = VideoRecorder(env, directory, video_size) 18 | if directory and save_episode: 19 | env = EpisodeRecorder(env, directory) 20 | self._env = env 21 | 22 | def __getattr__(self, name): 23 | if name.startswith('__'): 24 | raise AttributeError(name) 25 | return getattr(self._env, name) 26 | 27 | 28 | class StatsRecorder: 29 | 30 | def __init__(self, env, directory): 31 | self._env = env 32 | self._directory = pathlib.Path(directory).expanduser() 33 | self._directory.mkdir(exist_ok=True, parents=True) 34 | self._file = (self._directory / 'stats.jsonl').open('a') 35 | self._length = None 36 | self._reward = None 37 | self._unlocked = None 38 | self._stats = None 39 | 40 | def __getattr__(self, name): 41 | if name.startswith('__'): 42 | raise AttributeError(name) 43 | return getattr(self._env, name) 44 | 45 | def reset(self): 46 | obs = self._env.reset() 47 | self._length = 0 48 | self._reward = 0 49 | self._unlocked = None 50 | self._stats = None 51 | return obs 52 | 53 | def step(self, action): 54 | obs, reward, done, info = self._env.step(action) 55 | self._length += 1 56 | self._reward += info['reward'] 57 | if done: 58 | self._stats = {'length': self._length, 'reward': round(self._reward, 1)} 59 | for key, value in info['achievements'].items(): 60 | self._stats[f'achievement_{key}'] = value 61 | self._save() 62 | return obs, reward, done, info 63 | 64 | def _save(self): 65 | self._file.write(json.dumps(self._stats) + '\n') 66 | self._file.flush() 67 | 68 | 69 | class VideoRecorder: 70 | 71 | def __init__(self, env, directory, size=(512, 512)): 72 | if not hasattr(env, 'episode_name'): 73 | env = EpisodeName(env) 74 | self._env = env 75 | self._directory = pathlib.Path(directory).expanduser() 76 | self._directory.mkdir(exist_ok=True, parents=True) 77 | self._size = size 78 | self._frames = None 79 | 80 | def __getattr__(self, name): 81 | if name.startswith('__'): 82 | raise AttributeError(name) 83 | return getattr(self._env, name) 84 | 85 | def reset(self): 86 | obs = self._env.reset() 87 | self._frames = [self._env.render(self._size)] 88 | return obs 89 | 90 | def step(self, action): 91 | obs, reward, done, info = self._env.step(action) 92 | self._frames.append(self._env.render(self._size)) 93 | if done: 94 | self._save() 95 | return obs, reward, done, info 96 | 97 | def _save(self): 98 | filename = str(self._directory / (self._env.episode_name + '.mp4')) 99 | imageio.mimsave(filename, self._frames) 100 | 101 | 102 | class EpisodeRecorder: 103 | 104 | def __init__(self, env, directory): 105 | if not hasattr(env, 'episode_name'): 106 | env = EpisodeName(env) 107 | self._env = env 108 | self._directory = pathlib.Path(directory).expanduser() 109 | self._directory.mkdir(exist_ok=True, parents=True) 110 | self._episode = None 111 | 112 | def __getattr__(self, name): 113 | if name.startswith('__'): 114 | raise AttributeError(name) 115 | return getattr(self._env, name) 116 | 117 | def reset(self): 118 | obs = self._env.reset() 119 | self._episode = [{'image': obs}] 120 | return obs 121 | 122 | def step(self, action): 123 | # Transitions are defined from the environment perspective, meaning that a 124 | # transition contains the action and the resulting reward and next 125 | # observation produced by the environment in response to said action. 126 | obs, reward, done, info = self._env.step(action) 127 | transition = { 128 | 'action': action, 'image': obs, 'reward': reward, 'done': done, 129 | } 130 | for key, value in info.items(): 131 | if key in ('inventory', 'achievements'): 132 | continue 133 | transition[key] = value 134 | for key, value in info['achievements'].items(): 135 | transition[f'achievement_{key}'] = value 136 | for key, value in info['inventory'].items(): 137 | transition[f'ainventory_{key}'] = value 138 | self._episode.append(transition) 139 | if done: 140 | self._save() 141 | return obs, reward, done, info 142 | 143 | def _save(self): 144 | filename = str(self._directory / (self._env.episode_name + '.npz')) 145 | # Fill in zeros for keys missing at the first time step. 146 | for key, value in self._episode[1].items(): 147 | if key not in self._episode[0]: 148 | self._episode[0][key] = np.zeros_like(value) 149 | episode = { 150 | k: np.array([step[k] for step in self._episode]) 151 | for k in self._episode[0]} 152 | np.savez_compressed(filename, **episode) 153 | 154 | 155 | class EpisodeName: 156 | 157 | def __init__(self, env): 158 | self._env = env 159 | self._timestamp = None 160 | self._unlocked = None 161 | self._length = None 162 | 163 | def __getattr__(self, name): 164 | if name.startswith('__'): 165 | raise AttributeError(name) 166 | return getattr(self._env, name) 167 | 168 | def reset(self): 169 | obs = self._env.reset() 170 | self._timestamp = None 171 | self._unlocked = None 172 | self._length = 0 173 | return obs 174 | 175 | def step(self, action): 176 | obs, reward, done, info = self._env.step(action) 177 | self._length += 1 178 | if done: 179 | self._timestamp = datetime.datetime.now().strftime('%Y%m%dT%H%M%S') 180 | self._unlocked = sum(int(v >= 1) for v in info['achievements'].values()) 181 | return obs, reward, done, info 182 | 183 | @property 184 | def episode_name(self): 185 | return f'{self._timestamp}-ach{self._unlocked}-len{self._length}' 186 | -------------------------------------------------------------------------------- /crafter/run_gui.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import numpy as np 4 | try: 5 | import pygame 6 | except ImportError: 7 | print('Please install the pygame package to use the GUI.') 8 | raise 9 | from PIL import Image 10 | 11 | import crafter 12 | 13 | 14 | def main(): 15 | boolean = lambda x: bool(['False', 'True'].index(x)) 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument('--seed', type=int, default=None) 18 | parser.add_argument('--area', nargs=2, type=int, default=(64, 64)) 19 | parser.add_argument('--view', type=int, nargs=2, default=(9, 9)) 20 | parser.add_argument('--length', type=int, default=None) 21 | parser.add_argument('--health', type=int, default=9) 22 | parser.add_argument('--window', type=int, nargs=2, default=(600, 600)) 23 | parser.add_argument('--size', type=int, nargs=2, default=(0, 0)) 24 | parser.add_argument('--record', type=str, default=None) 25 | parser.add_argument('--fps', type=int, default=5) 26 | parser.add_argument('--wait', type=boolean, default=False) 27 | parser.add_argument('--death', type=str, default='reset', choices=[ 28 | 'continue', 'reset', 'quit']) 29 | args = parser.parse_args() 30 | 31 | keymap = { 32 | pygame.K_a: 'move_left', 33 | pygame.K_d: 'move_right', 34 | pygame.K_w: 'move_up', 35 | pygame.K_s: 'move_down', 36 | pygame.K_SPACE: 'do', 37 | pygame.K_TAB: 'sleep', 38 | 39 | pygame.K_r: 'place_stone', 40 | pygame.K_t: 'place_table', 41 | pygame.K_f: 'place_furnace', 42 | pygame.K_p: 'place_plant', 43 | 44 | pygame.K_1: 'make_wood_pickaxe', 45 | pygame.K_2: 'make_stone_pickaxe', 46 | pygame.K_3: 'make_iron_pickaxe', 47 | pygame.K_4: 'make_wood_sword', 48 | pygame.K_5: 'make_stone_sword', 49 | pygame.K_6: 'make_iron_sword', 50 | } 51 | print('Actions:') 52 | for key, action in keymap.items(): 53 | print(f' {pygame.key.name(key)}: {action}') 54 | 55 | crafter.constants.items['health']['max'] = args.health 56 | crafter.constants.items['health']['initial'] = args.health 57 | 58 | size = list(args.size) 59 | size[0] = size[0] or args.window[0] 60 | size[1] = size[1] or args.window[1] 61 | 62 | env = crafter.Env( 63 | area=args.area, view=args.view, length=args.length, seed=args.seed) 64 | env = crafter.Recorder(env, args.record) 65 | env.reset() 66 | achievements = set() 67 | duration = 0 68 | return_ = 0 69 | was_done = False 70 | print('Diamonds exist:', env._world.count('diamond')) 71 | 72 | pygame.init() 73 | screen = pygame.display.set_mode(args.window) 74 | clock = pygame.time.Clock() 75 | running = True 76 | while running: 77 | 78 | # Rendering. 79 | image = env.render(size) 80 | if size != args.window: 81 | image = Image.fromarray(image) 82 | image = image.resize(args.window, resample=Image.NEAREST) 83 | image = np.array(image) 84 | surface = pygame.surfarray.make_surface(image.transpose((1, 0, 2))) 85 | screen.blit(surface, (0, 0)) 86 | pygame.display.flip() 87 | clock.tick(args.fps) 88 | 89 | # Keyboard input. 90 | action = None 91 | pygame.event.pump() 92 | for event in pygame.event.get(): 93 | if event.type == pygame.QUIT: 94 | running = False 95 | elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: 96 | running = False 97 | elif event.type == pygame.KEYDOWN and event.key in keymap.keys(): 98 | action = keymap[event.key] 99 | if action is None: 100 | pressed = pygame.key.get_pressed() 101 | for key, action in keymap.items(): 102 | if pressed[key]: 103 | break 104 | else: 105 | if args.wait and not env._player.sleeping: 106 | continue 107 | else: 108 | action = 'noop' 109 | 110 | # Environment step. 111 | _, reward, done, _ = env.step(env.action_names.index(action)) 112 | duration += 1 113 | 114 | # Achievements. 115 | unlocked = { 116 | name for name, count in env._player.achievements.items() 117 | if count > 0 and name not in achievements} 118 | for name in unlocked: 119 | achievements |= unlocked 120 | total = len(env._player.achievements.keys()) 121 | print(f'Achievement ({len(achievements)}/{total}): {name}') 122 | if env._step > 0 and env._step % 100 == 0: 123 | print(f'Time step: {env._step}') 124 | if reward: 125 | print(f'Reward: {reward}') 126 | return_ += reward 127 | 128 | # Episode end. 129 | if done and not was_done: 130 | was_done = True 131 | print('Episode done!') 132 | print('Duration:', duration) 133 | print('Return:', return_) 134 | if args.death == 'quit': 135 | running = False 136 | if args.death == 'reset': 137 | print('\nStarting a new episode.') 138 | env.reset() 139 | achievements = set() 140 | was_done = False 141 | duration = 0 142 | return_ = 0 143 | if args.death == 'continue': 144 | pass 145 | 146 | pygame.quit() 147 | 148 | 149 | if __name__ == '__main__': 150 | main() 151 | -------------------------------------------------------------------------------- /crafter/run_random.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pathlib 3 | import time 4 | 5 | import numpy as np 6 | 7 | import crafter 8 | 9 | 10 | def main(): 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument('--seed', type=int, default=None) 13 | parser.add_argument('--area', nargs=2, type=int, default=(64, 64)) 14 | parser.add_argument('--length', type=int, default=10000) 15 | parser.add_argument('--health', type=int, default=9) 16 | parser.add_argument('--record', type=pathlib.Path, default=None) 17 | parser.add_argument('--episodes', type=int, default=1) 18 | args = parser.parse_args() 19 | 20 | random = np.random.RandomState(args.seed) 21 | crafter.constants.items['health']['max'] = args.health 22 | crafter.constants.items['health']['initial'] = args.health 23 | env = crafter.Env(area=args.area, length=args.length, seed=args.seed) 24 | env = crafter.Recorder(env, args.record) 25 | 26 | for _ in range(args.episodes): 27 | 28 | start = time.time() 29 | obs = env.reset() 30 | print('') 31 | print(f'Reset time: {1000*(time.time()-start):.2f}ms') 32 | print('Coal exist: ', env._world.count('coal')) 33 | print('Iron exist: ', env._world.count('iron')) 34 | print('Diamonds exist:', env._world.count('diamond')) 35 | 36 | start = time.time() 37 | done = False 38 | while not done: 39 | action = random.randint(0, env.action_space.n) 40 | obs, reward, done, info = env.step(action) 41 | duration = time.time() - start 42 | step = env._step 43 | print(f'Step time: {1000*duration/step:.2f}ms ({int(step/duration)} FPS)') 44 | print('Episode length:', step) 45 | 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /crafter/run_terrain.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import imageio 4 | import numpy as np 5 | 6 | import crafter 7 | 8 | 9 | def main(): 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('--seed', type=int, default=None) 12 | parser.add_argument('--amount', type=int, default=4) 13 | parser.add_argument('--cols', type=int, default=4) 14 | parser.add_argument('--area', nargs=2, type=int, default=(64, 64)) 15 | parser.add_argument('--size', type=int, default=1024) 16 | parser.add_argument('--filename', type=str, default='terrain.png') 17 | args = parser.parse_args() 18 | 19 | env = crafter.Env(args.area, args.area, args.size, seed=args.seed) 20 | images = [] 21 | for index in range(args.amount): 22 | images.append(env.reset()) 23 | diamonds = env._world.count('diamond') 24 | print(f'Map: {index:>2}, diamonds: {diamonds:>2}') 25 | 26 | rows = len(images) // args.cols 27 | strips = [] 28 | for row in range(rows): 29 | strip = [] 30 | for col in range(args.cols): 31 | try: 32 | strip.append(images[row * args.cols + col]) 33 | except IndexError: 34 | strip.append(np.zeros_like(strip[-1])) 35 | strips.append(np.concatenate(strip, 1)) 36 | grid = np.concatenate(strips, 0) 37 | 38 | imageio.imsave(args.filename, grid) 39 | print('Saved', args.filename) 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /crafter/worldgen.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import numpy as np 4 | import opensimplex 5 | 6 | from . import constants 7 | from . import objects 8 | 9 | 10 | def generate_world(world, player): 11 | simplex = opensimplex.OpenSimplex(seed=world.random.randint(0, 2 ** 31 - 1)) 12 | tunnels = np.zeros(world.area, bool) 13 | for x in range(world.area[0]): 14 | for y in range(world.area[1]): 15 | _set_material(world, (x, y), player, tunnels, simplex) 16 | for x in range(world.area[0]): 17 | for y in range(world.area[1]): 18 | _set_object(world, (x, y), player, tunnels) 19 | 20 | 21 | def _set_material(world, pos, player, tunnels, simplex): 22 | x, y = pos 23 | simplex = functools.partial(_simplex, simplex) 24 | uniform = world.random.uniform 25 | start = 4 - np.sqrt((x - player.pos[0]) ** 2 + (y - player.pos[1]) ** 2) 26 | start += 2 * simplex(x, y, 8, 3) 27 | start = 1 / (1 + np.exp(-start)) 28 | water = simplex(x, y, 3, {15: 1, 5: 0.15}, False) + 0.1 29 | water -= 2 * start 30 | mountain = simplex(x, y, 0, {15: 1, 5: 0.3}) 31 | mountain -= 4 * start + 0.3 * water 32 | if start > 0.5: 33 | world[x, y] = 'grass' 34 | elif mountain > 0.15: 35 | if (simplex(x, y, 6, 7) > 0.15 and mountain > 0.3): # cave 36 | world[x, y] = 'path' 37 | elif simplex(2 * x, y / 5, 7, 3) > 0.4: # horizonal tunnle 38 | world[x, y] = 'path' 39 | tunnels[x, y] = True 40 | elif simplex(x / 5, 2 * y, 7, 3) > 0.4: # vertical tunnle 41 | world[x, y] = 'path' 42 | tunnels[x, y] = True 43 | elif simplex(x, y, 1, 8) > 0 and uniform() > 0.85: 44 | world[x, y] = 'coal' 45 | elif simplex(x, y, 2, 6) > 0.4 and uniform() > 0.75: 46 | world[x, y] = 'iron' 47 | elif mountain > 0.18 and uniform() > 0.994: 48 | world[x, y] = 'diamond' 49 | elif mountain > 0.3 and simplex(x, y, 6, 5) > 0.35: 50 | world[x, y] = 'lava' 51 | else: 52 | world[x, y] = 'stone' 53 | elif 0.25 < water <= 0.35 and simplex(x, y, 4, 9) > -0.2: 54 | world[x, y] = 'sand' 55 | elif 0.3 < water: 56 | world[x, y] = 'water' 57 | else: # grassland 58 | if simplex(x, y, 5, 7) > 0 and uniform() > 0.8: 59 | world[x, y] = 'tree' 60 | else: 61 | world[x, y] = 'grass' 62 | 63 | 64 | def _set_object(world, pos, player, tunnels): 65 | x, y = pos 66 | uniform = world.random.uniform 67 | dist = np.sqrt((x - player.pos[0]) ** 2 + (y - player.pos[1]) ** 2) 68 | material, _ = world[x, y] 69 | if material not in constants.walkable: 70 | pass 71 | elif dist > 3 and material == 'grass' and uniform() > 0.985: 72 | world.add(objects.Cow(world, (x, y))) 73 | elif dist > 10 and uniform() > 0.993: 74 | world.add(objects.Zombie(world, (x, y), player)) 75 | elif material == 'path' and tunnels[x, y] and uniform() > 0.95: 76 | world.add(objects.Skeleton(world, (x, y), player)) 77 | 78 | 79 | def _simplex(simplex, x, y, z, sizes, normalize=True): 80 | if not isinstance(sizes, dict): 81 | sizes = {sizes: 1} 82 | value = 0 83 | for size, weight in sizes.items(): 84 | if hasattr(simplex, 'noise3d'): 85 | noise = simplex.noise3d(x / size, y / size, z) 86 | else: 87 | noise = simplex.noise3(x / size, y / size, z) 88 | value += weight * noise 89 | if normalize: 90 | value /= sum(sizes.values()) 91 | return value 92 | -------------------------------------------------------------------------------- /examples/run_ppo.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import crafter 4 | import stable_baselines3 5 | 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('--outdir', default='logdir/crafter_reward-ppo/0') 8 | parser.add_argument('--steps', type=float, default=1e6) 9 | args = parser.parse_args() 10 | 11 | env = crafter.Env() 12 | env = crafter.Recorder( 13 | env, args.outdir, 14 | save_stats=True, 15 | save_episode=False, 16 | save_video=False, 17 | ) 18 | 19 | model = stable_baselines3.PPO('CnnPolicy', env, verbose=1) 20 | model.learn(total_timesteps=args.steps) 21 | -------------------------------------------------------------------------------- /examples/run_random.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import crafter 4 | import tqdm 5 | 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('--outdir', default='logdir/crafter_noreward-random/0') 8 | parser.add_argument('--steps', type=float, default=1e6) 9 | args = parser.parse_args() 10 | 11 | env = crafter.Env() 12 | env = crafter.Recorder( 13 | env, args.outdir, 14 | save_stats=True, 15 | save_episode=False, 16 | save_video=False, 17 | ) 18 | action_space = env.action_space 19 | 20 | done = True 21 | step = 0 22 | bar = tqdm.tqdm(total=args.steps, smoothing=0) 23 | while step < args.steps or not done: 24 | if done: 25 | env.reset() 26 | done = False 27 | _, _, done, _ = env.step(action_space.sample()) 28 | step += 1 29 | bar.update(1) 30 | -------------------------------------------------------------------------------- /media/scores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/media/scores.png -------------------------------------------------------------------------------- /media/terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/media/terrain.png -------------------------------------------------------------------------------- /media/video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/media/video.gif -------------------------------------------------------------------------------- /media/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/media/video.mp4 -------------------------------------------------------------------------------- /plots/counts-dreamerv2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/counts-dreamerv2.pdf -------------------------------------------------------------------------------- /plots/counts-ppo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/counts-ppo.pdf -------------------------------------------------------------------------------- /plots/counts-rainbow.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/counts-rainbow.pdf -------------------------------------------------------------------------------- /plots/counts-random.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/counts-random.pdf -------------------------------------------------------------------------------- /plots/counts-unsup_plan2explore.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/counts-unsup_plan2explore.pdf -------------------------------------------------------------------------------- /plots/counts-unsup_rnd.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/counts-unsup_rnd.pdf -------------------------------------------------------------------------------- /plots/reward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/reward.pdf -------------------------------------------------------------------------------- /plots/scores-agents.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/scores-agents.pdf -------------------------------------------------------------------------------- /plots/scores-human.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/scores-human.pdf -------------------------------------------------------------------------------- /plots/spectrum-noreward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/spectrum-noreward.pdf -------------------------------------------------------------------------------- /plots/spectrum-reward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danijar/crafter/e04542a2159f1aad3d4c5ad52e8185717380ee3a/plots/spectrum-reward.pdf -------------------------------------------------------------------------------- /scores/crafter_reward-human.json: -------------------------------------------------------------------------------- 1 | [{"task": "crafter-reward", "method": "human", "seed": "0", "xs": [353, 634, 1170, 1896, 2799, 3249, 3863, 4145, 4503, 5492, 5780, 6110, 6392, 6974, 7525, 7809, 8660, 8840, 9484, 10020], "length": [353, 281, 536, 726, 903, 450, 614, 282, 358, 989, 288, 330, 282, 582, 551, 284, 851, 180, 644, 536], "reward": [12.1, 9.0, 15.1, 14.0, 13.0, 13.1, 9.1, 13.0, 13.1, 14.0, 10.1, 9.0, 10.1, 10.0, 12.1, 11.0, 14.1, 11.1, 13.1, 13.1], "achievement_collect_coal": [2, 5, 5, 3, 0, 1, 0, 2, 2, 2, 1, 1, 1, 4, 2, 4, 1, 2, 4, 2], "achievement_collect_diamond": [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_collect_drink": [13, 0, 15, 39, 27, 17, 24, 0, 8, 39, 10, 10, 12, 24, 17, 0, 28, 8, 18, 21], "achievement_collect_iron": [0, 0, 2, 2, 3, 1, 0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 0, 2, 2], "achievement_collect_sapling": [1, 6, 1, 5, 3, 4, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 7, 3, 0, 1], "achievement_collect_stone": [15, 20, 22, 15, 9, 22, 11, 15, 17, 17, 11, 5, 6, 10, 9, 6, 16, 2, 39, 13], "achievement_collect_wood": [11, 9, 6, 7, 20, 8, 12, 9, 7, 11, 9, 9, 7, 6, 9, 6, 10, 13, 8, 15], "achievement_defeat_skeleton": [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1], "achievement_defeat_zombie": [2, 1, 0, 1, 4, 4, 4, 2, 0, 4, 2, 1, 1, 0, 5, 0, 3, 3, 0, 0], "achievement_eat_cow": [1, 0, 2, 4, 6, 1, 3, 2, 1, 5, 0, 2, 2, 2, 4, 1, 3, 1, 3, 2], "achievement_eat_plant": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_make_iron_pickaxe": [0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], "achievement_make_iron_sword": [0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_make_stone_pickaxe": [1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1], "achievement_make_stone_sword": [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1], "achievement_make_wood_pickaxe": [1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "achievement_make_wood_sword": [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0], "achievement_place_furnace": [0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], "achievement_place_plant": [0, 0, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_place_stone": [1, 1, 5, 10, 3, 3, 9, 1, 1, 6, 2, 2, 1, 3, 2, 1, 4, 0, 8, 4], "achievement_place_table": [2, 1, 1, 1, 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2], "achievement_wake_up": [1, 0, 1, 1, 3, 0, 1, 0, 0, 3, 1, 0, 1, 3, 1, 1, 2, 0, 1, 1]}, {"task": "crafter-reward", "method": "human", "seed": "1", "xs": [664, 826, 3603, 3797, 4597, 5091, 5385, 5679, 6005, 6222, 6418, 6665, 7752, 8242, 8518, 8777, 9123, 9730, 10072, 10340], "length": [664, 162, 2777, 194, 800, 494, 294, 294, 326, 217, 196, 247, 1087, 490, 276, 259, 346, 607, 342, 268], "reward": [13.1, 9.1, 21.1, 11.1, 14.1, 13.1, 8.8, 13.1, 11.0, 10.8, 9.1, 15.1, 15.1, 16.0, 11.1, 10.0, 13.1, 16.1, 10.1, 9.1], "achievement_collect_coal": [3, 1, 4, 4, 3, 3, 1, 3, 1, 0, 4, 2, 4, 4, 0, 0, 1, 2, 4, 1], "achievement_collect_diamond": [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_collect_drink": [24, 5, 141, 7, 25, 21, 3, 5, 9, 8, 0, 7, 41, 14, 7, 4, 17, 16, 10, 8], "achievement_collect_iron": [2, 0, 5, 2, 0, 0, 0, 0, 0, 0, 6, 2, 3, 5, 0, 0, 2, 4, 0, 0], "achievement_collect_sapling": [6, 1, 10, 0, 5, 1, 2, 3, 1, 1, 0, 2, 4, 0, 4, 4, 1, 1, 2, 3], "achievement_collect_stone": [25, 7, 69, 2, 20, 8, 3, 7, 4, 4, 6, 11, 27, 18, 9, 1, 12, 11, 18, 1], "achievement_collect_wood": [9, 6, 17, 8, 12, 8, 4, 6, 7, 10, 10, 9, 8, 10, 8, 10, 7, 17, 7, 8], "achievement_defeat_skeleton": [0, 0, 4, 2, 2, 1, 0, 0, 0, 1, 0, 1, 6, 0, 0, 0, 0, 0, 0, 0], "achievement_defeat_zombie": [3, 0, 7, 1, 7, 1, 0, 2, 1, 1, 3, 2, 4, 2, 1, 3, 2, 3, 0, 2], "achievement_eat_cow": [3, 1, 11, 1, 6, 2, 1, 1, 2, 1, 1, 0, 5, 2, 1, 2, 3, 5, 1, 1], "achievement_eat_plant": [0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_make_iron_pickaxe": [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0], "achievement_make_iron_sword": [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0], "achievement_make_stone_pickaxe": [1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0], "achievement_make_stone_sword": [1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0], "achievement_make_wood_pickaxe": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "achievement_make_wood_sword": [0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1], "achievement_place_furnace": [0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0], "achievement_place_plant": [0, 0, 10, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0], "achievement_place_stone": [2, 0, 31, 0, 18, 1, 1, 2, 1, 2, 0, 1, 6, 1, 4, 1, 3, 3, 1, 0], "achievement_place_table": [1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1], "achievement_wake_up": [2, 0, 8, 0, 2, 1, 1, 1, 1, 0, 0, 0, 3, 1, 0, 0, 0, 2, 1, 0]}, {"task": "crafter-reward", "method": "human", "seed": "2", "xs": [590, 892, 1165, 1469, 1781, 2075, 2667, 4325, 4640, 6295, 6790, 9070, 9977, 12313, 13301, 13967, 14226, 14470, 14891, 15195], "length": [590, 302, 273, 304, 312, 294, 592, 1658, 315, 1655, 495, 2280, 907, 2336, 988, 666, 259, 244, 421, 304], "reward": [12.1, 11.1, 12.1, 11.1, 11.1, 9.1, 14.1, 19.1, 11.1, 21.1, 10.9, 20.1, 15.1, 21.0, 19.1, 17.1, 9.1, 9.0, 11.1, 9.0], "achievement_collect_coal": [2, 3, 3, 1, 1, 1, 4, 10, 2, 2, 0, 9, 6, 4, 5, 2, 0, 3, 1, 2], "achievement_collect_diamond": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], "achievement_collect_drink": [13, 6, 6, 9, 17, 10, 15, 62, 3, 83, 17, 119, 36, 121, 41, 26, 0, 10, 13, 0], "achievement_collect_iron": [4, 1, 1, 4, 0, 0, 0, 6, 2, 5, 0, 4, 0, 9, 2, 2, 0, 0, 0, 0], "achievement_collect_sapling": [0, 0, 0, 0, 1, 3, 3, 3, 0, 5, 4, 7, 2, 8, 2, 2, 0, 1, 0, 0], "achievement_collect_stone": [18, 7, 12, 10, 9, 3, 8, 44, 6, 48, 4, 84, 23, 72, 41, 17, 4, 5, 7, 8], "achievement_collect_wood": [8, 8, 6, 8, 5, 8, 7, 19, 10, 29, 14, 26, 10, 18, 11, 11, 7, 10, 10, 12], "achievement_defeat_skeleton": [0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 0, 4, 0, 3, 1, 2, 1, 0, 0, 0], "achievement_defeat_zombie": [1, 2, 2, 0, 1, 0, 2, 8, 0, 11, 4, 12, 1, 14, 1, 3, 2, 0, 1, 0], "achievement_eat_cow": [2, 0, 1, 1, 1, 0, 2, 10, 1, 10, 3, 13, 4, 9, 5, 2, 2, 0, 2, 1], "achievement_eat_plant": [0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 5, 0, 12, 0, 0, 0, 0, 0, 0], "achievement_make_iron_pickaxe": [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 2, 1, 1, 0, 0, 0, 0], "achievement_make_iron_sword": [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0], "achievement_make_stone_pickaxe": [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "achievement_make_stone_sword": [1, 1, 1, 1, 0, 0, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "achievement_make_wood_pickaxe": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], "achievement_make_wood_sword": [0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0], "achievement_place_furnace": [0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 2, 1, 1, 0, 0, 0, 0], "achievement_place_plant": [0, 0, 0, 0, 1, 0, 1, 1, 0, 4, 0, 6, 2, 6, 2, 0, 0, 0, 0, 0], "achievement_place_stone": [3, 1, 1, 1, 3, 1, 6, 10, 1, 17, 2, 20, 4, 21, 9, 4, 1, 0, 2, 2], "achievement_place_table": [1, 1, 1, 2, 1, 1, 1, 1, 1, 3, 1, 1, 2, 3, 2, 1, 2, 1, 2, 1], "achievement_wake_up": [1, 1, 1, 1, 0, 1, 1, 5, 1, 5, 1, 7, 3, 10, 3, 2, 0, 0, 1, 1]}, {"task": "crafter-reward", "method": "human", "seed": "3", "xs": [678, 949, 1235, 2043, 2529, 2824, 3154, 3383, 4183, 4699, 5476, 7209, 8520, 8824, 9271, 10096, 10861, 11179, 11483, 12468], "length": [678, 271, 286, 808, 486, 295, 330, 229, 800, 516, 777, 1733, 1311, 304, 447, 825, 765, 318, 304, 985], "reward": [17.1, 10.0, 10.1, 12.0, 12.1, 10.0, 8.9, 9.0, 17.0, 12.1, 13.1, 18.1, 17.0, 12.0, 14.0, 19.0, 18.1, 13.0, 11.1, 19.1], "achievement_collect_coal": [4, 6, 1, 4, 3, 1, 0, 2, 2, 1, 0, 3, 6, 0, 4, 4, 2, 2, 0, 9], "achievement_collect_diamond": [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1], "achievement_collect_drink": [30, 6, 4, 29, 14, 5, 7, 5, 28, 18, 27, 76, 36, 11, 15, 33, 18, 0, 6, 38], "achievement_collect_iron": [2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3, 2, 1, 0, 3, 2, 1, 2, 4], "achievement_collect_sapling": [5, 3, 0, 3, 4, 0, 1, 0, 0, 4, 3, 3, 3, 3, 2, 6, 3, 0, 0, 1], "achievement_collect_stone": [19, 6, 7, 12, 6, 7, 13, 2, 25, 8, 12, 22, 24, 11, 9, 18, 29, 25, 17, 35], "achievement_collect_wood": [8, 9, 7, 10, 6, 7, 4, 6, 9, 14, 12, 19, 9, 9, 10, 17, 12, 15, 5, 12], "achievement_defeat_skeleton": [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2], "achievement_defeat_zombie": [1, 1, 2, 2, 2, 1, 2, 2, 9, 6, 2, 8, 2, 2, 2, 9, 1, 1, 1, 4], "achievement_eat_cow": [2, 0, 1, 3, 4, 1, 0, 0, 7, 3, 4, 9, 6, 1, 4, 4, 5, 1, 2, 5], "achievement_eat_plant": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1, 0, 0, 0], "achievement_make_iron_pickaxe": [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1], "achievement_make_iron_sword": [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1], "achievement_make_stone_pickaxe": [1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "achievement_make_stone_sword": [1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "achievement_make_wood_pickaxe": [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "achievement_make_wood_sword": [0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 2, 1, 0, 0, 0, 1], "achievement_place_furnace": [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1], "achievement_place_plant": [4, 0, 0, 1, 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 6, 3, 0, 0, 0], "achievement_place_stone": [12, 1, 2, 5, 1, 3, 1, 0, 4, 1, 9, 12, 4, 1, 2, 3, 9, 1, 1, 10], "achievement_place_table": [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 1, 2, 1, 1, 2], "achievement_wake_up": [1, 1, 1, 2, 1, 0, 1, 0, 2, 1, 1, 6, 4, 0, 1, 2, 2, 0, 1, 4]}, {"task": "crafter-reward", "method": "human", "seed": "4", "xs": [896, 1171, 1609, 1815, 2145, 2844, 3446, 3968, 4922, 6919, 10062, 10351, 10795, 11121, 11876, 12150, 12802, 13109, 13453, 13879], "length": [896, 275, 438, 206, 330, 699, 602, 522, 954, 1997, 3143, 289, 444, 326, 755, 274, 652, 307, 344, 426], "reward": [17.1, 11.1, 14.1, 11.0, 10.1, 10.1, 17.9, 14.0, 12.1, 21.1, 21.1, 10.0, 11.0, 10.1, 12.1, 10.1, 15.0, 11.1, 11.1, 10.1], "achievement_collect_coal": [2, 3, 3, 1, 2, 0, 7, 2, 2, 5, 5, 0, 0, 2, 3, 2, 3, 2, 2, 4], "achievement_collect_diamond": [0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_collect_drink": [34, 9, 21, 2, 9, 22, 26, 20, 31, 115, 130, 4, 13, 10, 31, 0, 17, 8, 8, 10], "achievement_collect_iron": [4, 2, 2, 0, 0, 0, 2, 1, 3, 4, 2, 0, 2, 0, 0, 0, 2, 1, 0, 0], "achievement_collect_sapling": [2, 0, 2, 1, 1, 0, 1, 3, 0, 8, 8, 2, 2, 0, 0, 1, 1, 0, 4, 0], "achievement_collect_stone": [44, 12, 17, 5, 8, 12, 11, 14, 16, 41, 72, 6, 11, 4, 9, 6, 11, 19, 7, 8], "achievement_collect_wood": [14, 5, 9, 8, 12, 8, 13, 9, 10, 11, 20, 8, 15, 4, 9, 12, 9, 7, 12, 9], "achievement_defeat_skeleton": [3, 0, 1, 0, 0, 0, 0, 1, 0, 4, 2, 0, 0, 1, 2, 0, 2, 0, 0, 1], "achievement_defeat_zombie": [4, 2, 1, 2, 1, 1, 0, 2, 2, 8, 6, 1, 2, 0, 2, 1, 3, 2, 2, 2], "achievement_eat_cow": [4, 0, 1, 1, 2, 2, 2, 3, 3, 6, 6, 2, 3, 2, 5, 1, 4, 0, 1, 1], "achievement_eat_plant": [0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_make_iron_pickaxe": [1, 0, 0, 0, 0, 0, 1, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_make_iron_sword": [1, 0, 0, 0, 0, 0, 1, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_make_stone_pickaxe": [1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0], "achievement_make_stone_sword": [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0], "achievement_make_wood_pickaxe": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1], "achievement_make_wood_sword": [0, 0, 0, 1, 1, 2, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1], "achievement_place_furnace": [1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0], "achievement_place_plant": [0, 0, 0, 0, 0, 0, 1, 0, 0, 6, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0], "achievement_place_stone": [9, 1, 1, 0, 2, 6, 1, 6, 13, 22, 22, 1, 0, 1, 4, 1, 4, 1, 2, 0], "achievement_place_table": [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 2, 1], "achievement_wake_up": [2, 0, 1, 0, 0, 1, 2, 1, 1, 6, 10, 1, 1, 1, 2, 0, 2, 1, 0, 1]}] -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import pathlib 3 | 4 | 5 | setuptools.setup( 6 | name='crafter', 7 | version='1.8.3', 8 | description='Open world survival game for reinforcement learning.', 9 | url='http://github.com/danijar/crafter', 10 | long_description=pathlib.Path('README.md').read_text(), 11 | long_description_content_type='text/markdown', 12 | packages=['crafter'], 13 | package_data={'crafter': ['data.yaml', 'assets/*']}, 14 | entry_points={'console_scripts': ['crafter=crafter.run_gui:main']}, 15 | install_requires=[ 16 | 'numpy', 'imageio', 'pillow', 'opensimplex', 'ruamel.yaml', 17 | ], 18 | extras_require={'gui': ['pygame']}, 19 | classifiers=[ 20 | 'Intended Audience :: Science/Research', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Programming Language :: Python :: 3', 23 | 'Topic :: Games/Entertainment', 24 | 'Topic :: Scientific/Engineering :: Artificial Intelligence', 25 | ], 26 | ) 27 | --------------------------------------------------------------------------------