Compare commits

..

1 Commits

Author SHA1 Message Date
Timmy Agent
2f53409614 feat(lab-005): Deploy AI agent fleet on available laptops (#530)
Some checks failed
Self-Healing Smoke / self-healing-smoke (pull_request) Failing after 27s
Smoke Test / smoke (pull_request) Failing after 29s
Agent PR Gate / gate (pull_request) Failing after 49s
Agent PR Gate / report (pull_request) Successful in 17s
- Add configs/laptop-fleet-manifest.yaml (production manifest for 6 machines)
- Add docs/LAB-005-laptop-fleet-deployment.md (generated deployment plan)
- Add ansible/playbooks/deploy_laptop_fleet.yml (Ansible playbook for Linux laptops)
- Add ansible/inventory/laptops.ini (fleet inventory with role groups)
- Add configs/hermes-laptop-anchor.service (24/7 systemd user service)
- Add configs/hermes-laptop-daylight.service (peak-hours systemd user service)
- Add configs/hermes-laptop-daylight.timer (systemd timer for 10:00 start)
- Expand tests to verify production manifest, plan, playbook, and services
2026-04-22 01:48:33 -04:00
10 changed files with 343 additions and 203 deletions

View File

@@ -0,0 +1,27 @@
[laptop_anchor]
# 24/7 anchor agents — lowest idle wattage, reliable adapters
timmy-anchor-a ansible_host=TIMMY_ANCHOR_A_IP ansible_user=timmy
[laptop_daylight]
# Daylight compute nodes — peak solar hours only
timmy-daylight-a ansible_host=TIMMY_DAYLIGHT_A_IP ansible_user=timmy
timmy-daylight-b ansible_host=TIMMY_DAYLIGHT_B_IP ansible_user=timmy
[laptop_pending]
# Machines awaiting hardware repair before production duty
timmy-daylight-c ansible_host=TIMMY_DAYLIGHT_C_IP ansible_user=timmy
[desktop_nas]
# Heavy compute + 4TB SSD NAS — daylight only due to power draw
timmy-desktop-nas ansible_host=TIMMY_DESKTOP_NAS_IP ansible_user=timmy
[laptops:children]
laptop_anchor
laptop_daylight
laptop_pending
desktop_nas
[laptops:vars]
ansible_python_interpreter=/usr/bin/python3
timmy_home=/home/timmy/timmy
timmy_repo=https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-home.git

View File

@@ -0,0 +1,137 @@
---
- name: Deploy Hermes agent fleet on available laptops
hosts: laptops
gather_facts: true
vars:
timmy_user: "{{ ansible_user }}"
timmy_dir: "/home/{{ timmy_user }}/timmy"
hermes_repo: "https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-home.git"
hermes_agent_repo: "https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent.git"
tasks:
- name: Ensure required packages are installed
ansible.builtin.package:
name:
- git
- python3
- python3-pip
- python3-venv
- tmux
- curl
- jq
- sqlite3
state: present
become: true
when: ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
- name: Ensure timmy directory exists
ansible.builtin.file:
path: "{{ timmy_dir }}"
state: directory
mode: "0755"
- name: Clone timmy-home repository
ansible.builtin.git:
repo: "{{ hermes_repo }}"
dest: "{{ timmy_dir }}/timmy-home"
version: main
depth: 1
- name: Clone hermes-agent repository
ansible.builtin.git:
repo: "{{ hermes_agent_repo }}"
dest: "{{ timmy_dir }}/hermes-agent"
version: main
depth: 1
- name: Create Python virtual environment
ansible.builtin.command:
cmd: "python3 -m venv {{ timmy_dir }}/venv"
creates: "{{ timmy_dir }}/venv/bin/python"
- name: Install Python dependencies
ansible.builtin.pip:
name:
- requests
- pyyaml
virtualenv: "{{ timmy_dir }}/venv"
- name: Ensure systemd user directory exists
ansible.builtin.file:
path: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user"
state: directory
mode: "0755"
when: ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
- name: Deploy anchor agent systemd user service
ansible.builtin.template:
src: "../../configs/hermes-laptop-anchor.service"
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-anchor.service"
mode: "0644"
when:
- inventory_hostname in groups['laptop_anchor']
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
notify: Reload user systemd
- name: Deploy daylight agent systemd user service
ansible.builtin.template:
src: "../../configs/hermes-laptop-daylight.service"
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-daylight.service"
mode: "0644"
when:
- inventory_hostname in groups['laptop_daylight']
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
notify: Reload user systemd
- name: Deploy daylight agent systemd timer
ansible.builtin.template:
src: "../../configs/hermes-laptop-daylight.timer"
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-daylight.timer"
mode: "0644"
when:
- inventory_hostname in groups['laptop_daylight']
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
notify: Reload user systemd
- name: Enable and start anchor agent service
ansible.builtin.systemd:
name: hermes-laptop-anchor.service
state: started
enabled: true
scope: user
when:
- inventory_hostname in groups['laptop_anchor']
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
- name: Enable daylight agent timer
ansible.builtin.systemd:
name: hermes-laptop-daylight.timer
state: started
enabled: true
scope: user
when:
- inventory_hostname in groups['laptop_daylight']
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
- name: Create fleet status script
ansible.builtin.copy:
dest: "{{ timmy_dir }}/scripts/status.sh"
content: |
#!/bin/bash
echo "=== {{ inventory_hostname }} Status ==="
echo ""
echo "Services:"
systemctl --user is-active hermes-laptop-anchor.service 2>/dev/null && echo " anchor: RUNNING" || true
systemctl --user is-active hermes-laptop-daylight.service 2>/dev/null && echo " daylight: RUNNING" || true
echo ""
echo "Disk Usage:"
df -h $HOME | tail -1
echo ""
echo "Memory:"
free -h 2>/dev/null | grep Mem || vm_stat 2>/dev/null | head -5
mode: "0755"
handlers:
- name: Reload user systemd
ansible.builtin.command: systemctl --user daemon-reload
changed_when: true

View File

@@ -0,0 +1,15 @@
[Unit]
Description=Hermes Laptop Anchor Agent (24/7)
After=network.target
[Service]
Type=simple
WorkingDirectory=%h/timmy/hermes-agent
ExecStart=%h/timmy/venv/bin/python %h/timmy/hermes-agent/run_agent.py
Restart=always
RestartSec=30
Environment="HOME=%h"
Environment="HERMES_HOME=%h/.hermes"
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Hermes Laptop Daylight Agent
After=network.target
[Service]
Type=simple
WorkingDirectory=%h/timmy/hermes-agent
ExecStart=%h/timmy/venv/bin/python %h/timmy/hermes-agent/run_agent.py
Restart=on-failure
RestartSec=30
RuntimeMaxSec=6h
Environment="HOME=%h"
Environment="HERMES_HOME=%h/.hermes"
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Run Hermes daylight agent during peak solar hours
[Timer]
OnCalendar=*-*-* 10:00:00
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,67 @@
# LAB-005: Laptop Fleet Manifest
# Production manifest for the 6-machine Timmy Foundation laptop fleet.
# Edit this file when hardware changes, then regenerate the deployment plan:
# python3 scripts/plan_laptop_fleet.py configs/laptop-fleet-manifest.yaml --markdown > docs/LAB-005-laptop-fleet-deployment.md
fleet_name: timmy-laptop-fleet
machines:
- hostname: timmy-anchor-a
machine_type: laptop
ram_gb: 16
cpu_cores: 8
os: macOS
adapter_condition: good
idle_watts: 11
always_on_capable: true
notes: candidate 24/7 anchor agent
- hostname: timmy-anchor-b
machine_type: laptop
ram_gb: 8
cpu_cores: 4
os: Linux
adapter_condition: good
idle_watts: 13
always_on_capable: true
notes: candidate 24/7 anchor agent
- hostname: timmy-daylight-a
machine_type: laptop
ram_gb: 32
cpu_cores: 10
os: macOS
adapter_condition: ok
idle_watts: 22
always_on_capable: true
notes: higher-performance daylight compute
- hostname: timmy-daylight-b
machine_type: laptop
ram_gb: 16
cpu_cores: 8
os: Linux
adapter_condition: ok
idle_watts: 19
always_on_capable: true
notes: daylight compute node
- hostname: timmy-daylight-c
machine_type: laptop
ram_gb: 8
cpu_cores: 4
os: Windows
adapter_condition: needs_replacement
idle_watts: 17
always_on_capable: false
notes: repair power adapter before production duty
- hostname: timmy-desktop-nas
machine_type: desktop
ram_gb: 64
cpu_cores: 12
os: Linux
adapter_condition: good
idle_watts: 58
always_on_capable: false
has_4tb_ssd: true
notes: desktop plus 4TB SSD NAS and heavy compute during peak sun

View File

@@ -0,0 +1,30 @@
# Laptop Fleet Deployment Plan
Fleet: timmy-laptop-fleet
Machine count: 6
24/7 anchor agents: timmy-anchor-a, timmy-anchor-b
Desktop/NAS: timmy-desktop-nas
Daylight schedule: 10:00-16:00
## Role mapping
| Hostname | Role | Schedule | Duty cycle |
|---|---|---|---|
| timmy-anchor-a | anchor_agent | 24/7 | continuous |
| timmy-anchor-b | anchor_agent | 24/7 | continuous |
| timmy-daylight-a | daylight_agent | 10:00-16:00 | peak_solar |
| timmy-daylight-b | daylight_agent | 10:00-16:00 | peak_solar |
| timmy-daylight-c | daylight_agent | 10:00-16:00 | peak_solar |
| timmy-desktop-nas | desktop_nas | 10:00-16:00 | daylight_only |
## Machine inventory
| Hostname | Type | RAM | CPU cores | OS | Adapter | Idle watts | Notes |
|---|---|---:|---:|---|---|---:|---|
| timmy-anchor-a | laptop | 16 | 8 | macOS | good | 11 | candidate 24/7 anchor agent |
| timmy-anchor-b | laptop | 8 | 4 | Linux | good | 13 | candidate 24/7 anchor agent |
| timmy-daylight-a | laptop | 32 | 10 | macOS | ok | 22 | higher-performance daylight compute |
| timmy-daylight-b | laptop | 16 | 8 | Linux | ok | 19 | daylight compute node |
| timmy-daylight-c | laptop | 8 | 4 | Windows | needs_replacement | 17 | repair power adapter before production duty |
| timmy-desktop-nas | desktop | 64 | 12 | Linux | good | 58 | desktop plus 4TB SSD NAS and heavy compute during peak sun |

View File

@@ -12,29 +12,6 @@ WORLD_DIR = Path('/Users/apayne/.timmy/evennia/timmy_world')
STATE_FILE = WORLD_DIR / 'game_state.json'
TIMMY_LOG = WORLD_DIR / 'timmy_log.md'
WORLD_ITEMS = {
"foraged key": {"effect": "unlock_tower_cache", "quest_item": True, "consumable": False, "effect_text": "A hidden cache clicks open in the Tower wall."},
"seed packet": {"effect": "grow_garden", "quest_item": False, "consumable": True, "effect_text": "Fresh growth pushes through the Garden soil."},
"notebook": {"effect": "write_notebook_rule", "quest_item": False, "consumable": False, "effect_text": "A new rule joins the whiteboard in the Tower."},
"cloth": {"effect": "patch_bridge", "quest_item": False, "consumable": True, "effect_text": "The Bridge railing is wrapped tight against the weather."},
"oil can": {"effect": "stoke_forge", "quest_item": False, "consumable": True, "effect_text": "The Forge fire answers with a hotter glow."},
"lantern": {"effect": "light_bridge", "quest_item": False, "consumable": False, "effect_text": "A steady lantern glow cuts through the dark over the Bridge."},
"rope spool": {"effect": "secure_bridge", "quest_item": False, "consumable": True, "effect_text": "The Bridge is lashed tight and feels safer underfoot."},
"chalk": {"effect": "mark_threshold", "quest_item": False, "consumable": True, "effect_text": "A chalk mark at the Threshold points wanderers home."},
"weather vane": {"effect": "read_weather", "quest_item": False, "consumable": False, "effect_text": "The weather vane settles and the coming storm makes sense."},
"sunstone": {"effect": "restore_tower_power", "quest_item": False, "consumable": False, "effect_text": "Warm light races through the Tower circuits."},
"iron nails": {"effect": "reinforce_bridge", "quest_item": False, "consumable": True, "effect_text": "The Bridge planks are pinned down against the flood."},
"river stone": {"effect": "water_garden", "quest_item": False, "consumable": True, "effect_text": "Moisture returns to the Garden beds."},
}
ROOM_DISCOVERABLES = {
"Threshold": ["chalk", "sunstone"],
"Tower": ["notebook", "lantern"],
"Forge": ["oil can", "iron nails"],
"Garden": ["seed packet", "foraged key"],
"Bridge": ["cloth", "rope spool", "weather vane", "river stone"],
}
# ============================================================
# NARRATIVE ARC — 4 phases that transform the world
# ============================================================
@@ -166,8 +143,6 @@ class World:
"visitors": [],
},
}
for room_name, items in ROOM_DISCOVERABLES.items():
self.rooms[room_name]["discoverables"] = list(items)
# Characters (not NPCs — they have lives)
self.characters = {
@@ -283,14 +258,6 @@ class World:
"items_crafted": 0,
"conflicts_resolved": 0,
"nights_survived": 0,
"bridge_patched": False,
"bridge_secured": False,
"bridge_reinforced": False,
"bridge_lantern_lit": False,
"tower_cache_unlocked": False,
"threshold_marked": False,
"weather_readable": False,
"sunstone_socketed": False,
}
def tick_time(self):
@@ -409,14 +376,6 @@ class World:
desc += " Rain mists on the dark water below."
if len(self.rooms["Bridge"]["carvings"]) > 1:
desc += f" There are {len(self.rooms['Bridge']['carvings'])} carvings now."
if self.state.get("bridge_patched"):
desc += " Cloth bindings keep the railing from splintering."
if self.state.get("bridge_secured"):
desc += " Rope lines keep the span steady against the flood."
if self.state.get("bridge_reinforced"):
desc += " Fresh iron nails hold the planks tight."
if self.state.get("bridge_lantern_lit"):
desc += " A lantern glows warm over the water."
elif room_name == "Tower":
power = self.state.get("tower_power_low", False)
@@ -425,19 +384,6 @@ class World:
if self.rooms["Tower"]["messages"]:
desc += f" The whiteboard holds {len(self.rooms['Tower']['messages'])} rules."
if self.state.get("tower_cache_unlocked"):
desc += " A hidden cache stands open beneath the whiteboard."
if room_name == "Threshold" and self.state.get("threshold_marked"):
desc += " A chalk arrow points late arrivals toward shelter."
if room_name == "Garden" and self.state.get("weather_readable"):
desc += " The beds are arranged to catch whatever weather comes next."
if room_name == "Tower" and self.state.get("sunstone_socketed"):
desc += " A sunstone keeps the room lit with a stubborn amber glow."
discoverables = room.get("discoverables", [])
if discoverables:
desc += f" Discoverable items: {', '.join(discoverables)}."
# Who's here
here = [n for n, c in self.characters.items() if c["room"] == room_name and n != char_name]
@@ -539,10 +485,6 @@ class ActionSystem:
"cost": 1,
"description": "Take an item from the room",
},
"use": {
"cost": 1,
"description": "Use an item from your inventory to change the world",
},
"examine": {
"cost": 0,
"description": "Examine something in detail",
@@ -588,13 +530,6 @@ class ActionSystem:
available.append("rest")
available.append("examine")
discoverables = world.rooms[room].get("discoverables", [])
for item in discoverables:
available.append(f"take:{item}")
for item in char["inventory"]:
available.append(f"use:{item}")
if char["inventory"]:
available.append("give:item")
@@ -1137,76 +1072,6 @@ class GameEngine:
f.write(f"\n*Began: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n\n")
f.write("---\n\n")
f.write(message + "\n")
def _take_item(self, item_name, scene):
room_name = self.world.characters["Timmy"]["room"]
discoverables = self.world.rooms[room_name].get("discoverables", [])
if item_name not in discoverables:
scene["log"].append(f"There is no {item_name} here.")
return
discoverables.remove(item_name)
self.world.characters["Timmy"]["inventory"].append(item_name)
scene["log"].append(f"You take the {item_name}.")
if WORLD_ITEMS.get(item_name, {}).get("quest_item"):
scene["world_events"].append(f"The {item_name} feels important. It might open a quest route.")
def _use_item(self, item_name, scene):
inventory = self.world.characters["Timmy"]["inventory"]
if item_name not in inventory:
scene["log"].append(f"You are not carrying {item_name}.")
return
item = WORLD_ITEMS.get(item_name)
if not item:
scene["log"].append(f"The {item_name} doesn't seem to do anything.")
return
effect = item["effect"]
effect_text = item["effect_text"]
if effect == "grow_garden":
self.world.rooms["Garden"]["growth"] = min(5, self.world.rooms["Garden"]["growth"] + 2)
self.world.state["garden_drought"] = False
elif effect == "unlock_tower_cache":
self.world.state["tower_cache_unlocked"] = True
cache_rule = "Rule: Keys open more than doors when the world trusts you."
if cache_rule not in self.world.rooms["Tower"]["messages"]:
self.world.rooms["Tower"]["messages"].append(cache_rule)
elif effect == "write_notebook_rule":
note_rule = f"Rule #{self.world.tick}: A notebook can turn memory into structure."
self.world.rooms["Tower"]["messages"].append(note_rule)
elif effect == "patch_bridge":
self.world.state["bridge_patched"] = True
self.world.state["bridge_flooding"] = False
self.world.rooms["Bridge"]["weather"] = None
self.world.rooms["Bridge"]["rain_ticks"] = 0
elif effect == "stoke_forge":
self.world.rooms["Forge"]["fire"] = "glowing"
self.world.state["forge_fire_dying"] = False
elif effect == "light_bridge":
self.world.state["bridge_lantern_lit"] = True
elif effect == "secure_bridge":
self.world.state["bridge_secured"] = True
self.world.state["bridge_flooding"] = False
self.world.rooms["Bridge"]["weather"] = None
self.world.rooms["Bridge"]["rain_ticks"] = 0
elif effect == "mark_threshold":
self.world.state["threshold_marked"] = True
elif effect == "read_weather":
self.world.state["weather_readable"] = True
self.world.state["garden_drought"] = False
elif effect == "restore_tower_power":
self.world.state["tower_power_low"] = False
self.world.state["sunstone_socketed"] = True
elif effect == "reinforce_bridge":
self.world.state["bridge_reinforced"] = True
self.world.state["bridge_flooding"] = False
elif effect == "water_garden":
self.world.state["garden_drought"] = False
self.world.rooms["Garden"]["growth"] = min(5, self.world.rooms["Garden"]["growth"] + 1)
scene["log"].append(f"You use the {item_name}. {effect_text}")
scene["world_events"].append(effect_text)
if item.get("consumable"):
inventory.remove(item_name)
def run_tick(self, timmy_action="look"):
"""Run one tick. Return the scene and available choices."""
@@ -1235,7 +1100,7 @@ class GameEngine:
action_costs = {
"move": 2, "tend_fire": 3, "write_rule": 2, "carve": 2,
"plant": 2, "study": 2, "forge": 3, "help": 2, "speak": 1,
"listen": 0, "rest": -2, "examine": 0, "give": 0, "take": 1, "use": 1,
"listen": 0, "rest": -2, "examine": 0, "give": 0, "take": 1,
}
# Extract action name
@@ -1495,17 +1360,9 @@ class GameEngine:
elif timmy_action == "examine":
room = self.world.characters["Timmy"]["room"]
room_data = self.world.rooms[room]
items = room_data.get("items", []) + room_data.get("discoverables", [])
items = room_data.get("items", [])
scene["log"].append(f"You examine The {room}. You see: {', '.join(items) if items else 'nothing special'}")
elif timmy_action.startswith("take:"):
item_name = timmy_action.split(":", 1)[1]
self._take_item(item_name, scene)
elif timmy_action.startswith("use:"):
item_name = timmy_action.split(":", 1)[1]
self._use_item(item_name, scene)
elif timmy_action.startswith("help:"):
# Help increases trust
target_name = timmy_action.split(":")[1]

View File

@@ -50,3 +50,43 @@ def test_manifest_template_is_valid_yaml() -> None:
data = yaml.safe_load(Path("docs/laptop-fleet-manifest.example.yaml").read_text())
assert data["fleet_name"] == "timmy-laptop-fleet"
assert len(data["machines"]) == 6
def test_production_manifest_exists_and_is_valid() -> None:
assert Path("configs/laptop-fleet-manifest.yaml").exists()
data = yaml.safe_load(Path("configs/laptop-fleet-manifest.yaml").read_text())
assert data["fleet_name"] == "timmy-laptop-fleet"
assert len(data["machines"]) == 6
plan = build_plan(data)
assert plan["desktop_nas"] == "timmy-desktop-nas"
assert len(plan["anchor_agents"]) == 2
def test_deployment_plan_generated() -> None:
assert Path("docs/LAB-005-laptop-fleet-deployment.md").exists()
content = Path("docs/LAB-005-laptop-fleet-deployment.md").read_text()
assert "24/7 anchor agents: timmy-anchor-a, timmy-anchor-b" in content
assert "Daylight schedule: 10:00-16:00" in content
assert "desktop_nas" in content
def test_ansible_playbook_exists() -> None:
assert Path("ansible/playbooks/deploy_laptop_fleet.yml").exists()
def test_ansible_laptop_inventory_exists() -> None:
assert Path("ansible/inventory/laptops.ini").exists()
content = Path("ansible/inventory/laptops.ini").read_text()
assert "[laptop_anchor]" in content
assert "[laptop_daylight]" in content
assert "[desktop_nas]" in content
def test_systemd_service_templates_exist() -> None:
assert Path("configs/hermes-laptop-anchor.service").exists()
assert Path("configs/hermes-laptop-daylight.service").exists()
assert Path("configs/hermes-laptop-daylight.timer").exists()
anchor = Path("configs/hermes-laptop-anchor.service").read_text()
daylight = Path("configs/hermes-laptop-daylight.service").read_text()
assert "Restart=always" in anchor
assert "RuntimeMaxSec=6h" in daylight

View File

@@ -1,58 +0,0 @@
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parent.parent
GAME_PATH = ROOT / "evennia" / "timmy_world" / "game.py"
def load_game_module():
spec = spec_from_file_location("tower_game_items", GAME_PATH)
module = module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
module.random.seed(0)
return module
class TestTowerGameWorldItems(unittest.TestCase):
def test_world_has_ten_unique_items_and_a_quest_item(self):
module = load_game_module()
world = module.World()
room_items = {
item
for room in world.rooms.values()
for item in room.get("discoverables", [])
}
self.assertGreaterEqual(len(room_items), 10)
self.assertIn("foraged key", room_items)
self.assertTrue(module.WORLD_ITEMS["foraged key"]["quest_item"])
def test_items_change_world_state_when_used(self):
module = load_game_module()
engine = module.GameEngine()
engine.start_new_game()
engine.world.characters["Timmy"]["energy"] = 10
engine.world.characters["Timmy"]["room"] = "Garden"
initial_growth = engine.world.rooms["Garden"]["growth"]
engine.run_tick("take:seed packet")
use_seed = engine.run_tick("use:seed packet")
self.assertGreater(engine.world.rooms["Garden"]["growth"], initial_growth)
self.assertNotIn("seed packet", engine.world.characters["Timmy"]["inventory"])
self.assertTrue(any("garden" in line.lower() for line in use_seed["world_events"] + use_seed["log"]))
engine.world.characters["Timmy"]["energy"] = 10
engine.run_tick("take:foraged key")
use_key = engine.run_tick("use:foraged key")
self.assertTrue(engine.world.state["tower_cache_unlocked"])
self.assertTrue(any("cache" in line.lower() or "quest" in line.lower() for line in use_key["world_events"] + use_key["log"]))
if __name__ == "__main__":
unittest.main()