Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
89de5b2c69 fix: make tower world events affect gameplay
Some checks failed
Self-Healing Smoke / self-healing-smoke (pull_request) Failing after 16s
Agent PR Gate / gate (pull_request) Failing after 40s
Smoke Test / smoke (pull_request) Failing after 27s
Agent PR Gate / report (pull_request) Successful in 21s
Closes #513
2026-04-22 10:44:56 -04:00
5 changed files with 195 additions and 325 deletions

View File

@@ -1059,6 +1059,46 @@ class GameEngine:
self.log("It will always pulse. That much you know.")
self.log("")
self.world.save()
def _bridge_is_hazardous(self):
bridge = self.world.rooms["Bridge"]
return bool(
self.world.state.get("bridge_flooding")
or bridge.get("weather") == "rain"
or bridge.get("rain_ticks", 0) > 0
)
def _bridge_crossing_extra_cost(self, current_room, dest):
if "Bridge" not in (current_room, dest):
return 0
return 2 if self._bridge_is_hazardous() else 0
def _event_dialogue(self, char_name, room_name):
if char_name == "Bezalel" and room_name == "Forge":
if self.world.rooms["Forge"]["fire"] == "cold":
return random.choice([
"The forge is cold. We cannot work until the fire lives again.",
"No forging now. The hearth is dead cold.",
])
if self.world.state.get("forge_fire_dying"):
return random.choice([
"The fire is dying. Tend it before the forge goes dark.",
"The forge is losing heat. Help me keep it alive.",
])
if char_name == "Ezra" and room_name == "Tower" and self.world.state.get("tower_power_low"):
return random.choice([
"The Tower power is too low. The servers won't hold a clean study right now.",
"The LED is flickering. We need steady power before the Tower can be read properly.",
])
if char_name in {"Marcus", "Allegro"} and room_name == "Bridge" and self._bridge_is_hazardous():
return random.choice([
"The Bridge is slick with rain. Cross carefully or wait it out.",
"This rain changes the Bridge. Don't treat it like dry stone.",
])
return None
def log(self, message):
"""Add to Timmy's log."""
@@ -1094,6 +1134,7 @@ class GameEngine:
}
# Process Timmy's action
room_name = self.world.characters["Timmy"]["room"]
timmy_energy = self.world.characters["Timmy"]["energy"]
# Energy constraint checks
@@ -1156,8 +1197,17 @@ class GameEngine:
if direction in connections:
dest = connections[direction]
bridge_extra_cost = self._bridge_crossing_extra_cost(current_room, dest)
move_cost = 1 + bridge_extra_cost
if self.world.characters["Timmy"]["energy"] < move_cost:
scene["log"].append("The rain makes the Bridge too costly to cross right now. Rest first.")
scene["room_desc"] = self.world.get_room_desc(current_room, "Timmy")
here = [n for n in self.world.characters if self.world.characters[n]["room"] == current_room and n != "Timmy"]
scene["here"] = here
return scene
self.world.characters["Timmy"]["room"] = dest
self.world.characters["Timmy"]["energy"] -= 1
self.world.characters["Timmy"]["energy"] -= move_cost
scene["log"].append(f"You move {direction} to The {dest}.")
scene["timmy_room"] = dest
@@ -1165,6 +1215,8 @@ class GameEngine:
# Check for rain on bridge
if dest == "Bridge" and self.world.rooms["Bridge"]["weather"] == "rain":
scene["world_events"].append("Rain mists on the dark water below. The railing is slick.")
if bridge_extra_cost:
scene["log"].append("Rain turns the Bridge crossing into work. You brace against the slick stone. (-2 extra energy)")
# Check trust changes for arrival
here = [n for n in self.world.characters if self.world.characters[n]["room"] == dest and n != "Timmy"]
@@ -1310,25 +1362,69 @@ class GameEngine:
elif timmy_action == "write_rule":
if self.world.characters["Timmy"]["room"] == "Tower":
rules = [
f"Rule #{self.world.tick}: The room remembers those who enter it.",
f"Rule #{self.world.tick}: A man in the dark needs to know someone is in the room.",
f"Rule #{self.world.tick}: The forge does not care about your schedule.",
f"Rule #{self.world.tick}: Every footprint on the stone means someone made it here.",
f"Rule #{self.world.tick}: The bridge does not judge. It only carries.",
f"Rule #{self.world.tick}: A seed planted in patience grows in time.",
f"Rule #{self.world.tick}: What is carved in wood outlasts what is said in anger.",
f"Rule #{self.world.tick}: The garden grows whether anyone watches or not.",
f"Rule #{self.world.tick}: Trust is built one tick at a time.",
f"Rule #{self.world.tick}: The fire remembers who tended it.",
]
new_rule = random.choice(rules)
self.world.rooms["Tower"]["messages"].append(new_rule)
self.world.characters["Timmy"]["energy"] -= 1
scene["log"].append(f"You write on the Tower whiteboard: \"{new_rule}\"")
if self.world.state.get("tower_power_low"):
scene["world_events"].append("The Tower power is too low. The LED flickers over the whiteboard.")
scene["log"].append("The power is too low to write a new rule.")
else:
rules = [
f"Rule #{self.world.tick}: The room remembers those who enter it.",
f"Rule #{self.world.tick}: A man in the dark needs to know someone is in the room.",
f"Rule #{self.world.tick}: The forge does not care about your schedule.",
f"Rule #{self.world.tick}: Every footprint on the stone means someone made it here.",
f"Rule #{self.world.tick}: The bridge does not judge. It only carries.",
f"Rule #{self.world.tick}: A seed planted in patience grows in time.",
f"Rule #{self.world.tick}: What is carved in wood outlasts what is said in anger.",
f"Rule #{self.world.tick}: The garden grows whether anyone watches or not.",
f"Rule #{self.world.tick}: Trust is built one tick at a time.",
f"Rule #{self.world.tick}: The fire remembers who tended it.",
]
new_rule = random.choice(rules)
self.world.rooms["Tower"]["messages"].append(new_rule)
self.world.characters["Timmy"]["energy"] -= 1
scene["log"].append(f"You write on the Tower whiteboard: \"{new_rule}\"")
else:
scene["log"].append("You are not in the Tower.")
elif timmy_action == "study":
if self.world.characters["Timmy"]["room"] == "Tower":
if self.world.state.get("tower_power_low"):
scene["world_events"].append("The Tower power is too low. The servers stutter in weak light.")
scene["log"].append("The power is too low to study the servers.")
else:
insights = [
"You study the server rhythm until the pulse resolves into something readable.",
"You trace the signal paths and feel the Tower settle into focus.",
"You study the green LED and the server racks until the pattern becomes clear.",
]
insight = random.choice(insights)
self.world.characters["Timmy"]["energy"] -= 1
self.world.characters["Timmy"]["memories"].append(insight)
scene["log"].append(insight)
scene["world_events"].append("The Tower answers with a steady hum.")
else:
scene["log"].append("You are not in the Tower.")
elif timmy_action == "forge":
if self.world.characters["Timmy"]["room"] == "Forge":
forge_fire = self.world.rooms["Forge"]["fire"]
if forge_fire == "cold":
scene["world_events"].append("The forge is cold. No metal will take shape here yet.")
scene["log"].append("The forge is cold. Tend the fire before you try to forge.")
else:
forged_items = [
f"bridge nail #{self.world.tick}",
f"tower key blank #{self.world.tick}",
f"garden trowel #{self.world.tick}",
]
forged_item = random.choice(forged_items)
self.world.rooms["Forge"]["forged_items"].append(forged_item)
self.world.characters["Timmy"]["energy"] -= 2
self.world.state["items_crafted"] += 1
scene["log"].append(f"You forge {forged_item} at the anvil.")
scene["world_events"].append("The anvil rings and the hearth answers.")
else:
scene["log"].append("You are not in the Forge.")
elif timmy_action == "carve":
if self.world.characters["Timmy"]["room"] == "Bridge":
carvings = [
@@ -1414,7 +1510,11 @@ class GameEngine:
speech_chance = 0.20
if random.random() < speech_chance:
if char_name == "Marcus":
event_line = self._event_dialogue(char_name, room_name)
if event_line:
self.world.characters[char_name]["spoken"].append(event_line)
scene["log"].append(f"{char_name} says: \"{event_line}\"")
elif char_name == "Marcus":
marcus_pool = self.DIALOGUES["Marcus"].get(phase, self.DIALOGUES["Marcus"]["quietus"])
line = random.choice(marcus_pool)
self.world.characters[char_name]["spoken"].append(line)

View File

@@ -1,111 +0,0 @@
#!/bin/bash
# ============================================================================
# Agent Dispatch — One-shot prompt generator for fleet workers
# ============================================================================
# Refs: timmy-home #512
#
# Packages context, token, repo, issue, and Git/Gitea commands into a
# copy-pasteable prompt for any agent (Claude, Sonnet, Kimi, Grok, etc.).
#
# Usage:
# scripts/agent-dispatch.sh <agent> <repo> <issue#> [<org>]
#
# Supported agents:
# sonnet, claude, kimi, grok, gemini, ezra, bezalel, allegro, timmy
#
# Example:
# scripts/agent-dispatch.sh sonnet the-nexus 844 Timmy_Foundation
# ============================================================================
set -euo pipefail
AGENT="${1:-}"
REPO="${2:-}"
ISSUE="${3:-}"
ORG="${4:-Timmy_Foundation}"
TOKEN="${GITEA_TOKEN:-$(cat ~/.config/gitea/token 2>/dev/null || true)}"
FORGE="https://forge.alexanderwhitestone.com"
if [ -z "$AGENT" ] || [ -z "$REPO" ] || [ -z "$ISSUE" ]; then
echo "Usage: $0 <agent> <repo> <issue#> [<org>]"
echo ""
echo "Supported agents:"
echo " sonnet — Anthropic Claude Sonnet (cloud, high-reasoning)"
echo " claude — Anthropic Claude (general)"
echo " kimi — Moonshot Kimi K2.5 (cloud, long-context)"
echo " grok — xAI Grok (cloud, real-time)"
echo " gemini — Google Gemini (cloud, multimodal)"
echo " ezra — Local archivist house (read-before-write)"
echo " bezalel — Local artificer house (proof-required)"
echo " allegro — Local dispatch house (tempo-and-routing)"
echo " timmy — Local sovereign house (final review)"
exit 1
fi
# Validate agent
VALID_AGENTS="sonnet claude kimi grok gemini ezra bezalel allegro timmy"
if ! echo "$VALID_AGENTS" | grep -qw "$AGENT"; then
echo "ERROR: Unknown agent '$AGENT'"
echo "Valid agents: $VALID_AGENTS"
exit 1
fi
# Fetch issue details
if [ -n "$TOKEN" ]; then
ISSUE_JSON=$(curl -s -H "Authorization: token ${TOKEN}" \
"${FORGE}/api/v1/repos/${ORG}/${REPO}/issues/${ISSUE}" 2>/dev/null || true)
ISSUE_TITLE=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('title',''))" 2>/dev/null || true)
ISSUE_BODY=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('body',''))" 2>/dev/null || true)
else
echo "WARNING: No Gitea token found. Issue details will be blank."
ISSUE_TITLE=""
ISSUE_BODY=""
fi
cat <<EOF
================================================================================
DISPATCH PROMPT — ${AGENT} → ${ORG}/${REPO}#${ISSUE}
================================================================================
Agent: ${AGENT}
Repo: ${ORG}/${REPO}
Issue: #${ISSUE}
Title: ${ISSUE_TITLE}
--- ISSUE BODY ---
${ISSUE_BODY}
--- INSTRUCTIONS ---
1. Clone the repo:
git clone --depth 1 "https://\${TOKEN}@forge.alexanderwhitestone.com/${ORG}/${REPO}.git"
cd ${REPO}
2. Create branch:
git checkout -b ${AGENT}/${REPO}-${ISSUE}
3. Read the issue, implement the fix or feature.
4. Test your changes locally.
5. Commit and push:
git add -A
git commit -m "[${AGENT}] ${ISSUE_TITLE} (#${ISSUE})"
git push origin ${AGENT}/${REPO}-${ISSUE}
6. Open PR via Gitea API:
curl -X POST \\
-H "Authorization: token \${TOKEN}" \\
-H "Content-Type: application/json" \\
"${FORGE}/api/v1/repos/${ORG}/${REPO}/pulls" \\
-d '{"title":"[${AGENT}] ${ISSUE_TITLE}","head":"${AGENT}/${REPO}-${ISSUE}","base":"main","body":"Closes #${ISSUE}"}'
7. File new issues for anything discovered.
Token: \${GITEA_TOKEN} or ~/.config/gitea/token
Forge: ${FORGE}
Sovereignty and service always.
================================================================================
EOF

