/**
* @fileoverview Adapter für Tiefensuche (DFS) mit Suchbaum-Visualisierung
*
* Konvertiert Spielzustände in TreeVizEngine-Kommandos via postMessage.
* Unterstützt Backtracking-Visualisierung und State-Tracking.
*
* @class DFSTreeAdapter
* @author Alexander Wolf
* @version 2.3
*/
class DFSTreeAdapter {
/**
* Erstellt einen neuen DFS Tree Adapter.
* @param {HTMLIFrameElement} iframeElement
* Das iframe-Element mit der TreeVizEngine.
*/
constructor(iframeElement) {
/**
* Das iframe-Element mit der TreeVizEngine.
* @type {HTMLIFrameElement}
*/
this.iframe = iframeElement;
/**
* Zähler für eindeutige Node-IDs.
* @type {number}
*/
this.nodeIdCounter = 0;
/**
* Mapping von State-Keys zu Node-IDs.
* @type {Map<string, number>}
*/
this.nodeMap = new Map();
/**
* Aktuelle Suchtiefe.
* @type {number}
*/
this.currentDepth = 0;
/**
* Status, ob TreeVizEngine bereit ist.
* @type {boolean}
*/
this.ready = false;
/** @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;
// ==================== BRIDGE INTEGRATION ====================
/** @type {IframeBridgeHost|null} */
this._bridge = null;
if (typeof IframeBridgeHost !== 'undefined' && this.iframe) {
try {
this._bridge = new IframeBridgeHost(this.iframe, {
sourceId: 'tree-adapter-dfs',
acceptLegacy: true,
handshakeTimeout: 3000
});
this._bridge.on('SYSTEM:READY', () => {
this.ready = true;
this.sendCommand({
action: 'UPDATE_CONFIG',
config: { enableTreeExpansion: false }
});
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
});
// NODE:CLICKED / TREE:NODE_CLICKED Events
this._bridge.on('NODE:CLICKED', (payload) => {
if (typeof this.onNodeClicked === 'function') {
this.onNodeClicked(payload.nodeId, payload.boardData);
}
});
this._bridge.on('TREE:NODE_CLICKED', (payload) => {
if (typeof this.onNodeClicked === 'function') {
this.onNodeClicked(payload.nodeId, payload.boardData);
}
});
// NODE:FOCUSED Events
this._bridge.on('NODE:FOCUSED', (payload) => {
if (typeof this.onNodeFocused === 'function') {
this.onNodeFocused(payload.nodeId, payload.boardData);
}
});
} catch (err) {
this._bridge = null;
}
}
// Legacy-Fallback: Direktes postMessage-Listening
if (!this._bridge) {
window.addEventListener('message', (event) => {
if (event.data && event.data._bridge) return; // Bridge-Messages ignorieren
if (event.data && event.data.type === 'TREE_READY') {
this.ready = true;
this.sendCommand({
action: 'UPDATE_CONFIG',
config: { enableTreeExpansion: false }
});
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
// Legacy NODE_CLICKED
if (event.data && event.data.type === 'NODE_CLICKED') {
if (typeof this.onNodeClicked === 'function') {
this.onNodeClicked(event.data.nodeId, event.data.boardData);
}
}
// Legacy NODE_FOCUSED
if (event.data && event.data.type === 'NODE_FOCUSED') {
if (typeof this.onNodeFocused === 'function') {
this.onNodeFocused(event.data.nodeId, event.data.boardData);
}
}
});
}
// Start proactive handshake
this.startHandshake();
}
/**
* Initiiert den Handshake mit der TreeVizEngine.
* Sendet CHECK_READY Kommandos bis eine Antwort empfangen wird.
*/
startHandshake() {
// Send CHECK_READY every 200ms until we get a response
if (this.checkInterval) clearInterval(this.checkInterval);
const check = () => {
if (this.ready) return;
this.sendCommand({ action: 'CHECK_READY' });
};
// Immediate check
check();
// Periodic check
this.checkInterval = setInterval(check, 200);
// Stop checking after 10 seconds
setTimeout(() => {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}, 10000);
}
/**
* Baut einen DFS-Suchbaum bis zur angegebenen Tiefe auf.
* @param {Object} initialState
* Der initiale Spielzustand.
* @param {number} maxDepth
* Maximale Suchtiefe.
* @param {Object} options
* Zusätzliche Optionen (duplicates, backtracking).
* @returns {Object}
* Statistiken über den generierten Baum.
*/
async buildToDepth(initialState, maxDepth, options = {}) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug",
'[DFSAdapter] buildToDepth START',
{ maxDepth, markDuplicates: options.duplicates });
// Build DFS tree to specified depth
if (!this.ready) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "warn", 'TreeVizEngine not ready yet. Attempting optimized send anyway...');
// Fallthrough - do not return! Try to send anyway if iframe exists.
if (!this.iframe || !this.iframe.contentWindow) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "error", 'DFSTreeAdapter: Iframe contentWindow missing!');
return;
}
}
// Clear existing tree
this.sendCommand({ action: 'CLEAR' });
this.nodeIdCounter = 0;
this.nodeMap.clear();
// Configuration
const markDuplicates = options.duplicates === true;
const showBacktracking = options.backtracking !== false; // Default true
const commands = [];
// Track visited states to detect duplicates
const visited = new Map(); // stateKey -> nodeId
// Add root node
const rootId = this.nodeIdCounter++;
const rootKey = initialState.getStateKey();
this.nodeMap.set(rootKey, rootId);
visited.set(rootKey, rootId);
commands.push({
action: 'ADD_NODE',
id: rootId,
parentId: null,
label: '',
color: '#4a90e2',
boardData: initialState
});
// DFS recursive function
const dfs = (state, nodeId, depth) => {
// Stop if max depth reached
if (depth >= maxDepth) {
return;
}
// Get child states
const nextStates = state.getNextStates();
nextStates.forEach(({ state: childState, move }) => {
const childKey = childState.getStateKey();
// Duplicate handling
let isDuplicate = false;
if (visited.has(childKey)) {
isDuplicate = true;
if (!markDuplicates) {
isDuplicate = false; // Ignore duplicate status if we want full tree
}
}
// Create child node
const childId = this.nodeIdCounter++;
this.nodeMap.set(childKey, childId);
// Determine status
const status = [];
if (childState.isGoal && childState.isGoal()) {
status.push('WIN');
} else if (isDuplicate) {
status.push('DUPLICATE');
}
// Format move label
let label = '';
if (move && move.r !== undefined && move.c !== undefined) {
label = `(${move.r},${move.c})`;
} else if (typeof move === 'string') {
label = move;
}
// Add node command
commands.push({
action: 'ADD_NODE',
id: childId,
parentId: nodeId,
label: label,
edgeLabel: label, // ← FIX: Add edge label (shows move on edge)
status: status,
boardData: childState
});
if (isDuplicate && markDuplicates) {
return; // Stop expansion at duplicate
}
// Mark visited (only if tracking duplicates)
if (markDuplicates) {
visited.set(childKey, childId);
}
// Recursive DFS
dfs(childState, childId, depth + 1);
});
};
// Start DFS from root
dfs(initialState, rootId, 0);
// Send all commands as batch
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug",
'[DFSAdapter] buildToDepth: Sending batch',
{ commandCount: commands.length, totalNodes: this.nodeIdCounter });
this.sendCommand({ action: 'BATCH', commands: commands });
// Store stats
this.currentDepth = maxDepth;
this.stats = {
totalNodes: this.nodeIdCounter,
depth: maxDepth,
duplicatesMarked: markDuplicates
};
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug",
'[DFSAdapter] buildToDepth COMPLETE',
{ ...this.stats });
return this.stats;
}
/**
* Visualize a search with animated backtracking
* @param {Object} initialState - Starting state
* @param {Function} searchFn - Search function that yields steps
* @param {Object} options - Options
*/
async* visualizeSearch(initialState, searchFn, options = {}) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug",
'[DFSAdapter] visualizeSearch START');
if (!this.ready) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "warn", 'TreeVizEngine not ready yet');
return;
}
// Clear existing tree
this.sendCommand({ action: 'CLEAR' });
this.nodeIdCounter = 0;
this.nodeMap.clear();
// Add root
const rootId = this.nodeIdCounter++;
const rootKey = initialState.getStateKey();
this.nodeMap.set(rootKey, rootId);
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug",
'[DFSAdapter] visualizeSearch: Root node created and sent');
this.sendCommand({
action: 'BATCH',
commands: [{
action: 'ADD_NODE',
id: rootId,
parentId: null,
label: '',
color: '#4a90e2',
boardData: initialState
}]
});
yield { type: 'ROOT', nodeId: rootId };
// Run search and visualize steps
for await (const step of searchFn(initialState)) {
if (step.type === 'VISIT') {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug",
'[DFSAdapter] Step VISIT: child created',
{ childId: step.nodeId, isGoal: step.state.isGoal?.() });
// New node visited
const parentKey = step.parentState.getStateKey();
const parentId = this.nodeMap.get(parentKey);
const childId = this.nodeIdCounter++;
const childKey = step.state.getStateKey();
this.nodeMap.set(childKey, childId);
const status = ['ACTIVE'];
if (step.state.isGoal && step.state.isGoal()) {
status.push('WIN');
}
this.sendCommand({
action: 'BATCH',
commands: [{
action: 'ADD_NODE',
id: childId,
parentId: parentId,
label: step.label || '',
status: status,
boardData: step.state
}, {
action: 'SET_FOCUS',
id: childId
}]
});
yield { type: 'VISIT', nodeId: childId, state: step.state };
} else if (step.type === 'BACKTRACK') {
// Highlight backtracking
const nodeKey = step.state.getStateKey();
const nodeId = this.nodeMap.get(nodeKey);
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", `[ADAPTER] BACKTRACK: nodeKey=${nodeKey}, nodeId=${nodeId}`);
// Wenn wir einen BACKTRACK für diesen Knoten bekommen,
// bedeutet das: ALLE seine Kinder wurden bereits als VISIT erzeugt!
// Deshalb können wir jetzt überprüfen: Sind ALLE Kinder dieses Knotens DEAD_END?
this.sendCommand({
action: 'BATCH',
commands: [{
action: 'UPDATE_NODE',
id: nodeId,
data: {
removeStatus: 'ACTIVE',
addStatus: 'DEAD_END'
}
}, {
action: 'SET_FOCUS',
id: nodeId
}, {
action: 'CHECK_AND_MARK_DEAD_END',
id: nodeId
}]
});
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", `[ADAPTER] Sent CHECK_AND_MARK_DEAD_END for nodeId=${nodeId}`);
yield { type: 'BACKTRACK', nodeId: nodeId };
}
}
}
/**
* Hebt einen Pfad durch den Baum farblich hervor.
* @param {Array<string>} statePath
* Array von State-Keys die den Pfad bilden.
*/
highlightPath(statePath) {
if (!this.ready) return;
const commands = [];
// Remove existing highlights
commands.push({
action: 'REMOVE_HIGHLIGHT'
});
// Highlight each edge in the path
for (let i = 0; i < statePath.length - 1; i++) {
const fromKey = statePath[i];
const toKey = statePath[i + 1];
const fromId = this.nodeMap.get(fromKey);
const toId = this.nodeMap.get(toKey);
if (fromId !== undefined && toId !== undefined) {
commands.push({
action: 'HIGHLIGHT_EDGE',
fromId: fromId,
toId: toId,
color: '#ff9800',
width: 4
});
commands.push({
action: 'HIGHLIGHT_NODE',
id: toId,
color: '#ff9800',
style: 'glow'
});
}
}
this.sendCommand({ action: 'BATCH', commands: commands });
}
/**
* Fokussiert die Ansicht auf einen bestimmten Knoten.
* @param {string} stateKey
* State-Key des zu fokussierenden Knotens.
*/
focusNode(stateKey) {
const nodeId = this.nodeMap.get(stateKey);
if (nodeId !== undefined) {
this.sendCommand({ action: 'SET_FOCUS', id: nodeId });
}
}
/**
* Setzt die Ansicht auf die Standard-Position zurück.
*/
resetView() {
this.sendCommand({ action: 'RESET_VIEW' });
}
/**
* Gibt aktuelle Statistiken über den Suchbaum zurück.
* @returns {Object}
* Statistiken (totalNodes, depth, duplicatesMarked).
*/
getStats() {
return this.stats || {
totalNodes: 0,
depth: 0,
duplicatesMarked: false
};
}
/**
* Send command to TreeVizEngine via Bridge (bevorzugt) oder Legacy-postMessage.
* @private
*/
sendCommand(commandObj) {
if (!this.iframe || !this.iframe.contentWindow) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "error", 'Iframe not available');
return;
}
// Bridge bevorzugen
if (this._bridge) {
this._bridge.send('TREE:COMMAND', commandObj);
return;
}
// Legacy-Fallback
this.iframe.contentWindow.postMessage({
type: 'TREE_COMMAND',
command: commandObj
}, '*');
}
/**
* Get node count
*/
getNodeCount() {
return this.nodeIdCounter;
}
/**
* UNIFIED NAVIGATION: Navigate to a tree node and update game state accordingly.
* Works for both Warnsdorf and non-Warnsdorf modes.
*
* CRITICAL FIX: currentNodeId ist eine globale Variable, die sich in createNode() ändert!
* Der Adapter erhält sie als Parameter, aber das ist nur der INITIAL-Wert!
* Solution: Wir nutzen einen Getter-Callback um den aktuellen Wert zu erhalten
*
* @param {number} targetNodeId - Ziel-Node im Baum
* @param {Function} getCurrentNodeId - Getter für aktuelle Node (weil sie sich ändert!)
* @param {Map} nodeParentMap - Baum-Struktur (WIRD GELESEN)
* @param {Map} treeStructure - Knoten-Details (WIRD GELESEN)
* @param {Object} board - Spielzustand (WIRD MODIFIZIERT)
* @param {Function} handleBack - Undo-Callback (mit Tree-Sync)
* @param {Function} createNode - Node-Erstell-Callback (ändert currentNodeId!)
* @param {Function} generatePreviews - Preview-Erstell-Callback
* @param {Function} updateUI - Render-Callback
* @param {boolean} isWarnsdorf - Spielmodus
*
* @returns {Promise<boolean>} Success/failure of navigation
*/
async navigateInGame(
targetNodeId,
getCurrentNodeId,
nodeParentMap,
treeStructure,
board,
handleBack,
createNode,
generatePreviews,
updateUI,
isWarnsdorf
) {
const currentNodeId = getCurrentNodeId();
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", `[navigateInGame] Starting: current=${currentNodeId}, target=${targetNodeId}`);
// Use tree-engine's high-level abstraction
const pathInfo = reconstructTreePath(currentNodeId, targetNodeId, nodeParentMap, treeStructure);
if (!pathInfo.isValid) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "error", '[navigateInGame] Invalid path calculated', pathInfo);
return false;
}
const { backwardSteps, forwardMoves, lca, isValid } = pathInfo;
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", `[navigateInGame] Path: ${backwardSteps} back, ${forwardMoves.length} forward, LCA=${lca}`);
try {
// ========== PHASE 1: BACKWARD =========
// Undo moves until we reach the LCA
for (let i = 0; i < backwardSteps; i++) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", `[navigateInGame] Backward step ${i+1}/${backwardSteps}`);
handleBack(); // This updates both board and tree AND global currentNodeId
updateUI(); // CRITICAL: Update board display after undo
// Small delay to allow DOM updates
await new Promise(resolve => setTimeout(resolve, 50));
}
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", `[navigateInGame] Backward complete, now at LCA=${lca}`);
// ========== WARNSDORF SPECIAL HANDLING =========
// After handleBack() in Warnsdorf mode, preview-nodes are deleted
// We need to regenerate them for the LCA before moving forward
if (isWarnsdorf && forwardMoves.length > 0) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", '[navigateInGame] Regenerating previews for LCA (Warnsdorf mode)');
generatePreviews(lca); // Generate preview nodes at LCA
await new Promise(resolve => setTimeout(resolve, 50));
}
// ========== PHASE 2: FORWARD =========
// Move forward to the target
// CRITICAL: After each createNode(), the global currentNodeId is updated.
// We use getCurrentNodeId() GETTER to get the updated value!
for (let i = 0; i < forwardMoves.length; i++) {
const { r, c, moveKey } = forwardMoves[i];
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", `[navigateInGame] Forward step ${i+1}/${forwardMoves.length}: (${r},${c})`);
// Move on board
const moveSuccess = board.move(r, c);
if (!moveSuccess) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "error", `[navigateInGame] board.move(${r},${c}) failed!`);
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "error", `[navigateInGame] Board current position: (${board.currentPos.row},${board.currentPos.col})`);
return false;
}
// Get CURRENT value of currentNodeId (before createNode updates it)
const parentNodeId = getCurrentNodeId();
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", `[navigateInGame] Creating node: parent=${parentNodeId}, move=(${r},${c})`);
// Create tree node for this move
const edgeCoord = `(${r},${c})`;
createNode(board, parentNodeId, null, edgeCoord);
// NOTE: After createNode() returns, the global currentNodeId is updated!
// CRITICAL: Update board display after EACH move
updateUI();
await new Promise(resolve => setTimeout(resolve, 50));
}
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "debug", '[navigateInGame] Navigation complete!');
return true;
} catch (error) {
DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_DFS, "error", '[navigateInGame] Exception during navigation:', error);
return false;
}
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = DFSTreeAdapter;
}