189 lines
5.3 KiB
Python
189 lines
5.3 KiB
Python
|
|
#!/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.")
|