メインコンテンツへスキップ
見出し画像

3 D で 五 目 並 べ を 作 る ③

    AIを駆使して

    3D五目並べ

    を作ります。

    コード書くのってマジで大変


    画像
    正二十面体


    画像
    右クリックで〇


    画像
    別の面を右クリックで今度は*を表示


    画像
    別の面を右クリックで今度は〇を表示


    画像
    直線上に五つ並べることで勝ち。


    画像
    (✖勝利時VER)
    <!DOCTYPE html>
    <html lang="ja">
    
    <head>
        <meta charset="UTF-8">
        <title>Icosahedron</title>
        <style>
            body {
                margin: 0;
                overflow: hidden;
                background-color: #333;
            }
    
            canvas {
                display: block;
            }
        </style>
    </head>
    
    <body>
        <div id="info"
            style="position: absolute; top: 10px; left: 10px; color: white; font-family: sans-serif; pointer-events: none;">
            Right-click on the shape to detect face
        </div>
        <script type="module">
            import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
    
            let currentState = 'A';
    
            try {
                // Scene setup
                const scene = new THREE.Scene();
                scene.background = new THREE.Color(0xf0f0f0); // 単色背景
    
                // Camera setup
                const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
                camera.position.z = 3;
    
                // Renderer setup
                const renderer = new THREE.WebGLRenderer({ antialias: true });
                renderer.setSize(window.innerWidth, window.innerHeight);
                document.body.appendChild(renderer.domElement);
    
                // Icosahedron creation
                const geometry = new THREE.IcosahedronGeometry(1.2, 0);
    
                // Material: 単色, エフェクトなし (FlatShadingで立体感は出す)
                const material = new THREE.MeshLambertMaterial({
                    color: 0x44ddff, // 鮮やかな青
                    flatShading: true
                });
    
                const mesh = new THREE.Mesh(geometry, material);
                scene.add(mesh);
    
                // Lighting (to show 3D shape with flat shading)
                const light = new THREE.DirectionalLight(0xffffff, 1);
                light.position.set(1, 1, 1).normalize();
                scene.add(light);
    
                const ambientLight = new THREE.AmbientLight(0x999999);
                scene.add(ambientLight);
    
                // Markings Setup
                const faceMarks = []; // Store { circle: Mesh, asterisk: Group } for each face index
    
                // Helper: Iterate over faces to place marks
                // Scene setup
                // This section appears to be a duplicate and will be removed to avoid re-declaration errors.
                // const scene = new THREE.Scene();
                // scene.background = new THREE.Color(0x333333);
    
                // Camera setup
                // const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
                // camera.position.z = 3;
    
                // Renderer setup
                // const renderer = new THREE.WebGLRenderer({ antialias: true });
                // renderer.setSize(window.innerWidth, window.innerHeight);
                // document.body.appendChild(renderer.domElement);
    
                // Icosahedron creation
                // const geometry = new THREE.IcosahedronGeometry(1.2, 0);
    
                // Material
                // const material = new THREE.MeshLambertMaterial({
                //     color: 0x00aaff,
                //     flatShading: true
                // });
    
                // const mesh = new THREE.Mesh(geometry, material);
                // scene.add(mesh);
    
                // Lighting
                // const light = new THREE.DirectionalLight(0xffffff, 1);
                // light.position.set(1, 1, 1).normalize();
                // scene.add(light);
    
                // const ambientLight = new THREE.AmbientLight(0x444444);
                // scene.add(ambientLight);
    
                // Markings Setup
                // const faceMarks = []; // Store { circle: Mesh, asterisk: Group } for each face index
    
                // Helper to get vertex by index or position
                function getVertex(geo, index) {
                    const pos = geo.getAttribute('position');
                    if (geo.index) {
                        const i = geo.index.getX(index);
                        return new THREE.Vector3().fromBufferAttribute(pos, i);
                    } else {
                        return new THREE.Vector3().fromBufferAttribute(pos, index);
                    }
                }
    
                const positionAttribute = geometry.getAttribute('position');
                const indexAttribute = geometry.index;
                const count = indexAttribute ? indexAttribute.count : positionAttribute.count;
    
                // Circle Geometry (Red Torus)
                const circleGeo = new THREE.TorusGeometry(0.2, 0.05, 8, 16);
                const circleMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
    
                // Asterisk Geometry (Black Group of Boxes)
                const stickGeo = new THREE.BoxGeometry(0.05, 0.5, 0.05);
                const stickMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
    
                for (let i = 0; i < count; i += 3) {
                    const vA = getVertex(geometry, i);
                    const vB = getVertex(geometry, i + 1);
                    const vC = getVertex(geometry, i + 2);
    
                    // Calculate center
                    const center = new THREE.Vector3().addVectors(vA, vB).add(vC).divideScalar(3);
    
                    // Calculate normal
                    const normal = new THREE.Vector3().crossVectors(
                        new THREE.Vector3().subVectors(vB, vA),
                        new THREE.Vector3().subVectors(vC, vA)
                    ).normalize();
    
                    // Create Circle (A state mark)
                    const circle = new THREE.Mesh(circleGeo, circleMat);
                    circle.position.copy(center).add(normal.clone().multiplyScalar(0.01)); // Slight offset
                    circle.lookAt(center.clone().add(normal));
                    circle.visible = false;
                    mesh.add(circle);
    
                    // Create Asterisk (B state mark)
                    const asteriskGroup = new THREE.Group();
    
                    const stick1 = new THREE.Mesh(stickGeo, stickMat);
                    const stick2 = new THREE.Mesh(stickGeo, stickMat);
                    const stick3 = new THREE.Mesh(stickGeo, stickMat);
    
                    stick2.rotation.z = Math.PI / 3; // 60 deg
                    stick3.rotation.z = -Math.PI / 3; // -60 deg
    
                    asteriskGroup.add(stick1, stick2, stick3);
                    asteriskGroup.position.copy(center).add(normal.clone().multiplyScalar(0.01));
                    asteriskGroup.lookAt(center.clone().add(normal));
                    asteriskGroup.visible = false;
                    mesh.add(asteriskGroup);
    
                    // Store references
                    // Note: If non-indexed, i/3 maps correctly to faceIndex if raycaster uses triangle index
                    faceMarks.push({ circle, asterisk: asteriskGroup });
                }
    
    
                // Raycaster definition
                const raycaster = new THREE.Raycaster();
                const mouse = new THREE.Vector2();
                const infoDiv = document.getElementById('info');
    
                // Interaction logic
                let isDragging = false;
                let previousMousePosition = { x: 0, y: 0 };
    
                renderer.domElement.addEventListener('mousedown', (e) => {
                    if (e.button === 0) { // 左クリックのみ
                        isDragging = true;
                        previousMousePosition = { x: e.clientX, y: e.clientY };
                    }
                });
    
                // Global state
                // note: declared outside try block
    
                // Right-click face detection, mark placement, and state toggle
                renderer.domElement.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
    
                    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
                    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
    
                    raycaster.setFromCamera(mouse, camera);
                    const intersects = raycaster.intersectObject(mesh);
    
                    if (intersects.length > 0) {
                        const faceIndex = intersects[0].faceIndex;
    
                        if (faceMarks[faceIndex]) {
                            // 修正: 既にマークが表示されている(以前にクリックされた)場合は無視する
                            if (faceMarks[faceIndex].circle.visible || faceMarks[faceIndex].asterisk.visible) {
                                return;
                            }
    
                            if (currentState === 'A') {
                                // State A: Show Circle, Hide Asterisk
                                faceMarks[faceIndex].circle.visible = true;
                                faceMarks[faceIndex].asterisk.visible = false;
                            } else {
                                // State B: Show Asterisk, Hide Circle
                                faceMarks[faceIndex].asterisk.visible = true;
                                faceMarks[faceIndex].circle.visible = false;
                            }
    
                            // WIN CHECK
                            if (typeof checkWin === 'function') {
                                const winningPattern = checkWin(currentState);
                                if (winningPattern) {
                                    triggerVictory(currentState, winningPattern);
                                    return;
                                }
                            }
                        }
    
                        // Toggle State
                        currentState = (currentState === 'A') ? 'B' : 'A';
    
                        infoDiv.innerText = `Detected Face Index: ${faceIndex} | State: ${currentState}`;
                    } else {
                        infoDiv.innerText = `No face detected | State: ${currentState}`;
                    }
                });
    
                const onStopDrag = () => {
                    isDragging = false;
                };
                window.addEventListener('mouseup', onStopDrag);
                window.addEventListener('mouseleave', onStopDrag);
    
                renderer.domElement.addEventListener('mousemove', (e) => {
                    if (isDragging) {
                        const deltaMove = {
                            x: e.clientX - previousMousePosition.x,
                            y: e.clientY - previousMousePosition.y
                        };
    
                        const rotateSpeed = 0.005;
                        const xAxis = new THREE.Vector3(1, 0, 0);
                        const yAxis = new THREE.Vector3(0, 1, 0);
    
                        mesh.rotateOnWorldAxis(xAxis, deltaMove.y * rotateSpeed);
                        mesh.rotateOnWorldAxis(yAxis, deltaMove.x * rotateSpeed);
    
                        previousMousePosition = { x: e.clientX, y: e.clientY };
                    }
                });
    
                // Resize handling
                window.addEventListener('resize', () => {
                    camera.aspect = window.innerWidth / window.innerHeight;
                    camera.updateProjectionMatrix();
                    renderer.setSize(window.innerWidth, window.innerHeight);
                });
    
                // Animation loop
                // Animation loop
                function animate() {
                    if (window.isVictory) return;
                    requestAnimationFrame(animate);
                    renderer.render(scene, camera);
                }
                animate();
    
                // Expose for debugging/analysis
                window.THREE = THREE;
                window.mesh = mesh;
    
                /* ----------------------------------------------------
                   Victory Logic & Effects
                   ---------------------------------------------------- */
    
                let winningPatterns = [];
    
                function getFaceVertices(faceIndex) {
                    const v = [];
                    const geo = window.mesh.geometry;
                    const pos = geo.getAttribute('position');
                    const idx = geo.index;
                    if (!geo) return [];
                    if (idx) {
                        for (let i = 0; i < 3; i++) {
                            const vertexIndex = idx.getX(faceIndex * 3 + i);
                            v.push(new THREE.Vector3().fromBufferAttribute(pos, vertexIndex));
                        }
                    } else {
                        for (let i = 0; i < 3; i++) {
                            v.push(new THREE.Vector3().fromBufferAttribute(pos, faceIndex * 3 + i));
                        }
                    }
                    return v;
                }
    
                function getFaceCenter(faceIndex) {
                    const vertices = getFaceVertices(faceIndex);
                    if (vertices.length < 3) return new THREE.Vector3();
                    return new THREE.Vector3().addVectors(vertices[0], vertices[1]).add(vertices[2]).divideScalar(3);
                }
    
                function precomputeWinningPatterns() {
                    try {
                        if (!window.mesh) return;
                        const geo = window.mesh.geometry;
                        const countPos = geo.getAttribute('position').count;
                        const countIdx = geo.index ? geo.index.count : countPos;
                        const faceCount = countIdx / 3;
    
                        const adjacency = {};
                        const edgeToFaces = {};
                        const pos = geo.getAttribute('position');
    
                        function getPosKey(index) {
                            const x = pos.getX(index).toFixed(4);
                            const y = pos.getY(index).toFixed(4);
                            const z = pos.getZ(index).toFixed(4);
                            return `${x}_${y}_${z}`;
                        }
    
                        for (let i = 0; i < faceCount; i++) {
                            adjacency[i] = new Set();
                            let vertIndices = [];
                            if (geo.index) {
                                vertIndices = [geo.index.getX(i * 3), geo.index.getX(i * 3 + 1), geo.index.getX(i * 3 + 2)];
                            } else {
                                vertIndices = [i * 3, i * 3 + 1, i * 3 + 2];
                            }
    
                            const keys = vertIndices.map(getPosKey);
    
                            for (let j = 0; j < 3; j++) {
                                const k1 = keys[j];
                                const k2 = keys[(j + 1) % 3];
                                // Sort keys to ensure undirected edge match
                                const edgeKey = (k1 < k2) ? `${k1}|${k2}` : `${k2}|${k1}`;
    
                                if (!edgeToFaces[edgeKey]) edgeToFaces[edgeKey] = [];
                                edgeToFaces[edgeKey].push(i);
                            }
                        }
                        for (const key in edgeToFaces) {
                            const faces = edgeToFaces[key];
                            // An edge should be shared by exactly 2 faces in a closed mesh
                            if (faces.length === 2) {
                                adjacency[faces[0]].add(faces[1]);
                                adjacency[faces[1]].add(faces[0]);
                            }
                        }
                        const paths = [];
                        function findPaths(currentPath, visited) {
                            if (currentPath.length === 5) {
                                paths.push([...currentPath]);
                                return;
                            }
                            const last = currentPath[currentPath.length - 1];
                            if (adjacency[last]) {
                                adjacency[last].forEach(neighbor => {
                                    if (!visited.has(neighbor)) {
                                        visited.add(neighbor);
                                        currentPath.push(neighbor);
                                        findPaths(currentPath, visited);
                                        currentPath.pop();
                                        visited.delete(neighbor);
                                    }
                                });
                            }
                        }
                        for (let i = 0; i < faceCount; i++) {
                            findPaths([i], new Set([i]));
                        }
                        let maxDist = 0;
                        const pathInfo = paths.map(p => {
                            const start = getFaceCenter(p[0]);
                            const end = getFaceCenter(p[4]);
                            const d = start.distanceTo(end);
                            if (d > maxDist) maxDist = d;
                            return { path: p, distance: d };
                        });
                        const epsilon = 0.01;
                        winningPatterns = pathInfo.filter(p => p.distance > maxDist - epsilon).map(p => p.path);
                    } catch (e) {
                        console.error("Error in precomputeWinningPatterns:", e);
                    }
                }
                setTimeout(precomputeWinningPatterns, 100);
    
                function checkWin(playerState) {
                    const ownedFaces = new Set();
                    faceMarks.forEach((mark, index) => {
                        const isOwned = (playerState === 'A' && mark.circle.visible) ||
                            (playerState === 'B' && mark.asterisk.visible);
                        if (isOwned) ownedFaces.add(index);
                    });
                    for (const pattern of winningPatterns) {
                        if (pattern.every(idx => ownedFaces.has(idx))) {
                            return pattern;
                        }
                    }
                    return null;
                }
    
                function triggerVictory(winner, pattern) {
                    const overlay = document.createElement('div');
                    Object.assign(overlay.style, {
                        position: 'absolute', top: '0', left: '0', width: '100%', height: '100%',
                        display: 'flex', justifyContent: 'center', alignItems: 'center',
                        backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '9999', pointerEvents: 'none'
                    });
                    overlay.innerHTML = `
                    <div style="text-align: center; animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);">
                        <h1 style="font-family: 'Arial Black', sans-serif; font-size: 80px; color: gold; text-shadow: 0 0 20px #ffaa00, 0 0 10px white; margin: 0;">
                            ${winner === 'A' ? 'RED CIRCLE' : 'BLACK ASTERISK'} WINS!
                        </h1>
                        <p style="color: white; font-size: 24px;">Straight Line Achieved!</p>
                    </div>
                    <style>@keyframes popIn { 0% { transform: scale(0); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }</style>
                `;
                    document.body.appendChild(overlay);
                    pattern.forEach(index => {
                        const mark = faceMarks[index];
                        if (mark.circle.visible) {
                            mark.circle.material = new THREE.MeshBasicMaterial({ color: 0xffff00 });
                            mark.circle.scale.set(1.5, 1.5, 1.5);
                        }
                        if (mark.asterisk.visible) {
                            mark.asterisk.children.forEach(child => child.material = new THREE.MeshBasicMaterial({ color: 0xffff00 }));
                            mark.asterisk.scale.set(1.5, 1.5, 1.5);
                        }
                    });
                    let time = 0;
                    window.isVictory = true;
                    window.animateVictory = function () {
                        if (!window.isVictory) return;
                        requestAnimationFrame(window.animateVictory);
                        time += 0.02;
                        camera.position.x = Math.sin(time) * 4;
                        camera.position.z = Math.cos(time) * 4;
                        camera.position.y = Math.sin(time * 0.5) * 2;
                        camera.lookAt(0, 0, 0);
                        renderer.render(scene, camera);
                    };
                    window.animateVictory();
                }
    
                // Reset Logic
                window.resetGame = function () {
                    // 1. Reset Marks
                    faceMarks.forEach(mark => {
                        mark.circle.visible = false;
                        mark.circle.scale.set(1, 1, 1);
                        mark.circle.material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); // Reset color
    
                        mark.asterisk.visible = false;
                        mark.asterisk.scale.set(1, 1, 1);
                        mark.asterisk.children.forEach(child => child.material = new THREE.MeshBasicMaterial({ color: 0x000000 }));
                    });
    
                    // 2. Reset State
                    currentState = 'A';
                    document.getElementById('info').innerText = "Right-click on the shape to detect face";
    
                    // 3. Stop Victory Animation & Effects
                    window.isVictory = false;
                    const overlays = document.querySelectorAll('div[style*="position: absolute; top: 0"]'); // Simple heuristic to find our specific overlay
                    overlays.forEach(el => {
                        if (el.innerHTML.includes("WINS!")) el.remove();
                    });
    
                    // 4. Reset Camera
                    camera.position.set(0, 0, 3);
                    camera.lookAt(0, 0, 0);
    
                    // 5. Restart Main Loop
                    animate();
                };
    
                // Add Reset Button
                const resetBtn = document.createElement('button');
                resetBtn.innerText = "Reset Game";
                Object.assign(resetBtn.style, {
                    position: 'absolute', top: '10px', right: '10px',
                    padding: '10px 20px', fontSize: '16px', cursor: 'pointer', zIndex: '10001'
                });
                resetBtn.onclick = window.resetGame;
                document.body.appendChild(resetBtn);
    
                window.getFaceCenter = getFaceCenter;
                /* 
                   Debug exposures removed for production. 
                   window.precomputeWinningPatterns and window.winningPatterns getters were used for verification.
                */
            } catch (error) {
                console.error("Error during init:", error);
                // document.body.innerHTML = "<div style='color:red; background:black; padding:20px;'><h1>Error</h1>" + error.toString() + "<br>" + error.stack + "</div>";
            }
        </script>
    </body>
    
    </html>

    Created with assistance from AI

    改善できるよ~って所があれば教えてほしいです💦。




     
     
     
    フォロバ100%です。暇な時間を折り紙でつぶす人。たまに自分でも何を言っているのか分からなくなる。でもマイクラもフォトナもやる。(?)実は折り紙より得意。 …………………………

    あなたへのおすすめ