<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="importmap">
"three": "https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/"
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const canvasHeight = 500;
const blockSideLength = 3;
const fullHeightLength = heightLength + blockSideLength;
const dropInterval = 1000;
let gameState = 'initial';
const backgroundColor = 0x1A202C;
const gridColor = 0x888888;
const topLineColor = 0xff5b00;
const blockMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
const fixedBlockMaterial = new THREE.MeshLambertMaterial({ color: 0x00c3ff, transparent: true, opacity: 0.5 });
const completeMaterial = new THREE.MeshLambertMaterial({ color: 0xFFD700, transparent: true, opacity: 0.5 });
const overflowMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
const lineMaterial = new THREE.LineBasicMaterial({ color: gridColor });
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa, transparent: true, opacity: 0.5, side: THREE.DoubleSide });
const squareGeometry = new THREE.BoxGeometry(1, 1, 1);
const createRenderer = () => {
const canvas = document.getElementById('three-canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
canvas.style.maxWidth = canvasWidth + 'px';
renderer = new THREE.WebGLRenderer({ canvas: canvas });
const createScene = () => {
scene = new THREE.Scene();
scene.background = new THREE.Color(backgroundColor);
const createCamera = () => {
camera = new THREE.PerspectiveCamera(5, canvasWidth / canvasHeight);
camera.position.set(widthLength * 12, fullHeightLength * 6, widthLength * 25);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.4;
controls.target = new THREE.Vector3(widthLength / 2, fullHeightLength / 2, widthLength / 2);
const light = new THREE.DirectionalLight(0xFFFFFF, 10);
light.position.set(widthLength, fullHeightLength * 10, widthLength * 4);
const bottomGrid = new THREE.GridHelper(widthLength, widthLength, gridColor, gridColor);
bottomGrid.position.set(widthLength / 2, 0, widthLength / 2);
const topGrid = new THREE.GridHelper(widthLength, 1, topLineColor, topLineColor);
topGrid.position.set(widthLength / 2, heightLength, widthLength / 2);
const addVerticalLines = () => {
const lineGeometries = [];
for (let i = 0; i <= widthLength; i++) {
const xlineGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(i, 0, 0),
new THREE.Vector3(i, heightLength, 0),
lineGeometries.push(xlineGeometry);
const zlineGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, i),
new THREE.Vector3(0, heightLength, i),
lineGeometries.push(zlineGeometry);
const linesGeometry = mergeGeometries(lineGeometries);
const lines = new THREE.LineSegments(linesGeometry, lineMaterial);
const bottomWallGeometry = new THREE.PlaneGeometry(widthLength, widthLength);
bottomWallGeometry.rotateX(90 * Math.PI / 180);
bottomWallGeometry.translate(widthLength / 2, 0, widthLength / 2);
const backWallGeometry = new THREE.PlaneGeometry(widthLength, heightLength);
backWallGeometry.translate(widthLength / 2, heightLength / 2, 0);
const leftWallGeometry = new THREE.PlaneGeometry(widthLength, heightLength);
leftWallGeometry.rotateY(90 * Math.PI / 180);
leftWallGeometry.translate(0, heightLength / 2, widthLength / 2);
const wallGeometry = mergeGeometries([bottomWallGeometry, backWallGeometry, leftWallGeometry]);
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
renderer.render(scene, camera);
requestAnimationFrame(animate);
const initSquares = () => {
for (let y = 0; y < fullHeightLength; y++) {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
const square = new THREE.Mesh(squareGeometry, fixedBlockMaterial);
square.position.set(x + 0.5, y + 0.5, z + 0.5);
squares[y][z][x] = square;
const convertCoordinate = (coordinate, position) => {
x: coordinate.x + position.x,
y: coordinate.y + position.y,
z: coordinate.z + position.z
const initBlock = () => {
position: { x: 0, y: heightLength, z: 0 }
const randomIndex = Math.floor(Math.random() * blockShapeList.length);
const blockShape = blockShapeList[randomIndex];
for (let y = 0; y < blockSideLength; y++) {
for (let z = 0; z < blockSideLength; z++) {
for (let x = 0; x < blockSideLength; x++) {
if (blockShape[y][z][x]) {
block.coordinates.push({ x, y, z });
const displayBlock = (block) => {
block.coordinates.forEach((coordinate) => {
const { x, y, z } = convertCoordinate(coordinate, block.position);
squares[y][z][x].visible = true;
squares[y][z][x].material = blockMaterial;
const hideBlock = (block) => {
block.coordinates.forEach((coordinate) => {
const { x, y, z } = convertCoordinate(coordinate, block.position);
squares[y][z][x].visible = false;
squares[y][z][x].material = fixedBlockMaterial;
const replaceBlock = (newBlock) => {
const moveBlock = (event) => {
const movedBlock = structuredClone(block);
switch (event.target.value) {
case 'left': movedBlock.position.x += -1;
case 'right': movedBlock.position.x += 1;
case 'front': movedBlock.position.z += 1;
case 'back': movedBlock.position.z += -1;
if (!checkCollision(movedBlock)) {
replaceBlock(movedBlock);
const checkCollision = (block) => {
const isMobable = block.coordinates.some((coordinate) => {
const { x, y, z } = convertCoordinate(coordinate, block.position);
return checkWallCollision(x, y, z) || checkBlockCollision(x, y, z);
const checkWallCollision = (x, y, z) => {
return x < 0 || x >= widthLength || z < 0 || z >= widthLength || y < 0;
const checkBlockCollision = (x, y, z) => {
return squares[y][z][x].visible && squares[y][z][x].material === fixedBlockMaterial;
const rotateBlock = (event) => {
const rotatedBlock = structuredClone(block);
const tmp = blockSideLength - 1;
rotatedBlock.coordinates = block.coordinates.map((coordinate) => {
event.target.value === 'left' ? Math.abs(coordinate.z - tmp)
: event.target.value === 'right' ? coordinate.z : coordinate.x;
event.target.value === 'front' ? Math.abs(coordinate.z - tmp)
: event.target.value === 'back' ? coordinate.z : coordinate.y;
event.target.value === 'back' ? Math.abs(coordinate.y - tmp)
: event.target.value === 'right' ? Math.abs(coordinate.x - tmp)
: event.target.value === 'left' ? coordinate.x : coordinate.y;
return { x: rotatedX, y: rotatedY, z: rotatedZ };
if (!checkCollision(rotatedBlock)) {
replaceBlock(rotatedBlock);
document.querySelectorAll('.move-button').forEach((button) => {
button.addEventListener('click', moveBlock);
document.querySelectorAll('.rotate-button').forEach((button) => {
button.addEventListener('click', rotateBlock);
const dropBlock = () => {
timeoutId = setTimeout(dropBlock, dropInterval);
const droppedBlock = structuredClone(block);
droppedBlock.position.y--;
if (checkCollision(droppedBlock)) {
changeBlockMaterial(fixedBlockMaterial);
const completedLayerIndexes = findCompleteLayers();
if (completedLayerIndexes.length !== 0) {
hilightAndRemoveLayers(completedLayerIndexes);
replaceBlock(droppedBlock);
const changeBlockMaterial = (material) => {
block.coordinates.forEach((coordinate) => {
const { x, y, z } = convertCoordinate(coordinate, block.position);
squares[y][z][x].material = material;
const checkGameover = () => {
const isGameover = block.coordinates.some((coordinate) => {
const { y } = convertCoordinate(coordinate, block.position);
return y >= heightLength;
changeBlockMaterial(overflowMaterial);
const findCompleteLayers = () => {
const completedLayerIndexes = [];
for (let y = 0; y < heightLength; y++) {
if (checkCompleteLayer(y)) {
completedLayerIndexes.push(y);
return completedLayerIndexes;
const checkCompleteLayer = (y) => {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
if (!squares[y][z][x].visible) {
const highlightLayers = (completedLayerIndexes) => {
completedLayerIndexes.forEach((y) => {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
squares[y][z][x].material = completeMaterial;
const removeLayers = (completedLayerIndexes) => {
completedLayerIndexes.reverse().forEach((removingY) => {
for (let y = removingY; y < heightLength; y++) {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
squares[y][z][x].material = fixedBlockMaterial;
squares[y][z][x].visible = squares[y + 1][z][x].visible;
const hilightAndRemoveLayers = (completedLayerIndexes) => {
highlightLayers(completedLayerIndexes);
removeLayers(completedLayerIndexes);
timeoutId = setTimeout(dropBlock, dropInterval);
if (gameState === 'initial' || gameState === 'stop') {
if (gameState === 'running') {
if (!(gameState === 'initial')) {
const resetSquares = () => {
for (let y = 0; y < fullHeightLength; y++) {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
squares[y][z][x].material = fixedBlockMaterial;
squares[y][z][x].visible = false;
document.getElementById('start-button').addEventListener('click', start);
document.getElementById('stop-button').addEventListener('click', stop);
document.getElementById('reset-button').addEventListener('click', reset);
<canvas id="three-canvas"></canvas>
<button id="start-button">スタート</button>
<button id="stop-button">ストップ</button>
<button id="reset-button">リセット</button>
<button class="move-button" value="left">←</button>
<button class="move-button" value="back">↑</button>
<button class="move-button" value="front">↓</button>
<button class="move-button" value="right">→</button>
<button class="rotate-button" value="left">←</button>
<button class="rotate-button" value="back">↑</button>
<button class="rotate-button" value="front">↓</button>
<button class="rotate-button" value="right">→</button>