3 D で 五 目 並 べ を 作 る ③
AIを駆使して
3D五目並べ
を作ります。
コード書くのってマジで大変






<!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
改善できるよ~って所があれば教えてほしいです💦。
いいなと思ったら応援しよう!
⚠️警告⚠️
もしあなたがチップをくれたら、その分あなたのお金が減ります(?)。お金は大切に。