This guide explains how to make Gemini generate simple builds and apply them in Minecraft.
We need to get Gemini to output structured JSON with block placements.
Example Prompt:
You are a Minecraft building assistant. The user wants you to build: [description]
Current context (selected blocks):
- Block at (10, 64, 20): minecraft:stone
- Block at (11, 64, 20): minecraft:air
Output your response as JSON in this exact format:
{
"actions": [
{
"type": "place",
"x": 10,
"y": 64,
"z": 20,
"block": "minecraft:redstone_wire",
"state": {}
},
{
"type": "place",
"x": 11,
"y": 64,
"z": 20,
"block": "minecraft:repeater",
"state": {
"facing": "north",
"delay": 2
}
},
{
"type": "remove",
"x": 12,
"y": 64,
"z": 20
}
],
"explanation": "I'm placing a redstone wire and repeater to create a 2-tick delay circuit."
}
IMPORTANT:
- Only output valid JSON, no markdown code blocks
- Use absolute coordinates
- Block names must be valid Minecraft block IDs (e.g., "minecraft:redstone_wire")
- For state properties, use Minecraft NBT format
- Validate all placements before suggesting
Backend (Python) - Parse Gemini Response:
import json
import re
from typing import List, Dict, Optional
def parse_ai_response(response: str) -> Optional[Dict]:
"""
Parse Gemini's JSON response.
Handles cases where response might have markdown code blocks.
"""
# Remove markdown code blocks if present
response = response.strip()
if response.startswith("```json"):
response = response[7:] # Remove ```json
if response.startswith("```"):
response = response[3:] # Remove ```
if response.endswith("```"):
response = response[:-3] # Remove closing ```
response = response.strip()
try:
data = json.loads(response)
return data
except json.JSONDecodeError as e:
# Try to extract JSON from text
json_match = re.search(r'\{.*\}', response, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group())
except:
pass
raise ValueError(f"Failed to parse JSON: {e}")
def validate_actions(actions: List[Dict], world_bounds: Dict) -> List[Dict]:
"""
Validate block placement actions.
- Check coordinates are within world bounds
- Validate block names
- Check for conflicts
"""
valid_actions = []
for action in actions:
# Validate action type
if action.get("type") not in ["place", "remove", "modify"]:
continue
# Validate coordinates
x, y, z = action.get("x"), action.get("y"), action.get("z")
if not all(isinstance(coord, (int, float)) for coord in [x, y, z]):
continue
# Check world bounds (example)
if not (world_bounds["min_x"] <= x <= world_bounds["max_x"]):
continue
# ... similar for y, z
# Validate block name
if action.get("type") == "place":
block_name = action.get("block", "")
if not block_name.startswith("minecraft:"):
continue
valid_actions.append(action)
return valid_actionsBackend API Endpoint:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class BuildRequest(BaseModel):
description: str
context_blocks: List[Dict] # Selected blocks from player
world_bounds: Dict # Optional: bounds of build area
@app.post("/api/ai/build")
async def generate_build(request: BuildRequest):
"""
Generate build instructions from AI.
"""
# Format prompt with context
prompt = format_build_prompt(
description=request.description,
context_blocks=request.context_blocks
)
# Query Gemini
response = await gemini_client.generate(prompt)
# Parse response
try:
parsed = parse_ai_response(response)
actions = parsed.get("actions", [])
explanation = parsed.get("explanation", "")
except Exception as e:
raise HTTPException(400, f"Failed to parse AI response: {e}")
# Validate actions
valid_actions = validate_actions(actions, request.world_bounds)
return {
"actions": valid_actions,
"explanation": explanation,
"total_blocks": len(valid_actions)
}Mod (Java) - Apply Block Changes:
package com.blockbase;
import net.minecraft.block.BlockState;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.registry.Registry;
public class BuildApplier {
public static class BlockAction {
public String type; // "place", "remove", "modify"
public int x, y, z;
public String block; // e.g., "minecraft:redstone_wire"
public Map<String, Object> state; // Block state properties
}
/**
* Apply AI-generated build actions to the world.
*/
public static void applyBuild(ServerWorld world, List<BlockAction> actions) {
for (BlockAction action : actions) {
BlockPos pos = new BlockPos(action.x, action.y, action.z);
switch (action.type) {
case "place":
placeBlock(world, pos, action.block, action.state);
break;
case "remove":
removeBlock(world, pos);
break;
case "modify":
modifyBlock(world, pos, action.block, action.state);
break;
}
}
}
private static void placeBlock(ServerWorld world, BlockPos pos,
String blockId, Map<String, Object> state) {
// Parse block ID
Block block = Registry.BLOCK.get(new Identifier(blockId));
if (block == null) {
System.err.println("Invalid block: " + blockId);
return;
}
// Get default block state
BlockState blockState = block.getDefaultState();
// Apply state properties (e.g., facing, delay for repeaters)
if (state != null) {
blockState = applyBlockState(blockState, state);
}
// Place block
world.setBlockState(pos, blockState);
}
private static BlockState applyBlockState(BlockState state,
Map<String, Object> properties) {
// Apply properties like facing, delay, etc.
// This is Minecraft-specific - need to handle each block type
for (Map.Entry<String, Object> entry : properties.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// Example: Handle repeater delay
if (key.equals("delay") && state.getBlock() instanceof RepeaterBlock) {
int delay = (int) value;
state = state.with(RepeaterBlock.DELAY, Math.max(1, Math.min(4, delay)));
}
// Example: Handle facing direction
if (key.equals("facing") && state.contains(Properties.HORIZONTAL_FACING)) {
Direction dir = Direction.byName(value.toString());
if (dir != null) {
state = state.with(Properties.HORIZONTAL_FACING, dir);
}
}
// Add more property handlers as needed
}
return state;
}
private static void removeBlock(ServerWorld world, BlockPos pos) {
world.setBlockState(pos, Blocks.AIR.getDefaultState());
}
}Mod Command:
public class BlockbaseCommands {
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(CommandManager.literal("blockbase")
.then(CommandManager.literal("ai")
.then(CommandManager.literal("build")
.then(CommandManager.argument("description", StringArgumentType.greedyString())
.executes(context -> {
String description = StringArgumentType.getString(context, "description");
return executeBuild(context.getSource(), description);
})
)
)
)
);
}
private static int executeBuild(ServerCommandSource source, String description) {
// Get selected blocks (from AiContextManager)
List<BlockData> contextBlocks = AiContextManager.getSelectedBlocks(source.getPlayer());
// Call backend API
BuildResponse response = ApiClient.generateBuild(description, contextBlocks);
if (response.actions.isEmpty()) {
source.sendFeedback(new LiteralText("AI couldn't generate a build."), false);
return 0;
}
// Ask for confirmation
source.sendFeedback(new LiteralText(
String.format("AI wants to place %d blocks. Type '/blockbase ai build confirm' to apply.",
response.actions.size())
), false);
// Store pending build
PendingBuildManager.setPendingBuild(source.getPlayer(), response);
return 1;
}
private static int confirmBuild(ServerCommandSource source) {
BuildResponse build = PendingBuildManager.getPendingBuild(source.getPlayer());
if (build == null) {
source.sendFeedback(new LiteralText("No pending build."), false);
return 0;
}
// Apply build
ServerWorld world = source.getWorld();
BuildApplier.applyBuild(world, build.actions);
source.sendFeedback(new LiteralText(
String.format("Applied build: %s", build.explanation)
), false);
return 1;
}
}User Command:
/blockbase ai build "place a 3x3 square of redstone wire"
Gemini Response (JSON):
{
"actions": [
{"type": "place", "x": 10, "y": 64, "z": 20, "block": "minecraft:redstone_wire"},
{"type": "place", "x": 11, "y": 64, "z": 20, "block": "minecraft:redstone_wire"},
{"type": "place", "x": 12, "y": 64, "z": 20, "block": "minecraft:redstone_wire"},
{"type": "place", "x": 10, "y": 64, "z": 21, "block": "minecraft:redstone_wire"},
{"type": "place", "x": 11, "y": 64, "z": 21, "block": "minecraft:redstone_wire"},
{"type": "place", "x": 12, "y": 64, "z": 21, "block": "minecraft:redstone_wire"},
{"type": "place", "x": 10, "y": 64, "z": 22, "block": "minecraft:redstone_wire"},
{"type": "place", "x": 11, "y": 64, "z": 22, "block": "minecraft:redstone_wire"},
{"type": "place", "x": 12, "y": 64, "z": 22, "block": "minecraft:redstone_wire"}
],
"explanation": "Placed a 3x3 grid of redstone wire starting at (10, 64, 20)"
}Result:
- Backend parses JSON
- Validates actions
- Sends to mod
- Mod applies block placements
- User sees 3x3 redstone wire grid
- Gemini doesn't know player's exact position
- Solution: Use relative coordinates or player's position as origin
- Better: Let user select a "build origin" block
- Different blocks have different state properties
- Solution: Start with simple blocks (redstone wire, basic blocks)
- Expand to complex blocks (repeaters, comparators) later
- Need to check if placement is valid (not in air, etc.)
- Solution: Validate in backend before sending to mod
- Mod should also validate before placing
- What if Gemini outputs invalid JSON?
- What if block doesn't exist?
- Solution: Try-catch, fallback to text explanation
- Start with:
redstone_wire,stone,dirt,wood,glass - No complex state properties
- Just place/remove blocks
- Support
facingfor directional blocks - Support
delayfor repeaters - Support basic properties
- Multi-block structures
- Redstone circuits
- Timing considerations
-
Confirmation Required
- Always ask user to confirm before applying
- Show preview of what will be placed
-
Bounds Checking
- Limit build area (e.g., 50x50x50 blocks)
- Don't allow building outside bounds
-
Undo Capability
- Store original blocks before applying
- Allow
/blockbase ai build undo
-
Validation
- Check block names are valid
- Check coordinates are reasonable
- Don't place blocks in invalid positions
- Simple Test: "Place 5 redstone wire blocks in a line"
- Medium Test: "Create a 2x2 square of stone"
- Complex Test: "Build a simple redstone clock"
Start simple, expand gradually!