Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a6b53a37 | ||
|
|
5ce4fa6cb8 |
@@ -12,6 +12,29 @@ 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
|
||||
# ============================================================
|
||||
@@ -143,6 +166,8 @@ 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 = {
|
||||
@@ -258,6 +283,14 @@ 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):
|
||||
@@ -376,6 +409,14 @@ 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)
|
||||
@@ -384,6 +425,19 @@ 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]
|
||||
@@ -485,6 +539,10 @@ 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",
|
||||
@@ -530,6 +588,13 @@ 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")
|
||||
|
||||
@@ -1072,6 +1137,76 @@ 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."""
|
||||
@@ -1100,7 +1235,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,
|
||||
"listen": 0, "rest": -2, "examine": 0, "give": 0, "take": 1, "use": 1,
|
||||
}
|
||||
|
||||
# Extract action name
|
||||
@@ -1360,9 +1495,17 @@ class GameEngine:
|
||||
elif timmy_action == "examine":
|
||||
room = self.world.characters["Timmy"]["room"]
|
||||
room_data = self.world.rooms[room]
|
||||
items = room_data.get("items", [])
|
||||
items = room_data.get("items", []) + room_data.get("discoverables", [])
|
||||
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]
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fleet cost report generator.
|
||||
|
||||
Reads Timmy's sovereignty metrics database and estimates paid API spend by
|
||||
agent/provider lane. Default output targets the local timmy-config reports
|
||||
folder so the cost report can be filed from the sidecar repo.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
DB_PATH = Path.home() / ".timmy" / "metrics" / "model_metrics.db"
|
||||
|
||||
|
||||
AGENT_LANES = (
|
||||
{
|
||||
"agent": "Timmy Cloud Lane",
|
||||
"provider": "OpenRouter",
|
||||
"patterns": ("openrouter/", "google/", "deepseek/", "x-ai/", "mistral/"),
|
||||
"notes": "Cloud fallback and external reasoning routed through OpenRouter-compatible lanes.",
|
||||
},
|
||||
{
|
||||
"agent": "Ezra",
|
||||
"provider": "Anthropic",
|
||||
"patterns": ("claude-", "anthropic/claude"),
|
||||
"notes": "Archivist / long-form reasoning house on Claude-family models.",
|
||||
},
|
||||
{
|
||||
"agent": "Bezalel",
|
||||
"provider": "OpenAI",
|
||||
"patterns": ("gpt-", "openai/", "codex"),
|
||||
"notes": "Forge / implementation house on Codex/OpenAI-backed execution lanes.",
|
||||
},
|
||||
{
|
||||
"agent": "Allegro",
|
||||
"provider": "Kimi / Moonshot",
|
||||
"patterns": ("kimi", "moonshot"),
|
||||
"notes": "Tempo-and-dispatch house on Kimi / Moonshot direct API lanes.",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def default_report_path(report_date: str | None = None) -> Path:
|
||||
if report_date is None:
|
||||
report_date = datetime.now().strftime("%Y-%m-%d")
|
||||
return Path.home() / "code" / "timmy-config" / "reports" / "production" / f"{report_date}-fleet-cost-report.md"
|
||||
|
||||
|
||||
def match_lane(model: str) -> dict | None:
|
||||
lowered = (model or "").lower()
|
||||
for lane in AGENT_LANES:
|
||||
if any(pattern in lowered for pattern in lane["patterns"]):
|
||||
return lane
|
||||
return None
|
||||
|
||||
|
||||
def load_cost_rows(days: int = 30, db_path: Path = DB_PATH) -> list[tuple[str, int, int, int, float]]:
|
||||
if not db_path.exists():
|
||||
return []
|
||||
cutoff = (datetime.now() - timedelta(days=days)).timestamp()
|
||||
with sqlite3.connect(str(db_path)) as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT model, SUM(sessions), SUM(messages), SUM(tool_calls), SUM(est_cost_usd)
|
||||
FROM session_stats
|
||||
WHERE timestamp > ? AND is_local = 0
|
||||
GROUP BY model
|
||||
ORDER BY SUM(est_cost_usd) DESC, model ASC
|
||||
""",
|
||||
(cutoff,),
|
||||
).fetchall()
|
||||
return [
|
||||
(model, int(sessions or 0), int(messages or 0), int(tool_calls or 0), float(cost or 0.0))
|
||||
for model, sessions, messages, tool_calls, cost in rows
|
||||
]
|
||||
|
||||
|
||||
def summarize_rows(rows: Iterable[tuple[str, int, int, int, float]], days: int = 30) -> dict:
|
||||
rows = list(rows)
|
||||
agents: dict[str, dict] = {}
|
||||
providers_seen: set[str] = set()
|
||||
inventory = [
|
||||
{
|
||||
"agent": lane["agent"],
|
||||
"provider": lane["provider"],
|
||||
"notes": lane["notes"],
|
||||
}
|
||||
for lane in AGENT_LANES
|
||||
]
|
||||
|
||||
for lane in AGENT_LANES:
|
||||
agents[lane["agent"]] = {
|
||||
"provider": lane["provider"],
|
||||
"models": [],
|
||||
"sessions": 0,
|
||||
"messages": 0,
|
||||
"tool_calls": 0,
|
||||
"monthly_cost_usd": 0.0,
|
||||
"daily_cost_usd": 0.0,
|
||||
"notes": lane["notes"],
|
||||
}
|
||||
|
||||
unassigned = {
|
||||
"provider": "Unassigned",
|
||||
"models": [],
|
||||
"sessions": 0,
|
||||
"messages": 0,
|
||||
"tool_calls": 0,
|
||||
"monthly_cost_usd": 0.0,
|
||||
"daily_cost_usd": 0.0,
|
||||
"notes": "Observed paid-model spend not yet mapped to a named wizard house.",
|
||||
}
|
||||
|
||||
for model, sessions, messages, tool_calls, monthly_cost in rows:
|
||||
lane = match_lane(model)
|
||||
if lane is None:
|
||||
bucket = unassigned
|
||||
else:
|
||||
bucket = agents[lane["agent"]]
|
||||
providers_seen.add(lane["provider"])
|
||||
bucket["models"].append(
|
||||
{
|
||||
"model": model,
|
||||
"sessions": sessions,
|
||||
"messages": messages,
|
||||
"tool_calls": tool_calls,
|
||||
"monthly_cost_usd": round(monthly_cost, 4),
|
||||
}
|
||||
)
|
||||
bucket["sessions"] += sessions
|
||||
bucket["messages"] += messages
|
||||
bucket["tool_calls"] += tool_calls
|
||||
bucket["monthly_cost_usd"] += monthly_cost
|
||||
|
||||
for bucket in list(agents.values()) + [unassigned]:
|
||||
bucket["monthly_cost_usd"] = round(bucket["monthly_cost_usd"], 4)
|
||||
bucket["daily_cost_usd"] = round(bucket["monthly_cost_usd"] / max(days, 1), 4)
|
||||
|
||||
if unassigned["models"]:
|
||||
agents["Unassigned"] = unassigned
|
||||
providers_seen.add("Unassigned")
|
||||
|
||||
total_monthly = round(sum(item["monthly_cost_usd"] for item in agents.values()), 4)
|
||||
total_daily = round(sum(item["daily_cost_usd"] for item in agents.values()), 4)
|
||||
|
||||
provider_order = sorted(providers_seen)
|
||||
if "Unassigned" in provider_order:
|
||||
provider_order = [p for p in provider_order if p != "Unassigned"] + ["Unassigned"]
|
||||
|
||||
return {
|
||||
"days": days,
|
||||
"providers": provider_order,
|
||||
"inventory": inventory,
|
||||
"agents": agents,
|
||||
"total_monthly_cost_usd": total_monthly,
|
||||
"total_daily_cost_usd": total_daily,
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(summary: dict, report_date: str | None = None) -> str:
|
||||
if report_date is None:
|
||||
report_date = datetime.now().strftime("%Y-%m-%d")
|
||||
lines = [
|
||||
f"# Fleet Cost Report — {report_date}",
|
||||
"",
|
||||
f"Window: last {summary['days']} days of paid-model session stats from `~/.timmy/metrics/model_metrics.db`.",
|
||||
"",
|
||||
"## Paid API inventory",
|
||||
"",
|
||||
"| Agent | Provider | Notes |",
|
||||
"| --- | --- | --- |",
|
||||
]
|
||||
for item in summary["inventory"]:
|
||||
lines.append(f"| {item['agent']} | {item['provider']} | {item['notes']} |")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Estimated cost per agent per day",
|
||||
"",
|
||||
"| Agent | Provider | Daily cost | Monthly estimate | Sessions | Messages | Tool calls |",
|
||||
"| --- | --- | ---: | ---: | ---: | ---: | ---: |",
|
||||
]
|
||||
)
|
||||
for agent, data in summary["agents"].items():
|
||||
lines.append(
|
||||
f"| {agent} | {data['provider']} | ${data['daily_cost_usd']:.2f} | ${data['monthly_cost_usd']:.2f} | {data['sessions']} | {data['messages']} | {data['tool_calls']} |"
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"Total estimated daily paid spend: ${summary['total_daily_cost_usd']:.2f}",
|
||||
f"Total estimated monthly paid spend: ${summary['total_monthly_cost_usd']:.2f}",
|
||||
"",
|
||||
"## Model evidence",
|
||||
"",
|
||||
]
|
||||
)
|
||||
for agent, data in summary["agents"].items():
|
||||
lines.append(f"### {agent}")
|
||||
if not data["models"]:
|
||||
lines.append("- No paid-model sessions observed in the selected window.")
|
||||
else:
|
||||
for model in data["models"]:
|
||||
lines.append(
|
||||
f"- `{model['model']}` — {model['sessions']} sessions / {model['messages']} messages / {model['tool_calls']} tool calls / ${model['monthly_cost_usd']:.2f} est."
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
lines.append("Generated by `python3 scripts/fleet_cost_report.py --days 30`. Default output path targets the local timmy-config report lane.")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_report(output_path: Path, summary: dict, report_date: str | None = None) -> Path:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(render_markdown(summary, report_date=report_date), encoding="utf-8")
|
||||
return output_path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Estimate paid API spend per fleet agent")
|
||||
parser.add_argument("--days", type=int, default=30, help="Lookback window in days")
|
||||
parser.add_argument("--db-path", default=str(DB_PATH), help="Path to model_metrics.db")
|
||||
parser.add_argument("--output", help="Optional markdown output path")
|
||||
parser.add_argument("--date", help="Override report date (YYYY-MM-DD)")
|
||||
args = parser.parse_args()
|
||||
|
||||
rows = load_cost_rows(days=args.days, db_path=Path(args.db_path).expanduser())
|
||||
summary = summarize_rows(rows, days=args.days)
|
||||
report_date = args.date or datetime.now().strftime("%Y-%m-%d")
|
||||
output_path = Path(args.output).expanduser() if args.output else default_report_path(report_date)
|
||||
write_report(output_path, summary, report_date=report_date)
|
||||
print(output_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,77 +0,0 @@
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPT_PATH = ROOT / "scripts" / "fleet_cost_report.py"
|
||||
|
||||
|
||||
def load_module():
|
||||
spec = spec_from_file_location("fleet_cost_report", SCRIPT_PATH)
|
||||
module = module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class TestFleetCostReport(unittest.TestCase):
|
||||
def test_default_output_targets_timmy_config_report_path(self):
|
||||
module = load_module()
|
||||
output_path = module.default_report_path("2026-04-22")
|
||||
self.assertIn("timmy-config", str(output_path))
|
||||
self.assertTrue(str(output_path).endswith("2026-04-22-fleet-cost-report.md"))
|
||||
|
||||
def test_summary_groups_paid_costs_by_agent_and_provider(self):
|
||||
module = load_module()
|
||||
rows = [
|
||||
("claude-sonnet-4-6", 12, 120, 24, 6.0),
|
||||
("gpt-5.4", 6, 60, 12, 3.0),
|
||||
("openrouter/google/gemini-2.5-pro", 4, 40, 8, 2.0),
|
||||
("kimi-k2", 2, 20, 4, 1.0),
|
||||
]
|
||||
summary = module.summarize_rows(rows, days=30)
|
||||
|
||||
self.assertEqual(summary["providers"], ["Anthropic", "Kimi / Moonshot", "OpenAI", "OpenRouter"])
|
||||
self.assertAlmostEqual(summary["agents"]["Ezra"]["monthly_cost_usd"], 6.0)
|
||||
self.assertAlmostEqual(summary["agents"]["Bezalel"]["monthly_cost_usd"], 3.0)
|
||||
self.assertAlmostEqual(summary["agents"]["Timmy Cloud Lane"]["monthly_cost_usd"], 2.0)
|
||||
self.assertAlmostEqual(summary["agents"]["Allegro"]["monthly_cost_usd"], 1.0)
|
||||
self.assertAlmostEqual(summary["agents"]["Ezra"]["daily_cost_usd"], 0.2)
|
||||
|
||||
def test_report_render_mentions_inventory_and_agent_costs(self):
|
||||
module = load_module()
|
||||
rows = [
|
||||
("claude-sonnet-4-6", 12, 120, 24, 6.0),
|
||||
("gpt-5.4", 6, 60, 12, 3.0),
|
||||
("openrouter/google/gemini-2.5-pro", 4, 40, 8, 2.0),
|
||||
]
|
||||
summary = module.summarize_rows(rows, days=30)
|
||||
report = module.render_markdown(summary, report_date="2026-04-22")
|
||||
|
||||
self.assertIn("# Fleet Cost Report — 2026-04-22", report)
|
||||
self.assertIn("## Paid API inventory", report)
|
||||
self.assertIn("Anthropic", report)
|
||||
self.assertIn("OpenRouter", report)
|
||||
self.assertIn("OpenAI", report)
|
||||
self.assertIn("## Estimated cost per agent per day", report)
|
||||
self.assertIn("Timmy Cloud Lane", report)
|
||||
self.assertIn("Ezra", report)
|
||||
self.assertIn("Bezalel", report)
|
||||
|
||||
def test_write_report_creates_markdown_file(self):
|
||||
module = load_module()
|
||||
rows = [("claude-sonnet-4-6", 1, 10, 2, 0.5)]
|
||||
summary = module.summarize_rows(rows, days=30)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
dest = Path(tmpdir) / "fleet-cost.md"
|
||||
module.write_report(dest, summary, report_date="2026-04-22")
|
||||
self.assertTrue(dest.exists())
|
||||
text = dest.read_text()
|
||||
self.assertIn("Fleet Cost Report", text)
|
||||
self.assertIn("Ezra", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
58
tests/test_tower_game_items.py
Normal file
58
tests/test_tower_game_items.py
Normal file
@@ -0,0 +1,58 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user