<html><head><base href="https://api.websim.io/CrateCraze/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC Stack Challenge: Crypto Crate Craze</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&family=Press+Start+2P&display=swap');
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Roboto', sans-serif;
background-image: url('https://i.redd.it/o17eipie3ijb1.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
height: 100vh;
}
#game-container {
width: 100vw;
height: 100vh;
position: relative;
}
#crane {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 100px;
z-index: 10;
}
#rope {
position: absolute;
top: 0;
left: 50%;
width: 2px;
height: 300px;
background-color: #FFD700;
transform-origin: top center;
box-shadow: 0 0 10px #FFD700;
}
.block {
position: absolute;
width: 80px;
height: 80px;
bottom: -40px;
left: 50%;
transform: translateX(-50%);
background-image: url('https://static.wikia.nocookie.net/portalworldsgame/images/0/00/Wooden_Crate.png');
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
font-size: 40px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
border-radius: 10px;
}
#tower {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: calc(100% - 60px);
}
.tower-block {
width: 80px;
height: 80px;
background-image: url('https://static.wikia.nocookie.net/portalworldsgame/images/0/00/Wooden_Crate.png');
background-size: cover;
position: absolute;
transition: opacity 0.3s;
display: flex;
justify-content: center;
align-items: center;
font-size: 40px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
border-radius: 10px;
}
#score, #timer {
position: absolute;
left: 20px;
font-size: 24px;
color: #FFD700;
text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
font-family: 'Press Start 2P', cursive;
background-color: rgba(0, 0, 0, 0.6);
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
#score {
top: 20px;
}
#timer {
top: 80px;
}
#game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
padding: 40px;
border-radius: 20px;
text-align: center;
display: none;
color: #FFD700;
font-family: 'Press Start 2P', cursive;
box-shadow: 0 0 30px rgba(255, 215, 0, 0.5);
}
#restart-btn {
margin-top: 20px;
padding: 15px 30px;
font-size: 18px;
background-color: #FFD700;
color: #000;
border: none;
border-radius: 10px;
cursor: pointer;
font-family: 'Press Start 2P', cursive;
transition: all 0.3s ease;
}
#restart-btn:hover {
background-color: #FFA500;
transform: scale(1.05);
}
#ground {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60px;
display: flex;
justify-content: flex-start;
}
.ground-tile {
width: 60px;
height: 60px;
background-image: url('https://cdna.artstation.com/p/assets/images/images/034/060/716/large/diego-lopez-groundtile1.jpg');
background-size: cover;
box-shadow: inset 0 5px 10px rgba(0,0,0,0.3);
}
#rules {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.8);
padding: 20px;
border-radius: 15px;
font-size: 14px;
max-width: 300px;
color: #FFD700;
box-shadow: 0 0 20px rgba(255, 215, 0, 0.3);
}
#rules h3 {
font-family: 'Press Start 2P', cursive;
margin-bottom: 15px;
}
#rules ul {
padding-left: 20px;
}
#rules li {
margin-bottom: 10px;
}
#points-effect, #time-effect {
position: absolute;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
pointer-events: none;
opacity: 0;
transition: opacity 0.5s, transform 0.5s;
font-family: 'Press Start 2P', cursive;
}
#points-effect {
font-size: 28px;
color: #4CAF50;
}
#time-effect {
font-size: 24px;
color: #FFA500;
}
</style>
</head>
<body>
<div id="game-container">
<div id="crane">
<div id="rope">
<div class="block">₿</div>
</div>
</div>
<div id="tower"></div>
<div id="score">Score: 0</div>
<div id="timer">Time: 60s</div>
<div id="game-over">
<h2>Game Over!</h2>
<p>Final Score: <span id="final-score"></span></p>
<button id="restart-btn">Restart</button>
</div>
<div id="ground"></div>
<div id="rules">
<h3>Game Rules:</h3>
<ul>
<li>Click to drop BTC crates</li>
<li>Stack crates carefully</li>
<li>5 vertical crates = 25 points</li>
<li>5+ ground crates = Game Over</li>
<li>60 seconds to play</li>
<li>+5 seconds per 25 points</li>
<li>Faster swing under 10 seconds</li>
</ul>
</div>
<div id="points-effect">+25 points</div>
<div id="time-effect">+5 seconds</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<script>
const gameContainer = document.getElementById('game-container');
const rope = document.getElementById('rope');
const block = document.querySelector('.block');
const tower = document.getElementById('tower');
const scoreElement = document.getElementById('score');
const timerElement = document.getElementById('timer');
const gameOverElement = document.getElementById('game-over');
const finalScoreElement = document.getElementById('final-score');
const restartBtn = document.getElementById('restart-btn');
const ground = document.getElementById('ground');
const pointsEffect = document.getElementById('points-effect');
const timeEffect = document.getElementById('time-effect');
let score = 0;
let swingAngle = 0;
let swingDirection = 1;
let gameActive = true;
let swingSpeed = 0.02;
let blockFalling = false;
let timeRemaining = 60;
let timerInterval;
const engine = Matter.Engine.create({
gravity: { x: 0, y: 1 }
});
const world = engine.world;
const render = Matter.Render.create({
element: document.body,
engine: engine,
options: {
width: window.innerWidth,
height: window.innerHeight,
wireframes: false,
background: 'transparent'
}
});
Matter.Render.run(render);
const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);
const tileSize = 60;
const tilesNeeded = Math.ceil(window.innerWidth / tileSize);
for (let i = 0; i < tilesNeeded; i++) {
const tile = document.createElement('div');
tile.className = 'ground-tile';
ground.appendChild(tile);
const physicalTile = Matter.Bodies.rectangle(
i * tileSize + tileSize / 2,
window.innerHeight - tileSize / 2,
tileSize,
tileSize,
{ isStatic: true, friction: 0.5, restitution: 0.2 }
);
Matter.World.add(world, physicalTile);
}
function updateScore(points) {
score += points;
scoreElement.textContent = `Score: ${score}`;
if (points === 25) {
showPointsEffect();
addTime(5);
}
}
function showPointsEffect() {
const x = window.innerWidth / 2;
const y = window.innerHeight / 2;
pointsEffect.style.left = `${x}px`;
pointsEffect.style.top = `${y}px`;
pointsEffect.style.transform = 'translate(-50%, -50%) scale(1)';
pointsEffect.style.opacity = '1';
timeEffect.style.left = `${x}px`;
timeEffect.style.top = `${y + 40}px`;
timeEffect.style.transform = 'translate(-50%, -50%) scale(1)';
timeEffect.style.opacity = '1';
gsap.to(pointsEffect, {
opacity: 0,
y: -50,
scale: 1.5,
duration: 1,
ease: "power2.out"
});
gsap.to(timeEffect, {
opacity: 0,
y: 50,
scale: 1.5,
duration: 1,
ease: "power2.out"
});
}
function updateTimer() {
timeRemaining--;
timerElement.textContent = `Time: ${timeRemaining}s`;
updateTimerColor();
if (timeRemaining <= 0) {
gameOver();
} else if (timeRemaining <= 10) {
swingSpeed = 0.04;
}
}
function updateTimerColor() {
if (timeRemaining > 40) {
timerElement.style.color = '#4CAF50'; // Green
} else if (timeRemaining > 10) {
timerElement.style.color = '#FFA500'; // Orange
} else {
timerElement.style.color = '#FF0000'; // Red
}
}
function addTime(seconds) {
timeRemaining += seconds;
timerElement.textContent = `Time: ${timeRemaining}s`;
updateTimerColor();
}
function swingRope() {
if (!gameActive) return;
swingAngle += swingSpeed * swingDirection;
if (Math.abs(swingAngle) > 0.8) {
swingDirection *= -1;
}
gsap.to(rope, {
rotation: swingAngle * 50,
duration: 0.1
});
requestAnimationFrame(swingRope);
}
function dropBlock() {
if (!gameActive || blockFalling) return;
blockFalling = true;
block.style.visibility = 'hidden';
const ropeRect = rope.getBoundingClientRect();
const ropeEnd = {
x: ropeRect.left + ropeRect.width / 2,
y: ropeRect.bottom
};
const newBlock = createPhysicsBlock(ropeEnd.x, ropeEnd.y);
}
function createPhysicsBlock(x, y) {
const blockSize = 80;
const physicsBlock = Matter.Bodies.rectangle(x, y, blockSize, blockSize, {
friction: 0.5,
restitution: 0.2,
render: {
sprite: {
texture: 'https://static.wikia.nocookie.net/portalworldsgame/images/0/00/Wooden_Crate.png',
xScale: 0.5,
yScale: 0.5
}
}
});
Matter.World.add(world, physicsBlock);
const visualBlock = document.createElement('div');
visualBlock.className = 'tower-block';
visualBlock.innerHTML = '₿';
tower.appendChild(visualBlock);
Matter.Events.on(engine, 'afterUpdate', () => {
visualBlock.style.transform = `translate(${physicsBlock.position.x - blockSize/2}px, ${physicsBlock.position.y - blockSize/2}px) rotate(${physicsBlock.angle}rad)`;
checkBlockPosition(physicsBlock, visualBlock);
});
return { physicsBlock, visualBlock };
}
function checkBlockPosition(physicsBlock, visualBlock) {
if (Math.abs(physicsBlock.velocity.y) < 0.1 && Math.abs(physicsBlock.velocity.x) < 0.1) {
if (blockFalling) {
blockFalling = false;
block.style.visibility = 'visible';
checkForCompleteColumns();
checkForGameOver();
}
}
}
function checkForCompleteColumns() {
const blockSize = 80;
const tolerance = 10;
const blocks = Array.from(document.querySelectorAll('.tower-block'));
const columns = {};
blocks.forEach((block) => {
const transform = block.style.transform;
const x = parseInt(transform.split(',')[0].split('(')[1]);
const y = parseInt(transform.split(',')[1]);
const roundedX = Math.round(x / blockSize) * blockSize;
if (!columns[roundedX]) columns[roundedX] = [];
columns[roundedX].push({ block, y });
});
let columnsToRemove = [];
for (const [x, columnBlocks] of Object.entries(columns)) {
columnBlocks.sort((a, b) => b.y - a.y);
let consecutiveCount = 1;
let startIndex = 0;
for (let i = 1; i < columnBlocks.length; i++) {
if (Math.abs(columnBlocks[i].y - columnBlocks[i-1].y) <= blockSize + tolerance) {
consecutiveCount++;
if (consecutiveCount === 5) {
columnsToRemove.push({ columnBlocks, startIndex: startIndex, endIndex: i });
break;
}
} else {
consecutiveCount = 1;
startIndex = i;
}
}
}
if (columnsToRemove.length > 0) {
columnsToRemove.forEach(({ columnBlocks, startIndex, endIndex }) => {
removeVerticalBlocks(columnBlocks, startIndex, endIndex);
updateScore(25);
});
}
}
function removeVerticalBlocks(columnBlocks, startIndex, endIndex) {
for (let i = startIndex; i <= endIndex; i++) {
const block = columnBlocks[i].block;
const physicsBody = Matter.Composite.allBodies(world).find(
(body) => Math.abs(body.position.x - (parseFloat(block.style.transform.split(',')[0].split('(')[1]) + 40)) < 10 &&
Math.abs(body.position.y - (parseFloat(block.style.transform.split(',')[1]) + 40)) < 10
);
if (physicsBody) {
Matter.World.remove(world, physicsBody);
}
gsap.to(block, {
opacity: 0,
scale: 1.5,
duration: 0.3,
onComplete: () => block.remove()
});
}
// Move blocks above the removed ones down
for (let i = startIndex - 1; i >= 0; i--) {
const block = columnBlocks[i].block;
const physicsBody = Matter.Composite.allBodies(world).find(
(body) => Math.abs(body.position.x - (parseFloat(block.style.transform.split(',')[0].split('(')[1]) + 40)) < 10 &&
Math.abs(body.position.y - (parseFloat(block.style.transform.split(',')[1]) + 40)) < 10
);
if (physicsBody) {
Matter.Body.translate(physicsBody, { x: 0, y: 80 * 5 });
}
}
}
function checkForGameOver() {
const groundLevel = window.innerHeight - 60;
const blockSize = 80;
const tolerance = 10;
const blocks = Array.from(document.querySelectorAll('.tower-block'));
const groundTouchingBlocks = blocks.filter((block) => {
const y = parseFloat(block.style.transform.split(',')[1]);
return Math.abs(y + blockSize - groundLevel) < tolerance;
});
if (groundTouchingBlocks.length >= 5) {
gameOver();
}
}
function gameOver() {
gameActive = false;
finalScoreElement.textContent = score;
gsap.to(gameOverElement, {
display: 'block',
opacity: 1,
scale: 1,
duration: 0.5,
ease: "back.out(1.7)"
});
clearInterval(timerInterval);
}
function restartGame() {
score = 0;
swingAngle = 0;
swingDirection = 1;
gameActive = true;
swingSpeed = 0.02;
blockFalling = false;
timeRemaining = 60;
scoreElement.textContent = 'Score: 0';
timerElement.textContent = 'Time: 60s';
updateTimerColor();
gsap.to(gameOverElement, {
opacity: 0,
scale: 0.8,
duration: 0.3,
onComplete: () => {
gameOverElement.style.display = 'none';
}
});
block.style.visibility = 'visible';
Matter.World.clear(world);
Matter.Engine.clear(engine);
tower.innerHTML = '';
for (let i = 0; i < tilesNeeded; i++) {
const physicalTile = Matter.Bodies.rectangle(
i * tileSize + tileSize / 2,
window.innerHeight - tileSize / 2,
tileSize,
tileSize,
{ isStatic: true, friction: 0.5, restitution: 0.2 }
);
Matter.World.add(world, physicalTile);
}
swingRope();
clearInterval(timerInterval);
timerInterval = setInterval(updateTimer, 1000);
}
gameContainer.addEventListener('click', dropBlock);
restartBtn.addEventListener('click', restartGame);
swingRope();
timerInterval = setInterval(updateTimer, 1000);
// Continuous checking for complete columns and game over
Matter.Events.on(engine, 'afterUpdate', () => {
if (gameActive) {
checkForCompleteColumns();
checkForGameOver();
}
});
// Preload images
const images = [
'https://static.wikia.nocookie.net/portalworldsgame/images/0/00/Wooden_Crate.png',
'https://cdna.artstation.com/p/assets/images/images/034/060/716/large/diego-lopez-groundtile1.jpg'
];
images.forEach((src) => {
const img = new Image();
img.src = src;
img.onload = () => console.log(`Image loaded successfully: ${src}`);
img.onerror = () => console.error(`Failed to load image: ${src}`);
});
</script>
</body>
</html>