games/connect4/renderer.js

/**
 * @fileoverview Renderer für Connect4 Varianten
 * 
 * Bietet Render-Funktionen für Connect4 in 2D und 3D:
 * - drawRegular: Klassisches 6x7 Connect4 Board
 * - draw3D: 4x4x4 3D Connect4 mit Multi-View Rendering
 * 
 * @namespace Connect4Renderer
 * @author Alexander Wolf
 * @version 2.0
 */

const Connect4Renderer = {
    
    /**
     * Zeichnet das Standard Connect4 Board.
     * @param {HTMLCanvasElement} canvas - Canvas-Element zum Zeichnen
     * @param {Connect4Regular} game - Spiel mit rows, cols und grid
     * @returns {void}
     */
    drawRegular(canvas, game) {
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        const w = canvas.width, h = canvas.height;
        const cols = game.cols;
        const rows = game.rows;

        // Cell size
        const s = Math.min(w / cols, h / rows);
        const offsetX = (w - s * cols) / 2;
        const offsetY = (h - s * rows) / 2;

        ctx.clearRect(0, 0, w, h);
        
        // Background (Yellow board)
        ctx.fillStyle = "#f1c40f"; 
        ctx.fillRect(offsetX, offsetY, s * cols, s * rows);

        // Holes / Pieces
        for (let r = 0; r < rows; r++) {
            for (let c = 0; c < cols; c++) {
                const cx = offsetX + c * s + s/2;
                const cy = offsetY + r * s + s/2;
                const rCell = s * 0.4;
                
                const idx = r * cols + c;
                const val = game.grid[idx];

                ctx.beginPath();
                ctx.arc(cx, cy, rCell, 0, Math.PI * 2);
                
                if (val === 0) {
                    ctx.fillStyle = "white"; // Empty hole
                } else if (val === 1) {
                    ctx.fillStyle = "#3498db"; // Player 1 (Blue)
                } else if (val === 2) {
                    ctx.fillStyle = "#e74c3c"; // Player 2 (Red)
                }
                ctx.fill();
                ctx.strokeStyle = "#d4ac0d"; // Darker yellow for outline
                ctx.lineWidth = 2;
                ctx.stroke();
            }
        }
    },

    /**
     * Zeichnet das 3D Connect4 Board mit Multi-View (Slices).
     * @param {HTMLCanvasElement} canvas - Canvas-Element zum Zeichnen  
     * @param {Connect43D} game - 3D-Spiel mit 4x4x4 Grid
     * @returns {void}
     */
    draw3D(canvas, game) {
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        const w = canvas.width, h = canvas.height;
        const s = game.size; // 4

        ctx.clearRect(0,0,w,h);

        const pad = 10;
        
        // --- 1. Top View (Z Slices) ---
        // Arrange side by side: Z0 Z1 Z2 Z3
        
        // Width of one board
        const boardW = (w - pad * (s + 1)) / s;
        const boardH = boardW; // Square 4x4
        
        const topY = 40; // Margin for label

        // Draw Z Plans
        for (let z = 0; z < s; z++) {
            const startX = pad + z * (boardW + pad);

            // Label
            ctx.fillStyle = "#2c3e50";
            ctx.font = "bold 14px sans-serif";
            ctx.textAlign = "center";
            ctx.fillText(`Ebene Y=${z+1} (von vorne)`, startX + boardW/2, topY - 10);
            
            // Side Label (Z=1...Z=4) Left of the grid? 
            // The user asked "Label the rows left with Z=1...Z=4 both top and bottom".
            // Since top view is X-Y (technically X-Z in original, but now named Y), 
            // the rows within a grid are "Height".
            // It seems "Z" is now the depth slice? user says "Replace Z with Y".
            // If "Ebene Y=..." acts as the slice index.
            
            this.drawGrid(ctx, startX, topY, boardW, boardH, s);
            
            // Draw Pieces of Z-Slice
            for (let y = 0; y < s; y++) {
                const vizY = s - 1 - y; // Bottom y=0 is drawn at bottom
                // Row Label on the left of each board? Only needed for the first board or all?
                // "Beschrifte die Reihen links mit Z=1...Z=4"
                if (z === 0) {
                     ctx.fillStyle = "#333";
                     ctx.font = "12px sans-serif";
                     ctx.textAlign = "right";
                     // Rows inside the grid are Y coordinates (Height)
                     // If user wants Z=1..4 labeling rows, then they consider Height as Z?
                     // Confusing. Let's assume they mean Y=1..4 (Height).
                     // But bullet says "Ersetze Z durch Y".
                     // And "Row label left with Z=1..Z=4".
                     // This implies Height is Z. And Depth is Y.
                     ctx.fillText(`Z=${y+1}`, startX - 5, topY + vizY*(boardH/s) + (boardH/s)/2 + 4);
                }

                for (let x = 0; x < s; x++) {
                    const idx = game.getIdx(x, y, z);
                    const val = game.grid[idx];
                    this.drawPiece(ctx, startX, topY, boardW, s, x, vizY, val);
                }
            }
        }
        
        // --- 2. Side View (X Slices) ---
        // Display X-Slices below. X0 X1 X2 X3.
        // This shows Y vs Z for a fixed X. (Height vs Depth)
        // If Depth is Y now. And Height is Z.
        
        const secondY = topY + boardH + 60; // Offset
        
        for (let x = 0; x < s; x++) {
            const startX = pad + x * (boardW + pad);

            // Label
            ctx.fillStyle = "#2c3e50";
            ctx.font = "bold 14px sans-serif";
            ctx.textAlign = "center";
            ctx.fillText(`Seite X=${x+1} (von Seite)`, startX + boardW/2, secondY - 10);

            this.drawGrid(ctx, startX, secondY, boardW, boardH, s);

            for (let y = 0; y < s; y++) { // Height (Z according to row label instruction)
                 const vizY = s - 1 - y;
                 // Label rows again
                 if (x === 0) {
                     ctx.fillStyle = "#333";
                     ctx.font = "12px sans-serif";
                     ctx.textAlign = "right";
                     ctx.fillText(`Z=${y+1}`, startX - 5, secondY + vizY*(boardH/s) + (boardH/s)/2 + 4);
                 }

                 for (let z = 0; z < s; z++) { // Depth (Y according to slice instruction)
                     const idx = game.getIdx(x, y, z);
                     const val = game.grid[idx];
                     // In side view, x is fixed. We draw Z (Depth) on x-axis of grid?
                     // Standard Side View: columns are Z slices?
                     this.drawPiece(ctx, startX, secondY, boardW, s, z, vizY, val);
                 }
            }
        }
    },

    /** Helper to draw a generic 4x4 grid background */
    drawGrid(ctx, x, y, w, h, s) {
        // Background
        ctx.fillStyle = "#ecf0f1";
        ctx.fillRect(x, y, w, h);

        const cellS = w / s;

        // Grid lines
        ctx.strokeStyle = "#bdc3c7";
        ctx.lineWidth = 1;
        ctx.beginPath();
        for (let i = 0; i <= s; i++) {
            // Verts
            ctx.moveTo(x + i * cellS, y);
            ctx.lineTo(x + i * cellS, y + h);
            // Horis
            ctx.moveTo(x, y + i * cellS);
            ctx.lineTo(x + w, y + i * cellS);
        }
        ctx.stroke();
    },

    /** Helper to draw a piece on the grid */
    drawPiece(ctx, bx, by, bw, s, gx, gy, val) {
        if (val === 0) return;
        
        const cellS = bw / s;
        const cx = bx + gx * cellS + cellS/2;
        const cy = by + gy * cellS + cellS/2;
        const r = cellS * 0.35;
        
        ctx.beginPath();
        ctx.arc(cx, cy, r, 0, Math.PI * 2);
        // P1=Blue, P2=Red
        ctx.fillStyle = val === 1 ? "#3498db" : "#e74c3c";
        ctx.fill();
        ctx.strokeStyle = "rgba(0,0,0,0.2)";
        ctx.stroke();
    },

    /**
     * Helper to get click coordinates for 3D view.
     * Only reacts to clicks on the Top View (Z-Slices).
     * Returns {x, z} or null.
     */
    get3DClick(canvas, evt, game) {
        const rect = canvas.getBoundingClientRect();
        const mx = evt.clientX - rect.left;
        const my = evt.clientY - rect.top;
        
        const w = canvas.width;
        const s = game.size;
        const pad = 10;
        const boardW = (w - pad * (s + 1)) / s;
        // The top view starts at Y=40 (approx based on draw3D)
        const topY = 40;
        const boardH = boardW;

        if (my < topY || my > topY + boardH) return null;

        for (let z = 0; z < s; z++) {
             const startX = pad + z * (boardW + pad);
             if (mx >= startX && mx <= startX + boardW) {
                 // Found the Z plane
                 const x = Math.floor((mx - startX) / (boardW / s));
                 if (x >= 0 && x < s) {
                     return { x, z };
                 }
             }
        }
        return null;
    }
};