File size: 8,022 Bytes
0e61be5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env python3
"""
Wikipedia Speedrun CLI - Play the Wikipedia game with any agent.

Usage:
    python scripts/play.py --start "Python (programming language)" --target "Philosophy"
    python scripts/play.py --start "Cat" --target "Dog" --agent precomputed
    python scripts/play.py --start "Albert Einstein" --target "Pizza" --agent human --visualize
    python scripts/play.py --start "Random" --target "Random" --agent live
    python scripts/play.py --start "Potato" --target "Barack Obama" --agent llm --model google/gemini-2.0-flash-exp:free

Agents:
    human       - You play! Pick links manually (requires --visualize)
    random      - Random baseline
    precomputed - Greedy using pre-computed title embeddings
    live        - Greedy using on-the-fly sentence-transformers
    hybrid      - Pre-computed filter + live re-rank
    oracle      - BFS optimal path (uses pre-computed graph)
    live-oracle - BFS optimal path (live Wikipedia scraping, slow but complete)
    llm         - LLM via OpenRouter (use --model to specify)

LLM Models:
    Use any OpenRouter model ID directly (e.g., "anthropic/claude-haiku-4.5").

    Full model list with pricing: data/openrouter_models.json
    Analyze models: python scripts/analyze_models.py

    Examples:
        google/gemini-2.0-flash-exp:free  - FREE Google model (default)
        meta-llama/llama-3.3-70b-instruct:free - FREE Llama model
        openai/gpt-4o-mini                - Budget ($0.15/1M input)
        anthropic/claude-haiku-4.5        - Fast ($0.80/1M input)
        deepseek/deepseek-chat            - Value ($0.14/1M input)
"""

from __future__ import annotations

import argparse
import logging
import os
import random
import sys
import warnings
from pathlib import Path

# Suppress TensorFlow and protobuf warnings before any imports
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"  # Suppress TF logging
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"  # Suppress oneDNN messages
warnings.filterwarnings("ignore", category=UserWarning, module="google.protobuf")
warnings.filterwarnings("ignore", category=DeprecationWarning)

# Fix Windows console encoding
if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")

# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))

from src.agents import get_agent  # noqa: E402
from src.game import GameEngine  # noqa: E402


def get_random_article() -> str:
    """Get a random traversable article from the dataset."""
    from src.data.loader import wiki_data

    # Get a random article that has outgoing links
    while True:
        idx = random.randint(0, wiki_data.article_count() - 1)
        title = wiki_data.get_title(idx)
        if wiki_data.is_traversable(title):
            return title


def parse_args() -> argparse.Namespace:
    """Parse command line arguments."""
    parser = argparse.ArgumentParser(
        description="Play the Wikipedia Speedrun game",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )

    parser.add_argument(
        "--start",
        type=str,
        required=True,
        help="Starting article title (or 'Random' for random)",
    )
    parser.add_argument(
        "--target",
        type=str,
        required=True,
        help="Target article title (or 'Random' for random)",
    )
    parser.add_argument(
        "--agent",
        type=str,
        default="precomputed",
        choices=["human", "random", "precomputed", "live", "hybrid", "oracle", "live-oracle", "llm"],
        help="Agent to use (default: precomputed)",
    )
    parser.add_argument(
        "--model",
        type=str,
        default="gemini-2.0-flash-exp:free",
        help="LLM model for --agent llm (default: gemini-2.0-flash-exp:free)",
    )
    parser.add_argument(
        "--prefilter",
        type=int,
        default=None,
        help="Pre-filter links to top N using embeddings (default: no filter)",
    )
    parser.add_argument(
        "--visualize",
        action="store_true",
        help="Show browser visualization with Playwright",
    )
    parser.add_argument(
        "--max-steps",
        type=int,
        default=50,
        help="Maximum steps before game is lost (default: 50)",
    )
    parser.add_argument(
        "--slow-mo",
        type=int,
        default=0,
        help="Slow down visualization by this many ms per action",
    )
    parser.add_argument(
        "--verbose",
        "-v",
        action="store_true",
        help="Enable verbose logging",
    )

    return parser.parse_args()


def main() -> int:
    """Main entry point."""
    args = parse_args()

    # Set up logging
    log_level = logging.DEBUG if args.verbose else logging.INFO
    logging.basicConfig(
        level=log_level,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        datefmt="%H:%M:%S",
    )

    # Handle random article selection
    start = args.start
    target = args.target

    if start.lower() == "random":
        print("Selecting random start article...")
        start = get_random_article()
        print(f"  Start: {start}")

    if target.lower() == "random":
        print("Selecting random target article...")
        target = get_random_article()
        # Make sure it's different from start
        while target == start:
            target = get_random_article()
        print(f"  Target: {target}")

    # Get agent
    try:
        agent_kwargs = {}
        if args.agent == "llm":
            agent_kwargs["model"] = args.model
            if args.prefilter:
                agent_kwargs["prefilter"] = args.prefilter
        agent = get_agent(args.agent, **agent_kwargs)
    except ValueError as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

    # Pre-warm the agent (load models, data) before starting the game timer
    print("Warming up agent...")
    if hasattr(agent, "_ensure_loaded"):
        agent._ensure_loaded()
    print("Ready!")

    print("\n" + "=" * 60)
    print("Wikipedia Speedrun")
    print("=" * 60)
    print(f"  Start:  {start}")
    print(f"  Target: {target}")
    print(f"  Agent:  {agent.name} - {agent.description}")
    print(f"  Visualize: {args.visualize or agent.requires_visualization}")
    print("=" * 60 + "\n")

    # Create engine and run game
    visualize = args.visualize or agent.requires_visualization

    with GameEngine(visualize=visualize, slow_mo=args.slow_mo) as engine:
        try:
            result = engine.run(
                agent=agent,
                start=start,
                target=target,
                max_steps=args.max_steps,
            )
        except KeyboardInterrupt:
            print("\n\nGame interrupted by user")
            return 130  # Standard exit code for Ctrl+C

    # Print results
    print("\n" + "=" * 60)
    if result.won:
        print(f"Victory! Reached '{result.target_title}' in {result.total_clicks} clicks")
    else:
        print(f"Game Over. Did not reach '{result.target_title}'")
    print("=" * 60)

    print("\nPath taken:")
    for i, title in enumerate(result.path):
        marker = " (START)" if i == 0 else " (TARGET)" if title == result.target_title else ""
        print(f"  {i}. {title}{marker}")

    print(f"\nTotal time: {result.total_time_ms / 1000:.2f} seconds")
    print(f"Average decision time: {sum(s.decision_time_ms for s in result.steps) / len(result.steps):.0f}ms" if result.steps else "")

    # Print LLM stats if applicable
    if hasattr(agent, "get_stats"):
        stats = agent.get_stats()
        if stats.get("total_requests", 0) > 0:
            print(f"\nLLM Stats:")
            print(f"  Model: {stats['model']}")
            print(f"  Requests: {stats['total_requests']}")
            print(f"  Total tokens: {stats['total_tokens']}")
            print(f"  API time: {stats['total_time_seconds']}s")

    return 0 if result.won else 1


if __name__ == "__main__":
    sys.exit(main())