/**
* @fileoverview Base Tree Adapter - Basisklasse für Tree-Visualisierungs-Adapter
*
* Enthält gemeinsame Infrastruktur für:
* - IFrame-Kommunikation via IframeBridgeHost (bevorzugt) oder Legacy-postMessage (Fallback)
* - State Management (NodeMap, Commands)
* - Grundlegende Tree-Erstellung (createNode)
* - Handshake & Reset
*
* @class BaseTreeAdapter
* @author Alexander Wolf
* @version 2.0
*/
class BaseTreeAdapter {
/**
* @param {HTMLIFrameElement} iframeElement
*/
constructor(iframeElement) {
this.iframe = iframeElement;
this.nodeIdCounter = 0;
this.nodeMap = new Map();
this.ready = false;
this.commands = [];
// State
this.currentGameState = null;
this.currentConfig = null;
this.nodeStates = new Map(); // id -> GameState
this.treeStructure = new Map(); // id -> { parentId, childrenIds[], status, value, ... }
this.rootPlayer = null;
// Stats & Callbacks (gemeinsam für alle Adapter)
/** @type {{nodesVisited: number, nodesPruned: number, evaluatedNodes: number}} */
this.stats = {
nodesVisited: 0,
nodesPruned: 0,
evaluatedNodes: 0,
};
/** @type {Function|null} Callback bei Statistik-Änderungen */
this.onStatsChanged = null;
/** @type {Function|null} Callback bei aktivem Knoten-Wechsel */
this.onActiveNodeChanged = null;
/** @type {Function|null} Callback bei Knoten-Klick aus TreeViz (nodeId, boardData) */
this.onNodeClicked = null;
/** @type {Function|null} Callback bei Expansion-Klick aus TreeViz (nodeId, boardData) */
this.onNodeFocused = null;
// Debug-Konfiguration (überschreibbar in Subklassen)
/** @type {string} Debug-Domain-Key für DebugConfig */
this._debugDomain = 'VIZ_TREE_ADAPTER';
/** @type {string} Log-Prefix für diesen Adapter */
this._debugPrefix = 'TreeAdapter';
// ==================== BRIDGE INTEGRATION ====================
/** @type {IframeBridgeHost|null} Bridge-Host-Instanz (wenn verfügbar) */
this._bridge = null;
/** @type {boolean} Bridge ist aktiv und hat Handshake abgeschlossen */
this._bridgeActive = false;
if (typeof IframeBridgeHost !== 'undefined' && this.iframe) {
this._initBridge();
} else {
// Legacy-Fallback: Direktes postMessage
this._debugLog('IframeBridgeHost not available — using legacy postMessage');
this._initLegacyListener();
}
this.startHandshake();
}
/** @private Timeout für Handshake-Fallback (ms) */
static HANDSHAKE_TIMEOUT_MS = 500;
/**
* Initialisiert die IframeBridgeHost-Instanz für die Kommunikation mit dem TreeViz-iframe.
* @private
*/
_initBridge() {
try {
this._bridge = new IframeBridgeHost(this.iframe, {
sourceId: `tree-adapter-${this._debugPrefix.toLowerCase()}`,
acceptLegacy: true,
handshakeTimeout: 3000
});
// TREE:READY (Bridge-konform) oder SYSTEM:READY (auto-converted)
this._bridge.on('SYSTEM:READY', () => {
this._debugLog('Bridge: SYSTEM:READY received — adapter ready');
this._bridgeActive = true;
this.onTreeReady();
});
// TREE:NODE_CLICKED via Bridge
this._bridge.on('TREE:NODE_CLICKED', (payload) => {
this._debugLog('Bridge: NODE_CLICKED', { nodeId: payload?.nodeId });
this.handleNodeClick(payload.nodeId);
if (typeof this.onNodeClicked === 'function') {
this.onNodeClicked(payload.nodeId, payload.boardData);
}
});
// NODE:CLICKED (Tree-Viz-Engine sendet mit NODE: Namespace)
this._bridge.on('NODE:CLICKED', (payload) => {
this._debugLog('Bridge: NODE:CLICKED', { nodeId: payload?.nodeId });
this.handleNodeClick(payload.nodeId);
if (typeof this.onNodeClicked === 'function') {
this.onNodeClicked(payload.nodeId, payload.boardData);
}
});
// TREE:NODE_FOCUSED via Bridge
this._bridge.on('TREE:NODE_FOCUSED', (payload) => {
this._debugLog('Bridge: NODE_FOCUSED', { nodeId: payload?.nodeId });
if (typeof this.onNodeFocused === 'function') {
this.onNodeFocused(payload.nodeId, payload.boardData);
}
});
// NODE:FOCUSED (Tree-Viz-Engine sendet mit NODE: Namespace)
this._bridge.on('NODE:FOCUSED', (payload) => {
this._debugLog('Bridge: NODE:FOCUSED', { nodeId: payload?.nodeId });
if (typeof this.onNodeFocused === 'function') {
this.onNodeFocused(payload.nodeId, payload.boardData);
}
});
// TREE:EXPANSION_REQUEST via Bridge
this._bridge.on('TREE:EXPANSION_REQUEST', (payload) => {
this._debugLog('Bridge: EXPANSION_REQUEST', { nodeId: payload?.nodeId });
this.handleExpansionRequest(payload.nodeId);
});
this._debugLog('IframeBridgeHost initialized', { sourceId: this._bridge.sourceId });
} catch (err) {
this._debugLog('Bridge initialization failed — falling back to legacy', { error: err.message }, 'warn');
this._bridge = null;
this._initLegacyListener();
}
}
/**
* Initialisiert den Legacy-Message-Listener (Fallback ohne Bridge).
* @private
*/
_initLegacyListener() {
this._messageListener = (event) => {
if (!event.data) return;
// Bridge-Messages ignorieren
if (event.data._bridge) return;
this.handleMessage(event.data);
};
window.addEventListener('message', this._messageListener);
}
/**
* Entfernt Event-Listener und räumt Ressourcen auf.
*/
destroy() {
if (this._bridge) {
this._bridge.destroy();
this._bridge = null;
}
if (this._messageListener) {
window.removeEventListener('message', this._messageListener);
this._messageListener = null;
}
}
/**
* Verarbeitet eingehende Nachrichten vom Visualizer
* @param {Object} data
*/
handleMessage(data) {
this._debugLog('Message from iframe received', { type: data?.type });
if (data.type === 'TREE_READY') {
this._debugLog('TREE_READY received - adapter ready for communication');
this.onTreeReady();
}
else if (data.type === 'NODE_EXPANSION_REQUEST') {
this._debugLog('NODE_EXPANSION_REQUEST from iframe', { nodeId: data?.nodeId });
this.handleExpansionRequest(data.nodeId);
}
else if (data.type === 'NODE_CLICKED') {
this._debugLog('NODE_CLICKED from iframe', { nodeId: data?.nodeId });
this.handleNodeClick(data.nodeId);
if (typeof this.onNodeClicked === 'function') {
this.onNodeClicked(data.nodeId, data.boardData);
}
}
else if (data.type === 'NODE_FOCUSED') {
this._debugLog('NODE_FOCUSED from iframe', { nodeId: data?.nodeId });
if (typeof this.onNodeFocused === 'function') {
this.onNodeFocused(data.nodeId, data.boardData);
}
}
}
/**
* Startet den Handshake mit dem IFrame.
* Setzt ready-Flag als Fallback nach Timeout.
*/
startHandshake() {
setTimeout(() => { this.ready = true; }, BaseTreeAdapter.HANDSHAKE_TIMEOUT_MS);
}
/**
* Wird aufgerufen wenn das IFrame TREE_READY meldet.
* @protected
*/
onTreeReady() {
this.ready = true;
// Default impl: do nothing or send config
}
/**
* Sendet einen einzelnen Command an das IFrame.
* Nutzt IframeBridgeHost wenn verfügbar, sonst Legacy-postMessage.
* @param {Object} command - Command-Objekt mit action-Property
*/
sendCommand(command) {
if (!this.iframe || !this.iframe.contentWindow) {
this._debugLog('sendCommand: Iframe not available', { action: command?.action }, 'error');
return;
}
// Bridge bevorzugen
if (this._bridge) {
this._debugLog('sendCommand via Bridge', { action: command?.action });
this._bridge.send('TREE:COMMAND', command);
return;
}
// Legacy-Fallback
this._debugLog('sendCommand via postMessage (legacy)',
{ action: command?.action, hasCommands: !!command?.commands?.length });
this.iframe.contentWindow.postMessage({ type: 'TREE_COMMAND', command }, '*');
}
/**
* Sendet alle gepufferten Commands als Batch und leert den Puffer.
*/
flushCommands() {
if (this.commands.length > 0) {
this._debugLog('flushCommands: Sending batch',
{ commandCount: this.commands.length, batch: true });
this.sendCommand({ action: 'BATCH', commands: this.commands });
this.commands = [];
}
}
/**
* Erzeugt einen eindeutigen Schlüssel für ein Board/State-Objekt.
* @param {Object} board - Board- oder State-Objekt
* @returns {string} Eindeutiger Schlüssel
*/
getBoardKey(board) {
return board.getStateKey ? board.getStateKey() : JSON.stringify(board.grid || board);
}
/**
* Setzt den Adapter-Zustand zurück und sendet CLEAR an das IFrame.
*/
reset() {
this._debugLog('Reset: clearing state and sending CLEAR command');
this.nodeIdCounter = 0;
this.nodeMap.clear();
this.treeStructure.clear();
this.nodeStates.clear();
this.commands = [];
this.sendCommand({ action: 'CLEAR' });
// Config senden via Template Method
const config = this.getInitialConfig();
if (config) {
this._debugLog('Sending initial config', { config });
this.sendCommand({
action: 'UPDATE_CONFIG',
config: config
});
}
}
/**
* Liefert Initiale Config (überschreibbar)
*/
getInitialConfig() {
return {};
}
/**
* Erstellt einen Basis-Knoten und registriert ihn.
*
* Unterstützte boardTypes:
* - 'minimax': TicTacToe-Grid Darstellung (default für Rückwärtskompatibilität)
* - 'numeric': Kreis mit Zahlenwert (Diagramm-Modus, Lehrbuch-Darstellung)
* - undefined: Auto-Detection anhand boardData-Struktur (Knights-Tour, RotateBox)
*
* @param {GameState|Object} state - Spielzustand oder generisches Datenobjekt
* @param {number|null} parentId
* @param {Object} metadata - Metadaten (value, isMaximizing, alpha, beta, etc.)
* @param {string} status
* @param {string} [boardType='minimax'] - Expliziter boardType für Renderer-Routing
*/
createNode(state, parentId, metadata, status = 'WAIT', boardType = 'minimax') {
const nodeId = this.nodeIdCounter++;
const stateKey = this.getBoardKey(state);
this.nodeMap.set(stateKey, nodeId);
// boardData-Erstellung abhängig vom boardType
let boardData;
if (boardType === 'numeric') {
// Numerischer Modus: Minimalistische boardData mit Wert
boardData = {
value: metadata?.value ?? null,
};
} else {
// Standard-Modus (minimax/andere): Grid-basierte boardData
boardData = {
grid: [...(state.grid || state)],
currentPlayer: state.currentPlayer,
size: state.size || 3,
winner: state.winner
};
}
const command = {
action: 'ADD_NODE',
id: nodeId,
label: "",
boardData: boardData,
boardType: boardType,
metadata: { ...metadata },
status: status
};
if (parentId !== null) command.parentId = parentId;
this.commands.push(command);
return nodeId;
}
handleExpansionRequest(nodeId) {
if (this.nodeStates.has(nodeId)) {
this.expandNodeChildren(nodeId, this.nodeStates.get(nodeId));
}
this.flushCommands();
}
// ========================================================================
// GEMEINSAME METHODEN (geerbt von allen Subklassen)
// ========================================================================
/**
* Einheitlicher Debug-Logger für alle Tree-Adapter.
* Subklassen setzen this._debugDomain und this._debugPrefix im Konstruktor.
*
* @param {string} message - Log-Nachricht
* @param {Object} [payload] - Optionaler Payload
* @param {string} [level='debug'] - Log-Level ('debug', 'warn', 'error')
* @protected
*/
_debugLog(message, payload, level = 'debug') {
const domain = typeof DEBUG_DOMAINS !== 'undefined' && DEBUG_DOMAINS[this._debugDomain]
? DEBUG_DOMAINS[this._debugDomain]
: this._debugDomain;
if (payload !== undefined) {
DebugConfig.log(domain, level, `[${this._debugPrefix}] ${message}`, payload);
} else {
DebugConfig.log(domain, level, `[${this._debugPrefix}] ${message}`);
}
}
/**
* Benachrichtigt UI über Statistik-Änderungen.
* @protected
*/
_notifyStatsChanged() {
this._debugLog('stats-update', {
visited: this.stats.nodesVisited,
pruned: this.stats.nodesPruned,
evaluated: this.stats.evaluatedNodes,
});
if (typeof this.onStatsChanged !== 'function') return;
this.onStatsChanged({ ...this.stats });
}
/**
* Prüft ob ein Blattknoten terminal ist.
* Überschreibbar in Subklassen für spielspezifische Terminal-Erkennung.
*
* @param {number} nodeId - Knoten-ID
* @param {Object} data - treeStructure-Eintrag
* @returns {boolean}
* @protected
*/
_isLeafTerminal(nodeId, data) {
return data.isTerminal === true;
}
/**
* Prüft und aktualisiert den Status eines Knotens.
* - Blattknoten (terminal) → READY
* - Innerer Knoten (alle Kinder EVALUATED/PRUNED) → READY
* - Sonst → WAIT
*
* @param {number} nodeId - Zu prüfende Knoten-ID
*/
checkNodeStatus(nodeId) {
const data = this.treeStructure.get(nodeId);
if (!data) return;
if (data.status === 'EVALUATED' || data.status === 'PRUNED') return;
let newStatus = 'WAIT';
if (data.children.length === 0) {
newStatus = this._isLeafTerminal(nodeId, data) ? 'READY' : 'WAIT';
} else {
const allDone = data.children.every(childId => {
const child = this.treeStructure.get(childId);
return child && (child.status === 'EVALUATED' || child.status === 'PRUNED');
});
newStatus = allDone ? 'READY' : 'WAIT';
}
if (newStatus !== data.status) {
NodeStatusManager.setNodeStatus(nodeId, newStatus, [], this.treeStructure, this.commands);
}
}
/**
* Verarbeitet Klick auf einen Knoten.
* Nur READY-Knoten können evaluiert werden.
*
* @param {number} nodeId - Geklickte Knoten-ID
*/
handleNodeClick(nodeId) {
const data = this.treeStructure.get(nodeId);
if (!data || data.status !== 'READY') return;
this._debugLog('handleNodeClick', { nodeId, status: data.status });
if (typeof this.onActiveNodeChanged === 'function') {
this.onActiveNodeChanged(nodeId, data);
}
// Vorherige Kanten-Highlights zurücksetzen (AB-Propagation etc.)
this.sendCommand({ action: 'RESET_EDGE_HIGHLIGHTS' });
this.commands = [];
NodeStatusManager.setNodeStatus(nodeId, 'ACTIVE', [], this.treeStructure, this.commands);
this.flushCommands();
this.evaluateNode(nodeId);
}
/**
* Markiert die beste(n) Kante(n) eines inneren Knotens.
* Nutzt _getHighlightEdgeStyle() als Hook für adaptersspezifische Farben/Breiten.
*
* @param {number} nodeId - Elternknoten
* @param {number} bestValue - Bester Wert (MAX oder MIN)
* @protected
*/
_highlightBestEdges(nodeId, bestValue) {
const data = this.treeStructure.get(nodeId);
if (!data) return;
const style = this._getHighlightEdgeStyle(bestValue);
data.children.forEach(childId => {
const child = this.treeStructure.get(childId);
if (!child || child.value !== bestValue) return;
this.commands.push({
action: 'HIGHLIGHT_EDGE',
from: nodeId,
to: childId,
color: style.color,
width: style.width,
showArrow: true,
arrowDirection: 'from', // Kind → Eltern (bester Wert wurde gewählt)
});
});
}
/**
* Liefert Farbe und Breite für Best-Edge-Highlighting.
* Überschreibbar in Subklassen für spezifische Farblogik.
*
* @param {number} bestValue - Bester Wert des Knotens
* @returns {{color: string, width: number}}
* @protected
*/
_getHighlightEdgeStyle(bestValue) {
return { color: '#27ae60', width: 3 };
}
/**
* Markiert rekursiv alle Kanten eines Teilbaums ab parentId nach unten.
* Folgt allen offenen (nicht EVALUATED/PRUNED) Nachkommen.
* Wird für AB-Propagation-Pfeile verwendet.
*
* @param {number} parentId - Startknoten
* @param {string} color - Kantenfarbe
* @param {number} width - Kantenbreite
* @protected
*/
_highlightPropagationSubtree(parentId, color, width) {
const parent = this.treeStructure.get(parentId);
if (!parent || !parent.children) return;
parent.children.forEach(childId => {
const child = this.treeStructure.get(childId);
if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;
this.commands.push({
action: 'HIGHLIGHT_EDGE',
from: parentId,
to: childId,
color: color,
width: width,
showArrow: true,
arrowDirection: 'to',
});
// Rekursiv in Kinder absteigen
this._highlightPropagationSubtree(childId, color, width);
});
}
// Abstract/Empty methods to be implemented by subclasses
expandNodeChildren(nodeId, state) {}
evaluateNode(nodeId) {}
}