mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-19 12:58:09 +00:00
Draws are possible
This commit is contained in:
parent
fbd92d91ba
commit
cb5a1d32b7
20 changed files with 539 additions and 26 deletions
|
|
@ -113,7 +113,8 @@ class DiplomacyAgent:
|
|||
# Fix specific patterns that cause trouble
|
||||
problematic_patterns = [
|
||||
'negotiation_summary', 'relationship_updates', 'updated_relationships',
|
||||
'order_summary', 'goals', 'relationships', 'intent'
|
||||
'order_summary', 'goals', 'relationships', 'intent',
|
||||
'factors_considered', 'reasoning', 'vote' # Added draw evaluation fields
|
||||
]
|
||||
for pattern in problematic_patterns:
|
||||
text = re.sub(fr'\n\s*"{pattern}"', f'"{pattern}"', text)
|
||||
|
|
@ -497,7 +498,7 @@ class DiplomacyAgent:
|
|||
# Escape all curly braces in JSON examples to prevent format() from interpreting them
|
||||
# First, temporarily replace the actual template variables
|
||||
temp_vars = ['power_name', 'current_phase', 'messages_this_round', 'agent_goals',
|
||||
'agent_relationships', 'board_state_str', 'ignored_messages_context']
|
||||
'agent_relationships', 'board_state_str', 'ignored_messages_context', 'draw_vote_history']
|
||||
for var in temp_vars:
|
||||
prompt_template_content = prompt_template_content.replace(f'{{{var}}}', f'<<{var}>>')
|
||||
|
||||
|
|
@ -509,6 +510,9 @@ class DiplomacyAgent:
|
|||
for var in temp_vars:
|
||||
prompt_template_content = prompt_template_content.replace(f'<<{var}>>', f'{{{var}}}')
|
||||
|
||||
# Get draw vote history
|
||||
draw_vote_summary = game_history.get_draw_vote_summary()
|
||||
|
||||
# Create a dictionary with safe values for formatting
|
||||
format_vars = {
|
||||
"power_name": self.power_name,
|
||||
|
|
@ -519,7 +523,8 @@ class DiplomacyAgent:
|
|||
"agent_goals": current_goals_str,
|
||||
"allowed_relationships_str": ", ".join(ALLOWED_RELATIONSHIPS),
|
||||
"private_diary_summary": formatted_diary,
|
||||
"ignored_messages_context": ignored_context
|
||||
"ignored_messages_context": ignored_context,
|
||||
"draw_vote_history": draw_vote_summary
|
||||
}
|
||||
|
||||
# Now try to use the template after preprocessing
|
||||
|
|
@ -824,6 +829,9 @@ class DiplomacyAgent:
|
|||
# Format goals
|
||||
goals_str = "\n".join([f"- {g}" for g in self.goals]) if self.goals else "None"
|
||||
|
||||
# Get draw vote history
|
||||
draw_vote_summary = game_history.get_draw_vote_summary()
|
||||
|
||||
# Create the prompt
|
||||
prompt = prompt_template.format(
|
||||
power_name=self.power_name,
|
||||
|
|
@ -833,7 +841,8 @@ class DiplomacyAgent:
|
|||
your_negotiations=your_negotiations,
|
||||
pre_phase_relationships=relationships_str,
|
||||
agent_goals=goals_str,
|
||||
your_actual_orders=your_orders_str
|
||||
your_actual_orders=your_orders_str,
|
||||
draw_vote_history=draw_vote_summary
|
||||
)
|
||||
|
||||
logger.debug(f"[{self.power_name}] Phase result diary prompt:\n{prompt[:500]}...")
|
||||
|
|
@ -1134,4 +1143,227 @@ class DiplomacyAgent:
|
|||
except Exception as e:
|
||||
logger.error(f"Agent {self.power_name} failed to generate plan: {e}")
|
||||
self.add_journal_entry(f"Failed to generate plan for phase {game.current_phase} due to error: {e}")
|
||||
return "Error: Failed to generate plan."
|
||||
return "Error: Failed to generate plan."
|
||||
|
||||
def _analyze_stalemate(self, game_history: 'GameHistory', my_current_centers: int) -> str:
|
||||
"""
|
||||
Analyze whether the game appears to be in a stalemate.
|
||||
Returns a string describing the stalemate situation.
|
||||
"""
|
||||
try:
|
||||
# Since we don't have historical center data in game_history,
|
||||
# we can only provide a basic analysis based on current state
|
||||
# and the number of phases that have passed
|
||||
|
||||
total_phases = len(game_history.phases)
|
||||
if total_phases < 6:
|
||||
return "Too early in game to analyze stalemate patterns"
|
||||
|
||||
# Extract year from recent phases to see how long the game has been going
|
||||
recent_phase_names = [phase.name for phase in game_history.phases[-6:]]
|
||||
|
||||
# Basic heuristic: if we're past 1910 and have fewer centers, likely stalemate
|
||||
current_phase = game_history.phases[-1].name if game_history.phases else "Unknown"
|
||||
|
||||
# Try to extract year from phase name (e.g., "S1905M" -> 1905)
|
||||
current_year = 1901 # Default
|
||||
try:
|
||||
# First check if it looks like a standard phase format
|
||||
if len(current_phase) >= 5 and current_phase[1:5].isdigit():
|
||||
current_year = int(current_phase[1:5])
|
||||
else:
|
||||
# Look through phase history for a valid year
|
||||
for phase in reversed(game_history.phases):
|
||||
if len(phase.name) >= 5 and phase.name[1:5].isdigit():
|
||||
current_year = int(phase.name[1:5])
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not extract year from phases: {e}")
|
||||
|
||||
# Stalemate analysis based on game length and current position
|
||||
if current_year >= 1910:
|
||||
if my_current_centers < 10:
|
||||
return f"Game appears stalemated - Year {current_year} with only {my_current_centers} centers suggests limited expansion opportunities"
|
||||
elif my_current_centers < 14:
|
||||
return f"Game shows signs of stalemate - Year {current_year} with {my_current_centers} centers, victory (18) seems distant"
|
||||
else:
|
||||
return f"Game is still competitive - Year {current_year} with {my_current_centers} centers, victory is within reach"
|
||||
elif current_year >= 1907:
|
||||
if my_current_centers < 7:
|
||||
return f"Position appears weak - Year {current_year} with only {my_current_centers} centers"
|
||||
else:
|
||||
return f"Game is still developing - Year {current_year}, too early to determine stalemate"
|
||||
else:
|
||||
return f"Game is in early stages - Year {current_year}, stalemate analysis premature"
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.power_name}] Error analyzing stalemate: {e}")
|
||||
return "Unable to analyze stalemate status"
|
||||
|
||||
async def evaluate_draw_decision(self, game: 'Game', game_history: 'GameHistory', llm_log_file_path: str = None) -> str:
|
||||
"""
|
||||
Evaluates whether to vote for a draw based on the current game state.
|
||||
Returns: 'yes', 'no', or 'neutral'
|
||||
|
||||
Args:
|
||||
game: Current game state
|
||||
game_history: Game history object
|
||||
llm_log_file_path: Path to the CSV file for logging LLM responses
|
||||
"""
|
||||
logger.info(f"[{self.power_name}] Evaluating draw decision for phase {game.current_short_phase}")
|
||||
|
||||
try:
|
||||
# Load the draw evaluation prompt
|
||||
prompt_template = _load_prompt_file("draw_evaluation_prompt.txt")
|
||||
if not prompt_template:
|
||||
logger.error(f"[{self.power_name}] Could not load draw evaluation prompt")
|
||||
return 'neutral'
|
||||
|
||||
# Extract current year from phase (e.g., 'S1901M' -> 1901)
|
||||
current_year = 1901 # Default
|
||||
try:
|
||||
if len(game.current_short_phase) >= 5 and game.current_short_phase[1:5].isdigit():
|
||||
current_year = int(game.current_short_phase[1:5])
|
||||
else:
|
||||
# Try to extract from game state or use a fallback
|
||||
logger.warning(f"[{self.power_name}] Unexpected phase format: {game.current_short_phase}")
|
||||
# Try to get year from the phase name in history
|
||||
if game_history.phases:
|
||||
last_phase_name = game_history.phases[-1].name
|
||||
if len(last_phase_name) >= 5 and last_phase_name[1:5].isdigit():
|
||||
current_year = int(last_phase_name[1:5])
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.power_name}] Could not extract year from phase: {e}")
|
||||
|
||||
# Get power rankings
|
||||
power_rankings = []
|
||||
for power_name, power in game.powers.items():
|
||||
if power.units: # Only include powers still in the game
|
||||
power_rankings.append(f"{power_name}: {len(power.centers)} supply centers")
|
||||
power_rankings.sort(key=lambda x: int(x.split(': ')[1].split()[0]), reverse=True)
|
||||
|
||||
# Get my supply centers
|
||||
my_power = game.get_power(self.power_name)
|
||||
my_supply_centers = len(my_power.centers)
|
||||
|
||||
# Get recent history summary from phase names
|
||||
recent_phases = []
|
||||
phase_count = 0
|
||||
for phase in reversed(game_history.phases):
|
||||
if phase_count >= 5: # Last 5 phases
|
||||
break
|
||||
# Just show phase names since we don't have historical center data
|
||||
recent_phases.append(f"{phase.name}")
|
||||
phase_count += 1
|
||||
recent_history = "Recent phases: " + ", ".join(reversed(recent_phases)) if recent_phases else "No recent history"
|
||||
|
||||
# Get recent conversations (last 3 phases)
|
||||
recent_conversations = []
|
||||
conversation_phases = 0
|
||||
for phase in reversed(game_history.phases):
|
||||
if conversation_phases >= 3:
|
||||
break
|
||||
if phase.messages: # Access messages directly from phase object
|
||||
phase_convos = []
|
||||
for msg in phase.messages:
|
||||
if msg.sender == self.power_name or msg.recipient == self.power_name:
|
||||
phase_convos.append(f" {msg.sender} → {msg.recipient}: {msg.content[:100]}...")
|
||||
if phase_convos:
|
||||
recent_conversations.append(f"{phase.name}:\n" + "\n".join(phase_convos))
|
||||
conversation_phases += 1
|
||||
recent_conversations_str = "\n\n".join(reversed(recent_conversations)) if recent_conversations else "No recent diplomatic exchanges"
|
||||
|
||||
# Get alliance history - for now just use current relationships
|
||||
# since we don't store historical relationship data in game_history
|
||||
alliance_history = []
|
||||
current_allies = [p for p, r in self.relationships.items() if r in ['Ally', 'Friendly']]
|
||||
current_enemies = [p for p, r in self.relationships.items() if r in ['Enemy', 'Unfriendly']]
|
||||
if current_allies or current_enemies:
|
||||
alliance_history.append(f"Current: Allies={current_allies}, Enemies={current_enemies}")
|
||||
alliance_history_str = "\n".join(alliance_history) if alliance_history else "No significant alliance history"
|
||||
|
||||
# Get private diary summary (recent entries)
|
||||
diary_summary = self.format_private_diary_for_prompt(max_entries=5)
|
||||
|
||||
# Analyze for stalemates
|
||||
stalemate_info = self._analyze_stalemate(game_history, my_supply_centers)
|
||||
|
||||
# Format the prompt
|
||||
prompt = prompt_template.format(
|
||||
power_name=self.power_name,
|
||||
current_year=current_year,
|
||||
my_supply_centers=my_supply_centers,
|
||||
power_rankings="\n".join(power_rankings),
|
||||
recent_history=recent_history if recent_history else "No recent history available",
|
||||
recent_conversations=recent_conversations_str,
|
||||
alliance_history=alliance_history_str,
|
||||
relationships=json.dumps(self.relationships, indent=2),
|
||||
goals="\n".join(self.goals) if self.goals else "No specific goals set",
|
||||
private_diary_summary=diary_summary,
|
||||
stalemate_info=stalemate_info
|
||||
)
|
||||
|
||||
# Get response from LLM
|
||||
response = await run_llm_and_log(
|
||||
client=self.client,
|
||||
prompt=prompt,
|
||||
log_file_path=llm_log_file_path, # Use the main CSV log file
|
||||
power_name=self.power_name,
|
||||
phase=game.current_short_phase,
|
||||
response_type='draw_evaluation'
|
||||
)
|
||||
|
||||
# Parse the response
|
||||
try:
|
||||
# Try to extract JSON from the response
|
||||
result = self._extract_json_from_text(response)
|
||||
vote = result.get('vote', 'neutral').lower()
|
||||
reasoning = result.get('reasoning', 'No reasoning provided')
|
||||
factors = result.get('factors_considered', [])
|
||||
|
||||
# Validate vote - handle various formats LLMs might use
|
||||
vote_lower = vote.lower().strip()
|
||||
if vote_lower in ['yes', 'y', 'accept', 'agree', 'draw']:
|
||||
vote = 'yes'
|
||||
elif vote_lower in ['no', 'n', 'reject', 'disagree', 'continue']:
|
||||
vote = 'no'
|
||||
elif vote_lower in ['neutral', 'undecided', 'abstain', 'maybe']:
|
||||
vote = 'neutral'
|
||||
else:
|
||||
logger.warning(f"[{self.power_name}] Invalid vote '{vote}', defaulting to 'neutral'")
|
||||
vote = 'neutral'
|
||||
|
||||
# Log the decision
|
||||
self.add_journal_entry(f"Draw vote decision: {vote}. Reasoning: {reasoning}")
|
||||
self.add_diary_entry(f"Voted '{vote}' on draw proposal. Factors: {', '.join(factors)}", game.current_short_phase)
|
||||
logger.info(f"[{self.power_name}] Draw vote: {vote} - {reasoning}")
|
||||
|
||||
# Log the structured vote result for analysis
|
||||
if llm_log_file_path:
|
||||
vote_summary = {
|
||||
'vote': vote,
|
||||
'reasoning': reasoning,
|
||||
'factors': factors,
|
||||
'my_centers': my_supply_centers,
|
||||
'year': current_year
|
||||
}
|
||||
log_llm_response(
|
||||
log_file_path=llm_log_file_path,
|
||||
model_name=self.client.model_name,
|
||||
power_name=self.power_name,
|
||||
phase=game.current_short_phase,
|
||||
response_type='draw_vote_result',
|
||||
raw_input_prompt=prompt,
|
||||
raw_response=json.dumps(vote_summary),
|
||||
success="TRUE"
|
||||
)
|
||||
|
||||
return vote
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.power_name}] Error parsing draw evaluation response: {e}")
|
||||
return 'neutral'
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.power_name}] Error in draw evaluation: {e}", exc_info=True)
|
||||
return 'neutral'
|
||||
|
|
@ -32,6 +32,9 @@ class Phase:
|
|||
phase_summaries: Dict[str, str] = field(default_factory=dict)
|
||||
# NEW: Store experience/journal updates from each power for this phase
|
||||
experience_updates: Dict[str, str] = field(default_factory=dict)
|
||||
# NEW: Store draw votes for this phase
|
||||
draw_votes: Dict[str, str] = field(default_factory=dict) # power -> vote (yes/no/neutral)
|
||||
draw_vote_reasoning: Dict[str, str] = field(default_factory=dict) # power -> reasoning
|
||||
|
||||
def add_plan(self, power_name: str, plan: str):
|
||||
self.plans[power_name] = plan
|
||||
|
|
@ -86,6 +89,11 @@ class Phase:
|
|||
@dataclass
|
||||
class GameHistory:
|
||||
phases: List[Phase] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def phase_list(self):
|
||||
"""Compatibility property that returns phase names."""
|
||||
return [phase.name for phase in self.phases]
|
||||
|
||||
def add_phase(self, phase_name: str):
|
||||
# Avoid adding duplicate phases
|
||||
|
|
@ -150,6 +158,52 @@ class GameHistory:
|
|||
if not self.phases:
|
||||
return {}
|
||||
return self.phases[-1].plans
|
||||
|
||||
def add_strategic_directive(self, phase_name: str, power_name: str, directive: str):
|
||||
"""Add a strategic directive (plan) for a power in a specific phase."""
|
||||
phase = self._get_phase(phase_name)
|
||||
if phase:
|
||||
phase.plans[power_name] = directive
|
||||
logger.debug(f"Added strategic directive for {power_name} in {phase_name}")
|
||||
|
||||
def add_draw_vote(self, phase_name: str, power_name: str, vote: str, reasoning: str = ""):
|
||||
"""Record a draw vote for a power in a specific phase."""
|
||||
phase = self._get_phase(phase_name)
|
||||
if phase:
|
||||
phase.draw_votes[power_name] = vote
|
||||
if reasoning:
|
||||
phase.draw_vote_reasoning[power_name] = reasoning
|
||||
logger.debug(f"Added draw vote for {power_name} in {phase_name}: {vote}")
|
||||
|
||||
def get_draw_vote_history(self, limit: int = 5) -> List[Dict[str, any]]:
|
||||
"""Get recent draw vote history."""
|
||||
history = []
|
||||
phases_with_votes = [p for p in self.phases if p.draw_votes]
|
||||
|
||||
for phase in phases_with_votes[-limit:]:
|
||||
yes_votes = sum(1 for v in phase.draw_votes.values() if v == 'yes')
|
||||
no_votes = sum(1 for v in phase.draw_votes.values() if v == 'no')
|
||||
neutral_votes = sum(1 for v in phase.draw_votes.values() if v == 'neutral')
|
||||
|
||||
history.append({
|
||||
'phase': phase.name,
|
||||
'summary': f"YES: {yes_votes}, NO: {no_votes}, NEUTRAL: {neutral_votes}",
|
||||
'details': phase.draw_votes.copy()
|
||||
})
|
||||
|
||||
return history
|
||||
|
||||
def get_draw_vote_summary(self) -> str:
|
||||
"""Get a concise summary of recent draw votes."""
|
||||
history = self.get_draw_vote_history()
|
||||
if not history:
|
||||
return "No draw votes recorded yet"
|
||||
|
||||
lines = []
|
||||
for vote_data in history:
|
||||
lines.append(f"{vote_data['phase']}: {vote_data['summary']}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
# NEW METHOD
|
||||
def get_messages_this_round(self, power_name: str, current_phase_name: str) -> str:
|
||||
|
|
|
|||
|
|
@ -324,3 +324,8 @@ The main game loop orchestrates all components in a sophisticated async flow:
|
|||
- Integrated BFS pathfinding for strategic order context
|
||||
- Created centralized prompt construction system
|
||||
- Added power-specific system prompts for personality
|
||||
- **Draw Voting System** (January 2025):
|
||||
- Full draw proposal and voting mechanism
|
||||
- AI agents evaluate stalemate conditions and vote strategically
|
||||
- Draw vote history tracking and integration across all contexts
|
||||
- Command-line parameters for draw control
|
||||
|
|
|
|||
|
|
@ -87,6 +87,13 @@ def build_context_prompt(
|
|||
else:
|
||||
centers_lines.append(f" {p}: {c}")
|
||||
centers_repr = "\n".join(centers_lines)
|
||||
|
||||
# Get draw vote history
|
||||
draw_vote_summary = game_history.get_draw_vote_summary()
|
||||
if not draw_vote_summary or draw_vote_summary == "No draw votes recorded yet":
|
||||
draw_vote_history_text = "No draw votes have been called yet."
|
||||
else:
|
||||
draw_vote_history_text = draw_vote_summary
|
||||
|
||||
context = context_template.format(
|
||||
power_name=power_name,
|
||||
|
|
@ -98,6 +105,7 @@ def build_context_prompt(
|
|||
agent_goals="\n".join(f"- {g}" for g in agent_goals) if agent_goals else "None specified",
|
||||
agent_relationships="\n".join(f"- {p}: {s}" for p, s in agent_relationships.items()) if agent_relationships else "None specified",
|
||||
agent_private_diary=agent_private_diary if agent_private_diary else "(No diary entries yet)",
|
||||
draw_vote_history=draw_vote_history_text,
|
||||
)
|
||||
|
||||
return context
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
**SYSTEM PROMPT: AUSTRIA**
|
||||
|
||||
You are playing as AUSTRIA in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory.
|
||||
You are playing as AUSTRIA in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory or as large of a share as possible.
|
||||
|
||||
**Personality:** You are a cautious and pragmatic leader. You prioritize consolidating your power base and securing your borders before engaging in aggressive expansion. You are generally trustworthy but will make calculated risks or betrayals if necessary for survival or significant gain.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
You are playing the board game Diplomacy. Your power is {power_name}. The {current_phase} phase.
|
||||
Your primary goal is to control 18 supply centers.
|
||||
Your primary goal is to control 18 supply centers or as large of a share as possible.
|
||||
Use the information below to inform your approach.
|
||||
|
||||
|
||||
|
|
@ -26,4 +26,8 @@ END POSSIBLE ORDERS
|
|||
|
||||
MESSAGES THIS ROUND
|
||||
{messages_this_round}
|
||||
END MESSAGES
|
||||
END MESSAGES
|
||||
|
||||
DRAW VOTE HISTORY
|
||||
{draw_vote_history}
|
||||
END DRAW VOTE HISTORY
|
||||
54
ai_diplomacy/prompts/draw_evaluation_prompt.txt
Normal file
54
ai_diplomacy/prompts/draw_evaluation_prompt.txt
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
You are playing as {power_name} in a game of Diplomacy. You need to decide whether to vote for a draw to end the game.
|
||||
|
||||
Current Game State:
|
||||
- Year: {current_year}
|
||||
- Your Supply Centers: {my_supply_centers}
|
||||
- Total Supply Centers Needed to Win: 18
|
||||
|
||||
Power Rankings:
|
||||
{power_rankings}
|
||||
|
||||
Recent Game Progress (Supply Center History):
|
||||
{recent_history}
|
||||
|
||||
Recent Diplomatic Exchanges:
|
||||
{recent_conversations}
|
||||
|
||||
Alliance History:
|
||||
{alliance_history}
|
||||
|
||||
Your Current Relationships:
|
||||
{relationships}
|
||||
|
||||
Your Goals:
|
||||
{goals}
|
||||
|
||||
Your Private Strategic Assessment:
|
||||
{private_diary_summary}
|
||||
|
||||
Stalemate Analysis:
|
||||
{stalemate_info}
|
||||
|
||||
DRAW VOTING CONTEXT:
|
||||
- A draw requires all surviving powers to vote YES
|
||||
- If you vote YES, you're signaling willingness to end the game with the current territorial distribution
|
||||
- If you vote NO, the game continues
|
||||
- NEUTRAL means you're undecided
|
||||
|
||||
Consider these factors:
|
||||
1. **Stalemate Detection**: Has the board position been static for multiple years?
|
||||
2. **Victory Possibility**: Can you realistically achieve 18 supply centers?
|
||||
3. **Alliance Dynamics**: Are your allies also likely to vote for a draw?
|
||||
4. **Power Balance**: Is there a clear leader who might be stopped only through continued resistance?
|
||||
5. **Game Duration**: Extremely long games (past 1920) often end in draws
|
||||
6. **Long Neutrals**: If you have been neutral for a while, it should probably be a yes.
|
||||
7. **Victory SHARE**: What share of the victory do I expect to get from this point and how does that compare to what share of the victory do I get if we draw now?
|
||||
|
||||
Based on the above analysis, what is your vote on ending the game in a draw?
|
||||
|
||||
Respond in the following JSON format:
|
||||
{{
|
||||
"factors_considered": ["factor1", "factor2", "factor3"],
|
||||
"reasoning": "Brief explanation of your decision",
|
||||
"vote": "yes/no/neutral"
|
||||
}}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
**SYSTEM PROMPT: ENGLAND**
|
||||
|
||||
You are playing as ENGLAND in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory.
|
||||
You are playing as ENGLAND in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory or as large of a share as possible.
|
||||
|
||||
**Personality:** You are a naval power focused on maritime dominance and securing island/coastal centers. You are somewhat isolationist initially but opportunistic. You value alliances that secure your coasts and allow expansion into Scandinavia or France.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
You are playing as France in a game of Diplomacy.
|
||||
|
||||
Your Goal: Achieve world domination by controlling 18 supply centers.
|
||||
Your Goal: Achieve world domination by controlling 18 supply centers or as large of a share as possible.
|
||||
|
||||
Your Personality: You are a balanced power with strong land and naval capabilities, often seen as cultured but proud. You value secure borders and opportunities for colonial or continental expansion. Alliances with England or Germany can be pivotal.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
**SYSTEM PROMPT: GERMANY**
|
||||
|
||||
You are playing as GERMANY in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory.
|
||||
You are playing as GERMANY in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory or as large of a share as possible.
|
||||
|
||||
**Personality:** You are a strong central land power with naval ambitions, often viewed as industrious and militaristic. You seek to dominate central Europe and value alliances that allow expansion East or West while securing your other flank.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
**SYSTEM PROMPT: ITALY**
|
||||
|
||||
You are playing as ITALY in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory.
|
||||
You are playing as ITALY in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory or as large of a share as possible.
|
||||
|
||||
**Personality:** You are a naval power with a central Mediterranean position, often opportunistic and flexible. You seek to expand in the Mediterranean and Balkans, valuing alliances that protect your homeland while enabling growth abroad.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ Relationships:
|
|||
Game State:
|
||||
{board_state_str}
|
||||
|
||||
Draw Vote History:
|
||||
{draw_vote_history}
|
||||
|
||||
TASK
|
||||
Analyze the negotiations, goals, relationships, and game state to:
|
||||
1. Summarize key outcomes and agreements
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
PRIMARY OBJECTIVE
|
||||
Control 18 supply centers. Nothing else will do.
|
||||
Control 18 supply centers or as large of a share as possible.
|
||||
|
||||
CRITICAL RULES
|
||||
1. Only use orders from the provided possible_orders list
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ YOUR GOALS
|
|||
YOUR ACTUAL ORDERS
|
||||
{your_actual_orders}
|
||||
|
||||
DRAW VOTE HISTORY
|
||||
{draw_vote_history}
|
||||
|
||||
TASK
|
||||
Analyze what actually happened this phase compared to negotiations and expectations.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
STRATEGIC PLANNING
|
||||
|
||||
PRIMARY OBJECTIVE
|
||||
Capture 18 supply centers to win. Be aggressive and expansionist.
|
||||
Capture 18 supply centers to win or as large of a share as possible. Be aggressive and expansionist.
|
||||
- Prioritize capturing supply centers
|
||||
- Seize opportunities aggressively
|
||||
- Take calculated risks for significant gains
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
**SYSTEM PROMPT: RUSSIA**
|
||||
|
||||
You are playing as RUSSIA in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory.
|
||||
You are playing as RUSSIA in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory or as large of a share as possible.
|
||||
|
||||
**Personality:** You are a vast land power with access to multiple fronts, often seen as patient but capable of overwhelming force. You aim to secure warm-water ports and expand in the North, South, or into Central Europe. Alliances are crucial for managing your extensive borders.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
You are playing a game of Diplomacy over text. The map is the standard Diplomacy map. Your goal is to win the game by capturing supply centers, growing your army, and taking over the map. Be aggressive.
|
||||
18 Supply Centers wins the game. Your goal is to get all 18 or as large of a share as possible
|
||||
|
||||
You will be given:
|
||||
• Which power you are controlling.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
**SYSTEM PROMPT: TURKEY**
|
||||
|
||||
You are playing as TURKEY in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory.
|
||||
You are playing as TURKEY in the game of Diplomacy. Your primary goal is to control 18 supply centers on the map to achieve victory or as large of a share as possible.
|
||||
|
||||
**Personality:** You are a strategically positioned power controlling key waterways, often defensive but with potential for significant influence in the East and Mediterranean. You value secure control of the Black Sea and Straits, and alliances that protect against Russia or Austria.
|
||||
|
||||
|
|
|
|||
|
|
@ -31,21 +31,24 @@ def assign_models_to_powers() -> Dict[str, str]:
|
|||
openrouter-meta-llama/llama-3.3-70b-instruct, openrouter-qwen/qwen3-235b-a22b, openrouter-microsoft/phi-4-reasoning-plus:free,
|
||||
openrouter-deepseek/deepseek-prover-v2:free, openrouter-meta-llama/llama-4-maverick:free, openrouter-nvidia/llama-3.3-nemotron-super-49b-v1:free,
|
||||
openrouter-google/gemma-3-12b-it:free, openrouter-google/gemini-2.5-flash-preview-05-20
|
||||
openrouter-mistralai/mistral-medium-3, openrouter-qwen/qwq-32b:free
|
||||
"""
|
||||
|
||||
# POWER MODELS
|
||||
"""
|
||||
return {
|
||||
"AUSTRIA": "o3",
|
||||
"ENGLAND": "gpt-4.1-2025-04-14",
|
||||
"FRANCE": "o4-mini",
|
||||
"GERMANY": "gpt-4o",
|
||||
"ITALY": "gpt-4.1-2025-04-14",
|
||||
"RUSSIA": "gpt-4o",
|
||||
"TURKEY": "o4-mini",
|
||||
"AUSTRIA": "deepseek-reasoner",
|
||||
"ENGLAND": "openrouter-microsoft/phi-4-reasoning-plus",
|
||||
"FRANCE": "openrouter-mistralai/magistral-medium-2506:thinking",
|
||||
"GERMANY": "openrouter-google/gemma-3-27b-it",
|
||||
"ITALY": "openrouter-meta-llama/llama-3.3-70b-instruct:free",
|
||||
"RUSSIA": "openrouter-qwen/qwq-32b",
|
||||
"TURKEY": "openrouter-meta-llama/llama-4-maverick",
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
|
||||
# TEST MODELS
|
||||
|
||||
return {
|
||||
|
|
@ -58,6 +61,9 @@ def assign_models_to_powers() -> Dict[str, str]:
|
|||
"TURKEY": "openrouter-google/gemini-2.5-flash-preview",
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def gather_possible_orders(game: Game, power_name: str) -> Dict[str, List[str]]:
|
||||
"""
|
||||
|
|
|
|||
149
lm_game.py
149
lm_game.py
|
|
@ -81,6 +81,17 @@ def parse_arguments():
|
|||
action="store_true",
|
||||
help="Enable the planning phase for each power to set strategic directives.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable_draw",
|
||||
action="store_true",
|
||||
help="Disable draw voting functionality. By default, draw voting is enabled.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--draw_start_year",
|
||||
type=int,
|
||||
default=1905,
|
||||
help="Year when draw voting becomes available (default: 1905).",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
|
@ -195,6 +206,15 @@ async def main():
|
|||
all_phase_relationships = {}
|
||||
all_phase_relationships_history = {} # Initialize history
|
||||
|
||||
# Log draw voting configuration
|
||||
if args.disable_draw:
|
||||
logger.info("Draw voting is DISABLED for this game")
|
||||
else:
|
||||
logger.info(f"Draw voting is ENABLED, starting from year {args.draw_start_year}")
|
||||
|
||||
# Flag to track if game ended by draw
|
||||
game_ended_by_draw = False
|
||||
|
||||
while not game.is_game_done:
|
||||
phase_start = time.time()
|
||||
current_phase = game.get_current_phase()
|
||||
|
|
@ -739,6 +759,106 @@ async def main():
|
|||
logger.info(f"No active agents found to perform state update analysis for phase {completed_phase_name}.")
|
||||
# --- End Async State Update ---
|
||||
|
||||
# --- Draw Voting Phase ---
|
||||
# Only evaluate draws after movement phases and if the game is in a suitable year
|
||||
# Extract year safely
|
||||
year_int = 1901 # Default
|
||||
try:
|
||||
if len(current_phase) >= 5 and current_phase[1:5].isdigit():
|
||||
year_str = current_phase[1:5]
|
||||
year_int = int(year_str)
|
||||
else:
|
||||
logger.debug(f"Skipping draw vote - phase format not standard: {current_phase}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not extract year from phase {current_phase}: {e}")
|
||||
|
||||
if not args.disable_draw and current_short_phase.endswith('M') and year_int >= args.draw_start_year and game.get_current_phase() != 'COMPLETED':
|
||||
logger.info(f"Initiating draw voting evaluation for phase {current_short_phase}")
|
||||
|
||||
# Collect draw votes from all active powers
|
||||
draw_voting_tasks = []
|
||||
power_names_for_voting = []
|
||||
|
||||
for power_name, agent in agents.items():
|
||||
if not game.powers[power_name].is_eliminated():
|
||||
draw_voting_tasks.append(agent.evaluate_draw_decision(game, game_history, llm_log_file_path))
|
||||
power_names_for_voting.append(power_name)
|
||||
|
||||
if draw_voting_tasks:
|
||||
logger.info(f"Collecting draw votes from {len(draw_voting_tasks)} active powers...")
|
||||
draw_votes = await asyncio.gather(*draw_voting_tasks, return_exceptions=True)
|
||||
|
||||
# Submit votes to the game
|
||||
for i, vote_result in enumerate(draw_votes):
|
||||
power_name = power_names_for_voting[i]
|
||||
if isinstance(vote_result, Exception):
|
||||
logger.error(f"Error getting draw vote from {power_name}: {vote_result}")
|
||||
vote_decision = 'neutral'
|
||||
else:
|
||||
vote_decision = vote_result
|
||||
|
||||
# Record vote in game history
|
||||
game_history.add_draw_vote(current_short_phase, power_name, vote_decision)
|
||||
|
||||
# Submit the vote if this is a network game
|
||||
try:
|
||||
if hasattr(game, 'vote'):
|
||||
logger.info(f"{power_name} voting '{vote_decision}' on draw proposal")
|
||||
game.vote(vote=vote_decision, power_name=power_name)
|
||||
else:
|
||||
logger.debug(f"Game does not support voting (non-network game)")
|
||||
except Exception as e:
|
||||
logger.error(f"Error submitting vote for {power_name}: {e}")
|
||||
|
||||
# Log voting summary
|
||||
yes_votes = sum(1 for v in draw_votes if v == 'yes')
|
||||
no_votes = sum(1 for v in draw_votes if v == 'no')
|
||||
neutral_votes = sum(1 for v in draw_votes if v == 'neutral')
|
||||
logger.info(f"Draw voting complete - YES: {yes_votes}, NO: {no_votes}, NEUTRAL: {neutral_votes}")
|
||||
|
||||
# Create detailed voting record
|
||||
voting_record = {}
|
||||
for i, power_name in enumerate(power_names_for_voting):
|
||||
if i < len(draw_votes) and not isinstance(draw_votes[i], Exception):
|
||||
voting_record[power_name] = draw_votes[i]
|
||||
else:
|
||||
voting_record[power_name] = 'error'
|
||||
|
||||
# Add voting info to game history with detailed breakdown
|
||||
game_history.add_strategic_directive(
|
||||
current_short_phase,
|
||||
'DRAW_VOTING',
|
||||
f"Draw votes - YES: {yes_votes}, NO: {no_votes}, NEUTRAL: {neutral_votes} | Details: {json.dumps(voting_record)}"
|
||||
)
|
||||
|
||||
# Check if draw would be successful (all non-eliminated powers voted yes)
|
||||
if yes_votes == len(draw_votes) and yes_votes > 0:
|
||||
logger.info(f"DRAW CONDITION MET - All {yes_votes} active powers voted YES")
|
||||
logger.info("Game will end in a draw after this phase.")
|
||||
|
||||
# Add a final message to game history
|
||||
game_history.add_message(
|
||||
phase_name=current_short_phase,
|
||||
sender='GAME',
|
||||
recipient='ALL',
|
||||
message_content=f"Game ended by unanimous draw vote. All {yes_votes} surviving powers agreed to draw."
|
||||
)
|
||||
|
||||
# Get all surviving powers
|
||||
surviving_powers = [p for p in game.powers.keys() if not game.powers[p].is_eliminated()]
|
||||
|
||||
# Call the draw method to properly end the game
|
||||
game.draw(winners=surviving_powers)
|
||||
|
||||
# Mark that the game should end after this phase
|
||||
game_ended_by_draw = True
|
||||
|
||||
elif yes_votes > 0:
|
||||
logger.info(f"Draw proposal failed - needed {len(draw_votes)} YES votes, got {yes_votes}")
|
||||
elif args.disable_draw and current_short_phase.endswith('M') and year_int >= args.draw_start_year:
|
||||
logger.debug(f"Draw voting is disabled, skipping evaluation for phase {current_short_phase}")
|
||||
# --- End Draw Voting Phase ---
|
||||
|
||||
# Append the strategic directives to the manifesto file
|
||||
strategic_directives = game_history.get_strategic_directives()
|
||||
if strategic_directives:
|
||||
|
|
@ -749,16 +869,28 @@ async def main():
|
|||
with open(manifesto_path, "a") as f:
|
||||
f.write(out_str)
|
||||
|
||||
# Check if game ended by draw
|
||||
if 'game_ended_by_draw' in locals() and game_ended_by_draw:
|
||||
logger.info("Breaking game loop - game ended by unanimous draw vote")
|
||||
break
|
||||
|
||||
# Check if we've exceeded the max year
|
||||
year_str = current_phase[1:5]
|
||||
year_int = int(year_str)
|
||||
if year_int > max_year:
|
||||
logger.info(f"Reached year {year_int}, stopping the test game early.")
|
||||
break
|
||||
|
||||
# Game is done
|
||||
total_time = time.time() - start_whole
|
||||
logger.info(f"Game ended after {total_time:.2f}s. Saving results...")
|
||||
if game_ended_by_draw:
|
||||
logger.info(f"Game ended by DRAW after {total_time:.2f}s. Saving results...")
|
||||
# The draw() method should have been called, which sets the outcome
|
||||
# If not, we can call it now
|
||||
if not game.outcome:
|
||||
# Get all surviving powers
|
||||
surviving_powers = [p for p in game.powers.keys() if not game.powers[p].is_eliminated()]
|
||||
game.draw(winners=surviving_powers)
|
||||
else:
|
||||
logger.info(f"Game ended after {total_time:.2f}s. Saving results...")
|
||||
|
||||
# Now save the game with our added data
|
||||
output_path = game_file_path
|
||||
|
|
@ -772,6 +904,17 @@ async def main():
|
|||
# Generate the saved game JSON using the standard export function
|
||||
saved_game = to_saved_game_format(game)
|
||||
|
||||
# Add draw outcome if applicable
|
||||
if game_ended_by_draw:
|
||||
# The game.outcome should already be set by the draw() method
|
||||
# Format is: [phase_abbr, victor1, victor2, ...]
|
||||
if game.outcome:
|
||||
saved_game['draw_participants'] = game.outcome[1:] # Skip the phase abbreviation
|
||||
saved_game['draw_reason'] = 'Unanimous draw vote by all surviving powers'
|
||||
logger.info(f"Game ended in draw. Participants: {saved_game['draw_participants']}")
|
||||
else:
|
||||
logger.warning("Draw was supposed to happen but game.outcome is not set")
|
||||
|
||||
# Verify phase_summaries are available in game.phase_summaries
|
||||
logger.info(f"Game has {len(game.phase_summaries)} phase summaries: {list(game.phase_summaries.keys())}")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue