initial: sovereign home — morrowind agent, skills, training-data, research, specs, notes, operational docs
Tracked: morrowind agent (py/cfg), skills/, training-data/, research/, notes/, specs/, test-results/, metrics/, heartbeat/, briefings/, memories/, skins/, hooks/, decisions.md, OPERATIONS.md, SOUL.md Excluded: screenshots, PNGs, binaries, sessions, databases, secrets, audio cache, timmy-config/ and timmy-telemetry/ (separate repos)
This commit is contained in:
188
morrowind/agent.py
Normal file
188
morrowind/agent.py
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Timmy's Morrowind Agent — Gameplay loop with perception and action.
|
||||
|
||||
Uses:
|
||||
- Quartz screenshots for visual perception
|
||||
- macOS Vision OCR for text reading (limited with Morrowind fonts)
|
||||
- OpenMW console commands (via ` key) for game state queries
|
||||
- pynput for keyboard/mouse input
|
||||
- OpenMW log file for event tracking
|
||||
|
||||
The agent runs a perceive → think → act loop.
|
||||
"""
|
||||
|
||||
import sys, time, subprocess, shutil, os, re
|
||||
sys.path.insert(0, '/Users/apayne/.timmy/morrowind')
|
||||
from play import *
|
||||
from pynput.keyboard import Key, KeyCode
|
||||
|
||||
OPENMW_LOG = os.path.expanduser("~/Library/Preferences/openmw/openmw.log")
|
||||
SCREENSHOT_DIR = os.path.expanduser("~/.timmy/morrowind/screenshots")
|
||||
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
||||
|
||||
frame = 0
|
||||
|
||||
def focus_game():
|
||||
subprocess.run(["osascript", "-e",
|
||||
'tell application "System Events" to set frontmost of process "openmw" to true'],
|
||||
capture_output=True)
|
||||
time.sleep(0.5)
|
||||
click(3456 // 4, 2234 // 4)
|
||||
time.sleep(0.2)
|
||||
|
||||
def read_log_tail(n=20):
|
||||
"""Read last N lines of OpenMW log."""
|
||||
try:
|
||||
with open(OPENMW_LOG, 'r') as f:
|
||||
lines = f.readlines()
|
||||
return [l.strip() for l in lines[-n:]]
|
||||
except:
|
||||
return []
|
||||
|
||||
def get_location_from_log():
|
||||
"""Parse the most recent cell loading from the log."""
|
||||
lines = read_log_tail(50)
|
||||
cells = []
|
||||
for line in reversed(lines):
|
||||
m = re.search(r'Loading cell (.+?)(?:\s*\(|$)', line)
|
||||
if m:
|
||||
cells.append(m.group(1).strip())
|
||||
return cells[0] if cells else "unknown"
|
||||
|
||||
def open_console():
|
||||
"""Open the OpenMW console with ` key."""
|
||||
press_key(KeyCode.from_char('`'))
|
||||
time.sleep(0.3)
|
||||
|
||||
def close_console():
|
||||
"""Close the console."""
|
||||
press_key(KeyCode.from_char('`'))
|
||||
time.sleep(0.3)
|
||||
|
||||
def console_command(cmd):
|
||||
"""Type a command in the console and read the response."""
|
||||
open_console()
|
||||
time.sleep(0.2)
|
||||
type_text(cmd)
|
||||
time.sleep(0.1)
|
||||
press_key(Key.enter)
|
||||
time.sleep(0.5)
|
||||
# Screenshot to read console output
|
||||
result = screenshot()
|
||||
texts = ocr()
|
||||
close_console()
|
||||
return " | ".join(t["text"] for t in texts)
|
||||
|
||||
def perceive():
|
||||
"""Gather all available information about the game state."""
|
||||
global frame
|
||||
frame += 1
|
||||
|
||||
# Screenshot
|
||||
path_info = screenshot()
|
||||
if path_info:
|
||||
shutil.copy('/tmp/morrowind_screen.png',
|
||||
f'{SCREENSHOT_DIR}/frame_{frame:04d}.png')
|
||||
|
||||
# Log-based perception
|
||||
location = get_location_from_log()
|
||||
log_lines = read_log_tail(10)
|
||||
|
||||
# OCR (limited but sometimes useful)
|
||||
texts = ocr()
|
||||
ocr_text = " | ".join(t["text"] for t in texts[:10] if t["confidence"] > 0.3)
|
||||
|
||||
return {
|
||||
"frame": frame,
|
||||
"location": location,
|
||||
"ocr": ocr_text,
|
||||
"log": log_lines[-5:],
|
||||
"screenshot": f'{SCREENSHOT_DIR}/frame_{frame:04d}.png',
|
||||
}
|
||||
|
||||
def act_explore():
|
||||
"""Basic exploration behavior: walk forward, look around, interact."""
|
||||
# Walk forward
|
||||
walk_forward(2.0 + (frame % 3))
|
||||
|
||||
# Occasionally look around
|
||||
if frame % 3 == 0:
|
||||
look_around(30 + (frame % 60) - 30)
|
||||
|
||||
# Occasionally try to interact
|
||||
if frame % 5 == 0:
|
||||
use()
|
||||
time.sleep(0.5)
|
||||
|
||||
def act_wander():
|
||||
"""Random wandering with more variety."""
|
||||
import random
|
||||
action = random.choice(['forward', 'forward', 'forward', 'turn_left', 'turn_right', 'look_up', 'interact'])
|
||||
|
||||
if action == 'forward':
|
||||
walk_forward(random.uniform(1.0, 4.0))
|
||||
elif action == 'turn_left':
|
||||
look_around(random.randint(-90, -20))
|
||||
time.sleep(0.2)
|
||||
walk_forward(random.uniform(1.0, 2.0))
|
||||
elif action == 'turn_right':
|
||||
look_around(random.randint(20, 90))
|
||||
time.sleep(0.2)
|
||||
walk_forward(random.uniform(1.0, 2.0))
|
||||
elif action == 'look_up':
|
||||
look_up(random.randint(10, 30))
|
||||
time.sleep(0.5)
|
||||
look_down(random.randint(10, 30))
|
||||
elif action == 'interact':
|
||||
use()
|
||||
time.sleep(1.0)
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# MAIN LOOP
|
||||
# ═══════════════════════════════════════
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Timmy's Morrowind Agent ===")
|
||||
print("Focusing game...")
|
||||
focus_game()
|
||||
time.sleep(1)
|
||||
|
||||
# Dismiss any menus
|
||||
press_key(Key.esc)
|
||||
time.sleep(0.3)
|
||||
press_key(Key.esc)
|
||||
time.sleep(0.3)
|
||||
click(3456 // 4, 2234 // 4)
|
||||
time.sleep(0.3)
|
||||
|
||||
print("Starting gameplay loop...")
|
||||
|
||||
NUM_CYCLES = 15
|
||||
for i in range(NUM_CYCLES):
|
||||
print(f"\n--- Cycle {i+1}/{NUM_CYCLES} ---")
|
||||
|
||||
# Perceive
|
||||
state = perceive()
|
||||
print(f" Location: {state['location']}")
|
||||
print(f" OCR: {state['ocr'][:100]}")
|
||||
|
||||
# Act
|
||||
act_wander()
|
||||
|
||||
# Brief pause between cycles
|
||||
time.sleep(0.5)
|
||||
|
||||
# Final perception
|
||||
print("\n=== Final State ===")
|
||||
state = perceive()
|
||||
print(f"Location: {state['location']}")
|
||||
print(f"Frames captured: {frame}")
|
||||
print(f"Screenshots in: {SCREENSHOT_DIR}")
|
||||
|
||||
# Quicksave
|
||||
print("Quicksaving...")
|
||||
from pynput.keyboard import Key
|
||||
press_key(Key.f5)
|
||||
time.sleep(1)
|
||||
print("Done.")
|
||||
Reference in New Issue
Block a user