Recover enclosed Creative builders onto verified open surface sites
Browse files- README.md +6 -0
- tests/test_voyager_navigation.js +27 -0
- voyager_place.js +84 -1
README.md
CHANGED
|
@@ -116,6 +116,12 @@ Os logs `[llm_metric]` registram bot, papel, latência e tokens reais por chamad
|
|
| 116 |
de Curriculum, Action, Critic e Skill Manager. No Gemini CLI, a temperatura zero
|
| 117 |
é aplicada por `modelConfigs.customOverrides`, além do cliente Voyager.
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
- CLI fixado em `@google/gemini-cli@0.61.0`, modelo efetivo `gemini-3.5-flash-lite`.
|
| 120 |
- Segredos privados do Space: `GEMINI_BOT_KEY_1` a `GEMINI_BOT_KEY_6`.
|
| 121 |
- Nenhuma chave deve ser salva no Git, no checkpoint ou nas saídas de status.
|
|
|
|
| 116 |
de Curriculum, Action, Critic e Skill Manager. No Gemini CLI, a temperatura zero
|
| 117 |
é aplicada por `modelConfigs.customOverrides`, além do cliente Voyager.
|
| 118 |
|
| 119 |
+
Se as construções cercarem o bot, o helper procura uma área de superfície já
|
| 120 |
+
carregada e livre, a 8–12 blocos por eixo. O voo Creative de recuperação verifica
|
| 121 |
+
o volume do jogador em toda a rota, usa segmentos de até 0,4 bloco, sobe no máximo
|
| 122 |
+
4 blocos para ultrapassar a construção e pousa em solo confirmado. Não destrói
|
| 123 |
+
blocos, não teleporta e não altera a exploração horizontal do Curriculum.
|
| 124 |
+
|
| 125 |
- CLI fixado em `@google/gemini-cli@0.61.0`, modelo efetivo `gemini-3.5-flash-lite`.
|
| 126 |
- Segredos privados do Space: `GEMINI_BOT_KEY_1` a `GEMINI_BOT_KEY_6`.
|
| 127 |
- Nenhuma chave deve ser salva no Git, no checkpoint ou nas saídas de status.
|
tests/test_voyager_navigation.js
CHANGED
|
@@ -61,6 +61,33 @@ vm.runInContext(fs.readFileSync(path.join(__dirname,'../voyager_place.js'),'utf8
|
|
| 61 |
assert.equal(s.bot.blockAt(new Vec3(1,64,0)).name, 'snow_block');
|
| 62 |
console.log('PASS pinned GoalPlaceBlock raycast, blocked-sector recovery and 4 real simulated world updates');
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
// The old 1.3.1 liquid constants are undefined on MC 1.19. Compare actual
|
| 65 |
// physics integration, not a string/version assertion alone.
|
| 66 |
const liquidWorld = {getBlock(p) {
|
|
|
|
| 61 |
assert.equal(s.bot.blockAt(new Vec3(1,64,0)).name, 'snow_block');
|
| 62 |
console.log('PASS pinned GoalPlaceBlock raycast, blocked-sector recovery and 4 real simulated world updates');
|
| 63 |
|
| 64 |
+
const enclosed = () => {
|
| 65 |
+
const f=fixture();
|
| 66 |
+
for(let y=64;y<=65;y++) for(let x=-2;x<=2;x++) for(let z=-2;z<=2;z++) {
|
| 67 |
+
if(Math.max(Math.abs(x),Math.abs(z))===2) f.blocks.set(new Vec3(x,y,z).toString(),'stone');
|
| 68 |
+
}
|
| 69 |
+
f.bot.physics={gravity:.08}; f.bot.clearControlStates=()=>{};
|
| 70 |
+
f.bot.creative={startFlying:()=>{f.bot.physics.gravity=0;},stopFlying:()=>{f.bot.physics.gravity=.08;},
|
| 71 |
+
flyTo:async p=>{assert(f.bot.entity.position.distanceTo(p)<=.401);f.bot.entity.position=p.clone();}};
|
| 72 |
+
return f;
|
| 73 |
+
};
|
| 74 |
+
const boxed=enclosed(), original=new Map(boxed.blocks);
|
| 75 |
+
await context.placeBlocks(boxed.bot,'snow_block',3);
|
| 76 |
+
assert.equal(boxed.bot.entity.position.y,64);
|
| 77 |
+
assert(boxed.bot.entity.position.x>=8);
|
| 78 |
+
assert.equal(boxed.bot.physics.gravity,.08);
|
| 79 |
+
for(const [key,value] of original) assert.equal(boxed.blocks.get(key),value);
|
| 80 |
+
assert.equal(boxed.bot._voyagerVerifiedActions.length,3);
|
| 81 |
+
const interrupted=enclosed();
|
| 82 |
+
interrupted.bot.creative.flyTo=async()=>{throw Error('disconnected');};
|
| 83 |
+
await assert.rejects(context.moveToOpenPlacementSite(interrupted.bot),/disconnected/);
|
| 84 |
+
assert.equal(interrupted.bot.physics.gravity,.08);
|
| 85 |
+
const unloaded=enclosed(), getBlock=unloaded.bot.blockAt;
|
| 86 |
+
unloaded.bot.blockAt=p=>Math.abs(p.x)>5 || Math.abs(p.z)>5 ? null : getBlock(p);
|
| 87 |
+
await assert.rejects(context.moveToOpenPlacementSite(unloaded.bot),/No known clear route/);
|
| 88 |
+
assert.equal(unloaded.bot.entity.position.y,64);
|
| 89 |
+
console.log('PASS enclosed Creative recovery, preserved builds, surface landing, unknown-chunk refusal and gravity cleanup');
|
| 90 |
+
|
| 91 |
// The old 1.3.1 liquid constants are undefined on MC 1.19. Compare actual
|
| 92 |
// physics integration, not a string/version assertion alone.
|
| 93 |
const liquidWorld = {getBlock(p) {
|
voyager_place.js
CHANGED
|
@@ -39,7 +39,82 @@ async function placeItem(bot, name, position) {
|
|
| 39 |
bot.save(`${name}_placed`);
|
| 40 |
}
|
| 41 |
|
| 42 |
-
async function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
if (!Number.isInteger(count) || count < 1 || count > 4) throw new Error("Place 1..4 blocks per task");
|
| 44 |
const origin = bot.entity.position.floored();
|
| 45 |
let placed = 0, attempts = 0, lastError = "No supported empty cells nearby";
|
|
@@ -65,6 +140,10 @@ async function placeBlocks(bot, name, count = 1) {
|
|
| 65 |
distance: bot.entity.position.distanceTo(position),
|
| 66 |
}));
|
| 67 |
supported.sort((a,b) => Number(b.visible) - Number(a.visible) || a.distance - b.distance);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
for (const {position} of supported) {
|
| 69 |
try {
|
| 70 |
await placeItem(bot,name,position);
|
|
@@ -79,5 +158,9 @@ async function placeBlocks(bot, name, count = 1) {
|
|
| 79 |
}
|
| 80 |
if (++attempts >= 24) break;
|
| 81 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
throw new Error(`Placed ${placed}/${count} ${name}: ${lastError}; supported=${supported.length}, visible=${supported.filter(c=>c.visible).length}, position=${bot.entity.position}`);
|
| 83 |
}
|
|
|
|
| 39 |
bot.save(`${name}_placed`);
|
| 40 |
}
|
| 41 |
|
| 42 |
+
async function moveToOpenPlacementSite(bot) {
|
| 43 |
+
// Construction can enclose a Creative bot. Recover over known blocks,
|
| 44 |
+
// without digging the build, teleporting, or exploring underground.
|
| 45 |
+
const start = bot.entity.position.clone();
|
| 46 |
+
const clear = (p) => {
|
| 47 |
+
if (![p.x,p.y,p.z].every(Number.isFinite)) return false;
|
| 48 |
+
for (let x=Math.floor(p.x-.3);x<=Math.floor(p.x+.3);x++) {
|
| 49 |
+
for (let y=Math.floor(p.y+.001);y<=Math.floor(p.y+1.799);y++) {
|
| 50 |
+
for (let z=Math.floor(p.z-.3);z<=Math.floor(p.z+.3);z++) {
|
| 51 |
+
const b=bot.blockAt(new Vec3(x,y,z));
|
| 52 |
+
if (!b || b.boundingBox !== 'empty' || ['water','lava'].includes(b.name)) return false;
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
return true;
|
| 57 |
+
};
|
| 58 |
+
const segment = (from,to) => {
|
| 59 |
+
const n=Math.max(1,Math.ceil(from.distanceTo(to)/.4)), points=[];
|
| 60 |
+
for(let i=1;i<=n;i++) {
|
| 61 |
+
const p=from.plus(to.minus(from).scaled(i/n));
|
| 62 |
+
if(!clear(p)) return null;
|
| 63 |
+
points.push(p);
|
| 64 |
+
}
|
| 65 |
+
return points;
|
| 66 |
+
};
|
| 67 |
+
const openSite = (p) => {
|
| 68 |
+
for(let dx=-2;dx<=2;dx++) for(let dz=-2;dz<=2;dz++) {
|
| 69 |
+
const q=p.offset(dx,0,dz), support=bot.blockAt(q.offset(0,-.01,0));
|
| 70 |
+
if(!support || support.boundingBox !== 'block' || !clear(q)) return false;
|
| 71 |
+
}
|
| 72 |
+
return true;
|
| 73 |
+
};
|
| 74 |
+
let route, landing;
|
| 75 |
+
for(const radius of [8,12]) {
|
| 76 |
+
for(const [dx,dz] of [[1,0],[0,1],[-1,0],[0,-1],[1,1],[-1,1],[-1,-1],[1,-1]]) {
|
| 77 |
+
for(let y=Math.floor(start.y);y>=Math.max(64,Math.floor(start.y)-2);y--) {
|
| 78 |
+
const target=new Vec3(Math.floor(start.x)+dx*radius+.5,y,Math.floor(start.z)+dz*radius+.5);
|
| 79 |
+
if(!openSite(target)) continue;
|
| 80 |
+
for(const rise of [2,3,4]) {
|
| 81 |
+
const up=new Vec3(start.x,Math.floor(start.y)+rise,start.z);
|
| 82 |
+
const across=new Vec3(target.x,up.y,target.z);
|
| 83 |
+
const a=segment(start,up), b=a && segment(up,across), c=b && segment(across,target);
|
| 84 |
+
if(c) {route=[...a,...b,...c];landing=target;break;}
|
| 85 |
+
}
|
| 86 |
+
if(route) break;
|
| 87 |
+
}
|
| 88 |
+
if(route) break;
|
| 89 |
+
}
|
| 90 |
+
if(route) break;
|
| 91 |
+
}
|
| 92 |
+
if(!route) throw new Error('No known clear route to an open surface construction site');
|
| 93 |
+
bot.pathfinder.setGoal(null);
|
| 94 |
+
bot.clearControlStates();
|
| 95 |
+
const deadline=Date.now()+20000;
|
| 96 |
+
bot.creative.startFlying();
|
| 97 |
+
try {
|
| 98 |
+
for(const p of route) {
|
| 99 |
+
if(Date.now()>deadline || !clear(p) || bot.entity.position.distanceTo(p)>.6) {
|
| 100 |
+
throw new Error('Construction recovery route changed; stopping flight');
|
| 101 |
+
}
|
| 102 |
+
// The documented Creative API has no collision avoidance: every
|
| 103 |
+
// segment is prechecked and shorter than its 0.5-block inner loop.
|
| 104 |
+
await bot.creative.flyTo(p);
|
| 105 |
+
}
|
| 106 |
+
} finally {
|
| 107 |
+
bot.creative.stopFlying();
|
| 108 |
+
bot.clearControlStates();
|
| 109 |
+
}
|
| 110 |
+
await bot.waitForTicks(4);
|
| 111 |
+
if(bot.entity.position.distanceTo(landing)>1 || !openSite(landing)) {
|
| 112 |
+
throw new Error('Construction recovery did not reach the verified surface site');
|
| 113 |
+
}
|
| 114 |
+
console.info(`[placement recovery] surface ${start} -> ${bot.entity.position}`);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
async function placeBlocks(bot, name, count = 1, relocated = false) {
|
| 118 |
if (!Number.isInteger(count) || count < 1 || count > 4) throw new Error("Place 1..4 blocks per task");
|
| 119 |
const origin = bot.entity.position.floored();
|
| 120 |
let placed = 0, attempts = 0, lastError = "No supported empty cells nearby";
|
|
|
|
| 140 |
distance: bot.entity.position.distanceTo(position),
|
| 141 |
}));
|
| 142 |
supported.sort((a,b) => Number(b.visible) - Number(a.visible) || a.distance - b.distance);
|
| 143 |
+
if (!relocated && !supported.some(c=>c.visible) && bot.game.gameMode === 'creative') {
|
| 144 |
+
await moveToOpenPlacementSite(bot);
|
| 145 |
+
return placeBlocks(bot,name,count,true);
|
| 146 |
+
}
|
| 147 |
for (const {position} of supported) {
|
| 148 |
try {
|
| 149 |
await placeItem(bot,name,position);
|
|
|
|
| 158 |
}
|
| 159 |
if (++attempts >= 24) break;
|
| 160 |
}
|
| 161 |
+
if(!relocated && bot.game.gameMode === 'creative') {
|
| 162 |
+
await moveToOpenPlacementSite(bot);
|
| 163 |
+
return placeBlocks(bot,name,count-placed,true);
|
| 164 |
+
}
|
| 165 |
throw new Error(`Placed ${placed}/${count} ${name}: ${lastError}; supported=${supported.length}, visible=${supported.filter(c=>c.visible).length}, position=${bot.entity.position}`);
|
| 166 |
}
|