games/connect4/logic.js


/**
 * Spiellogik für Connect 4 und Connect 4 3D.
 * Implementiert die GameState-Interface-Konzepte.
 * @fileoverview
 */

// Verwende zentrale Konstanten aus config/constants.js
if (typeof GAME_CONSTANTS === 'undefined') {
    if (typeof DebugConfig !== 'undefined') {
        DebugConfig.log(DEBUG_DOMAINS.GAMES_CONNECT4, "error", '❌ FEHLER: GAME_CONSTANTS ist nicht geladen!');
    }
}

// Lokale Referenzen zu zentralen Konstanten (für Rückwärtskompatibilität)
// HINWEIS: NONE, PLAYER1, PLAYER2, DRAW, CELL_EMPTY, INVALID_INDEX sind bereits global
// in constants.js definiert und daher direkt verfügbar!

class Connect4Base {
    constructor() {
        this.currentPlayer = PLAYER1;
        this.winner = NONE; // 0 = running, 1 = p1, 2 = p2, 3 = draw
    }

    switchPlayer() {
        this.currentPlayer = (this.currentPlayer === PLAYER1) ? PLAYER2 : PLAYER1;
    }
}

/**
 * Standard Connect 4 (6 rows, 7 columns default).
 * Grid is standard reading order or maybe row-major.
 * Let's use row-major for simplicity in drawing, but move logic is column-based.
 * Rows 0..5, Cols 0..6.
 * 0,0 is top-left usually for rendering, but for gravity bottom-up is easier?
 * Let's use 0 = top, 5 = bottom. Gravity fills 5 then 4...
 */
class Connect4Regular extends Connect4Base {
    constructor(rows = 6, cols = 7) {
        super();
        this.rows = rows;
        this.cols = cols;
        // 0 = empty, 1 = p1, 2 = p2
        this.grid = Array(rows * cols).fill(CELL_EMPTY);
    }

    /**
     * Returns valid moves (columns that are not full).
     * @returns {number[]} list of column indices (0..cols-1)
     */
    getAllValidMoves() {
        const moves = [];
        if (this.winner !== NONE) return moves;

        for (let c = 0; c < this.cols; c++) {
            // Check if top cell is empty
            if (this.grid[c] === CELL_EMPTY) {
                moves.push(c);
            }
        }
        return moves;
    }

    /**
     * Makes a move in the given column.
     * @param {number} col 
     * @returns {boolean}
     */
    makeMove(col) {
        if (this.winner !== NONE || col < 0 || col >= this.cols) return false;

        // Find lowest empty row in col
        // grid index = row * cols + col
        let foundRow = INVALID_INDEX;
        for (let r = this.rows - 1; r >= 0; r--) {
            const idx = r * this.cols + col;
            if (this.grid[idx] === CELL_EMPTY) {
                foundRow = r;
                break;
            }
        }

        if (foundRow === INVALID_INDEX) return false; // Column full

        this.grid[foundRow * this.cols + col] = this.currentPlayer;
        this.checkWin(foundRow, col);
        
        if (this.winner === NONE) {
            // Check draw (board full)
            if (!this.grid.includes(CELL_EMPTY)) {
                this.winner = DRAW;
            } else {
                this.switchPlayer();
            }
        }
        return true;
    }

    checkWin(lastR, lastC) {
        // Check directions around last placed piece
        const directions = [
            [0, 1],  // Horizontal
            [1, 0],  // Vertical
            [1, 1],  // Diagonal \
            [1, -1]  // Diagonal /
        ];

        const player = this.grid[lastR * this.cols + lastC];

        for (const [dr, dc] of directions) {
            let count = 1;
            
            // Positive direction
            for (let i = 1; i < 4; i++) {
                const r = lastR + dr * i;
                const c = lastC + dc * i;
                if (r < 0 || r >= this.rows || c < 0 || c >= this.cols) break;
                if (this.grid[r * this.cols + c] === player) count++;
                else break;
            }

            // Negative direction
            for (let i = 1; i < 4; i++) {
                const r = lastR - dr * i;
                const c = lastC - dc * i;
                if (r < 0 || r >= this.rows || c < 0 || c >= this.cols) break;
                if (this.grid[r * this.cols + c] === player) count++;
                else break;
            }

            if (count >= 4) {
                this.winner = player;
                return;
            }
        }
    }
    
    // For cloning if needed by Minimax (if not using adapter)
    clone() {
        const copy = new Connect4Regular(this.rows, this.cols);
        copy.grid = [...this.grid];
        copy.currentPlayer = this.currentPlayer;
        copy.winner = this.winner;
        return copy;
    }
}

/**
 * 3D Connect 4 (4x4x4).
 * Grid: 4 planes (z), each 4 rows (y), 4 cols (x).
 * Gravity applies along Y (vertical).
 * Players place checks on a pole (xz position).
 * Dimensions: X=4, Z=4 (Base), Y=4 (Height).
 */
