let camera, scene, renderer;
const originalBoxSize = 3;
const scoreElement = document.getElementById("score");
const instructionsElement = document.getElementById("instructions");
const resultsElement = document.getElementById("results");
function setRobotPrecision() {
robotPrecision = Math.random() * 1 - 0.5;
world = new CANNON.World();
world.gravity.set(0, -10, 0);
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 40;
const aspect = window.innerWidth / window.innerHeight;
const height = width / aspect;
camera = new THREE.OrthographicCamera(
camera.position.set(4, 4, 4);
scene = new THREE.Scene();
addLayer(0, 0, originalBoxSize, originalBoxSize);
addLayer(-10, 0, originalBoxSize, originalBoxSize, "x");
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(10, 20, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animation);
document.body.appendChild(renderer.domElement);
if (instructionsElement) instructionsElement.style.display = "none";
if (resultsElement) resultsElement.style.display = "none";
if (scoreElement) scoreElement.innerText = 0;
while (world.bodies.length > 0) {
world.remove(world.bodies[0]);
while (scene.children.find((c) => c.type == "Mesh")) {
const mesh = scene.children.find((c) => c.type == "Mesh");
addLayer(0, 0, originalBoxSize, originalBoxSize);
addLayer(-10, 0, originalBoxSize, originalBoxSize, "x");
camera.position.set(4, 4, 4);
function addLayer(x, z, width, depth, direction) {
const y = boxHeight * stack.length;
const layer = generateBox(x, y, z, width, depth, false);
layer.direction = direction;
function addOverhang(x, z, width, depth) {
const y = boxHeight * (stack.length - 1);
const overhang = generateBox(x, y, z, width, depth, true);
overhangs.push(overhang);
function generateBox(x, y, z, width, depth, falls) {
const geometry = new THREE.BoxGeometry(width, boxHeight, depth);
const color = new THREE.Color(`hsl(${30 + stack.length * 4}, 100%, 50%)`);
const material = new THREE.MeshLambertMaterial({ color });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
const shape = new CANNON.Box(
new CANNON.Vec3(width / 2, boxHeight / 2, depth / 2)
let mass = falls ? 5 : 0;
mass *= width / originalBoxSize;
mass *= depth / originalBoxSize;
const body = new CANNON.Body({ mass, shape });
body.position.set(x, y, z);
function cutBox(topLayer, overlap, size, delta) {
const direction = topLayer.direction;
const newWidth = direction == "x" ? overlap : topLayer.width;
const newDepth = direction == "z" ? overlap : topLayer.depth;
topLayer.width = newWidth;
topLayer.depth = newDepth;
topLayer.threejs.scale[direction] = overlap / size;
topLayer.threejs.position[direction] -= delta / 2;
topLayer.cannonjs.position[direction] -= delta / 2;
const shape = new CANNON.Box(
new CANNON.Vec3(newWidth / 2, boxHeight / 2, newDepth / 2)
topLayer.cannonjs.shapes = [];
topLayer.cannonjs.addShape(shape);
window.addEventListener("click", eventHandler);
window.addEventListener("touchstart", eventHandler);
window.addEventListener("keydown", function (event) {
if (event.key == "R" || event.key == "r") {
function eventHandler() {
if (autopilot) startGame();
else splitBlockAndAddNextOneIfOverlaps();
function splitBlockAndAddNextOneIfOverlaps() {
const topLayer = stack[stack.length - 1];
const previousLayer = stack[stack.length - 2];
const direction = topLayer.direction;
const size = direction == "x" ? topLayer.width : topLayer.depth;
topLayer.threejs.position[direction] -
previousLayer.threejs.position[direction];
const overhangSize = Math.abs(delta);
const overlap = size - overhangSize;
cutBox(topLayer, overlap, size, delta);
const overhangShift = (overlap / 2 + overhangSize / 2) * Math.sign(delta);
? topLayer.threejs.position.x + overhangShift
: topLayer.threejs.position.x;
? topLayer.threejs.position.z + overhangShift
: topLayer.threejs.position.z;
const overhangWidth = direction == "x" ? overhangSize : topLayer.width;
const overhangDepth = direction == "z" ? overhangSize : topLayer.depth;
addOverhang(overhangX, overhangZ, overhangWidth, overhangDepth);
const nextX = direction == "x" ? topLayer.threejs.position.x : -10;
const nextZ = direction == "z" ? topLayer.threejs.position.z : -10;
const newWidth = topLayer.width;
const newDepth = topLayer.depth;
const nextDirection = direction == "x" ? "z" : "x";
if (scoreElement) scoreElement.innerText = stack.length - 1;
addLayer(nextX, nextZ, newWidth, newDepth, nextDirection);
function missedTheSpot() {
const topLayer = stack[stack.length - 1];
topLayer.threejs.position.x,
topLayer.threejs.position.z,
world.remove(topLayer.cannonjs);
scene.remove(topLayer.threejs);
if (resultsElement && !autopilot) resultsElement.style.display = "flex";
function animation(time) {
const timePassed = time - lastTime;
const topLayer = stack[stack.length - 1];
const previousLayer = stack[stack.length - 2];
topLayer.threejs.position[topLayer.direction] <
previousLayer.threejs.position[topLayer.direction] +
topLayer.threejs.position[topLayer.direction] += speed * timePassed;
topLayer.cannonjs.position[topLayer.direction] += speed * timePassed;
if (topLayer.threejs.position[topLayer.direction] > 10) {
splitBlockAndAddNextOneIfOverlaps();
if (camera.position.y < boxHeight * (stack.length - 2) + 4) {
camera.position.y += speed * timePassed;
updatePhysics(timePassed);
renderer.render(scene, camera);
function updatePhysics(timePassed) {
world.step(timePassed / 1000);
overhangs.forEach((element) => {
element.threejs.position.copy(element.cannonjs.position);
element.threejs.quaternion.copy(element.cannonjs.quaternion);
window.addEventListener("resize", () => {
console.log("resize", window.innerWidth, window.innerHeight);
const aspect = window.innerWidth / window.innerHeight;
const height = width / aspect;
camera.bottom = height / -2;
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);