/**
* @fileoverview GenericTreeAdapter - Spielunabhängiger Tree-Adapter für numerische Bäume
*
* Erbt von BaseTreeAdapter und arbeitet ausschließlich mit numerischen Werten.
* Kein Wissen über TicTacToe, Knights-Tour oder andere Spiele.
*
* Features:
* - boardType: 'numeric' für alle Knoten (Kreisdarstellung im Diagramm-Modus)
* - Interaktive Klick-Evaluation (handleNodeClick → evaluateNode)
* - Minimax-Logik (MAX/MIN-Ebenen, Wert-Propagation, Best-Edge-Highlighting)
* - Optional: Alpha-Beta-Pruning (enableAlphaBeta-Flag)
* - Statistik-Callbacks (onStatsChanged, onActiveNodeChanged)
*
* Separation of Concerns:
* - Adapter orchestriert Baum-Aufbau und Evaluation
* - Rendering delegiert an NumericNodeRenderer (via boardType: 'numeric')
* - Farben/Status aus zentraler StatusConfig
*
* @class GenericTreeAdapter
* @extends BaseTreeAdapter
* @author Alexander Wolf
* @version 1.0
* @see ENGINEERING_CONVENTIONS.md
*/
class GenericTreeAdapter extends BaseTreeAdapter {
/**
* @param {HTMLIFrameElement} iframeElement - Ziel-iFrame für Tree-Visualisierung
*/
constructor(iframeElement) {
super(iframeElement);
// Debug-Konfiguration für diesen Adapter
this._debugDomain = 'VIZ_TREE_ADAPTER_GENERIC';
this._debugPrefix = 'GenericTreeAdapter';
/** @type {boolean} Steuert ob Alpha-Beta-Pruning aktiv ist */
this.enableAlphaBeta = false;
this.constants = this._resolveConstants();
}
/**
* Lädt zentrale Konstanten mit robusten Fallbacks.
* @returns {Object} Aufgelöste Konstanten
* @private
*/
_resolveConstants() {
const numViz = typeof NUMERIC_VIZ_CONSTANTS !== 'undefined' ? NUMERIC_VIZ_CONSTANTS : {};
const edgeColors = numViz.EDGE_COLORS || {};
const edgeWidths = numViz.EDGE_WIDTHS || {};
return {
edgeBestColor: edgeColors.BEST || '#27ae60',
edgePrunedColor: edgeColors.PRUNED || '#bdc3c7',
edgeCutoffAlpha: edgeColors.ALPHA_CUTOFF || '#e74c3c',
edgeCutoffBeta: edgeColors.BETA_CUTOFF || '#3498db',
edgeBestWidth: edgeWidths.BEST || 3,
edgePrunedWidth: edgeWidths.PRUNED || 1,
edgeCutoffWidth: edgeWidths.CUTOFF || 2.5,
};
}
/**
* Liefert Farbe und Breite für Best-Edge-Highlighting.
* @param {number} bestValue
* @returns {{color: string, width: number}}
* @protected
*/
_getHighlightEdgeStyle(bestValue) {
return { color: this.constants.edgeBestColor, width: this.constants.edgeBestWidth };
}
/**
* Liefert initiale Konfiguration für die Tree-Visualisierung.
* @returns {Object} Config-Objekt für TreeEngine
*/
getInitialConfig() {
return {
showLevelIndicators: true,
levelIndicatorType: 'minimax',
rootPlayerColor: '#e74c3c',
opponentColor: '#3498db',
enableTreeExpansion: true,
};
}
// ========================================================================
// BAUM-AUFBAU (wird von TreeFactory aufgerufen)
// ========================================================================
/**
* Erstellt einen numerischen Knoten im Baum.
*
* @param {number|null} value - Knotenwert (null für innere Knoten vor Evaluation)
* @param {number|null} parentId - Elternknoten-ID (null für Root)
* @param {Object} opts - Zusätzliche Optionen
* @param {number} opts.depth - Tiefe im Baum
* @param {boolean} opts.isMaximizing - MAX- oder MIN-Knoten
* @param {boolean} [opts.isTerminal=false] - Blattknoten?
* @param {boolean} [opts.isPruned=false] - Geprunter Knoten?
* @param {number} [opts.alpha] - Alpha-Wert (nur bei AB)
* @param {number} [opts.beta] - Beta-Wert (nur bei AB)
* @returns {number} Neue Knoten-ID
*/
addNumericNode(value, parentId, opts) {
const metadata = {
value: value,
depth: opts.depth,
isMaximizing: opts.isMaximizing,
isTerminal: opts.isTerminal || false,
isPruned: opts.isPruned || false,
};
// Alpha/Beta nur setzen wenn AB aktiv
if (this.enableAlphaBeta) {
metadata.alpha = opts.alpha !== undefined ? opts.alpha : -Infinity;
metadata.beta = opts.beta !== undefined ? opts.beta : Infinity;
}
const status = opts.isPruned ? 'PRUNED' : 'WAIT';
// Synthetisches State-Objekt als Key für nodeMap
const syntheticState = { id: this.nodeIdCounter };
const nodeId = this.createNode(syntheticState, parentId, metadata, status, 'numeric');
// nodeStates speichert null (kein Spielzustand)
this.nodeStates.set(nodeId, null);
// treeStructure eintragen
const structData = {
parentId: parentId,
children: [],
status: status,
value: value,
depth: opts.depth,
isMaximizing: opts.isMaximizing,
isTerminal: opts.isTerminal || false,
};
if (this.enableAlphaBeta) {
structData.alpha = metadata.alpha;
structData.beta = metadata.beta;
structData.inheritedAlpha = metadata.alpha;
structData.inheritedBeta = metadata.beta;
}
this.treeStructure.set(nodeId, structData);
// Parent-Referenz aktualisieren
if (parentId !== null) {
const parentData = this.treeStructure.get(parentId);
if (parentData) {
parentData.children.push(nodeId);
}
}
return nodeId;
}
// ========================================================================
// BAUM-GENERIERUNG (Einstiegspunkte)
// ========================================================================
/**
* Baut einen vordefinierten Baum auf und startet die Visualisierung.
*
* @param {Object} treeDefinition - Baumdefinition aus TreeFactory
* @param {Object} treeDefinition.root - Wurzelknoten-Definition
* @param {Object} config - Konfiguration
* @param {boolean} [config.enableAlphaBeta=false] - AB-Pruning aktivieren
* @returns {Object} Ergebnis mit Statistiken
*/
visualizeTree(treeDefinition, config = {}) {
this._debugLog('visualizeTree:start', { enableAlphaBeta: config.enableAlphaBeta });
this.enableAlphaBeta = config.enableAlphaBeta === true;
this.stats = { nodesVisited: 0, nodesPruned: 0, evaluatedNodes: 0 };
this._notifyStatsChanged();
this.reset();
// Baum rekursiv aufbauen
this._buildSubtree(treeDefinition.root, null, 0, true);
// Status aller Knoten initial setzen
this._initializeAllStatuses();
this._debugLog('visualizeTree:complete', {
totalNodes: this.nodeIdCounter,
commandQueueSize: this.commands.length,
});
this.flushCommands();
return {
nodesVisited: this.stats.nodesVisited,
nodesPruned: this.stats.nodesPruned,
evaluatedNodes: this.stats.evaluatedNodes,
bestValue: null,
};
}
/**
* Baut einen Teilbaum rekursiv auf.
*
* @param {Object} nodeDef - Knoten-Definition {value, children[]}
* @param {number|null} parentId - Eltern-ID
* @param {number} depth - Aktuelle Tiefe
* @param {boolean} isMaximizing - MAX oder MIN Ebene
* @returns {number} Erstellte Knoten-ID
* @private
*/
_buildSubtree(nodeDef, parentId, depth, isMaximizing) {
const isLeaf = !nodeDef.children || nodeDef.children.length === 0;
const value = isLeaf ? nodeDef.value : null;
const nodeId = this.addNumericNode(value, parentId, {
depth: depth,
isMaximizing: isMaximizing,
isTerminal: isLeaf,
});
if (!isLeaf) {
for (const childDef of nodeDef.children) {
this._buildSubtree(childDef, nodeId, depth + 1, !isMaximizing);
}
}
return nodeId;
}
/**
* Setzt initiale Status für alle Knoten nach dem Aufbau.
* Ohne AB: Blattknoten → automatisch EVALUATED (Wert steht fest).
* Mit AB: Blattknoten → READY (Nutzer muss klicken, damit AB-Werte propagiert werden).
* @private
*/
_initializeAllStatuses() {
let autoEvaluated = 0;
// Schritt 1: Blattknoten-Initialisierung
for (const [nodeId, data] of this.treeStructure) {
if (data.isTerminal) {
const value = data.value !== null && data.value !== undefined ? data.value : 0;
if (this.enableAlphaBeta) {
// Mit AB: Blattknoten als READY markieren, damit Nutzer sie anklickt
// und AB-Werte korrekt propagiert werden
NodeStatusManager.setNodeStatus(nodeId, 'READY', [], this.treeStructure, this.commands);
this.commands.push({
action: 'UPDATE_NODE',
id: nodeId,
data: {
label: String(value),
boardData: { value: value },
metadata: {
depth: data.depth,
isMaximizing: data.isMaximizing,
value: value,
alpha: data.alpha,
beta: data.beta,
},
}
});
} else {
// Ohne AB: Blattknoten direkt als EVALUATED setzen
NodeStatusManager.setNodeStatus(nodeId, 'EVALUATED', [], this.treeStructure, this.commands);
this.commands.push({
action: 'UPDATE_NODE',
id: nodeId,
data: {
label: String(value),
boardData: { value: value },
metadata: {
depth: data.depth,
isMaximizing: data.isMaximizing,
value: value,
},
}
});
autoEvaluated++;
}
}
}
// Schritt 2: Innere Knoten prüfen
// Elternknoten von Blättern werden READY (alle Kinder EVALUATED)
for (const [nodeId, data] of this.treeStructure) {
if (!data.isTerminal) {
this.checkNodeStatus(nodeId);
}
}
// Statistiken für auto-evaluierte Blätter
this.stats.nodesVisited += autoEvaluated;
this.stats.evaluatedNodes += autoEvaluated;
this._notifyStatsChanged();
}
// ========================================================================
// INTERAKTIVE EVALUATION (handleNodeClick + checkNodeStatus geerbt von BaseTreeAdapter)
// ========================================================================
/**
* Bewertet einen Knoten (Blatt oder innerer Knoten).
*
* - Blatt: Übernimmt den gespeicherten Wert
* - Innerer Knoten: MAX/MIN über evaluierte Kinder
* - Bei AB: Prüft Pruning nach Evaluation
*
* @param {number} nodeId - Zu bewertende Knoten-ID
*/
evaluateNode(nodeId) {
const data = this.treeStructure.get(nodeId);
if (!data) {
this._debugLog('evaluateNode:missing-data', { nodeId });
return;
}
this.commands = [];
this.stats.nodesVisited += 1;
this.stats.evaluatedNodes += 1;
this._notifyStatsChanged();
let value = 0;
let labelText = '';
let statusesToAdd = ['EVALUATED'];
// CASE 1: Blattknoten
if (data.children.length === 0) {
value = data.value !== null && data.value !== undefined ? data.value : 0;
labelText = String(value);
// Blattknoten erhalten neutralen EVALUATED-Status
}
// CASE 2: Innerer Knoten
else {
const evaluatedChildren = data.children
.map(cid => this.treeStructure.get(cid))
.filter(c => c && c.status === 'EVALUATED');
if (evaluatedChildren.length === 0) {
this._debugLog('evaluateNode:no-evaluated-children', { nodeId });
return;
}
const values = evaluatedChildren.map(c => c.value);
if (data.isMaximizing) {
value = Math.max(...values);
labelText = `Max = ${value}`;
} else {
value = Math.min(...values);
labelText = `Min = ${value}`;
}
// Best-Edge hervorheben
this._highlightBestEdges(nodeId, value);
statusesToAdd = ['EVALUATED'];
}
// Wert speichern
data.value = value;
// Metadata-Update für Renderer (boardData.value aktualisieren)
const metadataUpdate = {
depth: data.depth,
isMaximizing: data.isMaximizing,
value: value,
};
if (this.enableAlphaBeta) {
metadataUpdate.alpha = data.alpha;
metadataUpdate.beta = data.beta;
}
// Status setzen
NodeStatusManager.setNodeStatus(nodeId, 'EVALUATED', statusesToAdd, this.treeStructure, this.commands);
// Node-Update senden (Label + Metadata + boardData.value)
this.commands.push({
action: 'UPDATE_NODE',
id: nodeId,
data: {
label: labelText,
boardData: { value: value },
metadata: metadataUpdate,
}
});
// Parent-Status und ggf. AB-Pruning prüfen
if (data.parentId !== null) {
if (this.enableAlphaBeta) {
this._checkPruning(data.parentId, nodeId);
}
this.checkNodeStatus(data.parentId);
}
this._debugLog('evaluateNode:done', {
nodeId, value, parentId: data.parentId,
enableAlphaBeta: this.enableAlphaBeta,
});
if (typeof this.onActiveNodeChanged === 'function') {
this.onActiveNodeChanged(nodeId, data);
}
this._notifyStatsChanged();
this.flushCommands();
}
// ========================================================================
// ALPHA-BETA PRUNING
// ========================================================================
/**
* Prüft ob am Elternknoten ein Alpha-Beta-Cutoff auftritt.
* Aktualisiert Alpha/Beta-Bounds und pruned ggf. verbleibende Geschwister.
*
* @param {number} parentId - Elternknoten-ID
* @param {number} [evaluatedChildId=null] - Gerade evaluiertes Kind (für Edge-Highlighting)
* @private
*/
_checkPruning(parentId, evaluatedChildId = null) {
const parent = this.treeStructure.get(parentId);
if (!parent || parent.status === 'EVALUATED') return;
// Bounds neu berechnen
this._recomputeBounds(parentId, evaluatedChildId);
if (parent.alpha >= parent.beta) {
this._debugLog('pruning:cutoff', {
parentId, alpha: parent.alpha, beta: parent.beta,
});
let newlyPruned = 0;
parent.children.forEach(childId => {
const child = this.treeStructure.get(childId);
if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;
newlyPruned += this._pruneSubtree(childId, parentId);
});
if (newlyPruned > 0) {
this.stats.nodesPruned += newlyPruned;
this._notifyStatsChanged();
}
}
}
/**
* Berechnet Alpha/Beta-Bounds eines Knotens aus seinen evaluierten Kindern.
* Visualisiert den AB-Informationsfluss über farbkodierte Kanten:
* - Rot: α-Update (Wert geht nach oben, MAX-Knoten)
* - Blau: β-Update (Wert geht nach oben, MIN-Knoten)
* - Propagation nach unten an offene Kinder in gleicher Farbe
*
* @param {number} nodeId - Knoten-ID
* @param {number} [evaluatedChildId=null] - Kind das gerade evaluiert wurde
* @private
*/
_recomputeBounds(nodeId, evaluatedChildId = null) {
const node = this.treeStructure.get(nodeId);
if (!node) return;
// Alte Bounds sichern für Änderungserkennung
const oldAlpha = node.alpha;
const oldBeta = node.beta;
let alpha = node.inheritedAlpha !== undefined ? node.inheritedAlpha : -Infinity;
let beta = node.inheritedBeta !== undefined ? node.inheritedBeta : Infinity;
const evaluatedChildren = node.children
.map(cid => this.treeStructure.get(cid))
.filter(c => c && c.status === 'EVALUATED');
if (node.isMaximizing && evaluatedChildren.length > 0) {
const maxVal = Math.max(...evaluatedChildren.map(c => c.value));
alpha = Math.max(alpha, maxVal);
}
if (!node.isMaximizing && evaluatedChildren.length > 0) {
const minVal = Math.min(...evaluatedChildren.map(c => c.value));
beta = Math.min(beta, minVal);
}
node.alpha = alpha;
node.beta = beta;
// ---- AB-Propagation Edge-Highlighting ----
const alphaChanged = node.alpha > oldAlpha;
const betaChanged = node.beta < oldBeta;
if (evaluatedChildId !== null && (alphaChanged || betaChanged)) {
// Edge vom evaluierten Kind zum Elternknoten (Wert geht nach oben)
const propColor = alphaChanged
? this.constants.edgeCutoffAlpha
: this.constants.edgeCutoffBeta;
this.commands.push({
action: 'HIGHLIGHT_EDGE',
from: nodeId,
to: evaluatedChildId,
color: propColor,
width: this.constants.edgeCutoffWidth,
showArrow: true,
arrowDirection: 'from', // Kind → Eltern (Wert fließt nach oben)
});
// Propagation nach unten: Bounds rekursiv an alle offenen Nachkommen
node.children.forEach(childId => {
if (childId === evaluatedChildId) return;
const child = this.treeStructure.get(childId);
if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;
this.commands.push({
action: 'HIGHLIGHT_EDGE',
from: nodeId,
to: childId,
color: propColor,
width: this.constants.edgeCutoffWidth,
showArrow: true,
arrowDirection: 'to',
});
});
}
// Metadata-Update an Visualisierung senden
this.commands.push({
action: 'UPDATE_NODE',
id: nodeId,
data: {
metadata: {
depth: node.depth,
isMaximizing: node.isMaximizing,
value: node.value,
alpha: node.alpha,
beta: node.beta,
},
},
});
// Bounds an offene Kinder propagieren
node.children.forEach(childId => {
const child = this.treeStructure.get(childId);
if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;
child.inheritedAlpha = node.alpha;
child.inheritedBeta = node.beta;
child.alpha = node.alpha;
child.beta = node.beta;
this.commands.push({
action: 'UPDATE_NODE',
id: childId,
data: {
metadata: {
depth: child.depth,
isMaximizing: child.isMaximizing,
value: child.value,
alpha: child.alpha,
beta: child.beta,
},
},
});
});
}
/**
* Markiert einen Teilbaum rekursiv als PRUNED.
*
* @param {number} nodeId - Start-Knoten
* @param {number|null} parentId - Elternknoten (für Kanten-Highlighting)
* @returns {number} Anzahl neu geprunter Knoten
* @private
*/
_pruneSubtree(nodeId, parentId) {
const node = this.treeStructure.get(nodeId);
if (!node) return 0;
if (node.status === 'EVALUATED') return 0;
let count = 0;
if (node.status !== 'PRUNED') {
// Wert beibehalten für Anzeige im Renderer (nicht auf null setzen)
NodeStatusManager.setNodeStatus(nodeId, 'PRUNED', [], this.treeStructure, this.commands);
// Pruned-Metadata aktualisieren für Renderer
this.commands.push({
action: 'UPDATE_NODE',
id: nodeId,
data: {
metadata: {
depth: node.depth,
isMaximizing: node.isMaximizing,
value: node.value,
isPruned: true,
},
},
});
// Kante als gepruned markieren
if (parentId !== null) {
this.commands.push({
action: 'HIGHLIGHT_EDGE',
from: parentId,
to: nodeId,
color: this.constants.edgePrunedColor,
width: this.constants.edgePrunedWidth,
});
}
count = 1;
}
node.children.forEach(childId => {
count += this._pruneSubtree(childId, nodeId);
});
return count;
}
// ========================================================================
// EXPANSION (not used in generic mode, but required by BaseTreeAdapter)
// ========================================================================
/**
* Nicht verwendet im generischen Modus (Baum wird vollständig aufgebaut).
* @param {number} nodeId
* @param {*} state
*/
expandNodeChildren(nodeId, state) {
// No-op: Generic trees are fully built before visualization
}
}