import { draftStore } from '../store/draft-store.js';
import { solve, slideWithHazards } from '../core/solver.js';
import { renderLevel } from '../core/ascii-renderer.js';
import { Direction, Position, DifficultyAnalysis } from '../core/types.js';
import { DIRECTION_VECTORS, ALL_DIRECTIONS, PLAYER_MAX_HEALTH, HOT_COALS_DAMAGE } from '../core/constants.js';
interface ToolDefinition {
name: string;
description: string;
inputSchema: object;
handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;
}
function checkActiveDraft(): string | null {
const draft = draftStore.getCurrentDraft();
if (!draft) {
return 'No active draft. Use create_level first.';
}
return null;
}
function detectMechanics(draft: any): string[] {
const mechanics: string[] = [];
// Check obstacles
const hasLava = draft.obstacles.some((o: any) => o.type === 'lava');
const hasHotCoals = draft.obstacles.some((o: any) => o.type === 'hot_coals' || o.type === 'spike');
if (hasLava) mechanics.push('lava');
if (hasHotCoals) mechanics.push('hot_coals');
// Check special elements
if (draft.thinIceTiles && draft.thinIceTiles.length > 0) {
mechanics.push('thin_ice');
}
if (draft.pushableRocks && draft.pushableRocks.length > 0) {
mechanics.push('pushable_rocks');
}
if (draft.warpPairs && draft.warpPairs.length > 0) {
mechanics.push('warps');
}
if (draft.pressurePlate && draft.barrier) {
mechanics.push('pressure_plate_barrier');
}
return mechanics;
}
function getQuadrant(x: number, y: number, width: number, height: number): number {
const midX = width / 2;
const midY = height / 2;
if (x < midX && y < midY) return 1;
if (x >= midX && y < midY) return 2;
if (x < midX && y >= midY) return 3;
return 4;
}
function countDeadEnds(draft: any, puzzleData: any): number {
let deadEndCount = 0;
for (let y = 0; y < draft.gridHeight; y++) {
for (let x = 0; x < draft.gridWidth; x++) {
const pos = { x, y };
// Skip if there's an obstacle here
const hasObstacle = puzzleData.obstacles.some((o: any) => o.x === x && o.y === y);
if (hasObstacle) continue;
// Count how many directions lead to walls or hazards
let blockedDirections = 0;
for (const dir of ALL_DIRECTIONS) {
const result = slideWithHazards(pos, dir, puzzleData, [], [], false);
// Consider blocked if we didn't move or hit something deadly immediately
if ((result.position.x === x && result.position.y === y) ||
result.hitLava ||
(result.hitBarrier && !result.crossedPressurePlate)) {
blockedDirections++;
}
}
if (blockedDirections >= 3) {
deadEndCount++;
}
}
}
return deadEndCount;
}
export function getSolverTools(): ToolDefinition[] {
return [
{
name: 'solve_level',
description: 'Run the BFS solver on the current level to check solvability and find the optimal solution',
inputSchema: {
type: 'object',
properties: {}
},
handler: async () => {
const error = checkActiveDraft();
if (error) {
return { content: [{ type: 'text', text: error }] };
}
const puzzleData = draftStore.exportPuzzleData();
if (!puzzleData) {
return { content: [{ type: 'text', text: 'Failed to export puzzle data' }] };
}
const result = solve(puzzleData);
draftStore.updateDraft({ lastSolverResult: result, isDirty: false });
const draft = draftStore.getCurrentDraft()!;
const viz = renderLevel(draft, { showCoords: true, showSolution: true });
let output = `${viz}\n\n`;
if (result.solvable) {
output += `✓ SOLVABLE\n`;
output += ` Moves: ${result.moves}\n`;
output += ` Solution: ${result.solution?.join(' → ')}\n`;
output += ` Iterations: ${result.iterations}\n`;
output += ` States visited: ${result.visitedCount}\n`;
} else {
output += `❌ UNSOLVABLE\n`;
output += ` Iterations: ${result.iterations}\n`;
output += ` States checked: ${result.visitedCount}\n`;
}
return { content: [{ type: 'text', text: output }] };
}
},
{
name: 'simulate_move',
description: 'Simulate a single move in a given direction from a position',
inputSchema: {
type: 'object',
properties: {
direction: {
type: 'string',
enum: ['up', 'down', 'left', 'right'],
description: 'Direction to move'
},
fromPosition: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' }
},
description: 'Starting position (defaults to start position if not provided)'
}
},
required: ['direction']
},
handler: async (args: { direction: Direction; fromPosition?: Position }) => {
const error = checkActiveDraft();
if (error) {
return { content: [{ type: 'text', text: error }] };
}
const draft = draftStore.getCurrentDraft()!;
const puzzleData = draftStore.exportPuzzleData();
if (!puzzleData) {
return { content: [{ type: 'text', text: 'Failed to export puzzle data' }] };
}
const startPos = args.fromPosition || draft.startPosition;
const result = slideWithHazards(startPos, args.direction, puzzleData, [], [], false);
let output = `Simulated move ${args.direction} from (${startPos.x},${startPos.y})\n\n`;
output += `Landing position: (${result.position.x},${result.position.y})\n`;
if (result.hitHotCoals) output += `⚠️ Hit hot coals (-${HOT_COALS_DAMAGE} HP on landing)\n`;
if (result.hitLava) output += `☠️ Hit lava (DEATH)\n`;
if (result.fellInHole) output += `🕳️ Fell in hole (DEATH)\n`;
if (result.hitBarrier) output += `🚧 Hit barrier (DEATH)\n`;
if (result.crossedPressurePlate) output += `🔘 Crossed pressure plate (barrier deactivated)\n`;
if (result.warpDestination) {
output += `🌀 Warped from (${result.warpEntrance!.x},${result.warpEntrance!.y}) to (${result.warpDestination.x},${result.warpDestination.y})\n`;
}
if (result.pushedRock) {
output += `📦 Pushed rock from (${result.pushedRock.from.x},${result.pushedRock.from.y}) to (${result.pushedRock.to.x},${result.pushedRock.to.y})\n`;
}
return { content: [{ type: 'text', text: output }] };
}
},
{
name: 'simulate_playthrough',
description: 'Simulate a full sequence of moves from the start position',
inputSchema: {
type: 'object',
properties: {
moves: {
type: 'array',
items: {
type: 'string',
enum: ['up', 'down', 'left', 'right']
},
description: 'Sequence of moves to simulate'
}
},
required: ['moves']
},
handler: async (args: { moves: Direction[] }) => {
const error = checkActiveDraft();
if (error) {
return { content: [{ type: 'text', text: error }] };
}
const draft = draftStore.getCurrentDraft()!;
const puzzleData = draftStore.exportPuzzleData();
if (!puzzleData) {
return { content: [{ type: 'text', text: 'Failed to export puzzle data' }] };
}
let output = `Simulating ${args.moves.length} moves from start position\n\n`;
let currentPos = { ...draft.startPosition };
let health = PLAYER_MAX_HEALTH;
let barrierActive = !!draft.barrier;
let brokenIce: Position[] = [];
let pushedRocks: Position[] = [];
const parLimit = puzzleData.par > 0 ? puzzleData.par : null;
for (let i = 0; i < args.moves.length; i++) {
const move = args.moves[i];
output += `Move ${i + 1}: ${move} from (${currentPos.x},${currentPos.y})\n`;
const result = slideWithHazards(currentPos, move, puzzleData, brokenIce, pushedRocks, barrierActive);
output += ` → Landed at (${result.position.x},${result.position.y})\n`;
if (result.hitHotCoals) {
health -= HOT_COALS_DAMAGE;
output += ` ⚠️ Hit hot coals! HP: ${health}/${PLAYER_MAX_HEALTH}\n`;
}
if (result.hitLava) {
output += ` ☠️ Hit lava - DEATH\n`;
output += `\nPlaythrough failed at move ${i + 1}\n`;
return { content: [{ type: 'text', text: output }] };
}
if (result.fellInHole) {
output += ` 🕳️ Fell in hole - DEATH\n`;
output += `\nPlaythrough failed at move ${i + 1}\n`;
return { content: [{ type: 'text', text: output }] };
}
if (result.hitBarrier) {
output += ` 🚧 Hit barrier - DEATH\n`;
output += `\nPlaythrough failed at move ${i + 1}\n`;
return { content: [{ type: 'text', text: output }] };
}
if (result.crossedPressurePlate) {
barrierActive = false;
output += ` 🔘 Pressure plate activated (barrier deactivated)\n`;
}
if (result.warpDestination) {
output += ` 🌀 Warped to (${result.warpDestination.x},${result.warpDestination.y})\n`;
}
if (result.pushedRock) {
output += ` 📦 Pushed rock to (${result.pushedRock.to.x},${result.pushedRock.to.y})\n`;
pushedRocks.push(result.pushedRock.to);
}
currentPos = result.position;
const reachedGoal = currentPos.x === draft.goalPosition.x && currentPos.y === draft.goalPosition.y;
if (parLimit !== null && (i + 1) >= parLimit && !reachedGoal) {
output += ` 🧊 Melt timeout at move ${i + 1} (par ${parLimit}) - WASTED\n`;
output += `\nPlaythrough failed at move ${i + 1}\n`;
return { content: [{ type: 'text', text: output }] };
}
if (health <= 0) {
output += ` ☠️ Health depleted - DEATH\n`;
output += `\nPlaythrough failed at move ${i + 1}\n`;
return { content: [{ type: 'text', text: output }] };
}
}
output += `\nPlaythrough completed successfully!\n`;
output += `Final position: (${currentPos.x},${currentPos.y})\n`;
output += `Final health: ${health}/${PLAYER_MAX_HEALTH}\n`;
if (currentPos.x === draft.goalPosition.x && currentPos.y === draft.goalPosition.y) {
output += `🎯 GOAL REACHED!\n`;
} else {
output += `❌ Did not reach goal at (${draft.goalPosition.x},${draft.goalPosition.y})\n`;
}
return { content: [{ type: 'text', text: output }] };
}
},
{
name: 'analyze_difficulty',
description: 'Analyze the difficulty and characteristics of the current level',
inputSchema: {
type: 'object',
properties: {}
},
handler: async () => {
const error = checkActiveDraft();
if (error) {
return { content: [{ type: 'text', text: error }] };
}
const draft = draftStore.getCurrentDraft()!;
const puzzleData = draftStore.exportPuzzleData();
if (!puzzleData) {
return { content: [{ type: 'text', text: 'Failed to export puzzle data' }] };
}
// Solve to get the solution
const solverResult = solve(puzzleData);
let output = `Difficulty Analysis\n${'='.repeat(50)}\n\n`;
// Solvability
output += `Solvable: ${solverResult.solvable ? '✓ Yes' : '❌ No'}\n`;
if (!solverResult.solvable) {
output += `\nCannot analyze unsolvable level.\n`;
return { content: [{ type: 'text', text: output }] };
}
const moves = solverResult.moves!;
const solution = solverResult.solution!;
// Move count
output += `Move count: ${moves}\n\n`;
// Mechanics used
const mechanics = detectMechanics(draft);
output += `Mechanics used (${mechanics.length}):\n`;
mechanics.forEach(m => output += ` • ${m}\n`);
output += `\n`;
// Direction balance
const directionBalance: Record<Direction, number> = {
up: 0,
down: 0,
left: 0,
right: 0
};
solution.forEach(dir => {
directionBalance[dir]++;
});
output += `Direction balance:\n`;
output += ` Up: ${directionBalance.up} (${Math.round(directionBalance.up / moves * 100)}%)\n`;
output += ` Down: ${directionBalance.down} (${Math.round(directionBalance.down / moves * 100)}%)\n`;
output += ` Left: ${directionBalance.left} (${Math.round(directionBalance.left / moves * 100)}%)\n`;
output += ` Right: ${directionBalance.right} (${Math.round(directionBalance.right / moves * 100)}%)\n`;
output += `\n`;
// Quadrants visited
const quadrants = new Set<number>();
let pos = { ...draft.startPosition };
quadrants.add(getQuadrant(pos.x, pos.y, draft.gridWidth, draft.gridHeight));
for (const dir of solution) {
const result = slideWithHazards(pos, dir, puzzleData, [], [], false);
pos = result.position;
quadrants.add(getQuadrant(pos.x, pos.y, draft.gridWidth, draft.gridHeight));
}
output += `Quadrants visited: ${Array.from(quadrants).sort().join(', ')} (${quadrants.size}/4)\n`;
output += `\n`;
// Dead ends
const deadEndCount = countDeadEnds(draft, puzzleData);
output += `Dead ends (3+ blocked directions): ${deadEndCount}\n`;
output += `\n`;
// Estimated difficulty
let difficulty: 'tutorial' | 'easy' | 'medium' | 'hard' | 'expert';
if (moves <= 6) difficulty = 'tutorial';
else if (moves <= 12) difficulty = 'easy';
else if (moves <= 17) difficulty = 'medium';
else if (moves <= 24) difficulty = 'hard';
else difficulty = 'expert';
// Adjust based on mechanics
if (mechanics.length >= 3 && difficulty === 'easy') difficulty = 'medium';
if (mechanics.length >= 4 && difficulty === 'medium') difficulty = 'hard';
output += `Estimated difficulty: ${difficulty.toUpperCase()}\n`;
output += ` (tutorial: 2-6 moves, easy: 6-12, medium: 10-17, hard: 14-24, expert: 18+)\n`;
return { content: [{ type: 'text', text: output }] };
}
}
];
}