rubiks-cube-mcp-server
Version:
MCP server for Rubik's Cube solving with real-time 3D visualization and MCP UI integration
260 lines (213 loc) • 9.2 kB
JavaScript
// Three.js 3D 큐브 렌더링 클래스
class Cube3DRenderer {
constructor() {
this.scene = null;
this.camera = null;
this.renderer = null;
this.cubeGroup = null;
this.cubies = [];
this.isRotating = false;
// 색상 매핑
this.colorMap = {
'W': 0xffffff, // 흰색
'Y': 0xffff00, // 노랑
'R': 0xff0000, // 빨강
'O': 0xff8000, // 주황
'B': 0x0000ff, // 파랑
'G': 0x00ff00 // 초록
};
this.init();
}
init() {
this.setupScene();
this.setupCamera();
this.setupRenderer();
this.setupLighting();
this.createRubiksCube();
this.setupMouseControls();
this.animate();
document.getElementById('loading').style.display = 'none';
}
setupScene() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x000000);
}
setupCamera() {
this.camera = new THREE.PerspectiveCamera(75, 800/600, 0.1, 1000);
this.camera.position.set(5, 5, 5);
this.camera.lookAt(0, 0, 0);
}
setupRenderer() {
const canvas = document.getElementById('cubeCanvas');
this.renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
this.renderer.setSize(800, 600);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
}
setupLighting() {
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 10, 5);
directionalLight.castShadow = true;
this.scene.add(directionalLight);
}
createRubiksCube() {
this.cubies = [];
// 큐브 그룹 생성
this.cubeGroup = new THREE.Group();
this.scene.add(this.cubeGroup);
const cubeSize = 0.95;
const gap = 0.05;
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
// 중앙 큐빅만 제외 (수정된 조건)
if (x === 0 && y === 0 && z === 0) continue;
const geometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
// 각 면에 다른 색상의 재질 적용
const materials = [
new THREE.MeshLambertMaterial({ color: 0x333333 }), // right
new THREE.MeshLambertMaterial({ color: 0x333333 }), // left
new THREE.MeshLambertMaterial({ color: 0x333333 }), // top
new THREE.MeshLambertMaterial({ color: 0x333333 }), // bottom
new THREE.MeshLambertMaterial({ color: 0x333333 }), // front
new THREE.MeshLambertMaterial({ color: 0x333333 }) // back
];
const cube = new THREE.Mesh(geometry, materials);
cube.position.set(
x * (cubeSize + gap),
y * (cubeSize + gap),
z * (cubeSize + gap)
);
cube.castShadow = true;
cube.receiveShadow = true;
cube.userData = { x, y, z };
this.cubeGroup.add(cube);
this.cubies.push(cube);
}
}
}
}
updateCubeColors(state) {
if (!state || !state.faces) return;
const faces = state.faces;
this.cubies.forEach(cube => {
const { x, y, z } = cube.userData;
const materials = cube.material;
if (Array.isArray(materials)) {
// Right face (x = 1)
if (x === 1) {
const faceIndex = this.getFaceIndex('right', y, z);
const row = Math.floor(faceIndex / 3);
const col = faceIndex % 3;
if (faces.right && faces.right[row] && faces.right[row][col]) {
materials[0].color.setHex(this.colorMap[faces.right[row][col]]);
}
}
// Left face (x = -1)
if (x === -1) {
const faceIndex = this.getFaceIndex('left', y, z);
const row = Math.floor(faceIndex / 3);
const col = faceIndex % 3;
if (faces.left && faces.left[row] && faces.left[row][col]) {
materials[1].color.setHex(this.colorMap[faces.left[row][col]]);
}
}
// Top face (y = 1)
if (y === 1) {
const faceIndex = this.getFaceIndex('top', x, z);
const row = Math.floor(faceIndex / 3);
const col = faceIndex % 3;
if (faces.top && faces.top[row] && faces.top[row][col]) {
materials[2].color.setHex(this.colorMap[faces.top[row][col]]);
}
}
// Bottom face (y = -1)
if (y === -1) {
const faceIndex = this.getFaceIndex('bottom', x, z);
const row = Math.floor(faceIndex / 3);
const col = faceIndex % 3;
if (faces.bottom && faces.bottom[row] && faces.bottom[row][col]) {
materials[3].color.setHex(this.colorMap[faces.bottom[row][col]]);
}
}
// Front face (z = 1)
if (z === 1) {
const faceIndex = this.getFaceIndex('front', x, y);
const row = Math.floor(faceIndex / 3);
const col = faceIndex % 3;
if (faces.front && faces.front[row] && faces.front[row][col]) {
materials[4].color.setHex(this.colorMap[faces.front[row][col]]);
}
}
// Back face (z = -1)
if (z === -1) {
const faceIndex = this.getFaceIndex('back', x, y);
const row = Math.floor(faceIndex / 3);
const col = faceIndex % 3;
if (faces.back && faces.back[row] && faces.back[row][col]) {
materials[5].color.setHex(this.colorMap[faces.back[row][col]]);
}
}
}
});
}
getFaceIndex(face, coord1, coord2) {
const normalize = (val) => val + 1; // -1,0,1 -> 0,1,2
switch(face) {
case 'top':
case 'bottom':
return normalize(-coord2) * 3 + normalize(coord1); // -z*3 + x (z축 뒤집기)
case 'front':
case 'back':
return normalize(-coord2) * 3 + normalize(coord1); // -y*3 + x
case 'right':
case 'left':
return normalize(-coord1) * 3 + normalize(coord2); // -y*3 + z
default:
return 0;
}
}
setupMouseControls() {
let isMouseDown = false;
let mouseX = 0, mouseY = 0;
const canvas = document.getElementById('cubeCanvas');
canvas.addEventListener('mousedown', (event) => {
isMouseDown = true;
mouseX = event.clientX;
mouseY = event.clientY;
});
canvas.addEventListener('mouseup', () => {
isMouseDown = false;
});
canvas.addEventListener('mousemove', (event) => {
if (!isMouseDown) return;
const deltaX = event.clientX - mouseX;
const deltaY = event.clientY - mouseY;
this.cubeGroup.rotation.y += deltaX * 0.01;
this.cubeGroup.rotation.x += deltaY * 0.01;
mouseX = event.clientX;
mouseY = event.clientY;
});
}
animate() {
requestAnimationFrame(() => this.animate());
// 자동 회전 (마우스 조작이 없을 때)
if (!this.isRotating) {
this.cubeGroup.rotation.y += 0.005;
}
this.renderer.render(this.scene, this.camera);
}
setRotating(rotating) {
this.isRotating = rotating;
}
}
// 전역 변수로 인스턴스 생성
let cubeRenderer = null;
// 게임이 있을 때만 초기화
if (gameId) {
cubeRenderer = new Cube3DRenderer();
} else {
document.getElementById('loading').innerHTML = '<h2>No game session provided</h2><p>Please start a game through the MCP server</p>';
}