/**
* @fileoverview Connect4-Heuristik-Evaluator für regular und 3d.
* Unterstützt drei Bewertungsprofile pro Variante:
* v1_baseline — Standard-Fensterbewertung + Center-Bias
* v2_positional — Starke Zentrumskontrolle, Höhenstrafe, Verbindungsbonus
* v3_aggressive — Drohungsmaximierung, Setup-Scoring, offensiver Bias
* @author Alexander Wolf
*/
/**
* Heuristik-Implementierung für Connect4-Varianten.
*
* ## Mathematische Intuition der evaluate()-Methode
*
* Die Bewertungsfunktion berechnet einen Gesamtscore S:
*
* S = S_terminal + S_center + S_windows + S_extras
*
* **S_terminal**: Sofort-Bewertung bei Spielende → ±win/loss/draw.
*
* **S_center**: Für jede besetzte Zelle:
* dist = |col − centerCol|
* bias = (centerCol − dist + 1) × centerWeight
* → Zentrale Spalten erhalten höheren Positionswert.
*
* **S_windows**: Gleitfenster der Länge 4 in allen Richtungen:
* - 4 eigene → +fourInRow (faktisch Sieg, aber als Safety-Net)
* - 3 eigene + 1 leer → +threeInLine
* - 2 eigene + 2 leer → +twoInLine
* - 3 Gegner + 1 leer → +opponentThreeInLine (negativ!)
* - 2 Gegner + 2 leer → +opponentTwoInLine (negativ!)
*
* **S_extras** (v2/v3):
* - connectivityBonus: Belohnung für angrenzende eigene Steine
* - heightPenalty: Bestrafung für zu hohe Steine (instabile Positionen)
* - pressureWeight: Drohungsdifferenz × Gewicht
* - setupBonus: Bonus für 2er-Fenster mit 2 Leerstellen (Vorbereitung)
*/
class Connect4Heuristic 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 getConnect4HeuristicsConfig === 'function')
? getConnect4HeuristicsConfig(variant, profile)
: null;
const mergedConfig = {
...(configFromFactory || {}),
...(config || {}),
game: 'connect4',
variant
};
const validated = (typeof validateHeuristicConfig === 'function')
? validateHeuristicConfig(mergedConfig)
: mergedConfig;
super(validated);
/** @type {string} Profilname */
this.profile = profile;
}
/** @returns {Object} Default-Gewichte (Baseline-Profil) */
getDefaultWeights() {
const defaultsByVariant = {
regular: {
win: 100000, loss: -100000, draw: 0,
fourInRow: 10000, threeInLine: 100, twoInLine: 10,
opponentThreeInLine: -90, opponentTwoInLine: -5,
centerWeight: 3
},
'3d': {
win: 100000, loss: -100000, draw: 0,
fourInRow: 100000, threeInLine: 1000, twoInLine: 50,
opponentThreeInLine: -1000, opponentTwoInLine: -50,
centerWeight: 2
}
};
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);
return this._evaluateRegular(gameState, player);
}
/**
* Öffentliche Methode für Linienbewertung (kompatibel mit Adapter).
* @param {GameState} gameState
* @param {number} player
* @returns {number}
*/
evaluateLines(gameState, player) {
return this._evaluateLines(gameState, player);
}
/**
* Regular Connect4 Bewertung.
* Score = Center-Bias + Gleitfenster-Analyse + Extras (v2/v3).
* @private
*/
_evaluateRegular(game, player) {
const w = this.weights;
const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
const centerCol = Math.floor(game.cols / 2);
let score = 0;
// S_center: Zentrumsdistanz-Bewertung
for (let row = 0; row < game.rows; row++) {
for (let col = 0; col < game.cols; col++) {
const idx = row * game.cols + col;
const value = game.grid[idx];
if (value === NONE) continue;
const distance = Math.abs(col - centerCol);
const centerScore = (centerCol - distance + 1) * w.centerWeight;
if (value === player) score += centerScore;
else if (value === opponent) score -= centerScore;
}
}
// S_windows: Gleitfenster-Analyse
score += this._evaluateLines(game, player);
// S_extras: Verbindungsbonus (v2)
if (w.connectivityBonus) {
score += this._connectivityScore(game, player, opponent);
}
// S_extras: Höhenstrafe (v2) — hohe Steine in instabilen Positionen
if (w.heightPenalty) {
score += this._heightScore(game, player, opponent);
}
// S_extras: Setup-Bonus (v3) — Vorbereitung auf 3er/4er
if (w.setupBonus) {
score += this._setupScore(game, player, opponent);
}
return score;
}
/**
* Gleitfenster-Analyse in allen 4 Richtungen.
* Bewertet jedes 4er-Fenster nach Inhalt.
* @private
*/
_evaluateLines(game, player) {
let score = 0;
const w = this.weights;
const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
const evaluateWindow = (cells) => {
let myCount = 0, oppCount = 0, emptyCount = 0;
for (const cell of cells) {
if (cell === player) myCount++;
else if (cell === opponent) oppCount++;
else emptyCount++;
}
if (myCount === 4) return w.fourInRow;
if (myCount === 3 && emptyCount === 1) return w.threeInLine;
if (myCount === 2 && emptyCount === 2) return w.twoInLine;
if (oppCount === 3 && emptyCount === 1) return w.opponentThreeInLine;
if (oppCount === 2 && emptyCount === 2) return w.opponentTwoInLine;
return 0;
};
// Horizontal
for (let row = 0; row < game.rows; row++) {
for (let col = 0; col < game.cols - 3; col++) {
const window = [];
for (let s = 0; s < 4; s++) window.push(game.grid[row * game.cols + col + s]);
score += evaluateWindow(window);
}
}
// Vertikal
for (let col = 0; col < game.cols; col++) {
for (let row = 0; row < game.rows - 3; row++) {
const window = [];
for (let s = 0; s < 4; s++) window.push(game.grid[(row + s) * game.cols + col]);
score += evaluateWindow(window);
}
}
// Diagonal ↘
for (let row = 0; row < game.rows - 3; row++) {
for (let col = 0; col < game.cols - 3; col++) {
const window = [];
for (let s = 0; s < 4; s++) window.push(game.grid[(row + s) * game.cols + col + s]);
score += evaluateWindow(window);
}
}
// Diagonal ↙
for (let row = 0; row < game.rows - 3; row++) {
for (let col = 3; col < game.cols; col++) {
const window = [];
for (let s = 0; s < 4; s++) window.push(game.grid[(row + s) * game.cols + (col - s)]);
score += evaluateWindow(window);
}
}
return score;
}
/**
* Verbindungsbewertung: Belohnt horizontal angrenzende eigene Steine.
* @private
*/
_connectivityScore(game, player, opponent) {
const w = this.weights;
let score = 0;
for (let row = 0; row < game.rows; row++) {
for (let col = 0; col < game.cols - 1; col++) {
const idx = row * game.cols + col;
const nextIdx = idx + 1;
if (game.grid[idx] === player && game.grid[nextIdx] === player) score += w.connectivityBonus;
if (game.grid[idx] === opponent && game.grid[nextIdx] === opponent) score -= w.connectivityBonus;
}
}
return score;
}
/**
* Höhenstrafbewertung: Steine in hohen Reihen (oben) sind instabiler.
* @private
*/
_heightScore(game, player, opponent) {
const w = this.weights;
let score = 0;
for (let row = 0; row < game.rows; row++) {
for (let col = 0; col < game.cols; col++) {
const idx = row * game.cols + col;
if (game.grid[idx] === NONE) continue;
const heightFromBottom = game.rows - 1 - row;
const penalty = heightFromBottom * Math.abs(w.heightPenalty);
if (game.grid[idx] === player) score -= penalty;
else if (game.grid[idx] === opponent) score += penalty;
}
}
return score;
}
/**
* Setup-Bewertung: Bonus für 2er-Gruppen mit 2 Leerstellen (Vorbereitung).
* Zählt wie viele offene Aufbaupositionen der Spieler hat.
* @private
*/
_setupScore(game, player, opponent) {
const w = this.weights;
let score = 0;
// Zähle offene 2er-Fenster (meine und gegnerische)
const countSetups = (p) => {
let count = 0;
// Horizontal
for (let row = 0; row < game.rows; row++) {
for (let col = 0; col < game.cols - 3; col++) {
let pCount = 0, empty = 0;
for (let s = 0; s < 4; s++) {
const v = game.grid[row * game.cols + col + s];
if (v === p) pCount++;
else if (v === NONE) empty++;
}
if (pCount === 2 && empty === 2) count++;
}
}
return count;
};
score += countSetups(player) * w.setupBonus;
score -= countSetups(opponent) * w.setupBonus;
return score;
}
/**
* 3D Connect4 (4×4×4) Bewertung.
* Score = Zentrums-Distanz + 3D-Linienbewertung + Extras.
* @private
*/
_evaluate3D(game, player) {
const w = this.weights;
const size = game.size;
const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
let score = 0;
// Zentrumsdistanz-Bewertung
const center = (size - 1) / 2;
for (let idx = 0; idx < game.grid.length; idx++) {
const cell = game.grid[idx];
if (cell === NONE) continue;
const x = idx % size;
const z = Math.floor(idx / size) % size;
const y = Math.floor(idx / (size * size));
const dist = Math.abs(x - center) + Math.abs(y - center) + Math.abs(z - center);
const centerScore = (size * 2 - dist) * w.centerWeight;
if (cell === player) score += centerScore;
else if (cell === opponent) score -= centerScore;
}
// 3D-Linienbewertung (Fenstergröße = size)
score += this._evaluateLines3D(game, player, opponent);
return score;
}
/**
* 3D-Gleitfenster-Analyse (4er-Fenster in 13 Richtungen).
* Generiert und cacht die 3D-Linien.
* @private
*/
_evaluateLines3D(game, player, opponent) {
const w = this.weights;
const size = game.size;
const lines = this._getLines3D(size);
let score = 0;
for (const line of lines) {
let myCount = 0, oppCount = 0, emptyCount = 0;
for (const idx of line) {
const cell = game.grid[idx];
if (cell === player) myCount++;
else if (cell === opponent) oppCount++;
else emptyCount++;
}
if (myCount === size && oppCount === 0) score += w.fourInRow;
if (myCount === size - 1 && emptyCount === 1) score += w.threeInLine;
if (myCount === size - 2 && emptyCount === 2) score += w.twoInLine;
if (oppCount === size - 1 && emptyCount === 1) score += w.opponentThreeInLine;
if (oppCount === size - 2 && emptyCount === 2) score += w.opponentTwoInLine;
}
return score;
}
/**
* Generiert und cacht alle Gewinnlinien in einem NxNxN 3D-Gitter.
* @private
* @param {number} size - Gittergröße (typisch 4).
* @returns {number[][]} Array von Linien (je size Indizes).
*/
_getLines3D(size) {
const cacheKey = `_lines3d_${size}`;
if (Connect4Heuristic[cacheKey]) return Connect4Heuristic[cacheKey];
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) => z * size * size + y * size + x;
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); }
}
}
}
}
}
Connect4Heuristic[cacheKey] = lines;
return lines;
}
}
// ═══════════════════════════════════════════════════════════════
// Registrierung: 3 Profile × 2 Varianten = 6 Instanzen
// ═══════════════════════════════════════════════════════════════
HeuristicRegistry.registerConstructor('connect4', Connect4Heuristic);
const C4_VARIANTS = ['regular', '3d'];
const C4_PROFILES = ['v1_baseline', 'v2_positional', 'v3_aggressive'];
for (const variant of C4_VARIANTS) {
for (const profile of C4_PROFILES) {
const cfg = (typeof getConnect4HeuristicsConfig === 'function')
? getConnect4HeuristicsConfig(variant, profile)
: { game: 'connect4', variant, profile };
const instance = new Connect4Heuristic({
...cfg,
variant,
profile,
name: cfg.name || `Connect4 ${variant} (${profile})`
});
if (profile === 'v1_baseline') {
HeuristicRegistry.register(instance);
}
instance.id = `connect4:${variant}:${profile}`;
HeuristicRegistry.register(instance);
}
}