/**
* @fileoverview TTT-Heuristik-Evaluator für regular, 3d und ultimate.
* Unterstützt drei Bewertungsprofile pro Variante:
* v1_baseline — Lineare Zählung (Sieg/Verlust + Linien)
* v2_positional — Positionswerte (Zentrum, Ecken, Raumkontrolle)
* v3_aggressive — Druckmaximierung (Forks, Drohungen, Mobilitätseinschränkung)
* @author Alexander Wolf
*/
/**
* Heuristik-Implementierung für alle Tic-Tac-Toe-Varianten.
*
* ## Mathematische Intuition der evaluate()-Methode
*
* Die Bewertungsfunktion berechnet einen Gesamtscore S für einen Spielzustand:
*
* S = S_terminal + S_lines + S_position + S_pressure
*
* **S_terminal**: Sofort-Bewertung bei Spielende → ±win/loss/draw.
*
* **S_lines**: Für jede Gewinnlinie L:
* - Eigene 2-in-Reihe (+ 1 leer) → +twoInLine
* - Gegner 2-in-Reihe (+ 1 leer) → −twoInLine
* - Eigene 1-in-Reihe (+ 2 leer) → +oneInLine
* - Gegner 1-in-Reihe (+ 2 leer) → −oneInLine
*
* **S_position** (v2/v3): Summe über alle besetzten Felder:
* - Zentrum(4) → ±centerBonus
* - Ecken(0,2,6,8) → ±cornerBonus
* - Kanten(1,3,5,7) → ±edgeBonus
*
* **S_pressure** (v3): Zusätzlich:
* - Fork-Erkennung (≥2 Drohungen nach Zug) → ±forkBonus
* - Gegner-Drohungen zählen → −pressureWeight × Anzahl
*/
class TTTHeuristic extends BaseHeuristic {
/**
* @param {Object} config - Konfiguration inkl. variant und optionalem profile.
*/
constructor(config) {
const variant = (config && config.variant) ? config.variant : 'regular';
const profile = (config && config.profile) ? config.profile : 'v1_baseline';
const configFromFactory = (typeof getTTTHeuristicsConfig === 'function')
? getTTTHeuristicsConfig(variant, profile)
: null;
const mergedConfig = {
...(configFromFactory || {}),
...(config || {}),
game: 'ttt',
variant
};
const validated = (typeof validateHeuristicConfig === 'function')
? validateHeuristicConfig(mergedConfig)
: mergedConfig;
super(validated);
/** @type {string} Profilname (v1_baseline, v2_positional, v3_aggressive) */
this.profile = profile;
}
/** @returns {Object} Default-Gewichte (Baseline-Profil) */
getDefaultWeights() {
const defaultsByVariant = {
regular: {
win: 1000, loss: -1000, draw: 0,
twoInLine: 10, oneInLine: 1
},
'3d': {
win: 10000, loss: -10000, draw: 0,
twoInLine: 10, oneInLine: 1, centerBonus: 20
},
ultimate: {
win: 100000, loss: -100000, draw: 0,
macroWeight: 50, boardWonBonus: 20,
twoInLine: 10, oneInLine: 1
}
};
return defaultsByVariant[this.variant] || defaultsByVariant.regular;
}
/**
* Hauptbewertung — delegiert an varianten-spezifische Methode.
* @param {GameState} gameState - Aktueller Spielzustand.
* @param {number} player - Perspektive (PLAYER1 oder PLAYER2).
* @returns {number} Numerischer Score. Positiv = gut für player.
*/
evaluate(gameState, player) {
const terminal = this.checkTerminal(gameState, player);
if (terminal !== null) return terminal;
if (this.variant === '3d') return this._evaluate3D(gameState, player);
if (this.variant === 'ultimate') return this._evaluateUltimate(gameState, player);
return this._evaluateRegular(gameState, player);
}
/**
* Regular 3×3 Bewertung.
* Score = Σ(linienScoring) + positionScoring + pressureScoring
* @private
*/
_evaluateRegular(game, player) {
const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
const w = this.weights;
let score = 0;
const lines = (typeof GAME_CONSTANTS !== 'undefined' && GAME_CONSTANTS.TTT_WIN_CONDITIONS)
? GAME_CONSTANTS.TTT_WIN_CONDITIONS
: [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];
// S_lines: Linienbewertung
for (const line of lines) {
let myCount = 0, oppCount = 0, emptyCount = 0;
for (const idx of line) {
const value = game.grid[idx];
if (value === player) myCount++;
else if (value === opponent) oppCount++;
else emptyCount++;
}
if (myCount === 2 && emptyCount === 1) score += w.twoInLine;
if (oppCount === 2 && emptyCount === 1) score -= w.twoInLine;
if (myCount === 1 && emptyCount === 2) score += w.oneInLine;
if (oppCount === 1 && emptyCount === 2) score -= w.oneInLine;
}
// S_position: Positionsbewertung (v2_positional, v3_aggressive)
if (w.centerBonus || w.cornerBonus || w.edgeBonus) {
score += this._positionScore(game.grid, player, opponent);
}
// S_pressure: Fork- und Druckbewertung (v3_aggressive)
if (w.forkBonus || w.pressureWeight) {
score += this._pressureScore(game, player, opponent, lines);
}
return score;
}
/**
* Bewertet Feldpositionen.
* Zentrum hat höchsten strategischen Wert, gefolgt von Ecken.
* @private
* @param {number[]} grid - Board-Array.
* @param {number} player
* @param {number} opponent
* @returns {number} Positionsscore.
*/
_positionScore(grid, player, opponent) {
const w = this.weights;
let score = 0;
const CENTER = 4;
const CORNERS = [0, 2, 6, 8];
const EDGES = [1, 3, 5, 7];
if (w.centerBonus) {
if (grid[CENTER] === player) score += w.centerBonus;
else if (grid[CENTER] === opponent) score -= w.centerBonus;
}
if (w.cornerBonus) {
for (const c of CORNERS) {
if (grid[c] === player) score += w.cornerBonus;
else if (grid[c] === opponent) score -= w.cornerBonus;
}
}
if (w.edgeBonus) {
for (const e of EDGES) {
if (grid[e] === player) score += w.edgeBonus;
else if (grid[e] === opponent) score -= w.edgeBonus;
}
}
return score;
}
/**
* Bewertet Fork-Potenzial und gegnerischen Druck.
* Fork = Züge die ≥2 Gewinndrohungen gleichzeitig erzeugen.
* @private
* @param {GameState} game
* @param {number} player
* @param {number} opponent
* @param {number[][]} lines - Gewinnlinien.
* @returns {number} Druck-Score.
*/
_pressureScore(game, player, opponent, lines) {
const w = this.weights;
let score = 0;
// Zähle eigene und gegnerische Drohungen (2-in-Linie mit 1 leer)
let myThreats = 0;
let oppThreats = 0;
for (const line of lines) {
let myCount = 0, oppCount = 0, emptyCount = 0;
for (const idx of line) {
const value = game.grid[idx];
if (value === player) myCount++;
else if (value === opponent) oppCount++;
else emptyCount++;
}
if (myCount === 2 && emptyCount === 1) myThreats++;
if (oppCount === 2 && emptyCount === 1) oppThreats++;
}
// Fork-Bonus: ≥2 gleichzeitige Drohungen
if (w.forkBonus && myThreats >= 2) score += w.forkBonus;
if (w.opponentForkPenalty && oppThreats >= 2) score += w.opponentForkPenalty;
// Drohungsdifferenz als Druckmaß
if (w.pressureWeight) {
score += (myThreats - oppThreats) * w.pressureWeight;
}
return score;
}
/**
* 3D 3×3×3 Bewertung.
* Score = Zentrumskontrolle + Raumlinienbewertung + Positionsboni
* @private
*/
_evaluate3D(game, player) {
const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
const w = this.weights;
let score = 0;
const CENTER_3D = 13;
// Zentrumsbonus (Index 13 = Mitte des 3×3×3 Würfels)
if (w.centerBonus) {
if (game.grid[CENTER_3D] === player) score += w.centerBonus;
else if (game.grid[CENTER_3D] === opponent) score -= w.centerBonus;
}
// Raumlinien-Analyse
const lines = this._getLines3D();
let myThreats = 0, oppThreats = 0;
for (const line of lines) {
let myCount = 0, oppCount = 0, emptyCount = 0;
for (const idx of line) {
const value = game.grid[idx];
if (value === player) myCount++;
else if (value === opponent) oppCount++;
else emptyCount++;
}
if (myCount === 2 && emptyCount === 1) {
score += w.twoInLine;
myThreats++;
}
if (oppCount === 2 && emptyCount === 1) {
score -= w.twoInLine;
oppThreats++;
}
if (myCount === 1 && emptyCount === 2) score += w.oneInLine;
if (oppCount === 1 && emptyCount === 2) score -= w.oneInLine;
}
// v2: Raumkontrolle — Abstand zur Mitte bewerten
if (w.spatialControl) {
for (let idx = 0; idx < 27; idx++) {
if (game.grid[idx] === NONE) continue;
const x = idx % 3, y = Math.floor(idx / 3) % 3, z = Math.floor(idx / 9);
const dist = Math.abs(x - 1) + Math.abs(y - 1) + Math.abs(z - 1);
const bonus = (3 - dist) * w.spatialControl;
if (game.grid[idx] === player) score += bonus;
else score -= bonus;
}
}
// v2: Eckenbonus für 3D-Ecken (8 Stück)
if (w.cornerBonus) {
const corners3D = [0, 2, 6, 8, 18, 20, 24, 26];
for (const c of corners3D) {
if (game.grid[c] === player) score += w.cornerBonus;
else if (game.grid[c] === opponent) score -= w.cornerBonus;
}
}
// v3: Fork/Druck
if (w.forkBonus && myThreats >= 2) score += w.forkBonus;
if (w.opponentForkPenalty && oppThreats >= 2) score += w.opponentForkPenalty;
if (w.pressureWeight) score += (myThreats - oppThreats) * w.pressureWeight;
return score;
}
/**
* Ultimate TTT Bewertung.
* Score = Makro-Linien × macroWeight + Board-Gewinn-Boni + lokale Bewertung
* @private
*/
_evaluateUltimate(game, player) {
const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
const w = this.weights;
let score = 0;
const macro = Array.isArray(game.macroBoard) ? game.macroBoard : Array(9).fill(NONE);
const lines = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];
// Makro-Board-Linienbewertung
let myMacroThreats = 0, oppMacroThreats = 0;
for (const line of lines) {
let myCount = 0, oppCount = 0, neutralCount = 0;
for (const idx of line) {
if (macro[idx] === player) myCount++;
else if (macro[idx] === opponent) oppCount++;
else if (macro[idx] === NONE || macro[idx] === 0) neutralCount++;
}
if (myCount === 2 && oppCount === 0) {
score += w.twoInLine * w.macroWeight;
myMacroThreats++;
}
if (oppCount === 2 && myCount === 0) {
score -= w.twoInLine * w.macroWeight;
oppMacroThreats++;
}
if (myCount === 1 && oppCount === 0 && neutralCount === 2) {
score += w.oneInLine * w.macroWeight;
}
if (oppCount === 1 && myCount === 0 && neutralCount === 2) {
score -= w.oneInLine * w.macroWeight;
}
}
// Board-Gewinn-Boni
for (let index = 0; index < 9; index++) {
if (macro[index] === player) score += w.boardWonBonus;
else if (macro[index] === opponent) score -= w.boardWonBonus;
}
// v2: Makro-Zentrumskontrolle (Board 4 = Mitte im Makro)
if (w.centerBonus) {
if (macro[4] === player) score += w.centerBonus;
else if (macro[4] === opponent) score -= w.centerBonus;
}
// v3: Makro-Druck
if (w.pressureWeight) {
score += (myMacroThreats - oppMacroThreats) * w.pressureWeight;
}
if (w.blockUrgency && oppMacroThreats >= 2) {
score -= w.blockUrgency;
}
return score;
}
/**
* Generiert und cacht alle Gewinnlinien im 3×3×3 Raum.
* 49 Linien insgesamt: 27 Achsen + 18 Flächendiagonalen + 4 Raumdiagonalen.
* @private
* @returns {number[][]}
*/
_getLines3D() {
if (TTTHeuristic._lines3d) return TTTHeuristic._lines3d;
const SIZE = 3;
const directions = [
[1,0,0], [0,1,0], [0,0,1],
[1,1,0], [1,-1,0], [1,0,1], [1,0,-1], [0,1,1], [0,-1,1],
[1,1,1], [1,1,-1], [1,-1,1], [1,-1,-1]
];
const lines = [];
const seen = new Set();
const isValid = (x, y, z) => x >= 0 && x < SIZE && y >= 0 && y < SIZE && z >= 0 && z < SIZE;
const getIdx = (x, y, z) => x + y * SIZE + z * SIZE * SIZE;
for (let z = 0; z < SIZE; z++) {
for (let y = 0; y < SIZE; y++) {
for (let x = 0; x < SIZE; x++) {
for (const [dx, dy, dz] of directions) {
const line = [];
for (let step = 0; step < SIZE; step++) {
const px = x + step * dx, py = y + step * dy, pz = z + step * dz;
if (!isValid(px, py, pz)) { line.length = 0; break; }
line.push(getIdx(px, py, pz));
}
if (line.length === SIZE) {
const key = [...line].sort((a, b) => a - b).join(',');
if (!seen.has(key)) { seen.add(key); lines.push(line); }
}
}
}
}
}
TTTHeuristic._lines3d = lines;
return lines;
}
}
// ═══════════════════════════════════════════════════════════════
// Registrierung: 3 Profile × 3 Varianten = 9 Instanzen
// ═══════════════════════════════════════════════════════════════
HeuristicRegistry.registerConstructor('ttt', TTTHeuristic);
const TTT_VARIANTS = ['regular', '3d', 'ultimate'];
const TTT_PROFILES = ['v1_baseline', 'v2_positional', 'v3_aggressive'];
for (const variant of TTT_VARIANTS) {
for (const profile of TTT_PROFILES) {
const cfg = (typeof getTTTHeuristicsConfig === 'function')
? getTTTHeuristicsConfig(variant, profile)
: { game: 'ttt', variant, profile };
const instance = new TTTHeuristic({
...cfg,
variant,
profile,
name: cfg.name || `TTT ${variant} (${profile})`
});
// Für Profile mit anderem Suffix: registriere unter game:variant:profile
if (profile === 'v1_baseline') {
// Default-Instanz unter dem Hauptschlüssel ttt:variant
HeuristicRegistry.register(instance);
}
// Alle Profile unter erweitertem Schlüssel (überschreibe id)
instance.id = `ttt:${variant}:${profile}`;
HeuristicRegistry.register(instance);
}
}