文件
BotStore/test/autoBoat.test.js
lyizai a12a24c716 feat: 用 Node.js + mineflayer 重构 AutoStore,UI 改为网页控制台并适配 Grim 反作弊
由 Fabric 客户端模组 AutoStore v1.3.1 重构为独立 Node.js 机器人,无需游戏客户端。

核心实现:
- 领土数据:Dynmap markers.json 拉取解析(奇偶填充识别国中飞地)、SimpCore 开放 API 第四数据源、
  带缓存击穿的时间戳请求与内容未变化跳过解析
- 扩地规划:四种策略(方形外扩 / 区域 S 形扫描 / 玩家周围 / 绕国一圈)逐行对照移植,
  接壤判定只算己方领土,飞地优先填补
- 扩地状态机:规划→寻路→等待开垦→成功/失败→下一目标→完成,含超时重试、
  往边上走两步自救、连续超时回城、耐久保护回城
- 生存辅助:自动进食、自动攻击(豁免中立生物)、自动游泳防溺水、自动用船(默认关闭)
- 寻路:Baritone → mineflayer-pathfinder,高度偏好用包装 Goal 等价移植

Grim 反作弊合规层(新增,核心差异点):
- 旋转不超前:接管 bot.look 按 ≤8.6°/tick 推进朝向,消除 mineflayer
  「本地朝向瞬间跳变、数据包滞后补发」导致的 Prediction 分歧
- 攻击三重校验:朝向已同步 + 距离 ≤3.0 格 + 视线可达 + 攻击冷却 + 非骑乘/进食
- 动作互斥:拦下疾跑+潜行、疾跑+使用物品等原版不可能出现的组合
- 物理参数只读自检 + 寻路约束(禁挖/禁放/禁跑酷/禁搭塔/限下落/绕开水域)

网页控制台(新增):
- 状态面板、Canvas 领土地图(缩放/平移/图层)、分组配置表单、实时日志流
- REST + WebSocket 接口,支持 WEB_TOKEN 令牌鉴权

修复:
- containsFuzzy 只取固定长度前缀的末行编辑距离,导致完全相同的子串也匹配不上
- 占领去重依赖 Set.add 返回值(JS 中恒为真),导致同一区块被重复计数
- 非法枚举值不再钳制到最近边界,改为回退默认值

测试:154 个用例,npm test 全绿;npm run check 语法自检通过。
2026-09-27 17:17:41 +08:00

119 行
4.4 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert');
const { Vec3 } = require('vec3');
const { scanWaterAhead, isWaterName, BoatState } = require('../src/bot/autoBoat');
const { FakeBot } = require('./helpers/fakeBot');
/**
* 构造一个「前方全是水」的假世界。
* @param {FakeBot} bot
* @param {(x: number, z: number) => boolean} isWater
*/
function fillWorld(bot, isWater, y) {
const level = y === undefined ? 63 : y;
for (let x = -40; x <= 40; x++) {
for (let z = -40; z <= 40; z++) {
for (const dy of [-1, 0]) {
bot.blockMap.set(`${x},${level + dy},${z}`, {
name: isWater(x, z) ? 'water' : 'stone',
boundingBox: 'block'
});
}
}
}
}
test('isWaterName 识别水与气泡柱', () => {
assert.strictEqual(isWaterName('water'), true);
assert.strictEqual(isWaterName('flowing_water'), true);
assert.strictEqual(isWaterName('bubble_column'), true);
assert.strictEqual(isWaterName('stone'), false);
assert.strictEqual(isWaterName(''), false);
assert.strictEqual(isWaterName(null), false);
});
test('前方开阔水域达到长度与宽度阈值时建议用船', () => {
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
// x 从 1 到 30 全是水,其余为石头
fillWorld(bot, (x) => x >= 1 && x <= 30);
const scan = scanWaterAhead(bot, 1, 0, 6, 4);
assert.strictEqual(scan.ok, true, scan.reason);
assert.ok(scan.length >= 6);
assert.ok(scan.width >= 4);
});
test('水域长度不足时不建议用船', () => {
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
// 只有 3 格宽的水沟
fillWorld(bot, (x) => x >= 1 && x <= 3);
const scan = scanWaterAhead(bot, 1, 0, 6, 1);
assert.strictEqual(scan.ok, false);
assert.ok(scan.reason.includes('长度'));
});
test('水面宽度不足时不建议用船', () => {
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
// 一条沿 x 方向的长水道,但 z 方向只有 1 格宽
fillWorld(bot, (x, z) => x >= 1 && x <= 30 && z === 0);
const scan = scanWaterAhead(bot, 1, 0, 6, 4);
assert.strictEqual(scan.ok, false);
assert.ok(scan.reason.includes('宽度'));
});
test('前方完全没有水时不建议用船', () => {
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
fillWorld(bot, () => false);
const scan = scanWaterAhead(bot, 1, 0, 6, 4);
assert.strictEqual(scan.ok, false);
assert.strictEqual(scan.length, 0);
});
test('用船状态机初始为空闲且描述为空', () => {
const bot = new FakeBot();
const { AutoBoatController } = require('../src/bot/autoBoat');
const { defaultConfig } = require('../src/config/expansionConfig');
const boat = new AutoBoatController(bot, defaultConfig(), null, null);
assert.strictEqual(boat.isActive(), false);
assert.strictEqual(boat.describe(), null);
assert.strictEqual(boat.state, BoatState.IDLE);
});
test('autoBoat 关闭时 tick 不会进入流程', () => {
const bot = new FakeBot();
const { AutoBoatController } = require('../src/bot/autoBoat');
const { defaultConfig } = require('../src/config/expansionConfig');
const config = defaultConfig();
config.autoBoat = false;
const boat = new AutoBoatController(bot, config, null, null);
boat.tick({ running: true, canStart: true, target: { x: 1, z: 1 } });
assert.strictEqual(boat.isActive(), false);
});
test('waterMode 非 boat 时 tick 不会进入流程(即使 autoBoat 打开)', () => {
const bot = new FakeBot();
const { AutoBoatController } = require('../src/bot/autoBoat');
const { defaultConfig } = require('../src/config/expansionConfig');
const config = defaultConfig();
config.autoBoat = true;
config.waterMode = 'avoid';
const boat = new AutoBoatController(bot, config, null, null);
boat.tick({ running: true, canStart: true, target: { x: 1, z: 1 } });
assert.strictEqual(boat.isActive(), false);
});
test('背包里没有船时不会启动用船流程', () => {
const bot = new FakeBot({ x: 0, y: 64, z: 0, slots: [] });
fillWorld(bot, (x) => x >= 1 && x <= 30);
const { AutoBoatController } = require('../src/bot/autoBoat');
const { defaultConfig } = require('../src/config/expansionConfig');
const config = defaultConfig();
config.autoBoat = true;
config.waterMode = 'boat';
const boat = new AutoBoatController(bot, config, null, null);
boat.tick({ running: true, canStart: true, target: { x: 2, z: 0 } });
assert.strictEqual(boat.isActive(), false);
});