synth-ai 0.2.4.dev4__py3-none-any.whl → 0.2.4.dev6__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.
Files changed (123) hide show
  1. synth_ai/environments/examples/__init__.py +1 -0
  2. synth_ai/environments/examples/crafter_classic/__init__.py +8 -0
  3. synth_ai/environments/examples/crafter_classic/config_logging.py +111 -0
  4. synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
  5. synth_ai/environments/examples/crafter_classic/engine.py +579 -0
  6. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +63 -0
  7. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +5 -0
  8. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +74 -0
  9. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +266 -0
  10. synth_ai/environments/examples/crafter_classic/environment.py +364 -0
  11. synth_ai/environments/examples/crafter_classic/taskset.py +233 -0
  12. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +229 -0
  13. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +298 -0
  14. synth_ai/environments/examples/crafter_custom/__init__.py +4 -0
  15. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +7 -0
  16. synth_ai/environments/examples/crafter_custom/crafter/config.py +182 -0
  17. synth_ai/environments/examples/crafter_custom/crafter/constants.py +8 -0
  18. synth_ai/environments/examples/crafter_custom/crafter/engine.py +269 -0
  19. synth_ai/environments/examples/crafter_custom/crafter/env.py +266 -0
  20. synth_ai/environments/examples/crafter_custom/crafter/objects.py +418 -0
  21. synth_ai/environments/examples/crafter_custom/crafter/recorder.py +187 -0
  22. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +119 -0
  23. synth_ai/environments/examples/crafter_custom/dataset_builder.py +373 -0
  24. synth_ai/environments/examples/crafter_custom/environment.py +312 -0
  25. synth_ai/environments/examples/crafter_custom/run_dataset.py +305 -0
  26. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +156 -0
  27. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +280 -0
  28. synth_ai/environments/examples/enron/art_helpers/types_enron.py +24 -0
  29. synth_ai/environments/examples/enron/engine.py +291 -0
  30. synth_ai/environments/examples/enron/environment.py +165 -0
  31. synth_ai/environments/examples/enron/taskset.py +112 -0
  32. synth_ai/environments/examples/minigrid/__init__.py +48 -0
  33. synth_ai/environments/examples/minigrid/engine.py +589 -0
  34. synth_ai/environments/examples/minigrid/environment.py +274 -0
  35. synth_ai/environments/examples/minigrid/environment_mapping.py +242 -0
  36. synth_ai/environments/examples/minigrid/puzzle_loader.py +416 -0
  37. synth_ai/environments/examples/minigrid/taskset.py +583 -0
  38. synth_ai/environments/examples/nethack/__init__.py +7 -0
  39. synth_ai/environments/examples/nethack/achievements.py +337 -0
  40. synth_ai/environments/examples/nethack/engine.py +738 -0
  41. synth_ai/environments/examples/nethack/environment.py +255 -0
  42. synth_ai/environments/examples/nethack/helpers/__init__.py +42 -0
  43. synth_ai/environments/examples/nethack/helpers/action_mapping.py +301 -0
  44. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +401 -0
  45. synth_ai/environments/examples/nethack/helpers/observation_utils.py +433 -0
  46. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +201 -0
  47. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +268 -0
  48. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +308 -0
  49. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +430 -0
  50. synth_ai/environments/examples/nethack/taskset.py +323 -0
  51. synth_ai/environments/examples/red/__init__.py +7 -0
  52. synth_ai/environments/examples/red/config_logging.py +110 -0
  53. synth_ai/environments/examples/red/engine.py +693 -0
  54. synth_ai/environments/examples/red/engine_helpers/__init__.py +1 -0
  55. synth_ai/environments/examples/red/engine_helpers/memory_map.py +28 -0
  56. synth_ai/environments/examples/red/engine_helpers/reward_components.py +275 -0
  57. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +142 -0
  58. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +56 -0
  59. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +283 -0
  60. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +149 -0
  61. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +137 -0
  62. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +56 -0
  63. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +330 -0
  64. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +120 -0
  65. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +558 -0
  66. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +312 -0
  67. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +147 -0
  68. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +246 -0
  69. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +367 -0
  70. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +139 -0
  71. synth_ai/environments/examples/red/environment.py +235 -0
  72. synth_ai/environments/examples/red/taskset.py +77 -0
  73. synth_ai/environments/examples/sokoban/__init__.py +1 -0
  74. synth_ai/environments/examples/sokoban/engine.py +675 -0
  75. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +1 -0
  76. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +656 -0
  77. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +17 -0
  78. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +3 -0
  79. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +129 -0
  80. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +370 -0
  81. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +331 -0
  82. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +305 -0
  83. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +66 -0
  84. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +114 -0
  85. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +122 -0
  86. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +394 -0
  87. synth_ai/environments/examples/sokoban/environment.py +228 -0
  88. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +438 -0
  89. synth_ai/environments/examples/sokoban/puzzle_loader.py +311 -0
  90. synth_ai/environments/examples/sokoban/taskset.py +425 -0
  91. synth_ai/environments/examples/tictactoe/__init__.py +1 -0
  92. synth_ai/environments/examples/tictactoe/engine.py +368 -0
  93. synth_ai/environments/examples/tictactoe/environment.py +239 -0
  94. synth_ai/environments/examples/tictactoe/taskset.py +214 -0
  95. synth_ai/environments/examples/verilog/__init__.py +10 -0
  96. synth_ai/environments/examples/verilog/engine.py +328 -0
  97. synth_ai/environments/examples/verilog/environment.py +349 -0
  98. synth_ai/environments/examples/verilog/taskset.py +418 -0
  99. synth_ai/environments/examples/wordle/__init__.py +29 -0
  100. synth_ai/environments/examples/wordle/engine.py +391 -0
  101. synth_ai/environments/examples/wordle/environment.py +154 -0
  102. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +75 -0
  103. synth_ai/environments/examples/wordle/taskset.py +222 -0
  104. synth_ai/environments/service/app.py +8 -0
  105. synth_ai/environments/service/core_routes.py +38 -0
  106. synth_ai/learning/prompts/banking77_injection_eval.py +163 -0
  107. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +201 -0
  108. synth_ai/learning/prompts/mipro.py +273 -1
  109. synth_ai/learning/prompts/random_search.py +247 -0
  110. synth_ai/learning/prompts/run_mipro_banking77.py +160 -0
  111. synth_ai/learning/prompts/run_random_search_banking77.py +305 -0
  112. synth_ai/lm/injection.py +81 -0
  113. synth_ai/lm/overrides.py +204 -0
  114. synth_ai/lm/provider_support/anthropic.py +39 -12
  115. synth_ai/lm/provider_support/openai.py +31 -4
  116. synth_ai/lm/vendors/core/anthropic_api.py +16 -0
  117. synth_ai/lm/vendors/openai_standard.py +35 -5
  118. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/METADATA +2 -1
  119. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/RECORD +123 -13
  120. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/WHEEL +0 -0
  121. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/entry_points.txt +0 -0
  122. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/licenses/LICENSE +0 -0
  123. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.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