synth-ai 0.2.4.dev3__py3-none-any.whl → 0.2.4.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- synth_ai/environments/examples/__init__.py +1 -0
- synth_ai/environments/examples/crafter_classic/__init__.py +8 -0
- synth_ai/environments/examples/crafter_classic/config_logging.py +111 -0
- synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
- synth_ai/environments/examples/crafter_classic/engine.py +575 -0
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +63 -0
- synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +5 -0
- synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +74 -0
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +266 -0
- synth_ai/environments/examples/crafter_classic/environment.py +364 -0
- synth_ai/environments/examples/crafter_classic/taskset.py +233 -0
- synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +229 -0
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +298 -0
- synth_ai/environments/examples/crafter_custom/__init__.py +4 -0
- synth_ai/environments/examples/crafter_custom/crafter/__init__.py +7 -0
- synth_ai/environments/examples/crafter_custom/crafter/config.py +182 -0
- synth_ai/environments/examples/crafter_custom/crafter/constants.py +8 -0
- synth_ai/environments/examples/crafter_custom/crafter/engine.py +269 -0
- synth_ai/environments/examples/crafter_custom/crafter/env.py +266 -0
- synth_ai/environments/examples/crafter_custom/crafter/objects.py +418 -0
- synth_ai/environments/examples/crafter_custom/crafter/recorder.py +187 -0
- synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +119 -0
- synth_ai/environments/examples/crafter_custom/dataset_builder.py +373 -0
- synth_ai/environments/examples/crafter_custom/environment.py +312 -0
- synth_ai/environments/examples/crafter_custom/run_dataset.py +305 -0
- synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +156 -0
- synth_ai/environments/examples/enron/art_helpers/local_email_db.py +280 -0
- synth_ai/environments/examples/enron/art_helpers/types_enron.py +24 -0
- synth_ai/environments/examples/enron/engine.py +291 -0
- synth_ai/environments/examples/enron/environment.py +165 -0
- synth_ai/environments/examples/enron/taskset.py +112 -0
- synth_ai/environments/examples/minigrid/__init__.py +48 -0
- synth_ai/environments/examples/minigrid/engine.py +589 -0
- synth_ai/environments/examples/minigrid/environment.py +274 -0
- synth_ai/environments/examples/minigrid/environment_mapping.py +242 -0
- synth_ai/environments/examples/minigrid/puzzle_loader.py +416 -0
- synth_ai/environments/examples/minigrid/taskset.py +583 -0
- synth_ai/environments/examples/nethack/__init__.py +7 -0
- synth_ai/environments/examples/nethack/achievements.py +337 -0
- synth_ai/environments/examples/nethack/engine.py +738 -0
- synth_ai/environments/examples/nethack/environment.py +255 -0
- synth_ai/environments/examples/nethack/helpers/__init__.py +42 -0
- synth_ai/environments/examples/nethack/helpers/action_mapping.py +301 -0
- synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +401 -0
- synth_ai/environments/examples/nethack/helpers/observation_utils.py +433 -0
- synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +201 -0
- synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +268 -0
- synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +308 -0
- synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +430 -0
- synth_ai/environments/examples/nethack/taskset.py +323 -0
- synth_ai/environments/examples/red/__init__.py +7 -0
- synth_ai/environments/examples/red/config_logging.py +110 -0
- synth_ai/environments/examples/red/engine.py +693 -0
- synth_ai/environments/examples/red/engine_helpers/__init__.py +1 -0
- synth_ai/environments/examples/red/engine_helpers/memory_map.py +28 -0
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +275 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +142 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +56 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +283 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +149 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +137 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +56 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +330 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +120 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +558 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +312 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +147 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +246 -0
- synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +367 -0
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +139 -0
- synth_ai/environments/examples/red/environment.py +235 -0
- synth_ai/environments/examples/red/taskset.py +77 -0
- synth_ai/environments/examples/sokoban/__init__.py +1 -0
- synth_ai/environments/examples/sokoban/engine.py +675 -0
- synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +1 -0
- synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +656 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +17 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +3 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +129 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +370 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +331 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +305 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +66 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +114 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +122 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +394 -0
- synth_ai/environments/examples/sokoban/environment.py +228 -0
- synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +438 -0
- synth_ai/environments/examples/sokoban/puzzle_loader.py +311 -0
- synth_ai/environments/examples/sokoban/taskset.py +425 -0
- synth_ai/environments/examples/tictactoe/__init__.py +1 -0
- synth_ai/environments/examples/tictactoe/engine.py +368 -0
- synth_ai/environments/examples/tictactoe/environment.py +239 -0
- synth_ai/environments/examples/tictactoe/taskset.py +214 -0
- synth_ai/environments/examples/verilog/__init__.py +10 -0
- synth_ai/environments/examples/verilog/engine.py +328 -0
- synth_ai/environments/examples/verilog/environment.py +349 -0
- synth_ai/environments/examples/verilog/taskset.py +418 -0
- synth_ai/tracing_v3/examples/basic_usage.py +188 -0
- {synth_ai-0.2.4.dev3.dist-info → synth_ai-0.2.4.dev5.dist-info}/METADATA +1 -1
- {synth_ai-0.2.4.dev3.dist-info → synth_ai-0.2.4.dev5.dist-info}/RECORD +105 -6
- {synth_ai-0.2.4.dev3.dist-info → synth_ai-0.2.4.dev5.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.4.dev3.dist-info → synth_ai-0.2.4.dev5.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.4.dev3.dist-info → synth_ai-0.2.4.dev5.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.4.dev3.dist-info → synth_ai-0.2.4.dev5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,266 @@
|
|
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
|
+
from .config import WorldGenConfig
|
10
|
+
|
11
|
+
|
12
|
+
# Gym is an optional dependency.
|
13
|
+
try:
|
14
|
+
import gym
|
15
|
+
|
16
|
+
DiscreteSpace = gym.spaces.Discrete
|
17
|
+
BoxSpace = gym.spaces.Box
|
18
|
+
DictSpace = gym.spaces.Dict
|
19
|
+
BaseClass = gym.Env
|
20
|
+
except ImportError:
|
21
|
+
DiscreteSpace = collections.namedtuple("DiscreteSpace", "n")
|
22
|
+
BoxSpace = collections.namedtuple("BoxSpace", "low, high, shape, dtype")
|
23
|
+
DictSpace = collections.namedtuple("DictSpace", "spaces")
|
24
|
+
BaseClass = object
|
25
|
+
|
26
|
+
|
27
|
+
class Env(BaseClass):
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
area=(64, 64),
|
31
|
+
view=(9, 9),
|
32
|
+
size=(64, 64),
|
33
|
+
reward=True,
|
34
|
+
length=10000,
|
35
|
+
seed=None,
|
36
|
+
world_config=None,
|
37
|
+
):
|
38
|
+
view = np.array(view if hasattr(view, "__len__") else (view, view))
|
39
|
+
size = np.array(size if hasattr(size, "__len__") else (size, size))
|
40
|
+
seed = np.random.randint(0, 2**31 - 1) if seed is None else seed
|
41
|
+
self._area = area
|
42
|
+
self._view = view
|
43
|
+
self._size = size
|
44
|
+
self._reward = reward
|
45
|
+
self._length = length
|
46
|
+
self._seed = seed
|
47
|
+
self._episode = 0
|
48
|
+
|
49
|
+
# Handle world configuration
|
50
|
+
if isinstance(world_config, str):
|
51
|
+
# Load from preset or file path
|
52
|
+
if world_config.endswith(".json"):
|
53
|
+
self._world_config = WorldGenConfig.from_json(world_config)
|
54
|
+
else:
|
55
|
+
self._world_config = WorldGenConfig.from_preset(world_config)
|
56
|
+
elif isinstance(world_config, dict):
|
57
|
+
self._world_config = WorldGenConfig(**world_config)
|
58
|
+
elif isinstance(world_config, WorldGenConfig):
|
59
|
+
self._world_config = world_config
|
60
|
+
else:
|
61
|
+
self._world_config = WorldGenConfig() # Default config
|
62
|
+
|
63
|
+
self._world = engine.World(area, constants.materials, (12, 12))
|
64
|
+
self._world._config = self._world_config # Pass config to world
|
65
|
+
self._textures = engine.Textures(constants.root / "assets")
|
66
|
+
item_rows = int(np.ceil(len(constants.items) / view[0]))
|
67
|
+
self._local_view = engine.LocalView(
|
68
|
+
self._world, self._textures, [view[0], view[1] - item_rows]
|
69
|
+
)
|
70
|
+
self._item_view = engine.ItemView(self._textures, [view[0], item_rows])
|
71
|
+
self._sem_view = engine.SemanticView(
|
72
|
+
self._world,
|
73
|
+
[
|
74
|
+
objects.Player,
|
75
|
+
objects.Cow,
|
76
|
+
objects.Zombie,
|
77
|
+
objects.Skeleton,
|
78
|
+
objects.Arrow,
|
79
|
+
objects.Plant,
|
80
|
+
],
|
81
|
+
)
|
82
|
+
self._step = None
|
83
|
+
self._player = None
|
84
|
+
self._last_health = None
|
85
|
+
self._unlocked = None
|
86
|
+
# Some libraries expect these attributes to be set.
|
87
|
+
self.reward_range = None
|
88
|
+
self.metadata = None
|
89
|
+
|
90
|
+
@property
|
91
|
+
def observation_space(self):
|
92
|
+
return BoxSpace(0, 255, tuple(self._size) + (3,), np.uint8)
|
93
|
+
|
94
|
+
@property
|
95
|
+
def action_space(self):
|
96
|
+
return DiscreteSpace(len(constants.actions))
|
97
|
+
|
98
|
+
@property
|
99
|
+
def action_names(self):
|
100
|
+
return constants.actions
|
101
|
+
|
102
|
+
def reset(self):
|
103
|
+
center = (self._world.area[0] // 2, self._world.area[1] // 2)
|
104
|
+
self._episode += 1
|
105
|
+
self._step = 0
|
106
|
+
self._world.reset(seed=hash((self._seed, self._episode)) % (2**31 - 1))
|
107
|
+
self._update_time()
|
108
|
+
self._player = objects.Player(self._world, center)
|
109
|
+
self._last_health = self._player.health
|
110
|
+
self._world.add(self._player)
|
111
|
+
self._unlocked = set()
|
112
|
+
worldgen.generate_world(self._world, self._player)
|
113
|
+
return self._obs()
|
114
|
+
|
115
|
+
def step(self, action):
|
116
|
+
self._step += 1
|
117
|
+
self._update_time()
|
118
|
+
self._player.action = constants.actions[action]
|
119
|
+
for obj in self._world.objects:
|
120
|
+
if self._player.distance(obj) < 2 * max(self._view):
|
121
|
+
obj.update()
|
122
|
+
if self._step % 10 == 0:
|
123
|
+
for chunk, objs in self._world.chunks.items():
|
124
|
+
# xmin, xmax, ymin, ymax = chunk
|
125
|
+
# center = (xmax - xmin) // 2, (ymax - ymin) // 2
|
126
|
+
# if self._player.distance(center) < 4 * max(self._view):
|
127
|
+
self._balance_chunk(chunk, objs)
|
128
|
+
obs = self._obs()
|
129
|
+
reward = (self._player.health - self._last_health) / 10
|
130
|
+
self._last_health = self._player.health
|
131
|
+
unlocked = {
|
132
|
+
name
|
133
|
+
for name, count in self._player.achievements.items()
|
134
|
+
if count > 0 and name not in self._unlocked
|
135
|
+
}
|
136
|
+
if unlocked:
|
137
|
+
self._unlocked |= unlocked
|
138
|
+
reward += 1.0
|
139
|
+
dead = self._player.health <= 0
|
140
|
+
over = self._length and self._step >= self._length
|
141
|
+
done = dead or over
|
142
|
+
info = {
|
143
|
+
"inventory": self._player.inventory.copy(),
|
144
|
+
"achievements": self._player.achievements.copy(),
|
145
|
+
"discount": 1 - float(dead),
|
146
|
+
"semantic": self._sem_view(),
|
147
|
+
"player_pos": self._player.pos,
|
148
|
+
"reward": reward,
|
149
|
+
}
|
150
|
+
if not self._reward:
|
151
|
+
reward = 0.0
|
152
|
+
return obs, reward, done, info
|
153
|
+
|
154
|
+
def render(self, size=None):
|
155
|
+
size = size or self._size
|
156
|
+
unit = size // self._view
|
157
|
+
canvas = np.zeros(tuple(size) + (3,), np.uint8)
|
158
|
+
local_view = self._local_view(self._player, unit)
|
159
|
+
item_view = self._item_view(self._player.inventory, unit)
|
160
|
+
view = np.concatenate([local_view, item_view], 1)
|
161
|
+
border = (size - (size // self._view) * self._view) // 2
|
162
|
+
(x, y), (w, h) = border, view.shape[:2]
|
163
|
+
canvas[x : x + w, y : y + h] = view
|
164
|
+
return canvas.transpose((1, 0, 2))
|
165
|
+
|
166
|
+
def _obs(self):
|
167
|
+
return self.render()
|
168
|
+
|
169
|
+
def _update_time(self):
|
170
|
+
# https://www.desmos.com/calculator/grfbc6rs3h
|
171
|
+
progress = (self._step / 300) % 1 + 0.3
|
172
|
+
daylight = 1 - np.abs(np.cos(np.pi * progress)) ** 3
|
173
|
+
self._world.daylight = daylight
|
174
|
+
|
175
|
+
def _balance_chunk(self, chunk, objs):
|
176
|
+
light = self._world.daylight
|
177
|
+
config = self._world_config
|
178
|
+
|
179
|
+
# Zombies - daylight affects count
|
180
|
+
self._balance_object(
|
181
|
+
chunk,
|
182
|
+
objs,
|
183
|
+
objects.Zombie,
|
184
|
+
"grass",
|
185
|
+
config.zombie_min_spawn_distance,
|
186
|
+
0,
|
187
|
+
config.zombie_spawn_rate,
|
188
|
+
config.zombie_despawn_rate,
|
189
|
+
lambda pos: objects.Zombie(self._world, pos, self._player),
|
190
|
+
lambda num, space: (
|
191
|
+
0
|
192
|
+
if space < 50
|
193
|
+
else config.zombie_max_count
|
194
|
+
- (config.zombie_max_count - config.zombie_min_count) * light,
|
195
|
+
config.zombie_max_count
|
196
|
+
- (config.zombie_max_count - config.zombie_min_count) * light,
|
197
|
+
),
|
198
|
+
)
|
199
|
+
|
200
|
+
# Skeletons - static count
|
201
|
+
self._balance_object(
|
202
|
+
chunk,
|
203
|
+
objs,
|
204
|
+
objects.Skeleton,
|
205
|
+
"path",
|
206
|
+
config.skeleton_min_spawn_distance,
|
207
|
+
config.skeleton_min_spawn_distance,
|
208
|
+
config.skeleton_spawn_rate,
|
209
|
+
config.skeleton_despawn_rate,
|
210
|
+
lambda pos: objects.Skeleton(self._world, pos, self._player),
|
211
|
+
lambda num, space: (
|
212
|
+
0 if space < 6 else config.skeleton_min_count,
|
213
|
+
config.skeleton_max_count,
|
214
|
+
),
|
215
|
+
)
|
216
|
+
|
217
|
+
# Cows - daylight affects count positively
|
218
|
+
self._balance_object(
|
219
|
+
chunk,
|
220
|
+
objs,
|
221
|
+
objects.Cow,
|
222
|
+
"grass",
|
223
|
+
config.cow_min_spawn_distance,
|
224
|
+
config.cow_min_spawn_distance,
|
225
|
+
config.cow_spawn_rate,
|
226
|
+
config.cow_despawn_rate,
|
227
|
+
lambda pos: objects.Cow(self._world, pos),
|
228
|
+
lambda num, space: (
|
229
|
+
0 if space < 30 else config.cow_min_count,
|
230
|
+
config.cow_min_count + (config.cow_max_count - config.cow_min_count) * light,
|
231
|
+
),
|
232
|
+
)
|
233
|
+
|
234
|
+
def _balance_object(
|
235
|
+
self,
|
236
|
+
chunk,
|
237
|
+
objs,
|
238
|
+
cls,
|
239
|
+
material,
|
240
|
+
span_dist,
|
241
|
+
despan_dist,
|
242
|
+
spawn_prob,
|
243
|
+
despawn_prob,
|
244
|
+
ctor,
|
245
|
+
target_fn,
|
246
|
+
):
|
247
|
+
xmin, xmax, ymin, ymax = chunk
|
248
|
+
random = self._world.random
|
249
|
+
creatures = [obj for obj in objs if isinstance(obj, cls)]
|
250
|
+
mask = self._world.mask(*chunk, material)
|
251
|
+
target_min, target_max = target_fn(len(creatures), mask.sum())
|
252
|
+
if len(creatures) < int(target_min) and random.uniform() < spawn_prob:
|
253
|
+
xs = np.tile(np.arange(xmin, xmax)[:, None], [1, ymax - ymin])
|
254
|
+
ys = np.tile(np.arange(ymin, ymax)[None, :], [xmax - xmin, 1])
|
255
|
+
xs, ys = xs[mask], ys[mask]
|
256
|
+
i = random.randint(0, len(xs))
|
257
|
+
pos = np.array((xs[i], ys[i]))
|
258
|
+
empty = self._world[pos][1] is None
|
259
|
+
away = self._player.distance(pos) >= span_dist
|
260
|
+
if empty and away:
|
261
|
+
self._world.add(ctor(pos))
|
262
|
+
elif len(creatures) > int(target_max) and random.uniform() < despawn_prob:
|
263
|
+
obj = creatures[random.randint(0, len(creatures))]
|
264
|
+
away = self._player.distance(obj.pos) >= despan_dist
|
265
|
+
if away:
|
266
|
+
self._world.remove(obj)
|
@@ -0,0 +1,418 @@
|
|
1
|
+
import numpy as np
|
2
|
+
|
3
|
+
from . import constants
|
4
|
+
from . import engine
|
5
|
+
|
6
|
+
|
7
|
+
class Object:
|
8
|
+
def __init__(self, world, pos):
|
9
|
+
self.world = world
|
10
|
+
self.pos = np.array(pos)
|
11
|
+
self.random = world.random
|
12
|
+
self.inventory = {"health": 0}
|
13
|
+
self.removed = False
|
14
|
+
|
15
|
+
@property
|
16
|
+
def texture(self):
|
17
|
+
raise "unknown"
|
18
|
+
|
19
|
+
@property
|
20
|
+
def walkable(self):
|
21
|
+
return constants.walkable
|
22
|
+
|
23
|
+
@property
|
24
|
+
def health(self):
|
25
|
+
return self.inventory["health"]
|
26
|
+
|
27
|
+
@health.setter
|
28
|
+
def health(self, value):
|
29
|
+
self.inventory["health"] = max(0, value)
|
30
|
+
|
31
|
+
@property
|
32
|
+
def all_dirs(self):
|
33
|
+
return ((-1, 0), (+1, 0), (0, -1), (0, +1))
|
34
|
+
|
35
|
+
def move(self, direction):
|
36
|
+
direction = np.array(direction)
|
37
|
+
target = self.pos + direction
|
38
|
+
if self.is_free(target):
|
39
|
+
self.world.move(self, target)
|
40
|
+
return True
|
41
|
+
return False
|
42
|
+
|
43
|
+
def is_free(self, target, materials=None):
|
44
|
+
materials = self.walkable if materials is None else materials
|
45
|
+
material, obj = self.world[target]
|
46
|
+
return obj is None and material in materials
|
47
|
+
|
48
|
+
def distance(self, target):
|
49
|
+
if hasattr(target, "pos"):
|
50
|
+
target = target.pos
|
51
|
+
return np.abs(target - self.pos).sum()
|
52
|
+
|
53
|
+
def toward(self, target, long_axis=True):
|
54
|
+
if hasattr(target, "pos"):
|
55
|
+
target = target.pos
|
56
|
+
offset = target - self.pos
|
57
|
+
dists = np.abs(offset)
|
58
|
+
if dists[0] > dists[1] if long_axis else dists[0] <= dists[1]:
|
59
|
+
return np.array((np.sign(offset[0]), 0))
|
60
|
+
else:
|
61
|
+
return np.array((0, np.sign(offset[1])))
|
62
|
+
|
63
|
+
def random_dir(self):
|
64
|
+
return self.all_dirs[self.random.randint(0, 4)]
|
65
|
+
|
66
|
+
|
67
|
+
class Player(Object):
|
68
|
+
def __init__(self, world, pos):
|
69
|
+
super().__init__(world, pos)
|
70
|
+
self.facing = (0, 1)
|
71
|
+
self.inventory = {name: info["initial"] for name, info in constants.items.items()}
|
72
|
+
self.achievements = {name: 0 for name in constants.achievements}
|
73
|
+
self.action = "noop"
|
74
|
+
self.sleeping = False
|
75
|
+
self._last_health = self.health
|
76
|
+
self._hunger = 0
|
77
|
+
self._thirst = 0
|
78
|
+
self._fatigue = 0
|
79
|
+
self._recover = 0
|
80
|
+
|
81
|
+
@property
|
82
|
+
def texture(self):
|
83
|
+
if self.sleeping:
|
84
|
+
return "player-sleep"
|
85
|
+
return {
|
86
|
+
(-1, 0): "player-left",
|
87
|
+
(+1, 0): "player-right",
|
88
|
+
(0, -1): "player-up",
|
89
|
+
(0, +1): "player-down",
|
90
|
+
}[tuple(self.facing)]
|
91
|
+
|
92
|
+
@property
|
93
|
+
def walkable(self):
|
94
|
+
return constants.walkable + ["lava"]
|
95
|
+
|
96
|
+
def update(self):
|
97
|
+
target = (self.pos[0] + self.facing[0], self.pos[1] + self.facing[1])
|
98
|
+
material, obj = self.world[target]
|
99
|
+
action = self.action
|
100
|
+
if self.sleeping:
|
101
|
+
if self.inventory["energy"] < constants.items["energy"]["max"]:
|
102
|
+
action = "sleep"
|
103
|
+
else:
|
104
|
+
self.sleeping = False
|
105
|
+
self.achievements["wake_up"] += 1
|
106
|
+
if action == "noop":
|
107
|
+
pass
|
108
|
+
elif action.startswith("move_"):
|
109
|
+
self._move(action[len("move_") :])
|
110
|
+
elif action == "do" and obj:
|
111
|
+
self._do_object(obj)
|
112
|
+
elif action == "do":
|
113
|
+
self._do_material(target, material)
|
114
|
+
elif action == "sleep":
|
115
|
+
if self.inventory["energy"] < constants.items["energy"]["max"]:
|
116
|
+
self.sleeping = True
|
117
|
+
elif action.startswith("place_"):
|
118
|
+
self._place(action[len("place_") :], target, material)
|
119
|
+
elif action.startswith("make_"):
|
120
|
+
self._make(action[len("make_") :])
|
121
|
+
self._update_life_stats()
|
122
|
+
self._degen_or_regen_health()
|
123
|
+
for name, amount in self.inventory.items():
|
124
|
+
maxmium = constants.items[name]["max"]
|
125
|
+
self.inventory[name] = max(0, min(amount, maxmium))
|
126
|
+
# This needs to happen after the inventory states are clamped
|
127
|
+
# because it involves the health water inventory count.
|
128
|
+
self._wake_up_when_hurt()
|
129
|
+
|
130
|
+
def _update_life_stats(self):
|
131
|
+
self._hunger += 0.5 if self.sleeping else 1
|
132
|
+
if self._hunger > 25:
|
133
|
+
self._hunger = 0
|
134
|
+
self.inventory["food"] -= 1
|
135
|
+
self._thirst += 0.5 if self.sleeping else 1
|
136
|
+
if self._thirst > 20:
|
137
|
+
self._thirst = 0
|
138
|
+
self.inventory["drink"] -= 1
|
139
|
+
if self.sleeping:
|
140
|
+
self._fatigue = min(self._fatigue - 1, 0)
|
141
|
+
else:
|
142
|
+
self._fatigue += 1
|
143
|
+
if self._fatigue < -10:
|
144
|
+
self._fatigue = 0
|
145
|
+
self.inventory["energy"] += 1
|
146
|
+
if self._fatigue > 30:
|
147
|
+
self._fatigue = 0
|
148
|
+
self.inventory["energy"] -= 1
|
149
|
+
|
150
|
+
def _degen_or_regen_health(self):
|
151
|
+
necessities = (
|
152
|
+
self.inventory["food"] > 0,
|
153
|
+
self.inventory["drink"] > 0,
|
154
|
+
self.inventory["energy"] > 0 or self.sleeping,
|
155
|
+
)
|
156
|
+
if all(necessities):
|
157
|
+
self._recover += 2 if self.sleeping else 1
|
158
|
+
else:
|
159
|
+
self._recover -= 0.5 if self.sleeping else 1
|
160
|
+
if self._recover > 25:
|
161
|
+
self._recover = 0
|
162
|
+
self.health += 1
|
163
|
+
if self._recover < -15:
|
164
|
+
self._recover = 0
|
165
|
+
self.health -= 1
|
166
|
+
|
167
|
+
def _wake_up_when_hurt(self):
|
168
|
+
if self.health < self._last_health:
|
169
|
+
self.sleeping = False
|
170
|
+
self._last_health = self.health
|
171
|
+
|
172
|
+
def _move(self, direction):
|
173
|
+
directions = dict(left=(-1, 0), right=(+1, 0), up=(0, -1), down=(0, +1))
|
174
|
+
self.facing = directions[direction]
|
175
|
+
self.move(self.facing)
|
176
|
+
if self.world[self.pos][0] == "lava":
|
177
|
+
self.health = 0
|
178
|
+
|
179
|
+
def _do_object(self, obj):
|
180
|
+
damage = max(
|
181
|
+
[
|
182
|
+
1,
|
183
|
+
self.inventory["wood_sword"] and 2,
|
184
|
+
self.inventory["stone_sword"] and 3,
|
185
|
+
self.inventory["iron_sword"] and 5,
|
186
|
+
]
|
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
|
+
def __init__(self, world, pos):
|
266
|
+
super().__init__(world, pos)
|
267
|
+
self.health = 3
|
268
|
+
|
269
|
+
@property
|
270
|
+
def texture(self):
|
271
|
+
return "cow"
|
272
|
+
|
273
|
+
def update(self):
|
274
|
+
if self.health <= 0:
|
275
|
+
self.world.remove(self)
|
276
|
+
if self.random.uniform() < 0.5:
|
277
|
+
direction = self.random_dir()
|
278
|
+
self.move(direction)
|
279
|
+
|
280
|
+
|
281
|
+
class Zombie(Object):
|
282
|
+
def __init__(self, world, pos, player):
|
283
|
+
super().__init__(world, pos)
|
284
|
+
self.player = player
|
285
|
+
self.health = 5
|
286
|
+
self.cooldown = 0
|
287
|
+
|
288
|
+
@property
|
289
|
+
def texture(self):
|
290
|
+
return "zombie"
|
291
|
+
|
292
|
+
def update(self):
|
293
|
+
if self.health <= 0:
|
294
|
+
self.world.remove(self)
|
295
|
+
dist = self.distance(self.player)
|
296
|
+
if dist <= 8 and self.random.uniform() < 0.9:
|
297
|
+
self.move(self.toward(self.player, self.random.uniform() < 0.8))
|
298
|
+
else:
|
299
|
+
self.move(self.random_dir())
|
300
|
+
dist = self.distance(self.player)
|
301
|
+
if dist <= 1:
|
302
|
+
if self.cooldown:
|
303
|
+
self.cooldown -= 1
|
304
|
+
else:
|
305
|
+
if self.player.sleeping:
|
306
|
+
damage = 7
|
307
|
+
else:
|
308
|
+
damage = 2
|
309
|
+
self.player.health -= damage
|
310
|
+
self.cooldown = 5
|
311
|
+
|
312
|
+
|
313
|
+
class Skeleton(Object):
|
314
|
+
def __init__(self, world, pos, player):
|
315
|
+
super().__init__(world, pos)
|
316
|
+
self.player = player
|
317
|
+
self.health = 3
|
318
|
+
self.reload = 0
|
319
|
+
|
320
|
+
@property
|
321
|
+
def texture(self):
|
322
|
+
return "skeleton"
|
323
|
+
|
324
|
+
def update(self):
|
325
|
+
if self.health <= 0:
|
326
|
+
self.world.remove(self)
|
327
|
+
self.reload = max(0, self.reload - 1)
|
328
|
+
dist = self.distance(self.player.pos)
|
329
|
+
if dist <= 3:
|
330
|
+
moved = self.move(-self.toward(self.player, self.random.uniform() < 0.6))
|
331
|
+
if moved:
|
332
|
+
return
|
333
|
+
if dist <= 5 and self.random.uniform() < 0.5:
|
334
|
+
self._shoot(self.toward(self.player))
|
335
|
+
elif dist <= 8 and self.random.uniform() < 0.3:
|
336
|
+
self.move(self.toward(self.player, self.random.uniform() < 0.6))
|
337
|
+
elif self.random.uniform() < 0.2:
|
338
|
+
self.move(self.random_dir())
|
339
|
+
|
340
|
+
def _shoot(self, direction):
|
341
|
+
if self.reload > 0:
|
342
|
+
return
|
343
|
+
if direction[0] == 0 and direction[1] == 0:
|
344
|
+
return
|
345
|
+
pos = self.pos + direction
|
346
|
+
if self.is_free(pos, Arrow.walkable):
|
347
|
+
self.world.add(Arrow(self.world, pos, direction))
|
348
|
+
self.reload = 4
|
349
|
+
|
350
|
+
|
351
|
+
class Arrow(Object):
|
352
|
+
def __init__(self, world, pos, facing):
|
353
|
+
super().__init__(world, pos)
|
354
|
+
self.facing = facing
|
355
|
+
|
356
|
+
@property
|
357
|
+
def texture(self):
|
358
|
+
return {
|
359
|
+
(-1, 0): "arrow-left",
|
360
|
+
(+1, 0): "arrow-right",
|
361
|
+
(0, -1): "arrow-up",
|
362
|
+
(0, +1): "arrow-down",
|
363
|
+
}[tuple(self.facing)]
|
364
|
+
|
365
|
+
@engine.staticproperty
|
366
|
+
def walkable():
|
367
|
+
return constants.walkable + ["water", "lava"]
|
368
|
+
|
369
|
+
def update(self):
|
370
|
+
target = self.pos + self.facing
|
371
|
+
material, obj = self.world[target]
|
372
|
+
if obj:
|
373
|
+
obj.health -= 2
|
374
|
+
self.world.remove(self)
|
375
|
+
elif material not in self.walkable:
|
376
|
+
self.world.remove(self)
|
377
|
+
if material in ["table", "furnace"]:
|
378
|
+
self.world[target] = "path"
|
379
|
+
else:
|
380
|
+
self.move(self.facing)
|
381
|
+
|
382
|
+
|
383
|
+
class Plant(Object):
|
384
|
+
def __init__(self, world, pos):
|
385
|
+
super().__init__(world, pos)
|
386
|
+
self.health = 1
|
387
|
+
self.grown = 0
|
388
|
+
|
389
|
+
@property
|
390
|
+
def texture(self):
|
391
|
+
if self.ripe:
|
392
|
+
return "plant-ripe"
|
393
|
+
else:
|
394
|
+
return "plant"
|
395
|
+
|
396
|
+
@property
|
397
|
+
def ripe(self):
|
398
|
+
return self.grown > 300
|
399
|
+
|
400
|
+
def update(self):
|
401
|
+
self.grown += 1
|
402
|
+
objs = [self.world[self.pos + dir_][1] for dir_ in self.all_dirs]
|
403
|
+
if any(isinstance(obj, (Zombie, Skeleton, Cow)) for obj in objs):
|
404
|
+
self.health -= 1
|
405
|
+
if self.health <= 0:
|
406
|
+
self.world.remove(self)
|
407
|
+
|
408
|
+
|
409
|
+
class Fence(Object):
|
410
|
+
def __init__(self, world, pos):
|
411
|
+
super().__init__(world, pos)
|
412
|
+
|
413
|
+
@property
|
414
|
+
def texture(self):
|
415
|
+
return "fence"
|
416
|
+
|
417
|
+
def update(self):
|
418
|
+
pass
|