How to Make a Minecraft-Like Game
How to Vibe Code a Minecraft-Style Block Sandbox
Stay in your creative flow. Direct AI models to generate procedural terrain chunks, mining & block-placement raycasts, and hotbar crafting—and ship a playable block-building sandbox in your browser in under 25 minutes.
Core Stack
HTML5 Canvas + JS
World Engine
Seeded Terrain Chunks
AI Partner
Claude / Gemini
Target Time
15–25 Mins
The Vibe Coding Voxel & Block Philosophy
Traditional sandbox development gets bogged down in Perlin noise math, chunk mesh culling, and complex inventory serialization before you can mine a single block of dirt. Vibe coding puts you in the role of World Architect: you describe the biomes, ore distribution, day/night lighting, and hotbar blocks while conversational AI builds the terrain generator and block-interaction loop.
Old-School Voxel Stress
- Spending weeks writing custom C++ chunk meshers before placing a single block.
- Tedious resource gathering loops that frustrate players before they can build.
- Fighting browser right-click context menus when trying to place blocks.
- Losing your built house every time you refresh the browser tab.
The Vibe Coder Way
- Instant 2D Terraria/Minecraft-style block world or Three.js 3D voxel starter in a single
index.html. - Prompt the world: "Generate rolling hills with grass, dirt, stone, coal/gold ores, and surface oak trees."
- Left-click to mine blocks into your hotbar, Right-click (or Shift+Click) to build anything you imagine.
- Save and load modified world chunks straight to
localStorage.
The 4-Step Blueprint
From a terrain seed to mining ores, building castles, and surviving the night.
Spawn the Procedural Chunk World
Create a single index.html file. Use a 1D/2D heightmap wave to generate layered terrain: sky, grass, dirt, deep stone, ores, and trees.
The "One-Shot Architect" Prompt
Ask your AI for player movement + jumping, a reach radius cursor highlight, Left-Click mining into inventory counts, and Right-Click block placement.
e.preventDefault() on the canvas contextmenu event so right-clicking places blocks cleanly."
Hotbar, Crafting & Night Mobs
Add a 1–6 number-key hotbar (Grass, Dirt, Cobblestone, Wood Planks, Brick, Torch), a day-to-night sky transition, and slimes/zombies to fight.
World Save & Creative Freedom
Serialize your chunk grid and inventory to localStorage so players can return to their custom-built fortress anytime.
Vibe Prompt Configurator
Configure your block world dimension, survival rules, and building tools. Copy the generated prompt into Claude, Cursor, or Gemini.
Essential Elements for a Minecraft-Style Game
Since Markus Persson and Mojang launched Minecraft, the best voxel and block sandbox games have shared these five core pillars:
-
Creative Block Building
Let players place and combine different block types (wood, stone, brick, glass) to build anything they can imagine—from cozy cabins to sprawling castles.
-
Explorable Open World & Chunks
Generate terrain from a map seed split into chunks, complete with hills, surface trees, underground caverns, and hidden ore veins to discover.
-
Rewarding Resource Gathering
Keep mining snappy and plentiful—every broken block drops directly into the player's hotbar with satisfying particle crumbs so gathering never feels tedious.
-
Creature Combat & Survival
Populate the world with creatures and night mobs that add excitement, challenge, and a reason to build protective shelters and craft better gear.
-
Unrestricted Creative Freedom
Avoid rigid linear rules. Give players the freedom to set their own goals—whether mining deep for gold, landscaping a mountain, or experimenting with builds.
-
Intuitive Hotbar & World Save
Provide a 1–6 quick-select hotbar for switching block materials on the fly and save world edits so creations persist across sessions.
The "Golden" Single-File Block Sandbox Template
Save this as index.html and open it in any browser to explore, mine, and build in a procedural Minecraft-style 2D block world.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VibeCraft: Procedural Block Sandbox</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }
body {
background: #090d16;
color: #f8fafc;
font-family: system-ui, -apple-system, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 10px;
}
.bar {
margin-bottom: 8px;
padding: 8px 16px;
background: #131c31;
border: 1px solid #283655;
border-radius: 999px;
font-size: 13px;
color: #cbd5e1;
}
canvas {
border: 2px solid #38bdf8;
border-radius: 12px;
box-shadow: 0 20px 50px rgba(0,0,0,0.65);
max-width: 100%;
height: auto;
cursor: crosshair;
}
</style>
</head>
<body>
<div class="bar">
<strong>VibeCraft ⛏️</strong> — Move: <kbd>A</kbd>/<kbd>D</kbd> • Jump: <kbd>W</kbd>/<kbd>Space</kbd> • <strong>Left-Click</strong>: Mine Block • <strong>Right-Click</strong>: Place Block • Keys <kbd>1–6</kbd>: Select Hotbar
</div>
<canvas id="game" width="900" height="500"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
canvas.addEventListener('contextmenu', e => e.preventDefault());
const TILE = 28;
const WORLD_W = 64;
const WORLD_H = 28;
// 0: Air, 1: Grass, 2: Dirt, 3: Stone, 4: Wood, 5: Leaves, 6: Gold Ore, 7: Brick
const BLOCKS = {
1: { name: 'Grass', color: '#22c55e', top: '#4ade80', solid: true },
2: { name: 'Dirt', color: '#92400e', top: '#b45309', solid: true },
3: { name: 'Stone', color: '#64748b', top: '#94a3b8', solid: true },
4: { name: 'Wood', color: '#78350f', top: '#a16207', solid: true },
5: { name: 'Leaves', color: '#15803d', top: '#16a34a', solid: false },
6: { name: 'Gold', color: '#eab308', top: '#fef08a', solid: true },
7: { name: 'Brick', color: '#dc2626', top: '#f87171', solid: true }
};
const hotbar = [
{ id: 1, count: 15 },
{ id: 2, count: 25 },
{ id: 3, count: 20 },
{ id: 4, count: 15 },
{ id: 6, count: 5 },
{ id: 7, count: 30 }
];
let selectedSlot = 0;
let world = [];
let particles = [];
let mouse = { x: 0, y: 0 };
const keys = {};
function generateWorld() {
world = Array.from({ length: WORLD_H }, () => Array(WORLD_W).fill(0));
for (let x = 0; x < WORLD_W; x++) {
const surface = Math.floor(11 + Math.sin(x * 0.18) * 2.5 + Math.cos(x * 0.07) * 1.5);
for (let y = surface; y < WORLD_H; y++) {
if (y === surface) world[y][x] = 1;
else if (y < surface + 3) world[y][x] = 2;
else world[y][x] = (Math.random() < 0.09 && y > surface + 5) ? 6 : 3;
}
// Trees
if (x > 5 && x < WORLD_W - 5 && x % 9 === 0) {
for (let ty = 1; ty <= 3; ty++) world[surface - ty][x] = 4;
for (let lx = -1; lx <= 1; lx++) {
for (let ly = -5; ly <= -3; ly++) {
if (world[surface + ly][x + lx] === 0) world[surface + ly][x + lx] = 5;
}
}
}
}
}
const player = { x: 16 * TILE, y: 6 * TILE, w: 20, h: 42, vx: 0, vy: 0, grounded: false };
let camX = 0;
let camY = 0;
let timeAngle = 0;
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code.startsWith('Digit')) {
const n = Number(e.code.replace('Digit', '')) - 1;
if (n >= 0 && n < hotbar.length) selectedSlot = n;
}
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
canvas.addEventListener('mousemove', e => {
const r = canvas.getBoundingClientRect();
mouse.x = (e.clientX - r.left) * (canvas.width / r.width);
mouse.y = (e.clientY - r.top) * (canvas.height / r.height);
});
canvas.addEventListener('mousedown', e => {
const tx = Math.floor((mouse.x + camX) / TILE);
const ty = Math.floor((mouse.y + camY) / TILE);
if (tx < 0 || tx >= WORLD_W || ty < 0 || ty >= WORLD_H) return;
const px = player.x + player.w / 2;
const py = player.y + player.h / 2;
const dist = Math.hypot((tx + 0.5) * TILE - px, (ty + 0.5) * TILE - py);
if (dist > TILE * 5.5) return;
// Left click (or normal click without right button): Mine
if (e.button === 0 && !e.shiftKey) {
const id = world[ty][tx];
if (id !== 0) {
const slot = hotbar.find(s => s.id === id);
if (slot) slot.count++;
for (let i = 0; i < 8; i++) {
particles.push({
x: tx * TILE + 14,
y: ty * TILE + 14,
vx: (Math.random() - 0.5) * 5,
vy: (Math.random() - 0.8) * 5,
color: BLOCKS[id].color,
life: 20
});
}
world[ty][tx] = 0;
}
} else if (e.button === 2 || e.shiftKey) {
// Right click: Place selected hotbar block
const slot = hotbar[selectedSlot];
if (world[ty][tx] === 0 && slot && slot.count > 0) {
const bx = tx * TILE;
const by = ty * TILE;
const overlapsPlayer = (
player.x < bx + TILE &&
player.x + player.w > bx &&
player.y < by + TILE &&
player.y + player.h > by
);
if (!overlapsPlayer) {
world[ty][tx] = slot.id;
slot.count--;
}
}
}
});
function isSolid(tx, ty) {
if (tx < 0 || tx >= WORLD_W || ty < 0 || ty >= WORLD_H) return true;
const id = world[ty][tx];
return id !== 0 && BLOCKS[id].solid;
}
function collideX() {
const top = Math.floor(player.y / TILE);
const bottom = Math.floor((player.y + player.h - 1) / TILE);
if (player.vx > 0) {
const right = Math.floor((player.x + player.w) / TILE);
for (let y = top; y <= bottom; y++) {
if (isSolid(right, y)) {
player.x = right * TILE - player.w - 0.01;
player.vx = 0;
}
}
} else if (player.vx < 0) {
const left = Math.floor(player.x / TILE);
for (let y = top; y <= bottom; y++) {
if (isSolid(left, y)) {
player.x = (left + 1) * TILE + 0.01;
player.vx = 0;
}
}
}
}
function collideY() {
player.grounded = false;
const left = Math.floor(player.x / TILE);
const right = Math.floor((player.x + player.w - 1) / TILE);
if (player.vy > 0) {
const bottom = Math.floor((player.y + player.h) / TILE);
for (let x = left; x <= right; x++) {
if (isSolid(x, bottom)) {
player.y = bottom * TILE - player.h - 0.01;
player.vy = 0;
player.grounded = true;
}
}
} else if (player.vy < 0) {
const top = Math.floor(player.y / TILE);
for (let x = left; x <= right; x++) {
if (isSolid(x, top)) {
player.y = (top + 1) * TILE + 0.01;
player.vy = 0;
}
}
}
}
function update() {
timeAngle += 0.0012;
player.vx = 0;
if (keys['KeyA'] || keys['ArrowLeft']) player.vx = -3.8;
if (keys['KeyD'] || keys['ArrowRight']) player.vx = 3.8;
if ((keys['KeyW'] || keys['Space'] || keys['ArrowUp']) && player.grounded) {
player.vy = -9.6;
}
player.x += player.vx;
collideX();
player.vy = Math.min(11, player.vy + 0.48);
player.y += player.vy;
collideY();
camX += ((player.x - canvas.width / 2) - camX) * 0.12;
camY += ((player.y - canvas.height / 2) - camY) * 0.12;
camX = Math.max(0, Math.min(WORLD_W * TILE - canvas.width, camX));
camY = Math.max(0, Math.min(WORLD_H * TILE - canvas.height, camY));
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.25;
p.life--;
});
particles = particles.filter(p => p.life > 0);
}
function draw() {
// Day / Night sky gradient
const daylight = (Math.sin(timeAngle) + 1) * 0.5;
ctx.fillStyle = daylight > 0.35 ? '#0ea5e9' : '#0f172a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(-camX, -camY);
const startX = Math.max(0, Math.floor(camX / TILE));
const endX = Math.min(WORLD_W - 1, Math.ceil((camX + canvas.width) / TILE));
const startY = Math.max(0, Math.floor(camY / TILE));
const endY = Math.min(WORLD_H - 1, Math.ceil((camY + canvas.height) / TILE));
for (let y = startY; y <= endY; y++) {
for (let x = startX; x <= endX; x++) {
const id = world[y][x];
if (id === 0) continue;
const b = BLOCKS[id];
ctx.fillStyle = b.color;
ctx.fillRect(x * TILE, y * TILE, TILE, TILE);
ctx.fillStyle = b.top;
ctx.fillRect(x * TILE, y * TILE, TILE, 5);
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.strokeRect(x * TILE, y * TILE, TILE, TILE);
}
}
// Cursor reach highlight
const tx = Math.floor((mouse.x + camX) / TILE);
const ty = Math.floor((mouse.y + camY) / TILE);
const dist = Math.hypot((tx + 0.5) * TILE - (player.x + 10), (ty + 0.5) * TILE - (player.y + 21));
if (dist <= TILE * 5.5) {
ctx.strokeStyle = '#facc15';
ctx.lineWidth = 2;
ctx.strokeRect(tx * TILE + 1, ty * TILE + 1, TILE - 2, TILE - 2);
}
// Player character
ctx.fillStyle = '#38bdf8';
ctx.fillRect(player.x, player.y + 12, player.w, player.h - 12);
ctx.fillStyle = '#fde047';
ctx.fillRect(player.x + 2, player.y, player.w - 4, 14);
particles.forEach(p => {
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, 5, 5);
});
ctx.restore();
// Hotbar UI
const hbW = hotbar.length * 56;
const hbX = (canvas.width - hbW) / 2;
const hbY = canvas.height - 62;
hotbar.forEach((slot, idx) => {
const x = hbX + idx * 56;
ctx.fillStyle = idx === selectedSlot ? '#1e293b' : 'rgba(15, 23, 42, 0.85)';
ctx.strokeStyle = idx === selectedSlot ? '#facc15' : '#475569';
ctx.lineWidth = idx === selectedSlot ? 3 : 1.5;
ctx.beginPath();
ctx.roundRect(x, hbY, 50, 50, 8);
ctx.fill();
ctx.stroke();
const b = BLOCKS[slot.id];
ctx.fillStyle = b.color;
ctx.fillRect(x + 13, hbY + 10, 24, 24);
ctx.fillStyle = '#f8fafc';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(slot.count, x + 45, hbY + 45);
ctx.textAlign = 'left';
ctx.fillStyle = '#94a3b8';
ctx.fillText(idx + 1, x + 5, hbY + 13);
});
requestAnimationFrame(() => {
update();
draw();
});
}
generateWorld();
draw();
</script>
</body>
</html>
How to Debug Without Losing Your Vibe
Block sandboxes have three classic grid and camera pitfalls. Use these vibe debugging prompts to solve them fast:
Block Browser Context Menu
So right-clicking places blocks cleanly without popping up the browser menu, prompt: "Add canvas.addEventListener('contextmenu', e => e.preventDefault()) and support Shift+Click for trackpad users."
Prevent Self-Burial Placement
Stop players from placing a stone block directly on top of their own character by telling the AI: "Check AABB overlap between the target tile and the player's hitbox before placing any solid block."
Viewport Frustum Culling
Keep large worlds running at 60fps by instructing your AI coding partner: "Only loop over tile indices visible inside the camera bounds (startX..endX, startY..endY) during draw()."
The Vibe Prompt Vault
Copy-paste prompts to add a Crafting Menu, Torch Lighting, or a 3D Three.js First-Person Voxel Engine.
🛠️ Prompt: Add a Crafting Bench Overlay & Pickaxe Mining Speeds
"Extend my HTML5 Canvas block sandbox in index.html with a Crafting menu toggled by pressing 'E'. Let the player craft Wood Planks (from Wood), Torches (from Wood + Coal), and a Gold Pickaxe (from Wood + Gold Ore) that doubles mining reach and spawns golden spark particles."
🕯️ Prompt: Add Underground Cave Darkness & Placeable Glowing Torches
"Add a tile lighting system to my 2D Minecraft sandbox. Darken underground tiles below surface depth unless they are within a 6-tile radius of a placed Torch block or the player's lantern, and animate a warm flickering radial glow around every torch."
🧊 Prompt: Build a 3D First-Person Voxel World with Three.js CDN
"Create a complete single-file 3D Minecraft-like voxel game in index.html using Three.js from CDN. Generate a 24x24 chunk of procedural grass, dirt, stone, and tree cubes using InstancedMesh, add PointerLock first-person WASD + Space jump physics, and use Raycaster so Left-Click removes the targeted cube and Right-Click places a new cube on the hit face."