ai/heuristics/adapter/heuristic-rule-adapter.js

/**
 * @fileoverview Bridge zwischen numerischen Heuristiken und booleschen Regelbäumen.
 * @author Alexander Wolf
 */

/**
 * Adapterklasse von Heuristik-Scoring auf Rule-Engine-Entscheidungen.
 */
class HeuristicRuleAdapter extends AtomicRule {
    /**
     * @param {BaseHeuristic} heuristic
     * @param {Object} [thresholds]
     * @param {number} [thresholds.must=500]
     * @param {number} [thresholds.should=100]
     * @param {number} [thresholds.could=10]
     * @param {string} [mode='best-of']
     */
    constructor(heuristic, thresholds = {}, mode = 'best-of') {
        super(
            `H:${heuristic.name}`,
            `Heuristik-Regel (${heuristic.name})`,
            null
        );

        this.heuristic = heuristic;
        this.thresholds = {
            must: thresholds.must ?? 500,
            should: thresholds.should ?? 100,
            could: thresholds.could ?? 10
        };
        this.mode = mode;

        this.logicFn = (gameState) => this._adaptedEvaluate(gameState);
    }

    /**
     * @private
     * @param {GameState} gameState
     * @returns {(number|Object|null)}
     */
    _adaptedEvaluate(gameState) {
        const moves = gameState.getAllValidMoves ? gameState.getAllValidMoves() : [];
        if (!moves || moves.length === 0) return null;

        const player = gameState.currentPlayer;
        const scoredMoves = [];

        for (const move of moves) {
            const sim = gameState.clone();
            sim.makeMove(move);
            const score = this.heuristic.evaluate(sim, player);
            scoredMoves.push({ move, score, priority: this._getPriority(score) });
        }

        scoredMoves.sort((a, b) => b.score - a.score);
        const best = scoredMoves[0];

        DebugConfig.log(DEBUG_DOMAINS.AI_HEURISTICS, 'debug', 'HeuristicRuleAdapter evaluated moves', {
            heuristic: this.heuristic.name,
            mode: this.mode,
            best,
            thresholds: this.thresholds
        });

        if (!best) return null;

        if (this.mode === 'first-above') {
            const mustCandidate = scoredMoves.find((entry) => entry.score >= this.thresholds.must);
            return mustCandidate ? mustCandidate.move : null;
        }

        if (this.mode === 'weighted-random') {
            const candidates = scoredMoves.filter((entry) => entry.score >= this.thresholds.could);
            if (candidates.length === 0) return null;
            return candidates[Math.floor(Math.random() * candidates.length)].move;
        }

        return best.score >= this.thresholds.could ? best.move : null;
    }

    /**
     * @private
     * @param {number} score
     * @returns {'MUST'|'SHOULD'|'COULD'|'SKIP'}
     */
    _getPriority(score) {
        if (score >= this.thresholds.must) return 'MUST';
        if (score >= this.thresholds.should) return 'SHOULD';
        if (score >= this.thresholds.could) return 'COULD';
        return 'SKIP';
    }
}