見出し画像

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

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




いいなと思ったら応援しよう!

Rikuriku|フォロバ100 ⚠️警告⚠️ もしあなたがチップをくれたら、その分あなたのお金が減ります(?)。お金は大切に。