Draws are possible

This commit is contained in:
AlxAI 2025-06-10 14:56:52 -04:00
parent fbd92d91ba
commit cb5a1d32b7
20 changed files with 539 additions and 26 deletions

View file

@ -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'

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View 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"
}}

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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]]:
"""

View file

@ -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())}")