Add Hermes Reasoner mode to battleship with game ending fixes

- Add new Hermes Reasoner AI mode that combines probability analysis with LLM reasoning
- Implement pre-script and post-script architecture in app.py for flexible YAML processing
- Fix game ending detection by adding transition override mechanism
- Add probability matrix visualization and strategic move analysis
- Support both legacy processing_script and new pre_script/post_script naming
- Restore full ship complement for complete battleship gameplay
This commit is contained in:
Russell Ballestrini 2025-08-10 11:57:22 -04:00
parent 38f414c5a9
commit 368c7d290e
2 changed files with 459 additions and 68 deletions

127
app.py
View file

@ -62,10 +62,12 @@ for i in range(MAX_ENDPOINTS):
continue
# API key is optional; if not provided, use a default.
api_key = os.environ.get(f"MODEL_API_KEY_{i}", "not-needed")
ENDPOINTS.append({
"base_url": endpoint,
"api_key": api_key,
})
ENDPOINTS.append(
{
"base_url": endpoint,
"api_key": api_key,
}
)
if not ENDPOINTS:
raise Exception("No MODEL_ENDPOINT_x environment variables found!")
@ -121,6 +123,7 @@ def get_client_for_model(model_name: str):
print(f"Completion Endpoint Processing: {MODEL_CLIENT_MAP[model_name][1]}")
return MODEL_CLIENT_MAP[model_name][0]
def get_openai_client_and_model(
model_name="adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
):
@ -353,21 +356,25 @@ def search_messages(keywords):
# Split the keywords by spaces and sanitize
keyword_list = keywords.lower().split()
# Sanitize keywords to prevent SQL injection
sanitized_keywords = []
for keyword in keyword_list:
# Remove potentially dangerous characters and limit length
sanitized_keyword = ''.join(c for c in keyword if c.isalnum() or c.isspace() or c in '-_')[:50]
sanitized_keyword = "".join(
c for c in keyword if c.isalnum() or c.isspace() or c in "-_"
)[:50]
if sanitized_keyword.strip(): # Only add non-empty keywords
sanitized_keywords.append(sanitized_keyword.strip())
if not sanitized_keywords:
return {}
# Search for messages containing any of the sanitized keywords using parameterized query
messages = Message.query.filter(
db.or_(*[Message.content.ilike(f"%{keyword}%") for keyword in sanitized_keywords])
db.or_(
*[Message.content.ilike(f"%{keyword}%") for keyword in sanitized_keywords]
)
).all()
for message in messages:
@ -827,7 +834,6 @@ def chat_gpt(username, room_name, model_name="gpt-4o-mini"):
if "o4-" in model_name:
temperature = 1
with app.app_context():
room = get_room(room_name)
last_messages = (
@ -1168,6 +1174,7 @@ def generate_dalle_image(room_name, message, username):
# Create an HTML img tag with the base64 data (escape user input for XSS protection)
import html
escaped_message = html.escape(message)
escaped_prompt = html.escape(revised_prompt)
content = f'<img src="data:image/jpeg;base64,{image_data}" alt="{escaped_message}"><p>{escaped_prompt}</p>'
@ -1449,24 +1456,28 @@ def get_activity_content(file_path):
if app.config["LOCAL_ACTIVITIES"]:
# Load the activity YAML from a local file with path traversal protection
import os.path
# Normalize the path and ensure it's within the research directory
normalized_path = os.path.normpath(file_path)
# Ensure path doesn't contain dangerous patterns
if '..' in normalized_path or normalized_path.startswith('/'):
if ".." in normalized_path or normalized_path.startswith("/"):
raise ValueError(f"Invalid file path: {file_path}")
# Ensure file is within research directory and has .yaml extension
if not normalized_path.startswith('research/') or not normalized_path.endswith('.yaml'):
raise ValueError(f"File must be in research/ directory and end with .yaml: {file_path}")
if not normalized_path.startswith("research/") or not normalized_path.endswith(
".yaml"
):
raise ValueError(
f"File must be in research/ directory and end with .yaml: {file_path}"
)
# Additional safety check - ensure resolved path is still in research dir
full_path = os.path.abspath(normalized_path)
research_dir = os.path.abspath('research/')
research_dir = os.path.abspath("research/")
if not full_path.startswith(research_dir):
raise ValueError(f"Path traversal attempt detected: {file_path}")
with open(normalized_path, "r") as file:
activity_yaml = file.read()
else:
@ -1725,6 +1736,17 @@ def handle_activity_response(room_name, user_response, username):
# Check if the step has a question
if "question" in step:
# Execute pre-script if it exists (runs before categorization)
if "pre_script" in step:
print(f"DEBUG: Executing pre-script")
pre_result = execute_processing_script(
activity_state.dict_metadata, step["pre_script"]
)
# Update metadata with pre-script results
for key, value in pre_result.get("metadata", {}).items():
activity_state.add_metadata(key, value)
print(f"DEBUG: Pre-script completed, updated metadata")
# Categorize the user's response
category = categorize_response(
step["question"],
@ -1956,12 +1978,15 @@ def handle_activity_response(room_name, user_response, username):
metadata_tmp_keys.append(random_key)
activity_state.add_metadata(random_key, random_value)
# Execute the processing script if it exists
if "processing_script" in step and transition.get(
"run_processing_script", False
# Execute the post-script if it exists (supports both old and new naming)
post_script = step.get("post_script") or step.get("processing_script")
if post_script and (
transition.get("run_post_script", False)
or transition.get("run_processing_script", False)
):
print(f"DEBUG: Executing post-script")
result = execute_processing_script(
activity_state.dict_metadata, step["processing_script"]
activity_state.dict_metadata, post_script
)
plot_image_base64 = result.pop("plot_image", None)
@ -1974,6 +1999,13 @@ def handle_activity_response(room_name, user_response, username):
for key, value in result.get("metadata", {}).items():
activity_state.add_metadata(key, value)
# Check if processing script wants to override the transition
if "next_section_and_step" in result:
next_section_and_step = result["next_section_and_step"]
print(
f"DEBUG: Processing script overriding transition to: {next_section_and_step}"
)
# Check if the result contains a plot image
if plot_image_base64:
plot_image_html = f'<img alt="Plot Image" src="data:image/png;base64,{plot_image_base64}">'
@ -2106,6 +2138,7 @@ def handle_activity_response(room_name, user_response, username):
"off_topic",
]
or activity_state.attempts >= activity_state.max_attempts
or next_section_and_step # Processing script override takes precedence
):
if next_section_and_step:
(
@ -2346,14 +2379,24 @@ def get_next_step(activity_content, current_section_id, current_step_id):
def categorize_response(question, response, buckets, tokens_for_ai):
openai_client, model_name = get_openai_client_and_model()
bucket_list = ", ".join([str(bucket) for bucket in buckets])
# Check if tokens_for_ai already includes format instructions (ANALYSIS/BUCKET format)
if "ANALYSIS:" in tokens_for_ai and "BUCKET:" in tokens_for_ai:
# YAML already specifies output format, don't override
system_content = f"{tokens_for_ai}"
user_content = f"Question: {question}\nResponse: {response}"
else:
# Use old simple format for backwards compatibility
system_content = f"{tokens_for_ai} Categorize the following response into one of the following buckets: {bucket_list}. Return ONLY a bucket label."
user_content = f"Question: {question}\nResponse: {response}\n\nCategory:"
messages = [
{
"role": "system",
"content": f"{tokens_for_ai} Categorize the following response into one of the following buckets: {bucket_list}. Return ONLY a bucket label.",
"content": system_content,
},
{
"role": "user",
"content": f"Question: {question}\nResponse: {response}\n\nCategory:",
"content": user_content,
},
]
@ -2362,12 +2405,40 @@ def categorize_response(question, response, buckets, tokens_for_ai):
model=model_name,
messages=messages,
n=1,
max_tokens=10,
max_tokens=150, # Increased for ANALYSIS + BUCKET format
temperature=0,
)
category = (
completion.choices[0].message.content.strip().lower().replace(" ", "_")
)
full_response = completion.choices[0].message.content.strip()
print(f"DEBUG BUCKET CATEGORIZATION: Full Hermes response: {full_response}")
# Handle both ANALYSIS/BUCKET format and simple bucket response
if "BUCKET:" in full_response:
# New ANALYSIS/BUCKET format
bucket_lines = [
line for line in full_response.split("\n") if "BUCKET:" in line
]
if bucket_lines:
category = (
bucket_lines[0]
.split("BUCKET:")[1]
.strip()
.lower()
.replace(" ", "_")
)
else:
category = full_response.lower().replace(" ", "_")
elif "ANALYSIS:" in full_response:
# Has analysis but no explicit BUCKET: line, try to extract from end
lines = [line.strip() for line in full_response.split("\n") if line.strip()]
if lines:
category = lines[-1].lower().replace(" ", "_")
else:
category = full_response.lower().replace(" ", "_")
else:
# Simple bucket response (old format)
category = full_response.lower().replace(" ", "_")
print(f"DEBUG BUCKET CATEGORIZATION: Extracted category: {category}")
return category
except Exception as e:
return f"Error: {e}"

View file

@ -1,18 +1,18 @@
default_max_attempts_per_step: 9
tokens_for_ai_rubric: |
based on the game without knowing where each ship was, score the process each player used to target ships.
be sure to look for moves or strategies in the game play that where _not_ smart given the obvious information uncovered.
use chain-of-thought to reason about the progression of the game and the winner.
first summarize the game, we don't need the turn by turn plays.
the game was battleship. the moves were done 1 by 1.
the game was battleship. the moves were done 1 by 1.
the grid is 0-99.
did any player blunder as the information was learned?
There was a user and an AI playing.
Depending on the game mode the player chooses they are going up against a different algo,
@ -27,10 +27,14 @@ tokens_for_ai_rubric: |
* super human hunter
* keeps track of hits and uses a probability grid normalized to 100 and always picks the max or random of any 100.
* keeps track of hits and uses a probability grid normalized to 100 and always picks the max or random of any 100.
* hermes reasoner
* uses the probability algorithm paired with hermes to reason for 3 sentences about what the next 0-99 move should be given the current game state and turn number
Did any player miss sinking a ship that was found? was it due to end game or a blunder?
Do not mix up ships, keep careful track of the order they were found and sunk.
sections:
@ -50,15 +54,17 @@ sections:
- step_id: "step_1"
title: "Choose AI Mode"
question: "Choose the AI mode: Random, Hunter, or Super Human Hunter?"
question: "Choose the AI mode: Random, Hunter, Super Human Hunter, or Hermes Reasoner?"
tokens_for_ai: |
If the user chooses Random, categorize as 'random_mode'.
If the user chooses Hunter, categorize as 'hunter_mode'.
If the user chooses Super Human Hunter, categorize as 'super_hunter_mode'.
If the user chooses Hermes Reasoner, categorize as 'hermes_reasoner_mode'.
feedback_tokens_for_ai: |
If the user chooses Random, acknowledge the choice.
If the user chooses Hunter, acknowledge the choice.
If the user chooses Super Human Hunter, acknowledge the choice.
If the user chooses Random, say: "Random mode selected! The AI will make completely random moves."
If the user chooses Hunter, say: "Hunter mode selected! The AI will systematically hunt around hits."
If the user chooses Super Human Hunter, say: "Super Human Hunter mode selected! The AI will use advanced probability analysis."
If the user chooses Hermes Reasoner, say: "Hermes Reasoner mode selected! The AI will use probability analysis combined with reasoning to make strategic decisions."
processing_script: |
import random
@ -97,7 +103,7 @@ sections:
return board
user_board = place_ships()
ai_board = place_ships()
ai_board = place_ships() # AI also gets randomly placed ships
script_result = {
"metadata": {
@ -110,6 +116,7 @@ sections:
- random_mode
- hunter_mode
- super_hunter_mode
- hermes_reasoner_mode
transitions:
random_mode:
run_processing_script: True
@ -132,22 +139,53 @@ sections:
metadata_add:
ai_mode: "super_hunter"
next_section_and_step: "section_1:step_2"
hermes_reasoner_mode:
run_processing_script: True
ai_feedback:
tokens_for_ai: "Hermes Reasoner Mode enabled for the AI. The AI will use probability analysis combined with reasoning to make strategic decisions."
metadata_add:
ai_mode: "hermes_reasoner"
next_section_and_step: "section_1:step_2"
- step_id: "step_2"
title: "Take a Shot"
question: "Choose a position to fire at (0-99)."
pre_script: |
# Check if moves match winning moves from previous turn
user_winning_move = metadata.get("user_winning_move")
ai_winning_move = metadata.get("ai_winning_move")
user_shot_input = metadata.get("user_shot", "")
print(f"PRE-SCRIPT DEBUG: user_shot_input = '{user_shot_input}', user_winning_move = {user_winning_move}, ai_winning_move = {ai_winning_move}")
ai_shot = metadata.get("ai_shot")
is_game_ending_move = False
# Check if user move wins
if user_shot_input and user_shot_input.isdigit():
user_move = int(user_shot_input)
if user_winning_move is not None and user_move == user_winning_move:
is_game_ending_move = True
print(f"PRE-SCRIPT: User winning move detected! user_move={user_move} matches user_winning_move={user_winning_move}")
# Check if AI move wins (from previous turn)
if ai_shot is not None and ai_winning_move is not None and ai_shot == ai_winning_move:
is_game_ending_move = True
print(f"PRE-SCRIPT: AI winning move detected! ai_shot={ai_shot} matches ai_winning_move={ai_winning_move}")
script_result = {
"metadata": {
"is_game_ending_move": is_game_ending_move
}
}
tokens_for_ai: |
1) If the user reply is *only* digits, and corresponds to a grid cell (099),
treat it as a valid move:
If the response matches the regex /^\d+$/ and 0 ≤ int(response) < 100, categorize as 'valid_move'.
2) Otherwise fall back to the usual buckets:
If the user wants to restart or play again, categorize as 'restart'.
If the user wants to exit, categorize as 'exit'.
Otherwise, categorize as 'invalid_move'.
Note: by ordering the digit-check *first*, you guarantee that “1”, “42”, etc.
always lands in 'valid_move' no matter what the LLM would otherwise decide.
feedback_tokens_for_ai: |
Important: Use the metadata to fill in the brackets and provide a conversational tone.
@ -156,22 +194,22 @@ sections:
The user's latest shot was [user_hit_result]:
- If user_hit_result is "hit", consider saying: "Great shot! [user_name] hit an AI ship!"
- If user_hit_result is "miss", consider saying: "Oh no, [user_name] missed the shot. Better luck next time!"
On a new line, announce the AI's move: "The AI fired at position [ai_shot] and it was a [ai_hit_result]."
The AI's latest shot was a [ai_hit_result]:
- If ai_hit_result is "hit", consider saying: "The AI hit one of [user_name]'s ships!"
- If ai_hit_result is "miss", consider saying: "The AI missed [user_name]'s ships this time."
If either [user_sunk_ship_this_round] or [ai_sunk_ship_this_round] is not None,
announce the destruction in a LOT of detail, use many sentences:
- If user_sunk_ship_this_round is not None, consider saying: "The user has sunk the AI's [user_sunk_ship_this_round]!"
- If ai_sunk_ship_this_round is not None, consider saying: "The AI has sunk the [user_name]'s [ai_sunk_ship_this_round]!"
If game_over = True, determine the winner:
- If user_wins = True, consider saying: "Congratulations! The [user_name] has sunk all AI ships and won the game!"
- If ai_wins = True, consider saying: "The AI has sunk all [user_name] ships and won the game!"
If game_over = True, suggest: "Would you like to restart and play again, or would you prefer to exit?"
processing_script: |
@ -179,6 +217,8 @@ sections:
import matplotlib.pyplot as plt
import io
import base64
import requests
import json
# Define ship sizes
ship_sizes = {
@ -201,6 +241,8 @@ sections:
# Retrieve the game state
user_board = metadata.get("user_board")
ai_board = metadata.get("ai_board")
# Normal processing code
user_shots = metadata.get("user_shots", [])
ai_shots = metadata.get("ai_shots", [])
user_hits = metadata.get("user_hits", [])
@ -217,7 +259,31 @@ sections:
# AI state variables
ai_mode = metadata.get("ai_mode", "random")
probability_matrix = metadata.get("probability_matrix", [[1] * 10 for _ in range(10)])
# Initialize probability matrix with realistic ship placement probabilities
if "probability_matrix" not in metadata:
probability_matrix = [[0] * 10 for _ in range(10)]
# Calculate how many ship placements use each cell
ship_lengths = [5, 4, 3, 3, 2]
for y in range(10):
for x in range(10):
count = 0
for ship_len in ship_lengths:
# Horizontal ships that would cover this cell
for start_x in range(max(0, x - ship_len + 1), min(x + 1, 10 - ship_len + 1)):
count += 1
# Vertical ships that would cover this cell
for start_y in range(max(0, y - ship_len + 1), min(y + 1, 10 - ship_len + 1)):
count += 1
probability_matrix[y][x] = count
print("DEBUG: Initial probability matrix created")
# Debug print the initial grid
print("DEBUG: Initial grid:")
for row in probability_matrix:
print(f" {' '.join(f'{x:2d}' for x in row)}")
else:
probability_matrix = metadata.get("probability_matrix")
print("DEBUG: Using existing probability matrix")
hits = metadata.get("hits", [])
misses = metadata.get("misses", [])
sunk_ships = metadata.get("sunk_ships", [])
@ -299,11 +365,210 @@ sections:
return True
return False
# Function to generate Hermes reasoning
def hermes_reason_move(game_state, turn_number, top_candidates):
global ai_hits, ai_shots, ai_sunk_ships, probability_matrix
import os
import requests
import json
# Get Hermes endpoint from environment
hermes_endpoint = os.environ.get('MODEL_ENDPOINT_1', 'https://hermes.ai.unturf.com/v1')
hermes_api_key = os.environ.get('MODEL_API_KEY_1', '')
# Prepare game state summary
hits_summary = f"AI hits so far: {len(ai_hits)} positions hit"
misses_summary = f"AI misses so far: {len(ai_shots) - len(ai_hits)} positions missed"
sunk_ships_summary = f"Ships sunk: {len(ai_sunk_ships)} out of 5"
available_positions = [i for i in range(100) if i not in ai_shots]
top_six_candidates = top_candidates[0:6] if len(top_candidates) >= 6 else top_candidates
# Create reasoning prompt
prompt = (
f"You are an expert Battleship AI. Turn {turn_number}.\n\n"
f"CRITICAL: You MUST choose from these TOP probability positions: {top_six_candidates}\n\n"
f"Game Data:\n"
f"- {hits_summary}\n"
f"- {misses_summary}\n"
f"- {sunk_ships_summary}\n\n"
f"INSTRUCTIONS: Pick ONE number from {top_six_candidates} - these are the mathematically optimal targets.\n\n"
f"Format your response EXACTLY like this:\n\n"
f"ANALYSIS: [3 sentences explaining why you chose from the top probability positions]\n\n"
f"MOVE: [ONE number from this list: {top_six_candidates}]\n\n"
f"You MUST pick from {top_six_candidates} - do not pick any other number."
)
try:
headers = {
'Authorization': f'Bearer {hermes_api_key}',
'Content-Type': 'application/json'
}
data = {
'model': 'adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic',
'messages': [{'role': 'user', 'content': prompt}],
'max_tokens': 300,
'temperature': 0.5
}
response = requests.post(f'{hermes_endpoint}/chat/completions',
headers=headers, json=data, timeout=10)
print(f"DEBUG: API Status: {response.status_code}")
if response.status_code == 200:
result = response.json()
reasoning = result['choices'][0]['message']['content'].strip()
print(f"DEBUG: Real API response: {reasoning}")
return reasoning
else:
print(f"DEBUG: API failed with status {response.status_code}: {response.text[:200]}")
fallback_move = top_candidates[0] if top_candidates else random.choice([i for i in range(100) if i not in ai_shots])
return f"ANALYSIS: Turn {turn_number} suggests targeting high-probability zones based on mathematical analysis. The current hit pattern indicates potential ship orientations that guide strategic decisions. Focusing on adjacent unexplored cells maximizes discovery potential.\n\nMOVE: {fallback_move}"
except Exception as e:
print(f"DEBUG: API exception: {str(e)}")
fallback_move = top_candidates[0] if top_candidates else random.choice([i for i in range(100) if i not in ai_shots])
return f"ANALYSIS: After {turn_number} turns, probability analysis guides optimal targeting strategies. Current data suggests focusing on clustered high-value positions for maximum efficiency. Strategic patience combined with mathematical precision will yield victory.\n\nMOVE: {fallback_move}"
# AI chooses a shot
def choose_ai_shot():
global can_fit_ship, update_probability, generate_hunt_targets, random_search, probability_matrix, ai_mode, ai_shots, random, user_board, ai_hits, ai_hit_result
global can_fit_ship, update_probability, generate_hunt_targets, random_search, probability_matrix, ai_mode, ai_shots, random, user_board, ai_hits, ai_hit_result, hermes_reason_move, ship_sizes, ai_sunk_ships
if ai_mode == "super_hunter":
if ai_mode == "hermes_reasoner":
# Use probability algorithm + Hermes reasoning
# Update probability matrix based on shots
remaining_ships = [ship for ship in ship_sizes.keys() if ship not in ai_sunk_ships]
remaining_ship_sizes = [ship_sizes[ship] for ship in remaining_ships]
print(f"DEBUG: Remaining ships: {remaining_ships}")
print(f"DEBUG: Total shots: {len(ai_shots)}, Hits: {len(ai_hits)}, Misses: {len(ai_shots) - len(ai_hits)}")
# Recalculate entire probability matrix
new_probability_matrix = [[0] * 10 for _ in range(10)]
for y in range(10):
for x in range(10):
pos = y * 10 + x
if pos in ai_shots:
new_probability_matrix[y][x] = 0 # Already shot
else:
# Count how many ship placements could use this cell
for ship_size in remaining_ship_sizes:
# Check horizontal placements
for start_x in range(max(0, x - ship_size + 1), min(x + 1, 10 - ship_size + 1)):
valid = True
includes_hit = False
for dx in range(ship_size):
check_pos = y * 10 + (start_x + dx)
if check_pos in ai_shots and check_pos not in ai_hits:
valid = False # Ship can't go through a miss
break
if check_pos in ai_hits:
includes_hit = True
if valid:
# Base probability for valid placement
new_probability_matrix[y][x] += 1
# Bonus if it includes a hit
if includes_hit:
new_probability_matrix[y][x] += 10
# Check vertical placements
for start_y in range(max(0, y - ship_size + 1), min(y + 1, 10 - ship_size + 1)):
valid = True
includes_hit = False
for dy in range(ship_size):
check_pos = (start_y + dy) * 10 + x
if check_pos in ai_shots and check_pos not in ai_hits:
valid = False # Ship can't go through a miss
break
if check_pos in ai_hits:
includes_hit = True
if valid:
# Base probability for valid placement
new_probability_matrix[y][x] += 1
# Bonus if it includes a hit
if includes_hit:
new_probability_matrix[y][x] += 10
# Replace the old matrix with the new one
probability_matrix = new_probability_matrix
# Boost probabilities around unsunk hits
for hit_pos in ai_hits:
hit_x, hit_y = hit_pos % 10, hit_pos // 10
# Check if this hit is part of a sunk ship
hit_is_sunk = False
for ship_name in ai_sunk_ships:
# This would need ship position tracking to work properly
pass # Skip for now, assume all hits need chasing
if not hit_is_sunk:
# Boost adjacent cells
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
adj_x, adj_y = hit_x + dx, hit_y + dy
if 0 <= adj_x < 10 and 0 <= adj_y < 10:
adj_pos = adj_y * 10 + adj_x
if adj_pos not in ai_shots:
# Only boost if not already boosted
if probability_matrix[adj_y][adj_x] < 50:
probability_matrix[adj_y][adj_x] = 50 # Set to fixed high value instead of adding
# Find top 6 highest probability positions
position_probs = []
for i in range(100):
if i not in ai_shots: # Only consider unshot positions
x, y = i % 10, i // 10
position_probs.append((probability_matrix[y][x], i))
# Sort by probability (descending) and take top positions
position_probs.sort(reverse=True)
candidates = [pos for prob, pos in position_probs[:20]] # Take top 20 for variety
max_prob = position_probs[0][0] if position_probs else 0
# Fallback if no candidates found
if not candidates:
candidates = [i for i in range(100) if i not in ai_shots]
# Debug: Log what we're working with
turn_number = len(ai_shots) + 1
print(f"DEBUG: Turn {turn_number}, Max prob: {max_prob}")
print("DEBUG: Probability grid:")
for y in range(10):
row = [f"{probability_matrix[y][x]:2d}" for x in range(10)]
print(f" {' '.join(row)}")
print(f"DEBUG: Top candidates: {candidates[:10]}")
reasoning_response = hermes_reason_move("battleship", turn_number, candidates)
# Analysis already logged in hermes_reason_move function
# Extract move from response - try multiple parsing methods
try:
if "MOVE:" in reasoning_response:
move_part = reasoning_response.split("MOVE:")[1].strip()
ai_shot = int(move_part.split()[0])
print(f"DEBUG: Hermes Move: {ai_shot} (from 'MOVE: {move_part.split()[0]}')")
else:
# Fallback: extract any number from the response that's in candidates
import re
numbers = re.findall(r'\b(\d+)\b', reasoning_response)
valid_moves = [int(n) for n in numbers if int(n) in candidates and int(n) not in ai_shots]
if valid_moves:
ai_shot = valid_moves[0]
print(f"DEBUG: Hermes Move (parsed): {ai_shot} from numbers {numbers}")
else:
raise Exception(f"ERROR: Hermes response had no valid moves! Response: {reasoning_response}, Candidates: {candidates}")
# Validate the shot is legal
if ai_shot in ai_shots or ai_shot < 0 or ai_shot > 99:
ai_shot = random.choice(candidates)
print(f"DEBUG: Invalid shot, using fallback: {ai_shot}")
except Exception as e:
ai_shot = random.choice(candidates)
print(f"DEBUG: Parse error: {e}, using fallback: {ai_shot}")
elif ai_mode == "super_hunter":
# Use probabilistic grid algorithm
max_prob = 0
candidates = []
@ -335,11 +600,11 @@ sections:
if user_board[ai_shot] != -1:
ai_hits.append(ai_shot)
ai_hit_result = "hit"
if ai_mode == "super_hunter":
if ai_mode == "super_hunter" or ai_mode == "hermes_reasoner":
update_probability(ai_shot % 10, ai_shot // 10, True)
else:
ai_hit_result = "miss"
if ai_mode == "super_hunter":
if ai_mode == "super_hunter" or ai_mode == "hermes_reasoner":
update_probability(ai_shot % 10, ai_shot // 10, False)
return ai_shot
@ -384,7 +649,14 @@ sections:
user_shot = -1
if game_over:
script_result = {}
script_result = {
"metadata": {
"game_over": True,
"user_wins": user_wins,
"ai_wins": ai_wins
}
}
print(f"DEBUG: Game over detected! User wins: {user_wins}, AI wins: {ai_wins}")
elif 0 <= user_shot < 100 and user_shot not in user_shots:
# The move is valid
user_shots.append(user_shot)
@ -415,10 +687,6 @@ sections:
if ai_board[pos] != -1 and pos not in user_hits:
all_ai_ships_hit = False
break
if all_ai_ships_hit:
game_over = True
user_wins = True
ai_wins = False
# Check if all User ships are hit
all_user_ships_hit = True
@ -426,10 +694,37 @@ sections:
if user_board[pos] != -1 and pos not in ai_hits:
all_user_ships_hit = False
break
if all_user_ships_hit:
if all_ai_ships_hit:
game_over = True
user_wins = True
ai_wins = False
print(f"DEBUG: USER WINS! All AI ships destroyed. Game over.")
elif all_user_ships_hit:
game_over = True
user_wins = False
ai_wins = True
print(f"DEBUG: AI WINS! All user ships destroyed. Game over.")
# Only track winning move if there's exactly 1 position left (for next turn's categorization)
user_winning_move = None
ai_winning_move = None
# Check which user move would win the game (AI ship positions left)
ai_ship_positions_left = [pos for pos in range(100) if ai_board[pos] != -1 and pos not in user_hits]
if len(ai_ship_positions_left) == 1:
user_winning_move = ai_ship_positions_left[0]
print(f"DEBUG: User has exactly 1 winning move at position {user_winning_move}")
else:
print(f"DEBUG: User has {len(ai_ship_positions_left)} AI positions left - no winning move")
# Check which AI move would win the game (user ship positions left)
user_ship_positions_left = [pos for pos in range(100) if user_board[pos] != -1 and pos not in ai_hits]
if len(user_ship_positions_left) == 1:
ai_winning_move = user_ship_positions_left[0]
print(f"DEBUG: AI has exactly 1 winning move at position {ai_winning_move}")
else:
print(f"DEBUG: AI has {len(user_ship_positions_left)} user positions left - no winning move")
# Plot the boards
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
@ -522,13 +817,20 @@ sections:
"probability_matrix": probability_matrix,
"hits": hits,
"misses": misses,
"sunk_ships": sunk_ships
"sunk_ships": sunk_ships,
"user_winning_move": user_winning_move,
"ai_winning_move": ai_winning_move
}
}
# Check if this was a winning move and override transition
if game_over:
script_result["next_section_and_step"] = "section_1:step_3"
print(f"POST-SCRIPT: Game over detected, overriding transition to step_3")
else:
script_result = {
"error": f"Invalid shot: {metadata.get('user_shot')}",
"metadata": {}
"error": f"Invalid shot: {metadata.get('user_shot')}",
"metadata": {}
}
buckets:
@ -558,9 +860,27 @@ sections:
- "Restarting the game. Let's start fresh!"
metadata_clear: True
next_section_and_step: "section_1:step_0"
game_end:
next_section_and_step: "section_1:step_3"
- step_id: "step_3"
title: "Goodbye"
content_blocks:
- "Thank you for playing Battleship! 🎉"
- "Feel free to come back anytime for another game."
title: "Game Over"
question: "Would you like to restart and play again, or would you prefer to exit?"
tokens_for_ai: |
If the user wants to restart or play again, categorize as 'restart'.
If the user wants to exit, categorize as 'exit'.
feedback_tokens_for_ai: |
Acknowledge the user's choice appropriately.
buckets:
- restart
- exit
transitions:
restart:
content_blocks:
- "Restarting the game. Let's start fresh!"
metadata_clear: True
next_section_and_step: "section_1:step_0"
exit:
content_blocks:
- "Thank you for playing Battleship! 🎉"
- "Feel free to come back anytime for another game."