File size: 7,639 Bytes
3ac4116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""

PromptManager: Simple prompt versioning and updates.

"""

from __future__ import annotations
import os
import json
import shutil
from datetime import datetime


class PromptManager:
    """

    Manages prompt versions and updates.

    

    Files:

        prompts/current_prompt.txt    (active prompt)

        prompts/base_prompt.txt       (original/fallback)

        logs/fullgame_history.json    (game log with prompts)

    """
    
    DEFAULT_PROMPT = """You are a strategic Stratego AI. The game rules are already provided.



STRATEGY FOCUS:

- Vary your moves - don't repeat the same move multiple times

- Use different pieces, not just one piece repeatedly

- Be cautious when attacking unknown pieces

- Protect high-value pieces (Marshal, General)

- Use Scouts to gather information before committing valuable pieces



OUTPUT:

- Respond with ONLY the move in format [SRC DST]

- No explanations, no reasoning, just the move

- Example: [D4 E4]"""
    
    def __init__(self, prompts_dir: str, logs_dir: str = "logs"):
        self.prompts_dir = prompts_dir
        self.logs_dir = logs_dir
        self.current_path = os.path.join(prompts_dir, "current_prompt.txt")
        self.base_path = os.path.join(prompts_dir, "base_prompt.txt")
        self.history_path = os.path.join(logs_dir, "fullgame_history.json")
        
        os.makedirs(prompts_dir, exist_ok=True)
        os.makedirs(logs_dir, exist_ok=True)
        self._ensure_base_prompt()
    
    def _ensure_base_prompt(self):
        """Create base prompt if it doesn't exist."""
        if not os.path.exists(self.base_path):
            with open(self.base_path, "w", encoding="utf-8") as f:
                f.write(self.DEFAULT_PROMPT)
    
    def get_base_prompt(self) -> str:
        """Get the base prompt (without any game-specific additions)."""
        if os.path.exists(self.base_path):
            with open(self.base_path, "r", encoding="utf-8") as f:
                return f.read()
        return self.DEFAULT_PROMPT

    @staticmethod
    def extract_improvements(prompt_text: str) -> list[str]:
        """Pull the strategic improvements section (lines starting with bullets)."""
        if not prompt_text:
            return []
        lines = prompt_text.splitlines()
        improvements: list[str] = []
        in_section = False
        for line in lines:
            if line.strip().startswith("--- STRATEGIC IMPROVEMENTS"):
                in_section = True
                continue
            if in_section:
                stripped = line.strip()
                if stripped.startswith(("??", "-", "•", "*")):
                    cleaned = stripped.lstrip("??•-* ").strip()
                    if cleaned:
                        improvements.append(f"• {cleaned}")
        return improvements

    @staticmethod
    def merge_improvements(existing: list[str], new: list[str], limit: int = 20) -> list[str]:
        """Deduplicate improvements while keeping order; cap length to avoid prompt bloat."""
        merged: list[str] = []
        seen = set()

        def _norm(s: str) -> str:
            s_clean = s.lstrip("•-*? ").strip().lower()
            return " ".join(s_clean.split())

        for item in (existing or []) + (new or []):
            if not item:
                continue
            key = _norm(item)
            if key in seen:
                continue
            seen.add(key)
            merged.append(item.strip())
            if len(merged) >= limit:
                break
        return merged

    @staticmethod
    def build_prompt(base_prompt: str, improvements: list[str]) -> str:
        """Reconstruct prompt with a merged improvements section."""
        section = ""
        if improvements:
            section = "\n\n--- STRATEGIC IMPROVEMENTS (from past games) ---\n" + "\n".join(improvements)
        return base_prompt.rstrip() + section
    
    def get_current_prompt(self) -> str:
        """Get the current active prompt (base + last game feedback)."""
        if os.path.exists(self.current_path):
            with open(self.current_path, "r", encoding="utf-8") as f:
                return f.read()
        return self.get_base_prompt()
    
    def update_prompt(self, new_prompt: str, reason: str = "", models: list = None, 

                       mistakes: list = None, game_duration_seconds: float = None,

                       total_turns: int = None, winner: int = None):
        """

        Update the current prompt and log the change.

        

        Args:

            new_prompt: The new prompt text

            reason: Why the prompt was updated

            models: List of models that played the game

            mistakes: List of mistake strings that were added

            game_duration_seconds: How long the game took in seconds

            total_turns: Total number of turns in the game

            winner: Player ID who won (0 or 1), None for draw

        """
        # Backup current
        if os.path.exists(self.current_path):
            backup = self.current_path.replace(".txt", "_prev.txt")
            shutil.copy2(self.current_path, backup)
        
        # Save new prompt
        with open(self.current_path, "w", encoding="utf-8") as f:
            f.write(new_prompt)
        
        # Log the update
        self._log_update(reason, new_prompt, models, mistakes, game_duration_seconds, total_turns, winner)
    
    def _log_update(self, reason: str, prompt_text: str = "", models: list = None, 

                    mistakes: list = None, game_duration_seconds: float = None,

                    total_turns: int = None, winner: int = None):
        """Log game and prompt update to history with full details."""
        history = []
        if os.path.exists(self.history_path):
            try:
                with open(self.history_path, "r", encoding="utf-8") as f:
                    history = json.load(f)
            except:
                history = []
        
        # Format duration nicely
        duration_str = ""
        if game_duration_seconds is not None:
            minutes = int(game_duration_seconds // 60)
            seconds = int(game_duration_seconds % 60)
            duration_str = f"{minutes}m {seconds}s"
        
        entry = {
            "game_number": len(history) + 1,
            "timestamp": datetime.now().isoformat(),
            "models": models or [],
            "winner": winner,
            "total_turns": total_turns,
            "game_duration": duration_str,
            "game_duration_seconds": round(game_duration_seconds, 2) if game_duration_seconds else None,
            "mistakes_detected": len(mistakes) if mistakes else 0,
            "mistakes_added": mistakes or [],
            "reason": reason,
            "prompt_text": prompt_text,
            "prompt_length": len(prompt_text)
        }
        history.append(entry)
        
        # Keep last 50 entries
        history = history[-50:]
        
        with open(self.history_path, "w", encoding="utf-8") as f:
            json.dump(history, f, indent=2)
    
    def reset_to_base(self):
        """Reset current prompt to base prompt."""
        if os.path.exists(self.base_path):
            shutil.copy2(self.base_path, self.current_path)
            with open(self.base_path, "r", encoding="utf-8") as f:
                base_text = f.read()
            self._log_update("Reset to base prompt", base_text, [], [], None, None, None)