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 { ALL_DIRECTIONS, DIRECTION_VECTORS, HOT_COALS_DAMAGE, MAX_SOLVER_ITERATIONS, PLAYER_MAX_HEALTH } from './constants.js';
import { slideWithHazards } from './solver.js';
import type { Direction, Position, PuzzleData, SolverResult } from './types.js';
interface ExploreState {
position: Position;
brokenIce: string[];
pushedRocks: string[];
health: number;
pressurePlateActivated: boolean;
moves: number;
}
export interface ReachablePositionInfo {
position: Position;
minMoves: number;
reachedWithPlateActive: boolean;
}
export interface ReachabilityReport {
positions: ReachablePositionInfo[];
iterations: number;
visitedStates: number;
exhausted: boolean;
goalReachable: boolean;
pressurePlateReachable: boolean;
barrierHitCount: number;
validFirstMoves: Direction[];
}
export interface StopPointSuggestion {
blocker: Position;
landAt: Position;
distanceFromStart: number;
}
export interface StopPointReport {
currentLanding: Position;
traversedPath: Position[];
suggestions: StopPointSuggestion[];
blockedCandidates: Array<{ blocker: Position; reason: string }>;
}
export interface UnsolvableDiagnostics {
reasons: string[];
suggestions: string[];
reachability: {
reachableTileCount: number;
goalLineReachableCount: number;
nearestReachableToGoal: Position | null;
report: ReachabilityReport;
};
}
export interface TraceResult {
positions: Position[];
}
function positionKey(position: Position): string {
return `${position.x},${position.y}`;
}
function parsePositionKey(key: string): Position {
const [x, y] = key.split(',').map(Number);
return { x, y };
}
function samePosition(a: Position, b: Position): boolean {
return a.x === b.x && a.y === b.y;
}
function isHotCoalsType(type: string): boolean {
return type === 'hot_coals' || type === 'spike';
}
function isOccupied(puzzle: PuzzleData, x: number, y: number): string | null {
if (puzzle.start.x === x && puzzle.start.y === y) return 'start';
if (puzzle.goal.x === x && puzzle.goal.y === y) return 'goal';
if (puzzle.obstacles.some((obs) => obs.x === x && obs.y === y)) return 'obstacle';
if (puzzle.warps?.some((warp) => warp.x === x && warp.y === y)) return 'warp';
if (puzzle.thinIceTiles?.some((tile) => tile.x === x && tile.y === y)) return 'thin_ice';
if (puzzle.pushableRocks?.some((rock) => rock.x === x && rock.y === y)) return 'pushable_rock';
if (puzzle.pressurePlate && puzzle.pressurePlate.x === x && puzzle.pressurePlate.y === y) return 'pressure_plate';
if (puzzle.barrier && puzzle.barrier.x === x && puzzle.barrier.y === y) return 'barrier';
return null;
}
function buildStateKey(state: ExploreState): string {
const brokenIceKey = [...state.brokenIce].sort().join('|');
const pushedRocksKey = [...state.pushedRocks].sort().join('|');
const plateKey = state.pressurePlateActivated ? 'P' : '';
return `${state.position.x},${state.position.y}:${brokenIceKey}:${pushedRocksKey}:${state.health}:${plateKey}`;
}
export function computeDirectionBalance(solution: Direction[] | null): Record<Direction, number> {
const balance: Record<Direction, number> = {
up: 0,
down: 0,
left: 0,
right: 0,
};
if (!solution) return balance;
for (const move of solution) {
balance[move] += 1;
}
return balance;
}
export function traceMovePositions(puzzle: PuzzleData, moves: Direction[], start?: Position): TraceResult {
const positions: Position[] = [{ ...(start || puzzle.start) }];
let currentPosition = { ...(start || puzzle.start) };
let health = PLAYER_MAX_HEALTH;
const brokenIce: Position[] = [];
let pushedRocks = puzzle.pushableRocks ? [...puzzle.pushableRocks] : [];
let pressurePlateActivated = false;
for (const move of moves) {
const slideResult = slideWithHazards(
currentPosition,
move,
puzzle,
brokenIce,
pushedRocks,
pressurePlateActivated
);
if (slideResult.hitHotCoals) {
health -= HOT_COALS_DAMAGE;
}
if (slideResult.crossedPressurePlate) {
pressurePlateActivated = true;
}
if (puzzle.thinIceTiles) {
const delta = DIRECTION_VECTORS[move];
const target = slideResult.warpEntrance || slideResult.position;
let cx = currentPosition.x;
let cy = currentPosition.y;
for (let steps = 0; steps < 50 && (cx !== target.x || cy !== target.y); steps++) {
cx += delta.x;
cy += delta.y;
if (
puzzle.thinIceTiles.some((tile) => tile.x === cx && tile.y === cy)
&& !brokenIce.some((tile) => tile.x === cx && tile.y === cy)
) {
brokenIce.push({ x: cx, y: cy });
}
}
}
if (slideResult.pushedRock) {
pushedRocks = pushedRocks.filter(
(rock) => rock.x !== slideResult.pushedRock!.from.x || rock.y !== slideResult.pushedRock!.from.y
);
const pushedIntoLava = puzzle.obstacles.some(
(obs) => obs.x === slideResult.pushedRock!.to.x
&& obs.y === slideResult.pushedRock!.to.y
&& obs.type === 'lava'
);
if (!pushedIntoLava) {
pushedRocks.push({ ...slideResult.pushedRock.to });
}
}
currentPosition = { ...slideResult.position };
positions.push({ ...currentPosition });
if (slideResult.hitLava || slideResult.fellInHole || slideResult.hitBarrier || health <= 0) {
break;
}
if (samePosition(currentPosition, puzzle.goal)) {
break;
}
}
return { positions };
}
export function exploreReachability(puzzle: PuzzleData, start?: Position): ReachabilityReport {
const initialPushedRocks = puzzle.pushableRocks
? puzzle.pushableRocks.map((rock) => `${rock.x},${rock.y}`)
: [];
const initialState: ExploreState = {
position: { ...(start || puzzle.start) },
brokenIce: [],
pushedRocks: initialPushedRocks,
health: PLAYER_MAX_HEALTH,
pressurePlateActivated: false,
moves: 0,
};
const queue: ExploreState[] = [initialState];
const visited = new Set<string>();
const minMovesByPosition = new Map<string, number>();
const plateActivePositions = new Set<string>();
const validFirstMoves = new Set<Direction>();
let iterations = 0;
let barrierHitCount = 0;
let pressurePlateReachable = false;
const initialKey = positionKey(initialState.position);
minMovesByPosition.set(initialKey, 0);
while (queue.length > 0 && iterations < MAX_SOLVER_ITERATIONS) {
iterations++;
const state = queue.shift()!;
const stateKey = buildStateKey(state);
if (visited.has(stateKey)) continue;
visited.add(stateKey);
if (state.pressurePlateActivated) {
plateActivePositions.add(positionKey(state.position));
pressurePlateReachable = true;
}
for (const direction of ALL_DIRECTIONS) {
const brokenIcePositions = state.brokenIce.map(parsePositionKey);
const pushableRockPositions = state.pushedRocks.map(parsePositionKey);
const slideResult = slideWithHazards(
state.position,
direction,
puzzle,
brokenIcePositions,
pushableRockPositions,
state.pressurePlateActivated
);
if (slideResult.hitBarrier) {
barrierHitCount++;
continue;
}
if (slideResult.hitLava || slideResult.fellInHole) continue;
let newHealth = state.health;
if (slideResult.hitHotCoals) {
newHealth -= HOT_COALS_DAMAGE;
}
if (newHealth <= 0) continue;
const moved = slideResult.position.x !== state.position.x
|| slideResult.position.y !== state.position.y
|| Boolean(slideResult.pushedRock);
if (!moved) continue;
if (state.moves === 0) {
validFirstMoves.add(direction);
}
const newMoves = state.moves + 1;
const landingKey = positionKey(slideResult.position);
const bestKnownMoves = minMovesByPosition.get(landingKey);
if (bestKnownMoves === undefined || newMoves < bestKnownMoves) {
minMovesByPosition.set(landingKey, newMoves);
}
const newBrokenIce = [...state.brokenIce];
if (puzzle.thinIceTiles) {
const delta = DIRECTION_VECTORS[direction];
const target = slideResult.warpEntrance || slideResult.position;
let cx = state.position.x;
let cy = state.position.y;
for (let safety = 0; safety < 50 && (cx !== target.x || cy !== target.y); safety++) {
cx += delta.x;
cy += delta.y;
for (const tile of puzzle.thinIceTiles) {
if (tile.x === cx && tile.y === cy) {
const tileKey = positionKey(tile);
if (!newBrokenIce.includes(tileKey)) {
newBrokenIce.push(tileKey);
}
}
}
}
if (slideResult.warpDestination) {
cx = slideResult.warpDestination.x;
cy = slideResult.warpDestination.y;
for (let safety = 0; safety < 50 && (cx !== slideResult.position.x || cy !== slideResult.position.y); safety++) {
cx += delta.x;
cy += delta.y;
for (const tile of puzzle.thinIceTiles) {
if (tile.x === cx && tile.y === cy) {
const tileKey = positionKey(tile);
if (!newBrokenIce.includes(tileKey)) {
newBrokenIce.push(tileKey);
}
}
}
}
}
}
let newPushedRocks = [...state.pushedRocks];
if (slideResult.pushedRock) {
const oldKey = positionKey(slideResult.pushedRock.from);
const newKey = positionKey(slideResult.pushedRock.to);
newPushedRocks = newPushedRocks.filter((key) => key !== oldKey);
const pushedIntoLava = puzzle.obstacles.some(
(obs) => obs.x === slideResult.pushedRock!.to.x
&& obs.y === slideResult.pushedRock!.to.y
&& obs.type === 'lava'
);
if (!pushedIntoLava) {
newPushedRocks.push(newKey);
}
}
const nextState: ExploreState = {
position: { ...slideResult.position },
brokenIce: newBrokenIce,
pushedRocks: newPushedRocks,
health: newHealth,
pressurePlateActivated: state.pressurePlateActivated || slideResult.crossedPressurePlate,
moves: newMoves,
};
const nextStateKey = buildStateKey(nextState);
if (!visited.has(nextStateKey)) {
queue.push(nextState);
}
}
}
const positions = Array.from(minMovesByPosition.entries())
.map(([key, minMoves]) => ({
position: parsePositionKey(key),
minMoves,
reachedWithPlateActive: plateActivePositions.has(key),
}))
.sort((a, b) => {
if (a.minMoves !== b.minMoves) return a.minMoves - b.minMoves;
if (a.position.y !== b.position.y) return a.position.y - b.position.y;
return a.position.x - b.position.x;
});
return {
positions,
iterations,
visitedStates: visited.size,
exhausted: iterations >= MAX_SOLVER_ITERATIONS,
goalReachable: minMovesByPosition.has(positionKey(puzzle.goal)),
pressurePlateReachable,
barrierHitCount,
validFirstMoves: Array.from(validFirstMoves),
};
}
function getNearestToGoal(positions: ReachablePositionInfo[], goal: Position): Position | null {
if (positions.length === 0) return null;
let nearest: Position | null = null;
let nearestDistance = Number.POSITIVE_INFINITY;
for (const item of positions) {
const distance = Math.abs(item.position.x - goal.x) + Math.abs(item.position.y - goal.y);
if (distance < nearestDistance) {
nearestDistance = distance;
nearest = item.position;
}
}
return nearest;
}
export function buildUnsolvableDiagnostics(puzzle: PuzzleData, solverResult: SolverResult): UnsolvableDiagnostics {
const report = exploreReachability(puzzle, puzzle.start);
const reasons: string[] = [];
const suggestions: string[] = [];
if (solverResult.exhausted || report.exhausted) {
reasons.push(`Solver hit iteration limit (${solverResult.iterations}). Complexity may be too high for exhaustive search.`);
}
if (report.validFirstMoves.length === 0) {
reasons.push(`No valid first move from start (${puzzle.start.x}, ${puzzle.start.y}).`);
}
if (!report.goalReachable) {
reasons.push(`No survivable landing path reaches goal (${puzzle.goal.x}, ${puzzle.goal.y}).`);
}
const goalLineReachableCount = report.positions.filter((item) => (
item.position.x === puzzle.goal.x || item.position.y === puzzle.goal.y
)).length;
if (goalLineReachableCount === 0) {
reasons.push(`No reachable landing tile shares goal row/column (x=${puzzle.goal.x} or y=${puzzle.goal.y}).`);
}
if (puzzle.pressurePlate && !report.pressurePlateReachable) {
reasons.push(`Pressure plate at (${puzzle.pressurePlate.x}, ${puzzle.pressurePlate.y}) is unreachable.`);
if (puzzle.barrier && report.barrierHitCount > 0) {
reasons.push(`Barrier at (${puzzle.barrier.x}, ${puzzle.barrier.y}) blocks attempted routes while plate stays inactive.`);
}
} else if (puzzle.barrier && report.barrierHitCount > 0) {
reasons.push(`Barrier at (${puzzle.barrier.x}, ${puzzle.barrier.y}) is still hit on explored routes. Sequencing may be incorrect.`);
}
const nearest = getNearestToGoal(report.positions, puzzle.goal);
if (nearest && !samePosition(nearest, puzzle.goal)) {
reasons.push(`Nearest reachable landing to goal is (${nearest.x}, ${nearest.y}), still blocked from finishing.`);
}
if (report.validFirstMoves.length > 0) {
suggestions.push(`Try simulate_move from start with one of: ${report.validFirstMoves.join(', ')}.`);
} else {
suggestions.push(`Use test_placement near start (${puzzle.start.x}, ${puzzle.start.y}) to create an initial stopping point.`);
}
if (nearest) {
suggestions.push(`Run reachable_from at (${nearest.x}, ${nearest.y}) to inspect local landing options.`);
suggestions.push(`Use suggest_stop_points from (${nearest.x}, ${nearest.y}) in key directions to find blocker placements.`);
} else {
suggestions.push(`Run reachable_from at start (${puzzle.start.x}, ${puzzle.start.y}) to inspect current movement graph.`);
}
if (reasons.length === 0) {
reasons.push('No direct blocker category identified. Inspect reachability frontier and mechanic sequencing.');
}
return {
reasons,
suggestions,
reachability: {
reachableTileCount: report.positions.length,
goalLineReachableCount,
nearestReachableToGoal: nearest,
report,
},
};
}
export function suggestStopPoints(puzzle: PuzzleData, from: Position, direction: Direction): StopPointReport {
const delta = DIRECTION_VECTORS[direction];
const traversedPath: Position[] = [];
const blockedCandidates: Array<{ blocker: Position; reason: string }> = [];
const suggestions: StopPointSuggestion[] = [];
let pressurePlateActivated = false;
let cx = from.x;
let cy = from.y;
while (true) {
const nx = cx + delta.x;
const ny = cy + delta.y;
const isGoalOnEdge = puzzle.goal.x === nx && puzzle.goal.y === ny;
if (!isGoalOnEdge && (nx <= 0 || nx >= puzzle.width - 1 || ny <= 0 || ny >= puzzle.height - 1)) {
break;
}
const pushableRock = puzzle.pushableRocks?.some((rock) => rock.x === nx && rock.y === ny) ?? false;
if (pushableRock) break;
const obstacle = puzzle.obstacles.find((obs) => obs.x === nx && obs.y === ny);
if (obstacle && (obstacle.type === 'rock' || obstacle.type === 'wall')) {
break;
}
cx = nx;
cy = ny;
traversedPath.push({ x: cx, y: cy });
if (puzzle.pressurePlate && puzzle.pressurePlate.x === cx && puzzle.pressurePlate.y === cy) {
pressurePlateActivated = true;
}
if (puzzle.barrier && puzzle.barrier.x === cx && puzzle.barrier.y === cy && !pressurePlateActivated) {
break;
}
if (obstacle && (obstacle.type === 'lava' || isHotCoalsType(obstacle.type))) {
break;
}
if (puzzle.warps?.some((warp) => warp.x === cx && warp.y === cy)) {
break;
}
if (cx === puzzle.goal.x && cy === puzzle.goal.y) {
break;
}
}
for (const landing of traversedPath) {
const blocker = { x: landing.x + delta.x, y: landing.y + delta.y };
if (blocker.x <= 0 || blocker.x >= puzzle.width - 1 || blocker.y <= 0 || blocker.y >= puzzle.height - 1) {
blockedCandidates.push({ blocker, reason: 'outside playable area' });
continue;
}
const occupied = isOccupied(puzzle, blocker.x, blocker.y);
if (occupied) {
blockedCandidates.push({ blocker, reason: `occupied by ${occupied}` });
continue;
}
const distanceFromStart = Math.abs(landing.x - from.x) + Math.abs(landing.y - from.y);
suggestions.push({
blocker,
landAt: { ...landing },
distanceFromStart,
});
}
const currentLandingResult = slideWithHazards(from, direction, puzzle);
return {
currentLanding: currentLandingResult.position,
traversedPath,
suggestions,
blockedCandidates,
};
}