import { Position, ObstacleType, PlaceableTileType, Direction, PuzzleData, DraftState } from './types.js';
import { GRID_MIN, GRID_MAX, MAX_OBSTACLES } from './constants.js';
export function validatePosition(
x: number,
y: number,
width: number,
height: number
): { valid: boolean; error?: string } {
if (x < 1 || x > width - 2) {
return { valid: false, error: `X position ${x} is out of playable area (1 to ${width - 2})` };
}
if (y < 1 || y > height - 2) {
return { valid: false, error: `Y position ${y} is out of playable area (1 to ${height - 2})` };
}
return { valid: true };
}
export function validateGridSize(
width: number,
height: number
): { valid: boolean; error?: string } {
if (width < GRID_MIN || width > GRID_MAX) {
return { valid: false, error: `Width ${width} is out of range (${GRID_MIN}-${GRID_MAX})` };
}
if (height < GRID_MIN || height > GRID_MAX) {
return { valid: false, error: `Height ${height} is out of range (${GRID_MIN}-${GRID_MAX})` };
}
return { valid: true };
}
export function validateObstacleType(type: string): type is ObstacleType {
const validTypes: ObstacleType[] = ['rock', 'wall', 'lava', 'hot_coals', 'spike', 'thin_ice', 'pushable_rock', 'barrier'];
return validTypes.includes(type as ObstacleType);
}
export function validatePlaceableTileType(type: string): type is PlaceableTileType {
const validTypes: PlaceableTileType[] = ['rock', 'lava', 'hot_coals', 'spike'];
return validTypes.includes(type as PlaceableTileType);
}
export function validateDirection(dir: string): dir is Direction {
const validDirections: Direction[] = ['up', 'down', 'left', 'right'];
return validDirections.includes(dir as Direction);
}
export function validatePuzzleData(data: unknown): {
valid: boolean;
error?: string;
data?: PuzzleData;
} {
if (!data || typeof data !== 'object') {
return { valid: false, error: 'Puzzle data must be an object' };
}
const puzzle = data as Partial<PuzzleData>;
const rawData = data as Record<string, unknown>;
// Dirt has been removed from the MCP schema.
if ('dirtTiles' in rawData) {
return { valid: false, error: 'dirtTiles is no longer supported' };
}
// Required fields
if (!puzzle.id || typeof puzzle.id !== 'string') {
return { valid: false, error: 'Missing or invalid id' };
}
if (!puzzle.name || typeof puzzle.name !== 'string') {
return { valid: false, error: 'Missing or invalid name' };
}
if (!puzzle.theme || typeof puzzle.theme !== 'string') {
return { valid: false, error: 'Missing or invalid theme' };
}
if (typeof puzzle.width !== 'number' || typeof puzzle.height !== 'number') {
return { valid: false, error: 'Missing or invalid width/height' };
}
if (typeof puzzle.par !== 'number') {
return { valid: false, error: 'Missing or invalid par' };
}
// Validate grid size
const gridValidation = validateGridSize(puzzle.width, puzzle.height);
if (!gridValidation.valid) {
return gridValidation;
}
// Validate start position
if (!puzzle.start || typeof puzzle.start.x !== 'number' || typeof puzzle.start.y !== 'number') {
return { valid: false, error: 'Missing or invalid start position' };
}
const startValidation = validatePosition(puzzle.start.x, puzzle.start.y, puzzle.width, puzzle.height);
if (!startValidation.valid) {
return { valid: false, error: `Invalid start position: ${startValidation.error}` };
}
// Validate goal position
if (!puzzle.goal || typeof puzzle.goal.x !== 'number' || typeof puzzle.goal.y !== 'number') {
return { valid: false, error: 'Missing or invalid goal position' };
}
const goalValidation = validateGoalPosition(puzzle.goal.x, puzzle.goal.y, puzzle.width, puzzle.height);
if (!goalValidation.valid) {
return { valid: false, error: `Invalid goal position: ${goalValidation.error}` };
}
if (puzzle.start.x === puzzle.goal.x && puzzle.start.y === puzzle.goal.y) {
return { valid: false, error: 'Start and goal positions cannot be the same' };
}
// Validate obstacles
if (!Array.isArray(puzzle.obstacles)) {
return { valid: false, error: 'Obstacles must be an array' };
}
for (const obstacle of puzzle.obstacles) {
if (typeof obstacle.x !== 'number' || typeof obstacle.y !== 'number') {
return { valid: false, error: 'Invalid obstacle position' };
}
if (!validateObstacleType(obstacle.type)) {
return { valid: false, error: `Invalid obstacle type: ${obstacle.type}` };
}
}
// Optional arrays validation
if (puzzle.warps && !Array.isArray(puzzle.warps)) {
return { valid: false, error: 'Warps must be an array' };
}
if (puzzle.thinIceTiles && !Array.isArray(puzzle.thinIceTiles)) {
return { valid: false, error: 'Thin ice tiles must be an array' };
}
if (puzzle.pushableRocks && !Array.isArray(puzzle.pushableRocks)) {
return { valid: false, error: 'Pushable rocks must be an array' };
}
return { valid: true, data: puzzle as PuzzleData };
}
export function validateNotOnStartOrGoal(
x: number,
y: number,
draft: DraftState
): { valid: boolean; error?: string } {
if (draft.startPosition.x === x && draft.startPosition.y === y) {
return { valid: false, error: 'Cannot place element on start position' };
}
if (draft.goalPosition.x === x && draft.goalPosition.y === y) {
return { valid: false, error: 'Cannot place element on goal position' };
}
return { valid: true };
}
export function validateMaxObstacles(draft: DraftState): { valid: boolean; error?: string } {
const totalObstacles = draft.obstacles.length;
if (totalObstacles >= MAX_OBSTACLES) {
return { valid: false, error: `Maximum obstacles (${MAX_OBSTACLES}) reached` };
}
return { valid: true };
}
export function validateStartGoalDifferent(
startX: number,
startY: number,
goalX: number,
goalY: number
): { valid: boolean; error?: string } {
if (startX === goalX && startY === goalY) {
return { valid: false, error: 'Start and goal positions cannot be the same' };
}
return { valid: true };
}
export function validateGoalPosition(
x: number,
y: number,
width: number,
height: number
): { valid: boolean; error?: string; isEdge?: boolean } {
// Goal can be on edges (replaces wall), but not corners (unreachable)
const isCorner = (x === 0 || x === width - 1) && (y === 0 || y === height - 1);
if (isCorner) {
return { valid: false, error: `Cannot place goal on corner (${x}, ${y}) - corners are unreachable` };
}
// Must be within grid bounds
if (x < 0 || x >= width || y < 0 || y >= height) {
return { valid: false, error: `Position (${x}, ${y}) is outside the grid (0-${width - 1}, 0-${height - 1})` };
}
const isEdge = x === 0 || x === width - 1 || y === 0 || y === height - 1;
return { valid: true, isEdge };
}