ai/heuristics/config/heuristic-config-schema.js

/**
 * @fileoverview Schema-Definition und Validierung für Heuristik-Konfigurationen.
 * Unterstützt Profile v1–v7 (baseline, positional, aggressive, defensive, offensive, local, global).
 * Validierung clampt Range-Verletzungen mit Warning statt throw (§5 ENGINEERING_CONVENTIONS).
 * @author Alexander Wolf
 */

const HEURISTIC_CONFIG_SCHEMA = {
    required: ['game', 'variant'],
    games: ['ttt', 'connect4', 'knights-tour', 'chess-variant', 'nim', 'generic'],
    variants: {
        ttt: ['regular', '3d', 'ultimate'],
        connect4: ['regular', '3d'],
        'knights-tour': ['warnsdorf'],
        'chess-variant': ['pawn-chess', 'anti-chess'],
        nim: ['standard'],
        generic: ['winLoss']
    },
    /** Erlaubte Profilnamen für differenzierte Heuristik-Varianten */
    profiles: [
        'v1_baseline', 'v2_positional', 'v3_aggressive',
        'v4_defensive', 'v5_offensive', 'v6_local', 'v7_global'
    ],
    weightDefinitions: {
        // ── Terminal-Bewertungen ──
        // Skalierung: Regular=1000, 3D=10000, Ultimate/C4=100000
        // Regel: Taktische Werte (fork, pressure) ≤ 1/10 von win
        win:                  { type: 'number', default: 1000,  min: 1,         max: 10000000 },
        loss:                 { type: 'number', default: -1000, min: -10000000, max: -1 },
        // draw skaliert mit dem Spieltyp: Regular=±100, 3D=±1000, Ultimate/C4=±10000
        // Bereich ±100000 deckt alle Varianten ab (Regular draw 0-100 << win 1000)
        draw:                 { type: 'number', default: 0,     min: -100000,   max: 100000 },

        // ── Linien-Bewertungen ──
        oneInLine:            { type: 'number', default: 1,     min: 0,   max: 100 },
        twoInLine:            { type: 'number', default: 10,    min: 0,   max: 500 },
        threeInLine:          { type: 'number', default: 100,   min: 0,   max: 5000 },
        fourInRow:            { type: 'number', default: 500,   min: 0,   max: 100000 },

        // ── Gegner-Linien (negativ) ──
        opponentTwoInLine:    { type: 'number', default: -10,   min: -500,  max: 0 },
        opponentThreeInLine:  { type: 'number', default: -90,   min: -5000, max: 0 },

        // ── Positionelle Gewichte ──
        centerBonus:          { type: 'number', default: 20,    min: 0,   max: 500 },
        centerWeight:         { type: 'number', default: 3,     min: 0,   max: 50 },
        spatialControl:       { type: 'number', default: 2,     min: 0,   max: 100 },
        cornerBonus:          { type: 'number', default: 3,     min: 0,   max: 200 },
        edgeBonus:            { type: 'number', default: 1,     min: 0,   max: 100 },

        // ── Taktische Gewichte ──
        forkBonus:            { type: 'number', default: 50,    min: 0,    max: 500 },
        opponentForkPenalty:  { type: 'number', default: -50,   min: -500, max: 0 },
        pressureWeight:       { type: 'number', default: 15,    min: 0,    max: 200 },
        blockUrgency:         { type: 'number', default: 80,    min: 0,    max: 500 },
        threatMultiplier:     { type: 'number', default: 5,     min: 0,    max: 50 },
        setupBonus:           { type: 'number', default: 15,    min: 0,    max: 200 },

        // ── Strukturelle Gewichte ──
        connectivityBonus:    { type: 'number', default: 8,     min: 0,    max: 100 },
        heightPenalty:        { type: 'number', default: -2,    min: -100, max: 0 },
        mobilityWeight:       { type: 'number', default: 1,     min: 0,    max: 100 },

        // ── Ultimate TTT spezifisch ──
        macroWeight:          { type: 'number', default: 50,    min: 0,    max: 1000 },
        boardWonBonus:        { type: 'number', default: 20,    min: 0,    max: 500 }
    }
};

/**
 * Validiert eine Heuristik-Konfiguration gegen das zentrale Schema.
 * Unterstützt optionales `profile`-Feld und `description`-Feld.
 * @param {Object} config - Rohkonfiguration.
 * @returns {Object} Validierte Konfiguration mit normalisierten Gewichten.
 */
function validateHeuristicConfig(config) {
    for (const field of HEURISTIC_CONFIG_SCHEMA.required) {
        if (config[field] === undefined || config[field] === null || config[field] === '') {
            throw new Error(`Pflichtfeld '${field}' fehlt in Heuristik-Config.`);
        }
    }

    if (!HEURISTIC_CONFIG_SCHEMA.games.includes(config.game)) {
        throw new Error(`Unbekanntes Spiel: '${config.game}'.`);
    }

    const allowedVariants = HEURISTIC_CONFIG_SCHEMA.variants[config.game] || [];
    if (!allowedVariants.includes(config.variant)) {
        throw new Error(`Unbekannte Variante '${config.variant}' für Spiel '${config.game}'.`);
    }

    const inputWeights = config.weights || {};
    const validatedWeights = {};

    for (const [key, definition] of Object.entries(HEURISTIC_CONFIG_SCHEMA.weightDefinitions)) {
        if (!(key in inputWeights)) {
            validatedWeights[key] = definition.default;
            continue;
        }

        const value = inputWeights[key];

        if (typeof value !== definition.type) {
            if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
                DebugConfig.log(DEBUG_DOMAINS.AI_HEURISTICS, 'warn',
                    `Parameter '${key}' hat falschen Typ '${typeof value}', erwartet '${definition.type}'. Verwende Default.`);
            }
            validatedWeights[key] = definition.default;
            continue;
        }

        let clamped = value;
        if (definition.min !== undefined && value < definition.min) {
            clamped = definition.min;
            if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
                DebugConfig.log(DEBUG_DOMAINS.AI_HEURISTICS, 'warn',
                    `Parameter '${key}': ${value} < min ${definition.min}, clamped auf ${clamped}.`);
            }
        }
        if (definition.max !== undefined && value > definition.max) {
            clamped = definition.max;
            if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
                DebugConfig.log(DEBUG_DOMAINS.AI_HEURISTICS, 'warn',
                    `Parameter '${key}': ${value} > max ${definition.max}, clamped auf ${clamped}.`);
            }
        }

        validatedWeights[key] = clamped;
    }

    for (const key of Object.keys(inputWeights)) {
        if (!HEURISTIC_CONFIG_SCHEMA.weightDefinitions[key]) {
            if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
                DebugConfig.log(DEBUG_DOMAINS.AI_HEURISTICS, 'warn', 'Unknown heuristic weight ignored', { key });
            }
        }
    }

    return {
        ...config,
        name: config.name || `${config.game}-${config.variant}`,
        profile: config.profile || null,
        description: config.description || '',
        weights: validatedWeights
    };
}