- Fix battleship feedback perspective confusion with better Hermes prompting - Add auto-play TTS button with localStorage persistence and queueing system - Move activity controls below model/voice selectors in sidebar - Add activity controls to mobile hamburger menu - Fix model/activity dropdowns to stay within container bounds - Filter activities API to only show .yaml/.yml files - Clean up system message labels by moving to usernames (System (Feedback), System (Question)) - Apply black formatting to app.py
901 lines
42 KiB
YAML
901 lines
42 KiB
YAML
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 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,
|
||
|
||
* random
|
||
|
||
* always plays randomly
|
||
|
||
* hunter
|
||
|
||
* keeps track of hits and targets every cell around it no matter what, randomly, else random
|
||
|
||
* 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.
|
||
|
||
* 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:
|
||
- section_id: "section_1"
|
||
title: "Battleship"
|
||
steps:
|
||
- step_id: "step_0"
|
||
title: "Introduction"
|
||
content_blocks:
|
||
- |
|
||
Welcome to Battleship! 🚢
|
||
In this game, both you and the AI have a fleet of ships placed randomly on a 10x10 grid.
|
||
The grid positions are numbered 0 to 99.
|
||
|
||
Your goal is to sink all of the AI's ships before it sinks yours.
|
||
Let's get started!
|
||
|
||
- step_id: "step_1"
|
||
title: "Choose AI Mode"
|
||
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, 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
|
||
|
||
def place_ships():
|
||
global random
|
||
# Define ship sizes and names
|
||
ships = {
|
||
"Carrier": 5,
|
||
"Battleship": 4,
|
||
"Cruiser": 3,
|
||
"Submarine": 3,
|
||
"Destroyer": 2
|
||
}
|
||
|
||
board = [-1] * 100
|
||
for ship, size in ships.items():
|
||
placed = False
|
||
while not placed:
|
||
orientation = random.choice(['horizontal', 'vertical'])
|
||
if orientation == 'horizontal':
|
||
row = random.randint(0, 9)
|
||
col = random.randint(0, 9 - size)
|
||
start = row * 10 + col
|
||
if all(board[start + i] == -1 for i in range(size)):
|
||
for i in range(size):
|
||
board[start + i] = ship
|
||
placed = True
|
||
else:
|
||
row = random.randint(0, 9 - size)
|
||
col = random.randint(0, 9)
|
||
start = row * 10 + col
|
||
if all(board[start + i * 10] == -1 for i in range(size)):
|
||
for i in range(size):
|
||
board[start + i * 10] = ship
|
||
placed = True
|
||
return board
|
||
|
||
user_board = place_ships()
|
||
ai_board = place_ships() # AI also gets randomly placed ships
|
||
|
||
script_result = {
|
||
"metadata": {
|
||
"user_board": user_board,
|
||
"ai_board": ai_board
|
||
}
|
||
}
|
||
|
||
buckets:
|
||
- random_mode
|
||
- hunter_mode
|
||
- super_hunter_mode
|
||
- hermes_reasoner_mode
|
||
transitions:
|
||
random_mode:
|
||
run_processing_script: True
|
||
ai_feedback:
|
||
tokens_for_ai: "Random Mode enabled for the AI."
|
||
metadata_add:
|
||
ai_mode: "random"
|
||
next_section_and_step: "section_1:step_2"
|
||
hunter_mode:
|
||
run_processing_script: True
|
||
ai_feedback:
|
||
tokens_for_ai: "Hunter Mode enabled for the AI."
|
||
metadata_add:
|
||
ai_mode: "hunter"
|
||
next_section_and_step: "section_1:step_2"
|
||
super_hunter_mode:
|
||
run_processing_script: True
|
||
ai_feedback:
|
||
tokens_for_ai: "Super Human Hunter Mode enabled for the AI."
|
||
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_response", "")
|
||
# 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 (0–99),
|
||
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'.
|
||
feedback_tokens_for_ai: |
|
||
You are the naval battle narrator. Look at the metadata provided and report what happened.
|
||
|
||
STEP 1 - CHECK SHIP DESTRUCTION (MANDATORY):
|
||
Look in the metadata for these exact fields:
|
||
- user_sunk_ship_this_round: If this contains a ship name like "Carrier" or "Battleship", say: "💥 SHIP DESTROYED! You have sunk the enemy's [ship name]! The enemy vessel explodes and sinks! Victory!"
|
||
- ai_sunk_ship_this_round: If this contains a ship name, say: "🔥 YOUR SHIP SUNK! The enemy destroyed your [ship name]! Your vessel burns and sinks!"
|
||
|
||
STEP 2 - REPORT SHOTS:
|
||
- Your shot result (user_hit_result): "hit" or "miss"
|
||
- Enemy shot result (ai_hit_result): "hit" or "miss"
|
||
|
||
EXAMPLE RESPONSE FORMAT:
|
||
If user_sunk_ship_this_round = "Carrier": "💥 SHIP DESTROYED! You have sunk the enemy's Carrier! [shot details]"
|
||
If ai_sunk_ship_this_round = "Destroyer": "🔥 YOUR SHIP SUNK! The enemy destroyed your Destroyer! [shot details]"
|
||
|
||
Always check the metadata for user_sunk_ship_this_round and ai_sunk_ship_this_round first. These are the most important events to report.
|
||
|
||
processing_script: |
|
||
import random
|
||
import matplotlib
|
||
matplotlib.use("Agg")
|
||
import matplotlib.pyplot as plt
|
||
import io
|
||
import base64
|
||
import requests
|
||
import json
|
||
|
||
# Define ship sizes
|
||
ship_sizes = {
|
||
"Carrier": 5,
|
||
"Battleship": 4,
|
||
"Cruiser": 3,
|
||
"Submarine": 3,
|
||
"Destroyer": 2
|
||
}
|
||
|
||
# Define colors for ships
|
||
ship_colors = {
|
||
"Carrier": "blue",
|
||
"Battleship": "green",
|
||
"Cruiser": "orange",
|
||
"Submarine": "purple",
|
||
"Destroyer": "pink"
|
||
}
|
||
|
||
# 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", [])
|
||
ai_hits = metadata.get("ai_hits", [])
|
||
game_over = metadata.get("game_over", False)
|
||
user_wins = False
|
||
ai_wins = False
|
||
user_hit_result = "miss"
|
||
ai_hit_result = "miss"
|
||
user_sunk_ships = metadata.get("user_sunk_ships", [])
|
||
ai_sunk_ships = metadata.get("ai_sunk_ships", [])
|
||
user_sunk_ship_this_round = None
|
||
ai_sunk_ship_this_round = None
|
||
|
||
# AI state variables
|
||
ai_mode = metadata.get("ai_mode", "random")
|
||
|
||
# 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", [])
|
||
|
||
# Function to check if a ship is sunk
|
||
def check_sunk(board, hits, ship_name):
|
||
ship_positions = []
|
||
for i, ship in enumerate(board):
|
||
if ship == ship_name:
|
||
ship_positions.append(i)
|
||
for pos in ship_positions:
|
||
if pos not in hits:
|
||
return False
|
||
return True
|
||
|
||
# Function to draw a line across a sunken ship
|
||
def draw_line(ax, board, ship_name):
|
||
ship_positions = []
|
||
for i, ship in enumerate(board):
|
||
if ship == ship_name:
|
||
ship_positions.append(i)
|
||
if not ship_positions:
|
||
return
|
||
|
||
# Determine if the ship is horizontal or vertical
|
||
first_pos = ship_positions[0]
|
||
last_pos = ship_positions[-1]
|
||
if last_pos - first_pos < 10: # Horizontal
|
||
x_start, y_start = first_pos % 10 + 0.5, 9 - first_pos // 10 + 0.5
|
||
x_end, y_end = last_pos % 10 + 0.5, 9 - last_pos // 10 + 0.5
|
||
else: # Vertical
|
||
x_start, y_start = first_pos % 10 + 0.5, 9 - first_pos // 10 + 0.5
|
||
x_end, y_end = first_pos % 10 + 0.5, 9 - last_pos // 10 + 0.5
|
||
|
||
ax.plot([x_start, x_end], [y_start, y_end], color='red', linewidth=2)
|
||
|
||
# Function to update probability matrix
|
||
def update_probability(x, y, hit):
|
||
global probability_matrix, hits, misses, sunk_ships, ship_sizes
|
||
|
||
if hit:
|
||
hits.append((x, y))
|
||
probability_matrix[y][x] = 0 # Mark hit
|
||
# Increase probabilities for adjacent cells
|
||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||
nx, ny = x + dx, y + dy
|
||
if 0 <= nx < 10 and 0 <= ny < 10 and probability_matrix[ny][nx] > 0:
|
||
probability_matrix[ny][nx] += 5 # Increase probability significantly
|
||
else:
|
||
misses.append((x, y))
|
||
probability_matrix[y][x] = -1 # Mark miss
|
||
|
||
# Set probabilities to 1 for cells that can't fit any remaining ships
|
||
max_ship_size = max(size for ship, size in ship_sizes.items() if ship not in sunk_ships)
|
||
for y in range(10):
|
||
for x in range(10):
|
||
if probability_matrix[y][x] > 0 and not can_fit_ship(x, y, max_ship_size):
|
||
probability_matrix[y][x] = 1 # Minimum probability
|
||
|
||
# Function to check if a ship can fit
|
||
def can_fit_ship(x, y, ship_size):
|
||
# Check horizontal fit
|
||
if x + ship_size <= 10:
|
||
fit = True
|
||
for i in range(ship_size):
|
||
if probability_matrix[y][x+i] <= 0:
|
||
fit = False
|
||
break
|
||
if fit:
|
||
return True
|
||
# Check vertical fit
|
||
if y + ship_size <= 10:
|
||
fit = True
|
||
for i in range(ship_size):
|
||
if probability_matrix[y+i][x] <= 0:
|
||
fit = False
|
||
break
|
||
if fit:
|
||
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, hermes_reason_move, ship_sizes, ai_sunk_ships
|
||
|
||
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 = []
|
||
for i in range(100):
|
||
if i not in ai_shots: # Exclude already-fired cells
|
||
x, y = i % 10, i // 10
|
||
if probability_matrix[y][x] > max_prob:
|
||
max_prob = probability_matrix[y][x]
|
||
candidates = [i]
|
||
elif probability_matrix[y][x] == max_prob:
|
||
candidates.append(i)
|
||
ai_shot = random.choice(candidates)
|
||
elif ai_mode == "hunter":
|
||
# Simple hunter mode logic
|
||
if hits:
|
||
# Target adjacent cells of the last hit
|
||
last_hit = hits[-1]
|
||
hunt_targets = generate_hunt_targets(last_hit, ai_shots)
|
||
if hunt_targets:
|
||
ai_shot = hunt_targets.pop(0)
|
||
else:
|
||
ai_shot = random_search()
|
||
else:
|
||
ai_shot = random_search()
|
||
else:
|
||
# Random mode
|
||
ai_shot = random_search()
|
||
|
||
# Update AI state after the shot
|
||
if user_board[ai_shot] != -1:
|
||
ai_hits.append(ai_shot)
|
||
ai_hit_result = "hit"
|
||
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" or ai_mode == "hermes_reasoner":
|
||
update_probability(ai_shot % 10, ai_shot // 10, False)
|
||
|
||
return ai_shot
|
||
|
||
# Function for random search
|
||
def random_search():
|
||
available_positions = []
|
||
for i in range(100):
|
||
if i not in ai_shots:
|
||
available_positions.append(i)
|
||
return random.choice(available_positions)
|
||
|
||
# Function to generate hunt targets around a hit
|
||
def generate_hunt_targets(hit_position, ai_shots):
|
||
potential_targets = []
|
||
row, col = divmod(hit_position, 10)
|
||
|
||
# Up
|
||
if row > 0:
|
||
potential_targets.append(hit_position - 10)
|
||
# Down
|
||
if row < 9:
|
||
potential_targets.append(hit_position + 10)
|
||
# Left
|
||
if col > 0:
|
||
potential_targets.append(hit_position - 1)
|
||
# Right
|
||
if col < 9:
|
||
potential_targets.append(hit_position + 1)
|
||
|
||
# Filter out already fired positions
|
||
filtered_targets = []
|
||
for pos in potential_targets:
|
||
if pos not in ai_shots:
|
||
filtered_targets.append(pos)
|
||
return filtered_targets
|
||
|
||
# Get the user's shot
|
||
try:
|
||
user_shot = int(metadata.get("user_shot"))
|
||
except (IndexError, ValueError) as e:
|
||
user_shot = -1
|
||
|
||
if game_over:
|
||
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)
|
||
user_hit_result = "miss"
|
||
if ai_board[user_shot] != -1:
|
||
user_hits.append(user_shot)
|
||
user_hit_result = "hit"
|
||
|
||
# AI makes a move
|
||
ai_shot = choose_ai_shot()
|
||
ai_shots.append(ai_shot)
|
||
|
||
# Check if any AI ship is sunk
|
||
for ship_name in ship_sizes.keys():
|
||
if check_sunk(ai_board, user_hits, ship_name) and ship_name not in user_sunk_ships:
|
||
user_sunk_ships.append(ship_name)
|
||
user_sunk_ship_this_round = ship_name
|
||
# print(f"DEBUG: USER SUNK AI SHIP: {ship_name}")
|
||
|
||
# Check if any User ship is sunk
|
||
for ship_name in ship_sizes.keys():
|
||
if check_sunk(user_board, ai_hits, ship_name) and ship_name not in ai_sunk_ships:
|
||
ai_sunk_ships.append(ship_name)
|
||
ai_sunk_ship_this_round = ship_name
|
||
# print(f"DEBUG: AI SUNK USER SHIP: {ship_name}")
|
||
|
||
# Check if all AI ships are hit
|
||
all_ai_ships_hit = True
|
||
for pos in range(100):
|
||
if ai_board[pos] != -1 and pos not in user_hits:
|
||
all_ai_ships_hit = False
|
||
break
|
||
|
||
# Check if all User ships are hit
|
||
all_user_ships_hit = True
|
||
for pos in range(100):
|
||
if user_board[pos] != -1 and pos not in ai_hits:
|
||
all_user_ships_hit = False
|
||
break
|
||
|
||
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")
|
||
pass
|
||
|
||
# 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")
|
||
pass
|
||
|
||
# Plot the boards
|
||
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
|
||
fig.suptitle("Battleship", fontsize=16)
|
||
|
||
# User's view of AI's board
|
||
axs[0].set_xlim(0, 10)
|
||
axs[0].set_ylim(0, 10)
|
||
axs[0].set_xticks([])
|
||
axs[0].set_yticks([])
|
||
axs[0].grid(True)
|
||
axs[0].set_title("Your Shots", fontsize=12)
|
||
|
||
# Plot user shots on AI's board
|
||
for i in range(100):
|
||
x, y = i % 10, 9 - i // 10
|
||
if i in user_shots:
|
||
if i in user_hits:
|
||
axs[0].text(x + 0.5, y + 0.5, 'X', fontsize=12, ha='center', va='center', color='red')
|
||
else:
|
||
axs[0].text(x + 0.5, y + 0.5, 'O', fontsize=12, ha='center', va='center', color='black')
|
||
axs[0].text(x + 0.5, y + 0.5, str(i), fontsize=8, ha='center', va='center', color='gray')
|
||
|
||
# AI's view of User's board
|
||
axs[1].set_xlim(0, 10)
|
||
axs[1].set_ylim(0, 10)
|
||
axs[1].set_xticks([])
|
||
axs[1].set_yticks([])
|
||
axs[1].grid(True)
|
||
axs[1].set_title("Your Ships", fontsize=12)
|
||
|
||
# Plot user ships
|
||
for i, ship in enumerate(user_board):
|
||
x, y = i % 10, 9 - i // 10
|
||
if ship != -1:
|
||
axs[1].add_patch(plt.Rectangle((x, y), 1, 1, color=ship_colors[ship], alpha=0.5))
|
||
|
||
# Plot AI shots on User's board
|
||
for i in range(100):
|
||
x, y = i % 10, 9 - i // 10
|
||
if i in ai_shots:
|
||
if i in ai_hits:
|
||
axs[1].text(x + 0.5, y + 0.5, 'X', fontsize=12, ha='center', va='center', color='red')
|
||
else:
|
||
axs[1].text(x + 0.5, y + 0.5, 'O', fontsize=12, ha='center', va='center', color='black')
|
||
axs[1].text(x + 0.5, y + 0.5, str(i), fontsize=8, ha='center', va='center', color='gray')
|
||
|
||
# Draw lines across sunk ships
|
||
for ship_name in user_sunk_ships:
|
||
draw_line(axs[0], ai_board, ship_name)
|
||
|
||
for ship_name in ai_sunk_ships:
|
||
draw_line(axs[1], user_board, ship_name)
|
||
|
||
# Add legend
|
||
handles = []
|
||
for color in ship_colors.values():
|
||
handles.append(plt.Rectangle((0, 0), 1, 1, color=color, alpha=0.5))
|
||
axs[1].legend(handles, ship_colors.keys(), loc='upper right', fontsize=8)
|
||
|
||
buf = io.BytesIO()
|
||
plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0.1)
|
||
plt.close(fig)
|
||
buf.seek(0)
|
||
plot_image = base64.b64encode(buf.getvalue()).decode('utf-8')
|
||
|
||
# gpt-4: If "plot_image" is in the result, set it as the background image
|
||
# print(f"DEBUG: Setting metadata for feedback - user_sunk_ship_this_round: {user_sunk_ship_this_round}, ai_sunk_ship_this_round: {ai_sunk_ship_this_round}")
|
||
|
||
script_result = {
|
||
"plot_image": plot_image,
|
||
"set_background": True,
|
||
"metadata": {
|
||
"user_board": user_board,
|
||
"ai_board": ai_board,
|
||
"user_shot": user_shot,
|
||
"ai_shot": ai_shot,
|
||
"user_shots": user_shots,
|
||
"ai_shots": ai_shots,
|
||
"user_hits": user_hits,
|
||
"ai_hits": ai_hits,
|
||
"game_over": game_over,
|
||
"user_wins": user_wins,
|
||
"ai_wins": ai_wins,
|
||
"user_hit_result": user_hit_result,
|
||
"ai_hit_result": ai_hit_result,
|
||
"user_sunk_ships": user_sunk_ships,
|
||
"ai_sunk_ships": ai_sunk_ships,
|
||
"user_sunk_ship_this_round": user_sunk_ship_this_round,
|
||
"ai_sunk_ship_this_round": ai_sunk_ship_this_round,
|
||
"ai_mode": ai_mode,
|
||
"probability_matrix": probability_matrix,
|
||
"hits": hits,
|
||
"misses": misses,
|
||
"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": {}
|
||
}
|
||
|
||
buckets:
|
||
- valid_move
|
||
- invalid_move
|
||
- exit
|
||
- restart
|
||
transitions:
|
||
valid_move:
|
||
run_processing_script: True
|
||
ai_feedback:
|
||
tokens_for_ai: |
|
||
The user shot seems valid.
|
||
metadata_tmp_add:
|
||
user_shot: "the-users-response"
|
||
metadata_feedback_filter:
|
||
- user_hit_result
|
||
- ai_hit_result
|
||
- ai_shot
|
||
- user_shot
|
||
- user_sunk_ship_this_round
|
||
- ai_sunk_ship_this_round
|
||
- game_over
|
||
- user_wins
|
||
- ai_wins
|
||
next_section_and_step: "section_1:step_2"
|
||
invalid_move:
|
||
content_blocks:
|
||
- "That move is invalid. Please choose a position between 0 and 99."
|
||
metadata_tmp_add:
|
||
user_shot: "the-users-response"
|
||
next_section_and_step: "section_1:step_2"
|
||
exit:
|
||
next_section_and_step: "section_1:step_4"
|
||
restart:
|
||
content_blocks:
|
||
- "Restarting the game. Let's start fresh!"
|
||
metadata_clear: True
|
||
next_section_and_step: "section_1:step_0"
|
||
|
||
- step_id: "step_3"
|
||
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."
|
||
next_section_and_step: "section_1:step_4"
|
||
|
||
- step_id: "step_4"
|
||
title: "Goodbye"
|
||
content_blocks:
|
||
- "Thanks for playing! Hope you enjoyed the battle at sea."
|