View File

@@ -1,195 +0,0 @@
#!/bin/bash
# ============================================================================
# Sonnet Workforce Smoke Test
# ============================================================================
# Refs: timmy-home #512
#
# Validates that the Sonnet workforce agent can perform the full
# clone → code → commit → push → PR workflow via Gitea HTTP.
#
# Usage:
# scripts/sonnet-smoke-test.sh [--cleanup]
#
# Exit codes:
# 0 — all checks passed
# 1 — one or more checks failed
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
TOKEN="${GITEA_TOKEN:-$(cat ~/.config/gitea/token 2>/dev/null || true)}"
FORGE="https://forge.alexanderwhitestone.com"
ORG="Timmy_Foundation"
REPO="timmy-home"
TEST_BRANCH="smoke/sonnet-$(date +%s)"
# Colors
GREEN='\\033[0;32m'
RED='\\033[0;31m'
YELLOW='\\033[0;33m'
NC='\\033[0m'
PASS=0
FAIL=0
log_pass() { echo -e "${GREEN}${NC} $1"; PASS=$((PASS + 1)); }
log_fail() { echo -e "${RED}${NC} $1"; FAIL=$((FAIL + 1)); }
log_info() { echo -e "${YELLOW}${NC} $1"; }
# ── Prerequisites ──────────────────────────────────────────────────────────────────────────────────────
log_info "Checking prerequisites..."
if [ -z "$TOKEN" ]; then
log_fail "Gitea token not found (checked GITEA_TOKEN env and ~/.config/gitea/token)"
exit 1
fi
if ! command -v git &>/dev/null; then
log_fail "git not installed"
exit 1
fi
if ! command -v curl &>/dev/null; then
log_fail "curl not installed"
exit 1
fi
if ! command -v python3 &>/dev/null; then
log_fail "python3 not installed"
exit 1
fi
log_pass "Prerequisites OK"
# ── 1. Clone via Gitea HTTP ───────────────────────────────────────────────────────────────────────────────────────────────────────
log_info "Step 1: Clone repo via Gitea HTTP..."
TMPDIR=$(mktemp -d)
CLONE_URL="${FORGE}/${ORG}/${REPO}.git"
cd "$TMPDIR"
if git clone --depth 1 "https://${TOKEN}@${FORGE#https://}/${ORG}/${REPO}.git" smoke-clone 2>/dev/null; then
log_pass "Clone via Gitea HTTP"
else
log_fail "Clone via Gitea HTTP"
rm -rf "$TMPDIR"
exit 1
fi
# ── 2. Commit ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
log_info "Step 2: Create branch and commit..."
cd "$TMPDIR/smoke-clone"
git checkout -b "$TEST_BRANCH" 2>/dev/null || true
# Make a harmless change
printf "# Sonnet smoke test marker\\n# timestamp: %s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > SONNET_SMOKE_MARKER.md
git add SONNET_SMOKE_MARKER.md
if git -c user.email="sonnet@timmy.local" -c user.name="Sonnet Smoke Test" \
commit -m "test: sonnet smoke test marker" 2>/dev/null; then
log_pass "Commit created"
else
log_fail "Commit failed"
rm -rf "$TMPDIR"
exit 1
fi
# ── 3. Push ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
log_info "Step 3: Push branch..."
if git push origin "$TEST_BRANCH" 2>/dev/null; then
log_pass "Push to origin"
else
log_fail "Push to origin"
rm -rf "$TMPDIR"
exit 1
fi
# ── 4. Create PR ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
log_info "Step 4: Create PR via Gitea API..."
PR_RESPONSE=$(curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${FORGE}/api/v1/repos/${ORG}/${REPO}/pulls" \
-d "{
\"title\": \"test: sonnet smoke test ${TEST_BRANCH}\",
\"head\": \"${TEST_BRANCH}\",
\"base\": \"main\",
\"body\": \"Automated smoke test verifying Sonnet can clone, commit, push, and open a PR.\\n\\nRefs #512\"
}" 2>/dev/null)
PR_NUMBER=$(echo "$PR_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('number',''))")
if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "None" ]; then
log_pass "PR created (#${PR_NUMBER})"
PR_URL="${FORGE}/${ORG}/${REPO}/pulls/${PR_NUMBER}"
echo " URL: $PR_URL"
else
log_fail "PR creation failed"
echo " Response: $PR_RESPONSE"
rm -rf "$TMPDIR"
exit 1
fi
# ── 5. Verify PR exists ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
log_info "Step 5: Verify PR exists via API..."
PR_CHECK=$(curl -s -H "Authorization: token ${TOKEN}" \
"${FORGE}/api/v1/repos/${ORG}/${REPO}/pulls/${PR_NUMBER}" 2>/dev/null)
PR_STATE=$(echo "$PR_CHECK" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state',''))")
if [ "$PR_STATE" = "open" ]; then
log_pass "PR verified open via API"
else
log_fail "PR state is '$PR_STATE', expected 'open'"
fi
# ── Cleanup (optional) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
if [ "${1:-}" = "--cleanup" ]; then
log_info "Cleaning up smoke test artifacts..."
curl -s -X PATCH -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${FORGE}/api/v1/repos/${ORG}/${REPO}/pulls/${PR_NUMBER}" \
-d '{"state":"closed"}' >/dev/null 2>&1 || true
git push origin --delete "$TEST_BRANCH" 2>/dev/null || true
log_pass "Cleanup complete"
fi
rm -rf "$TMPDIR"
# ── Summary ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
echo ""
echo "================================================================"
echo " Sonnet Smoke Test Summary"
echo "================================================================"
echo -e " Passed: ${GREEN}${PASS}${NC}"
echo -e " Failed: ${RED}${FAIL}${NC}"
echo ""
if [ "$FAIL" -gt 0 ]; then
echo -e "${RED}RESULT: FAILED${NC}"
exit 1
else
echo -e "${GREEN}RESULT: PASSED${NC}"
echo ""
echo "Sonnet workforce is verified end-to-end:"
echo " ✓ Clone via Gitea HTTP"
echo " ✓ Branch + commit"
echo " ✓ Push to origin"
echo " ✓ Open PR via API"
echo " ✓ Verify PR state"
exit 0
fi

View File

@@ -1,6 +1,7 @@
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parent.parent
@@ -66,6 +67,82 @@ class TestEvenniaLocalWorldGame(unittest.TestCase):
self.assertIn("Ezra is already here.", result["log"])
self.assertIn("The servers hum steady. The green LED pulses.", result["world_events"])
def test_bridge_rain_crossing_costs_extra_energy_and_warns(self):
module = load_game_module()
dry_engine = module.GameEngine()
dry_engine.start_new_game()
dry_engine.world.update_world_state = lambda: None
dry_engine.world.characters["Timmy"]["energy"] = 10
dry_result = dry_engine.run_tick("move:south")
dry_energy = dry_engine.world.characters["Timmy"]["energy"]
rainy_engine = module.GameEngine()
rainy_engine.start_new_game()
rainy_engine.world.update_world_state = lambda: None
rainy_engine.world.characters["Timmy"]["energy"] = 10
rainy_engine.world.rooms["Bridge"]["weather"] = "rain"
rainy_engine.world.rooms["Bridge"]["rain_ticks"] = 3
rainy_engine.world.state["bridge_flooding"] = True
rainy_result = rainy_engine.run_tick("move:south")
self.assertEqual(rainy_engine.world.characters["Timmy"]["room"], "Bridge")
self.assertLess(rainy_engine.world.characters["Timmy"]["energy"], dry_energy)
self.assertTrue(
any("bridge" in line.lower() and ("rain" in line.lower() or "slick" in line.lower()) for line in rainy_result["log"] + rainy_result["world_events"]),
rainy_result,
)
def test_tower_power_low_blocks_study_and_write_rule(self):
module = load_game_module()
engine = module.GameEngine()
engine.start_new_game()
engine.world.update_world_state = lambda: None
engine.world.characters["Timmy"]["room"] = "Tower"
engine.world.characters["Timmy"]["energy"] = 10
engine.world.state["tower_power_low"] = True
rules_before = list(engine.world.rooms["Tower"]["messages"])
study_result = engine.run_tick("study")
self.assertEqual(engine.world.characters["Timmy"]["energy"], 10)
self.assertTrue(
any("power" in line.lower() and ("study" in line.lower() or "servers" in line.lower()) for line in study_result["log"] + study_result["world_events"]),
study_result,
)
write_result = engine.run_tick("write_rule")
self.assertEqual(engine.world.rooms["Tower"]["messages"], rules_before)
self.assertTrue(
any("power" in line.lower() and ("write" in line.lower() or "whiteboard" in line.lower()) for line in write_result["log"] + write_result["world_events"]),
write_result,
)
def test_cold_forge_blocks_forge_action_and_bezalel_reacts(self):
module = load_game_module()
engine = module.GameEngine()
engine.start_new_game()
engine.world.update_world_state = lambda: None
engine.npc_ai.make_choice = lambda _name: None
engine.world.characters["Timmy"]["room"] = "Forge"
engine.world.characters["Timmy"]["energy"] = 10
engine.world.characters["Bezalel"]["room"] = "Forge"
engine.world.rooms["Forge"]["fire"] = "cold"
engine.world.state["forge_fire_dying"] = True
forged_before = list(engine.world.rooms["Forge"]["forged_items"])
with patch.object(module.random, "random", return_value=0.0), patch.object(module.random, "choice", side_effect=lambda seq: seq[0]):
result = engine.run_tick("forge")
self.assertEqual(engine.world.rooms["Forge"]["forged_items"], forged_before)
self.assertTrue(
any("forge" in line.lower() and ("cold" in line.lower() or "fire" in line.lower()) for line in result["log"] + result["world_events"]),
result,
)
self.assertTrue(
any(line.startswith("Bezalel says:") and ("fire" in line.lower() or "forge" in line.lower()) for line in result["log"]),
result,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -38,7 +38,6 @@ class House(Enum):
EZRA = "ezra" # Archivist, reader
BEZALEL = "bezalel" # Artificer, builder
ALLEGRO = "allegro" # Tempo-and-dispatch, connected
SONNET = "sonnet" # Anthropic Claude Sonnet (cloud, high-reasoning)
class Mode(Enum):