class Connect43D extends Connect4Base {
    constructor(size = 4) {
        super();
        this.size = size;
        // Flat array. Index = y * size*size + z * size + x
        // or x + z*size + y*size*size
        // Let's stick to standard: x, y, z.
        // We want gravity on Y. So we select X and Z.
        this.grid = Array(size * size * size).fill(CELL_EMPTY);
    }

    getIdx(x, y, z) {
        // y is height (0..3). 0 is bottom.
        return y * this.size * this.size + z * this.size + x;
    }

    getAllValidMoves() {
        const moves = []; // encoded as x + z * size usually?
        if (this.winner !== NONE) return moves;

        for (let x = 0; x < this.size; x++) {
            for (let z = 0; z < this.size; z++) {
                // Check if top is empty (y=size-1)
                const topIdx = this.getIdx(x, this.size - 1, z);
                if (this.grid[topIdx] === CELL_EMPTY) {
                    moves.push(x + z * this.size); // Move ID
                }
            }
        }
        return moves;
    }

    makeMove(moveId) {
        if (this.winner !== NONE) return false;

        const x = moveId % this.size;
        const z = Math.floor(moveId / this.size);

        // Find first empty y from bottom (0)
        let foundY = INVALID_INDEX;
        for (let y = 0; y < this.size; y++) {
            const idx = this.getIdx(x, y, z);
            if (this.grid[idx] === CELL_EMPTY) {
                foundY = y;
                break;
            }
        }

        if (foundY === INVALID_INDEX) return false;

        const idx = this.getIdx(x, foundY, z);
        this.grid[idx] = this.currentPlayer;
        
        this.checkWin(x, foundY, z);

        if (this.winner === NONE) {
            if (!this.grid.includes(CELL_EMPTY)) this.winner = DRAW;
            else this.switchPlayer();
        }
        return true;
    }

    checkWin(lx, ly, lz) {
        // Need to check all 3D lines passing through lx, ly, lz.
        // Directions: (dx, dy, dz) in {-1, 0, 1}, excluding (0,0,0)
        // There are 13 unique directions (26 neighbors / 2).
        
        const player = this.grid[this.getIdx(lx, ly, lz)];
        
        // Iterate all 13 vectors
        const directions = [];
        for(let dx=-1; dx<=1; dx++) {
            for(let dy=-1; dy<=1; dy++) {
                for(let dz=-1; dz<=1; dz++) {
                    if (dx===0 && dy===0 && dz===0) continue;
                    // Only add if not already added (ignore negative partner)
                    // We can just iterate all and divide by 2 or be smart.
                    // simpler: iterate all, handled by the loop logic below naturally or just iterate positive hemisphere.
                    // Let's just list 13.
                    directions.push([dx, dy, dz]); 
                }
            }
        }

        // We only need unique lines.
        // Actually, just checking all directions is fine with the "negative/positive" loop used in regular C4.
        // But need to be careful not to double count.
        // The regular C4 implementation checks unique lines by picking 4 directions.
        // In 3D there are 13 lines.
        // Let's filter directions to unique lines (e.g. start with positive X, etc.)
        // Or just use the loop I wrote for Regular but generalized.
        
        // Unique axes:
        // 3 orthogonal (1,0,0), (0,1,0), (0,0,1)
        // 6 face diagonals (1,1,0), (1,-1,0), (1,0,1), (1,0,-1), (0,1,1), (0,1,-1)
        // 4 space diagonals (1,1,1), (1,1,-1), (1,-1,1), (1,-1,-1)
        // Total 13.

        const uniqueDirs = [
            [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]
        ];

        for (const [dx, dy, dz] of uniqueDirs) {
             let count = 1;
             // Positive
             for(let i=1; i<4; i++) {
                 const x = lx + dx*i, y = ly + dy*i, z = lz + dz*i;
                 if(x<0||x>=this.size||y<0||y>=this.size||z<0||z>=this.size) break;
                 if(this.grid[this.getIdx(x,y,z)] === player) count++; else break;
             }
             // Negative
             for(let i=1; i<4; i++) {
                 const x = lx - dx*i, y = ly - dy*i, z = lz - dz*i;
                 if(x<0||x>=this.size||y<0||y>=this.size||z<0||z>=this.size) break;
                 if(this.grid[this.getIdx(x,y,z)] === player) count++; else break;
             }
             if (count >= 4) {
                 this.winner = player;
                 return;
             }
        }
    }

    clone() {
        const copy = new Connect43D(this.size);
        copy.grid = [...this.grid];
        copy.currentPlayer = this.currentPlayer;
        copy.winner = this.winner;
        return copy;
    }
}