We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/wmoten/ice-puzzle-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import type { DraftState, Direction, SolverResult, Position } from './types.js';
/**
* Renders a level as ASCII art with legend and optional solution overlay
*/
export function renderLevel(
draft: DraftState,
options?: {
showSolution?: boolean;
showCoords?: boolean;
solution?: Direction[] | null;
showStepNumbers?: boolean;
stepPositions?: Position[];
}
): string {
const {
showSolution = false,
showCoords = false,
solution = null,
showStepNumbers = false,
stepPositions = [],
} = options || {};
const width = draft.gridWidth;
const height = draft.gridHeight;
// Track out-of-bounds elements
const outOfBounds: string[] = [];
// Initialize grid with ice tiles
const grid: string[][] = Array.from({ length: height }, () =>
Array.from({ length: width }, () => '.')
);
// Add walls on borders
for (let x = 0; x < width; x++) {
grid[0][x] = '#';
grid[height - 1][x] = '#';
}
for (let y = 0; y < height; y++) {
grid[y][0] = '#';
grid[y][width - 1] = '#';
}
// Add obstacles
for (const obstacle of draft.obstacles) {
const { x, y, type } = obstacle;
if (y >= 0 && y < height && x >= 0 && x < width) {
grid[y][x] = getObstacleChar(type);
} else {
outOfBounds.push(`Obstacle ${type} at (${x},${y}) is outside grid`);
}
}
// Add thin ice tiles
for (const pos of draft.thinIceTiles) {
if (pos.y >= 0 && pos.y < height && pos.x >= 0 && pos.x < width) {
grid[pos.y][pos.x] = '~';
} else {
outOfBounds.push(`Thin ice tile at (${pos.x},${pos.y}) is outside grid`);
}
}
// Add pushable rocks
for (const pos of draft.pushableRocks) {
if (pos.y >= 0 && pos.y < height && pos.x >= 0 && pos.x < width) {
grid[pos.y][pos.x] = 'P';
} else {
outOfBounds.push(`Pushable rock at (${pos.x},${pos.y}) is outside grid`);
}
}
// Add pressure plate
if (draft.pressurePlate) {
const { x, y } = draft.pressurePlate;
if (y >= 0 && y < height && x >= 0 && x < width) {
grid[y][x] = '!';
} else {
outOfBounds.push(`Pressure plate at (${x},${y}) is outside grid`);
}
}
// Add barrier
if (draft.barrier) {
const { x, y } = draft.barrier;
if (y >= 0 && y < height && x >= 0 && x < width) {
grid[y][x] = 'B';
} else {
outOfBounds.push(`Barrier at (${x},${y}) is outside grid`);
}
}
// Add warps
for (const warpPair of draft.warpPairs) {
for (const pos of warpPair.positions) {
if (pos.y >= 0 && pos.y < height && pos.x >= 0 && pos.x < width) {
grid[pos.y][pos.x] = 'W';
} else {
outOfBounds.push(`Warp at (${pos.x},${pos.y}) is outside grid`);
}
}
}
// Add start and goal (these override other tiles)
const { startPosition, goalPosition } = draft;
if (startPosition.y >= 0 && startPosition.y < height &&
startPosition.x >= 0 && startPosition.x < width) {
grid[startPosition.y][startPosition.x] = 'S';
}
if (goalPosition.y >= 0 && goalPosition.y < height &&
goalPosition.x >= 0 && goalPosition.x < width) {
grid[goalPosition.y][goalPosition.x] = 'G';
}
const stepMarkerLegend: string[] = [];
if (showStepNumbers && stepPositions.length > 0) {
const markerChars = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const seenStepCells = new Set<string>();
stepMarkerLegend.push(`0=start (${startPosition.x},${startPosition.y})`);
for (let i = 1; i < stepPositions.length; i++) {
const position = stepPositions[i];
const isGoalStep = position.x === goalPosition.x && position.y === goalPosition.y;
if (isGoalStep) {
stepMarkerLegend.push(`${i}=goal (${position.x},${position.y})`);
continue;
}
if (position.x < 0 || position.x >= width || position.y < 0 || position.y >= height) {
continue;
}
const key = `${position.x},${position.y}`;
const markerIndex = i - 1;
const marker = markerIndex < markerChars.length ? markerChars[markerIndex] : '*';
const finalMarker = seenStepCells.has(key) ? '*' : marker;
seenStepCells.add(key);
grid[position.y][position.x] = finalMarker;
stepMarkerLegend.push(`${finalMarker}=step ${i} (${position.x},${position.y})`);
}
}
// Build output
const lines: string[] = [];
// Legend
lines.push('Legend: # Wall . Ice S Start G Goal R Rock L Lava');
lines.push(' ^ Hot Coals ~ ThinIce W Warp P PushRock');
lines.push(' ! Plate B Barrier');
if (showStepNumbers) {
lines.push(' 1-9/A-Z/* Path step markers');
}
lines.push('');
// Column headers (if showCoords)
if (showCoords) {
const header = ' ' + Array.from({ length: width }, (_, i) => i).join(' ');
lines.push(header);
}
// Grid rows
for (let y = 0; y < height; y++) {
const rowPrefix = showCoords ? `${y.toString().padStart(2, ' ')} ` : '';
const rowContent = grid[y].join(' ');
lines.push(rowPrefix + rowContent);
}
lines.push('');
// Solution info
if (showSolution && solution) {
lines.push(`[SOLVABLE] Optimal: ${solution.length} moves`);
lines.push(`Solution: ${formatDirections(solution)}`);
} else if (showSolution && solution === null) {
lines.push('[UNSOLVABLE] No solution found');
}
if (showStepNumbers && stepMarkerLegend.length > 0) {
lines.push('');
lines.push('Step Marker Key:');
for (const marker of stepMarkerLegend) {
lines.push(`- ${marker}`);
}
}
// Solver result (if available)
if (draft.lastSolverResult) {
lines.push('');
lines.push(formatSolverResult(draft.lastSolverResult));
}
// Out-of-bounds warnings
if (outOfBounds.length > 0) {
lines.push('');
lines.push('[WARNINGS]');
for (const warning of outOfBounds) {
lines.push(`- ${warning}`);
}
}
return lines.join('\n');
}
/**
* Formats a solver result as a human-readable string
*/
export function formatSolverResult(result: SolverResult | null): string {
if (!result) {
return 'Not yet solved';
}
if (result.solvable && result.moves !== null) {
return `[SOLVABLE] Optimal: ${result.moves} moves`;
} else if (!result.solvable) {
if (result.exhausted) {
return `[UNSOLVABLE] Warning: solver hit iteration limit (${result.iterations} iterations). The level may be solvable but too complex for the solver.`;
}
return '[UNSOLVABLE]';
}
return 'Solver status unknown';
}
/**
* Gets the ASCII character for an obstacle type
*/
function getObstacleChar(type: string): string {
switch (type) {
case 'rock': return 'R';
case 'lava': return 'L';
case 'hot_coals': return '^';
case 'spike': return '^';
case 'thin_ice': return '~';
case 'pushable_rock': return 'P';
case 'barrier': return 'B';
case 'wall': return '#';
default: return '?';
}
}
/**
* Formats a sequence of directions as a readable string
*/
function formatDirections(directions: Direction[]): string {
return directions
.map(d => d.toUpperCase())
.join(' -> ');
}