SAwww / bot-model.js
SAyyeedd's picture
Upload 30 files
309ec79 verified
Raw
History Blame Contribute Delete
31.6 kB
'use strict';
const fs = require('fs');
const path = require('path');
const ts = () => new Date().toISOString().replace('T', ' ').slice(0, 19);
const log = (tag, msg) => console.log(`[${ts()}] [AI] ${tag} ${msg}`);
const FEATURES = [
// أساسيات (5)
'health','food','y_level','xp_level','time_of_day',
// البيئة (7)
'biome','dimension','light_level','is_raining','can_see_sky',
'is_in_water','is_on_lava',
// البلوكات حوله (5)
'block_below','block_above','is_on_ladder','is_in_shelter',
'is_in_danger',
// الموبات والأغراض (3)
'mobs_nearby','danger_mobs','items_nearby',
// الاحتياطي (6)
'inventory_pct','has_weapon','has_pickaxe','has_axe',
'has_shield','has_bow',
// الدروع والطعام (5)
'has_chestplate','has_boots','has_food','has_bed','has_torches',
// الموارد (5)
'has_logs','has_stone','has_iron_raw','has_diamond_raw','has_building_blocks',
// حالة + اجتماعي (4)
'player_nearby','bot_count','is_sleeping','is_sprinting',
// اتجاهات البلوكات (8)
'block_north','block_south','block_east','block_west',
'block_north2','block_south2','block_east2','block_west2',
// عدادات الموارد (8)
'count_logs','count_stone','count_iron','count_gold','count_diamond','count_coal','count_food_items','count_torches',
// مستويات الأدوات (4)
'pickaxe_level','axe_level','sword_level','shovel_level',
// دروع (2)
'total_armor','has_extra_shield',
// تضاريس (2)
'terrain_flatness','elevation_change',
// قرب ماء/حمم (2)
'near_water','near_lava',
// وقت منذ الضرر (2)
'time_since_hurt','is_thundering',
// بورتال/ستراكتشر (3)
'portal_nearby','structure_nearby','y_relative_surface',
// أدوات الإند (4)
'has_obsidian','has_flint_steel','has_ender_pearl','has_blaze_rod',
// حيوانات وقرويون (2)
'animals_nearby','villagers_nearby',
// المساحة (3)
'explored_radius','trees_nearby','flowers_nearby',
];
const ACTIONS = [
// أساسيات (25)
'WANDER','EXPLORE','COLLECT','MINE','CRAFT','BUILD_SHELTER','LIGHT_AREA','FIGHT','FLEE',
'EAT','SLEEP','SWIM','CHAT','EMOTE','FOLLOW','HELP_ALLY','TRADE','SMELT','EQUIP_ARMOR',
'GO_HOME','FARM','IDLE','JUMP','LOOK_AROUND','SPRINT',
// بناء (5)
'BUILD_PATH','BUILD_BRIDGE','BUILD_WALL','PLACE_DOOR','PLACE_LADDER',
// تعدين متقدم (4)
'MINE_IRON','MINE_GOLD','MINE_DIAMOND','MINE_OBSIDIAN',
// كرافت متقدم (5)
'CRAFT_ENCHANT_TABLE','CRAFT_ANVIL','CRAFT_BREWING_STAND','CRAFT_BED','CRAFT_BOAT',
// نذر (3)
'ENTER_NETHER','EXPLORE_NETHER','FIGHT_BLAZE',
// إند (2)
'ENTER_END','FIGHT_DRAGON',
// زراعة (5)
'PLANT_CROP','HARVEST_CROP','FISH','SHEAR','MILK',
// قرية (1)
'SLEEP_AT_VILLAGE',
// محادثة متقدمة (5)
'CHAT_GREETING','CHAT_STATUS','CHAT_THANK','CHAT_QUESTION','CHAT_ALERT',
// تفكير (1)
'THINK',
];
const SEQ_LEN = 50;
const INPUT_DIM = FEATURES.length;
const FEATURE_MINMAX = {
'health':[0,20],'food':[0,20],'y_level':[-64,320],'xp_level':[0,100],'time_of_day':[0,24000],
'biome':[0,14],'dimension':[0,2],'light_level':[0,15],'is_raining':[0,1],'can_see_sky':[0,1],
'is_in_water':[0,1],'is_on_lava':[0,1],
'block_below':[0,80],'block_above':[0,100],'is_on_ladder':[0,1],'is_in_shelter':[0,1],'is_in_danger':[0,1],
'mobs_nearby':[0,8],'danger_mobs':[0,5],'items_nearby':[0,10],
'inventory_pct':[0,100],'has_weapon':[0,1],'has_pickaxe':[0,1],'has_axe':[0,1],'has_shield':[0,1],'has_bow':[0,1],
'has_chestplate':[0,1],'has_boots':[0,1],'has_food':[0,1],'has_bed':[0,1],'has_torches':[0,1],
'has_logs':[0,1],'has_stone':[0,1],'has_iron_raw':[0,1],'has_diamond_raw':[0,1],'has_building_blocks':[0,1],
'player_nearby':[0,1],'bot_count':[0,4],'is_sleeping':[0,1],'is_sprinting':[0,1],
// اتجاهات البلوكات (8)
'block_north':[0,99],'block_south':[0,99],'block_east':[0,99],'block_west':[0,99],
'block_north2':[0,99],'block_south2':[0,99],'block_east2':[0,99],'block_west2':[0,99],
// عدادات الموارد (8)
'count_logs':[0,64],'count_stone':[0,64],'count_iron':[0,64],'count_gold':[0,64],
'count_diamond':[0,64],'count_coal':[0,64],'count_food_items':[0,64],'count_torches':[0,64],
// مستويات الأدوات (4)
'pickaxe_level':[0,5],'axe_level':[0,5],'sword_level':[0,5],'shovel_level':[0,5],
// دروع (2)
'total_armor':[0,20],'has_extra_shield':[0,1],
// تضاريس (2)
'terrain_flatness':[0,10],'elevation_change':[-10,10],
// قرب ماء/حمم (2)
'near_water':[0,1],'near_lava':[0,1],
// وقت منذ الضرر (2)
'time_since_hurt':[0,1000],'is_thundering':[0,1],
// بورتال/ستراكتشر (3)
'portal_nearby':[0,1],'structure_nearby':[0,5],'y_relative_surface':[-64,100],
// أدوات الإند (4)
'has_obsidian':[0,1],'has_flint_steel':[0,1],'has_ender_pearl':[0,1],'has_blaze_rod':[0,1],
// حيوانات وقرويون (2)
'animals_nearby':[0,5],'villagers_nearby':[0,3],
// المساحة (3)
'explored_radius':[0,500],'trees_nearby':[0,10],'flowers_nearby':[0,5],
};
// تصنيفات للميزات التي تحتاج معايرة
const BLOCK_TYPES = {
'stone':1,'cobblestone':2,'dirt':3,'grass_block':4,'grass':4,
'sand':5,'gravel':6,'water':7,'lava':8,'bedrock':9,
'oak_log':10,'spruce_log':10,'birch_log':10,'jungle_log':10,
'acacia_log':10,'dark_oak_log':10,'mangrove_log':10,'cherry_log':10,
'oak_planks':11,'spruce_planks':11,'birch_planks':11,
'glass':12,'glass_pane':12,'coal_ore':13,'iron_ore':14,
'gold_ore':15,'diamond_ore':16,'redstone_ore':17,'lapis_ore':18,
'air':0,'cave_air':0,'void_air':0,
// بناء إضافي
'deepslate':19,'andesite':20,'diorite':21,'granite':22,'tuff':23,
'cobbled_deepslate':24,'calcite':25,'dripstone_block':26,'moss_block':27,
'oak_leaves':28,'spruce_leaves':28,'birch_leaves':28,
'stone_bricks':29,'mossy_stone_bricks':29,'cracked_stone_bricks':29,
'bricks':30,'nether_bricks':31,'red_nether_bricks':31,
// خامات متقدمة
'netherite_block':32,'ancient_debris':33,
'copper_ore':34,'emerald_ore':35,
'obsidian':36,'crying_obsidian':36,
// بورتال وستراكتشر
'nether_portal':37,'end_portal':37,'end_gateway':37,
'spawner':38,'chest':39,'ender_chest':39,
'furnace':40,'blast_furnace':40,'smoker':40,
'crafting_table':41,'enchanting_table':42,'anvil':43,
'brewing_stand':44,'beacon':45,
// نباتات
'oak_sapling':46,'spruce_sapling':46,'wheat':47,'wheat_seeds':47,
'carrots':48,'potatoes':49,'beetroots':50,
'dandelion':51,'poppy':51,'cornflower':51,'azure_bluet':51,
'tall_grass':52,'fern':52,'dead_bush':52,
'vine':53,'lily_pad':54,'sugar_cane':55,'cactus':56,
'pumpkin':57,'melon':58,
// أضواء
'torch':59,'lantern':59,'soul_torch':59,'soul_lantern':59,
'glowstone':60,'shroomlight':60,'sea_lantern':60,
// بنايات
'ladder':61,'scaffolding':62,
'oak_door':63,'spruce_door':63,'iron_door':63,
'oak_fence':64,'spruce_fence':64,'nether_brick_fence':64,
'oak_slab':65,'spruce_slab':65,'stone_slab':65,
'oak_stairs':66,'spruce_stairs':66,'cobblestone_stairs':66,
// أرضيات
'snow_block':67,'ice':68,'blue_ice':68,'packed_ice':68,
'clay':69,'mud':70,'podzol':70,'mycelium':70,
// نذر
'netherrack':71,'soul_sand':72,'soul_soil':72,
'crimson_nylium':73,'warped_nylium':73,
'basalt':74,'blackstone':75,
'crimson_stem':76,'warped_stem':76,
'shroomlight':77,
// إند
'end_stone':78,'end_stone_bricks':78,
'purpur_block':79,'purpur_pillar':79,
'chorus_plant':80,'chorus_flower':80,
};
const BIOME_MAP = {
'plains':0,'forest':1,'dark_forest':1,'wooded_hills':1,
'desert':2,'taiga':3,'swamp':4,'ocean':5,'river':6,
'jungle':7,'savanna':8,'badlands':9,'mountains':10,
'mountain':10,'stony_peaks':10,'cave':11,'dripstone_caves':11,
'lush_caves':11,'nether_wastes':12,'soul_sand_valley':12,
'crimson_forest':12,'warped_forest':12,'the_end':13,'end':13,
};
const DIM_MAP = { 'minecraft:overworld':0, 'minecraft:the_nether':1, 'minecraft:the_end':2 };
class ContextManager {
constructor(maxLen) {
this.maxLen = maxLen || SEQ_LEN;
this.buffer = [];
}
addState(state) {
this.buffer.push({ ...state });
if (this.buffer.length > this.maxLen) this.buffer.shift();
}
getContextTensor() {
const total = this.maxLen * INPUT_DIM;
const data = new Float32Array(total);
for (let i = 0; i < this.maxLen; i++) {
const bufIdx = this.buffer.length - this.maxLen + i;
const state = bufIdx >= 0 ? this.buffer[bufIdx] : null;
for (let j = 0; j < INPUT_DIM; j++) {
const raw = state ? (state[FEATURES[j]] ?? 0) : 0;
const mm = FEATURE_MINMAX[FEATURES[j]];
data[i * INPUT_DIM + j] = mm ? (raw - mm[0]) / (mm[1] - mm[0]) : 0;
}
}
return data;
}
reset() { this.buffer = []; }
get length() { return this.buffer.length; }
}
class BotModel {
constructor() {
this.session = null;
this.inputName = 'float_input';
this.loaded = false;
this.fallbackAction = 1; // EXPLORE
this.stats = { predictions: 0, fallbacks: 0, avgTime: 0 };
this._modelPath = null;
this.context = new ContextManager(SEQ_LEN);
}
async loadFromHF(username, repoName, fileName = 'best_model.onnx') {
const repo = repoName && repoName.includes('/') ? repoName : `${username}/${repoName}`;
const url = `https://huggingface.co/${repo}/resolve/main/${fileName}`;
const localPath = path.join(__dirname, 'cache', fileName);
// Try local cache first
if (fs.existsSync(localPath)) {
const stat = fs.statSync(localPath);
if (stat.size > 50 * 1024 * 1024) {
log('💾', `وجد cache محلي (${(stat.size / 1024 / 1024).toFixed(1)} MB)`);
const ok = await this.loadLocal(localPath);
if (ok) return true;
log('🔄', 'cache تالف، إعادة تحميل...');
} else {
log('⚠️', 'cache صغير جداً (>50MB متوقع)، إعادة تحميل');
}
}
const maxAttempts = 3;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
log('⬇️', `تحميل النموذج... (محاولة ${attempt+1}/${maxAttempts})`);
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 600000);
const resp = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const contentLength = parseInt(resp.headers.get('content-length') || '0', 10);
let downloaded = 0;
const reader = resp.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
downloaded += value.length;
if (contentLength > 0) {
const pct = (downloaded / contentLength * 100).toFixed(0);
if (downloaded % (10 * 1024 * 1024) < value.length) {
log('⬇️', `${pct}% (${(downloaded / 1024 / 1024).toFixed(1)} MB)`);
}
}
}
const buffer = Buffer.concat(chunks);
if (buffer.length < 50 * 1024 * 1024) {
log('⚠️', `ملف صغير جداً (${(buffer.length / 1024 / 1024).toFixed(1)} MB) — يتلف؟`);
}
fs.mkdirSync(path.join(__dirname, 'cache'), { recursive: true });
fs.writeFileSync(localPath, buffer);
log('✅', `تم التحميل (${(buffer.length / 1024 / 1024).toFixed(1)} MB)`);
const ok = await this.loadLocal(localPath);
if (ok) return true;
try { fs.unlinkSync(localPath); } catch(_) {}
log('🔄', 'إعادة المحاولة...');
} catch (err) {
log('⚠️', `فشل تحميل النموذج: ${err.message}`);
if (attempt < maxAttempts - 1) {
const wait = (attempt + 1) * 10000;
log('⏳', `انتظار ${wait/1000}ث قبل إعادة المحاولة...`);
await new Promise(r => setTimeout(r, wait));
}
}
}
log('❌', `فشل تحميل النموذج بعد ${maxAttempts} محاولات`);
return false;
}
_softmax(logits) {
if (!logits || logits.length === 0) return [];
const max = Math.max(...logits);
const exps = logits.map(v => Math.exp(v - max));
const sum = exps.reduce((a, b) => a + b, 0);
return sum > 0 ? exps.map(v => v / sum) : exps;
}
async loadLocal(filePath) {
try {
const ort = require('onnxruntime-node');
this._modelPath = filePath;
let sessionOpts = { executionProviders: ['dml', 'cpu'], interOpNumThreads: 4, intraOpNumThreads: 4, graphOptimizationLevel: 'all' };
try {
this.session = await ort.InferenceSession.create(filePath, sessionOpts);
} catch (_) {
this.session = await ort.InferenceSession.create(filePath, { executionProviders: ['cpu'], interOpNumThreads: 4, intraOpNumThreads: 4, graphOptimizationLevel: 'all' });
}
if (this.session.inputNames) {
this.inputName = this.session.inputNames[0];
} else {
this.inputName = this.session.inputs[0].name;
}
this.loaded = true;
log('🧠', `نموذج AI جاهز (Input: ${this.inputName})`);
this.context.reset();
return true;
} catch (err) {
log('❌', `فشل تحميل النموذج المحلي: ${err.message}`);
return false;
}
}
extractState(bot) {
if (!bot || !bot.entity) return null;
const pos = bot.entity.position;
const inv = bot.inventory.items();
const entities = Object.values(bot.entities || {});
const time = bot.time ? bot.time.timeOfDay : 0;
// ─── البيئة ───
let biome = 0;
try {
const b = bot.world.biomeAt ? bot.world.biomeAt(pos) : null;
if (b) biome = biomeAt(b.id || b.name || b);
} catch (_) {}
let dimension = 0;
try { dimension = DIM_MAP[bot.game.dimension] ?? 0; } catch (_) {}
let lightLevel = 0;
try {
const block = bot.blockAt(pos);
if (block) lightLevel = block.skyLight ?? block.light ?? 0;
} catch (_) {}
const isRaining = bot.isRaining ? 1 : 0;
let canSeeSky = 1;
try {
for (let y = pos.y + 1; y < 256; y++) {
const b = bot.blockAt(pos.offset(0, y - pos.y, 0));
if (b && b.name !== 'air' && b.name !== 'cave_air' && b.name !== 'void_air') {
canSeeSky = 0; break;
}
}
} catch (_) { canSeeSky = 0; }
const isInWater = bot.entity.isInWater ? 1 : 0;
let isOnLava = 0;
try {
const below = bot.blockAt(pos.offset(0, -1, 0));
if (below && (below.name.includes('lava') || below.name.includes('fire'))) isOnLava = 1;
} catch (_) {}
const isSprinting = bot.entity.isSprinting ? 1 : 0;
// ─── البلوكات حوله ───
let blockBelow = 0, blockAbove = 0, isOnLadder = 0, isInShelter = 0;
try {
const bBelow = bot.blockAt(pos.offset(0, -1, 0));
if (bBelow) {
blockBelow = BLOCK_TYPES[bBelow.name] ?? 0;
if (bBelow.name.includes('ladder')) isOnLadder = 1;
}
const bAbove = bot.blockAt(pos.offset(0, 1, 0));
if (bAbove) blockAbove = BLOCK_TYPES[bAbove.name] ?? (['air','cave_air','void_air'].includes(bAbove.name) ? 0 : 99);
// فحص المأوى: هل هناك بلوك فوق الرأس؟
for (let y = 1; y <= 3; y++) {
const b = bot.blockAt(pos.offset(0, y, 0));
if (b && !['air','cave_air','void_air'].includes(b.name)) { isInShelter = 1; break; }
}
} catch (_) {}
// ─── الموبات والأغراض ───
let mobsNearby = 0, dangerMobs = 0, itemsNearby = 0;
for (const e of entities) {
if (!e || e === bot.entity) continue;
const d = pos.distanceTo(e.position);
if (d > 10) continue;
if (e.type === 'mob') {
mobsNearby++;
const mobName = (e.mobType || e.name || '').toLowerCase();
if (['zombie','skeleton','creeper','spider','enderman',
'witch','phantom','drowned','husk','stray','blaze',
'piglin','zombified piglin'].includes(mobName)) {
dangerMobs++;
}
}
if (e.type === 'object') itemsNearby++;
}
// ─── خطر ───
let isInDanger = 0;
if (dangerMobs > 0) {
for (const e of entities) {
if (!e || e === bot.entity || e.type !== 'mob') continue;
if (pos.distanceTo(e.position) < 6) { isInDanger = 1; break; }
}
}
if (isOnLava || (pos.y < -10)) isInDanger = 1;
// ─── المخزون ───
const invPct = Math.min(100, Math.round(inv.length / 36 * 100));
const hasWeapon = inv.some(i => i.name.includes('sword') || i.name.includes('axe')) ? 1 : 0;
const hasPickaxe = inv.some(i => i.name.includes('pickaxe')) ? 1 : 0;
const hasAxe = inv.some(i => i.name.includes('axe') && !i.name.includes('pickaxe')) ? 1 : 0;
const hasShield = inv.some(i => i.name.includes('shield')) ? 1 : 0;
const hasBow = inv.some(i => i.name.includes('bow')) ? 1 : 0;
const hasChestplate = inv.some(i => i.name.includes('chestplate')) ? 1 : 0;
const hasBoots = inv.some(i => i.name.includes('boots')) ? 1 : 0;
const hasFood = inv.some(i => ['apple','bread','cooked_beef','cooked_porkchop','cooked_chicken',
'baked_potato','carrot','golden_carrot','beetroot','melon_slice',
'cooked_mutton','cooked_cod','cooked_salmon','steak','porkchop',
'mushroom_stew','suspicious_stew'].includes(i.name)) ? 1 : 0;
const hasBed = inv.some(i => i.name.includes('bed')) ? 1 : 0;
const hasTorches = inv.some(i => i.name.includes('torch')) ? 1 : 0;
const hasLogs = inv.some(i => i.name.includes('_log') || i.name === 'log') ? 1 : 0;
const hasStone = inv.some(i => ['cobblestone','stone','andesite','diorite','granite','deepslate','tuff'].includes(i.name)) ? 1 : 0;
const hasIronRaw = inv.some(i => i.name.includes('raw_iron') || i.name === 'iron_ingot') ? 1 : 0;
const hasDiamondRaw = inv.some(i => i.name.includes('diamond')) ? 1 : 0;
const hasBuildingBlocks = inv.some(i => ['cobblestone','stone','oak_planks','spruce_planks','birch_planks',
'dirt','sand','gravel','deepslate'].includes(i.name)) ? 1 : 0;
// ─── لاعبون ───
const playerNearby = Object.keys(bot.players).filter(n => n !== bot.username).length > 0 ? 1 : 0;
const botCount = Math.max(0, Object.keys(bot.players).length - 1);
const isSleeping = bot.isSleeping ? 1 : 0;
// ─── اتجاهات البلوكات (NSEW + NSEW2) ───
let blockNorth = 0, blockSouth = 0, blockEast = 0, blockWest = 0;
let blockNorth2 = 0, blockSouth2 = 0, blockEast2 = 0, blockWest2 = 0;
try {
const bN = bot.blockAt(pos.offset(0, 0, -1)); if (bN) blockNorth = BLOCK_TYPES[bN.name] ?? 0;
const bS = bot.blockAt(pos.offset(0, 0, 1)); if (bS) blockSouth = BLOCK_TYPES[bS.name] ?? 0;
const bE = bot.blockAt(pos.offset(1, 0, 0)); if (bE) blockEast = BLOCK_TYPES[bE.name] ?? 0;
const bW = bot.blockAt(pos.offset(-1, 0, 0)); if (bW) blockWest = BLOCK_TYPES[bW.name] ?? 0;
const bN2 = bot.blockAt(pos.offset(0, 0, -2)); if (bN2) blockNorth2 = BLOCK_TYPES[bN2.name] ?? 0;
const bS2 = bot.blockAt(pos.offset(0, 0, 2)); if (bS2) blockSouth2 = BLOCK_TYPES[bS2.name] ?? 0;
const bE2 = bot.blockAt(pos.offset(2, 0, 0)); if (bE2) blockEast2 = BLOCK_TYPES[bE2.name] ?? 0;
const bW2 = bot.blockAt(pos.offset(-2, 0, 0)); if (bW2) blockWest2 = BLOCK_TYPES[bW2.name] ?? 0;
} catch (_) {}
// ─── عدادات المواد في المخزون ───
let countLogs = 0, countStone = 0, countIron = 0, countGold = 0;
let countDiamond = 0, countCoal = 0, countFoodItems = 0, countTorches = 0;
for (const i of inv) {
if (i.name.includes('_log') || i.name === 'log') countLogs += i.count;
if (['cobblestone','stone','andesite','diorite','granite','deepslate','tuff'].includes(i.name)) countStone += i.count;
if (i.name.includes('raw_iron') || i.name === 'iron_ingot' || i.name.includes('iron_')) countIron += i.count;
if (i.name.includes('raw_gold') || i.name === 'gold_ingot' || i.name.includes('gold_')) countGold += i.count;
if (i.name.includes('diamond')) countDiamond += i.count;
if (i.name === 'coal' || i.name.includes('coal_')) countCoal += i.count;
if (['apple','bread','cooked_beef','cooked_porkchop','cooked_chicken','cooked_cod','cooked_salmon',
'baked_potato','carrot','golden_carrot','beetroot','melon_slice','cooked_mutton','steak',
'porkchop','mushroom_stew','suspicious_stew','beef','chicken','cod','salmon',
'potato','sweet_berries','cookie','pumpkin_pie','rabbit_stew','dried_kelp',
'chorus_fruit','enchanted_golden_apple','golden_apple','rotten_flesh'].includes(i.name)) countFoodItems += i.count;
if (i.name.includes('torch')) countTorches += i.count;
}
// ─── مستويات الأدوات ───
const toolTier = name => {
if (name.includes('netherite')) return 5;
if (name.includes('diamond')) return 4;
if (name.includes('iron')) return 3;
if (name.includes('stone')) return 2;
if (name.includes('wooden')) return 1;
return 0;
};
let pickaxeLevel = 0, axeLevel = 0, swordLevel = 0, shovelLevel = 0;
for (const i of inv) {
if (i.name.includes('pickaxe')) pickaxeLevel = Math.max(pickaxeLevel, toolTier(i.name));
if (i.name.includes('axe') && !i.name.includes('pickaxe')) axeLevel = Math.max(axeLevel, toolTier(i.name));
if (i.name.includes('sword')) swordLevel = Math.max(swordLevel, toolTier(i.name));
if (i.name.includes('shovel')) shovelLevel = Math.max(shovelLevel, toolTier(i.name));
}
// ─── الدروع ───
let totalArmor = 0;
let hasExtraShield = 0;
for (const i of inv) {
if (i.name.includes('helmet')) totalArmor += 2;
if (i.name.includes('chestplate')) totalArmor += 6;
if (i.name.includes('leggings')) totalArmor += 5;
if (i.name.includes('boots')) totalArmor += 2;
if (i.name.includes('shield')) hasExtraShield++;
}
if (hasExtraShield > 1) hasExtraShield = 1;
// ─── التضاريس ───
let terrainFlatness = 0, elevationChange = 0;
try {
let minY = 999, maxY = -999;
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
const b = bot.blockAt(pos.offset(dx, -1, dz));
if (b && b.name !== 'air') {
const by = Math.floor(pos.y + (dx === 0 && dz === 0 ? 0 : 0));
minY = Math.min(minY, by);
maxY = Math.max(maxY, by);
}
}
}
if (minY < 999) terrainFlatness = maxY - minY;
elevationChange = Math.round(pos.y - 64);
} catch (_) {}
// ─── ماء/حمم قريبة ───
let nearWater = 0, nearLava = 0;
for (let dx = -4; dx <= 4; dx++) {
for (let dz = -4; dz <= 4; dz++) {
for (let dy = -2; dy <= 2; dy++) {
const b = bot.blockAt(pos.offset(dx, dy, dz));
if (!b) continue;
if (b.name.includes('water') || b.name === 'water') nearWater = 1;
if (b.name.includes('lava') || b.name === 'lava') nearLava = 1;
}
}
}
// ─── البورتالات والهياكل ───
let portalNearby = 0, structureNearby = 0;
let yRelativeSurface = Math.round(pos.y - 64);
for (let dx = -8; dx <= 8; dx++) {
for (let dz = -8; dz <= 8; dz++) {
for (let dy = -3; dy <= 3; dy++) {
const b = bot.blockAt(pos.offset(dx, dy, dz));
if (!b) continue;
if (b.name.includes('portal') || b.name.includes('gateway')) portalNearby = 1;
if (['chest','ender_chest','crafting_table','furnace','enchanting_table',
'anvil','brewing_stand','beacon'].includes(b.name)) structureNearby++;
}
}
}
if (structureNearby > 5) structureNearby = 5;
// ─── أدوات النذر والإند ───
const hasObsidian = inv.some(i => i.name.includes('obsidian')) ? 1 : 0;
const hasFlintSteel = inv.some(i => i.name.includes('flint') && i.name.includes('steel') || i.name === 'flint_and_steel') ? 1 : 0;
const hasEnderPearl = inv.some(i => i.name.includes('ender_pearl')) ? 1 : 0;
const hasBlazeRod = inv.some(i => i.name.includes('blaze_rod')) ? 1 : 0;
// ─── الحيوانات والقرويون ───
let animalsNearby = 0, villagersNearby = 0;
for (const e of entities) {
if (!e || e === bot.entity || e.type !== 'mob') continue;
const d = pos.distanceTo(e.position);
if (d > 10) continue;
const mobName = (e.name || e.displayName || e.mobType || '').toLowerCase();
if (['cow','sheep','pig','chicken','horse','donkey','mule','rabbit','goat',
'llama','wolf','cat','ocelot','fox','bee','strider'].includes(mobName)) animalsNearby++;
if (mobName === 'villager' || mobName === 'wandering_trader') villagersNearby++;
}
// ─── الأشجار والزهور ───
let treesNearby = 0, flowersNearby = 0;
for (let dx = -5; dx <= 5; dx++) {
for (let dz = -5; dz <= 5; dz++) {
for (let dy = -2; dy <= 2; dy++) {
const b = bot.blockAt(pos.offset(dx, dy, dz));
if (!b) continue;
if (b.name.includes('_log') || b.name === 'log' || b.name.includes('_leaves') || b.name === 'leaves') treesNearby++;
if (['dandelion','poppy','cornflower','azure_bluet','allium','oxeye_daisy',
'lily_of_the_valley','wither_rose','sunflower','lilac','peony','rose_bush',
'blue_orchid','tulip'].includes(b.name)) flowersNearby++;
}
}
}
if (treesNearby > 10) treesNearby = 10;
if (flowersNearby > 5) flowersNearby = 5;
return {
health: Math.round(bot.health || 20),
food: Math.round(bot.food || 20),
y_level: Math.round(pos.y),
xp_level: Math.round((bot.experience?.level || 0)),
time_of_day: Math.round(time),
biome,
dimension,
light_level: lightLevel,
is_raining: isRaining,
can_see_sky: canSeeSky,
is_in_water: isInWater,
is_on_lava: isOnLava,
block_below: blockBelow,
block_above: blockAbove,
is_on_ladder: isOnLadder,
is_in_shelter: isInShelter,
is_in_danger: isInDanger,
mobs_nearby: mobsNearby,
danger_mobs: dangerMobs,
items_nearby: itemsNearby,
inventory_pct: invPct,
has_weapon: hasWeapon,
has_pickaxe: hasPickaxe,
has_axe: hasAxe,
has_shield: hasShield,
has_bow: hasBow,
has_chestplate: hasChestplate,
has_boots: hasBoots,
has_food: hasFood,
has_bed: hasBed,
has_torches: hasTorches,
has_logs: hasLogs,
has_stone: hasStone,
has_iron_raw: hasIronRaw,
has_diamond_raw: hasDiamondRaw,
has_building_blocks: hasBuildingBlocks,
player_nearby: playerNearby,
bot_count: botCount,
is_sleeping: isSleeping,
is_sprinting: isSprinting,
// اتجاهات
block_north: blockNorth, block_south: blockSouth,
block_east: blockEast, block_west: blockWest,
block_north2: blockNorth2, block_south2: blockSouth2,
block_east2: blockEast2, block_west2: blockWest2,
// عدادات
count_logs: countLogs, count_stone: countStone,
count_iron: countIron, count_gold: countGold,
count_diamond: countDiamond, count_coal: countCoal,
count_food_items: countFoodItems, count_torches: countTorches,
// مستويات الأدوات
pickaxe_level: pickaxeLevel, axe_level: axeLevel,
sword_level: swordLevel, shovel_level: shovelLevel,
// دروع
total_armor: totalArmor, has_extra_shield: hasExtraShield,
// تضاريس
terrain_flatness: terrainFlatness, elevation_change: elevationChange,
// ماء/حمم
near_water: nearWater, near_lava: nearLava,
// وقت منذ الضرر ورعد
time_since_hurt: Math.min(1000, Math.floor((Date.now() - (bot._lastHurtTime || Date.now())) / 1000)),
is_thundering: (bot.thunderState || bot.isThundering ? 1 : 0),
// بورتال وستراكتشر
portal_nearby: portalNearby, structure_nearby: structureNearby,
y_relative_surface: Math.round(yRelativeSurface),
// أدوات الإند
has_obsidian: hasObsidian, has_flint_steel: hasFlintSteel,
has_ender_pearl: hasEnderPearl, has_blaze_rod: hasBlazeRod,
// حيوانات وقرويون
animals_nearby: animalsNearby, villagers_nearby: villagersNearby,
// مساحة
explored_radius: Math.round(Math.min(500, (bot.exploredChunks ? Object.keys(bot.exploredChunks).length : 0) * 5)),
trees_nearby: treesNearby, flowers_nearby: flowersNearby,
};
}
async predict(state) {
const start = Date.now();
this.context.addState(state);
if (!this.loaded || !this.session) {
this.stats.fallbacks++;
return this.fallbackAction;
}
try {
const contextData = this.context.getContextTensor();
const ort = require('onnxruntime-node');
const tensor = new ort.Tensor('float32', contextData, [1, SEQ_LEN, INPUT_DIM]);
const results = await this.session.run({ [this.inputName]: tensor });
const values = Object.values(results);
const actionData = values[0].data;
const dirData = values.length > 1 ? values[1].data : null;
let bestAction = 0;
for (let i = 1; i < actionData.length; i++) {
if (actionData[i] > actionData[bestAction]) bestAction = i;
}
let bestDir = 0;
if (dirData) {
for (let i = 1; i < dirData.length; i++) {
if (dirData[i] > dirData[bestDir]) bestDir = i;
}
}
this.stats.predictions++;
this.stats.avgTime = (this.stats.avgTime * (this.stats.predictions - 1) + (Date.now() - start)) / this.stats.predictions;
return { actionIdx: bestAction, dirIdx: bestDir };
} catch (err) {
log('⚠️', `Inference error: ${err.message}`);
this.stats.fallbacks++;
return this.fallbackAction;
}
}
async predictActionName(state) {
const result = await this.predict(state);
const idx = typeof result === 'object' ? result.actionIdx : result;
return ACTIONS[idx] || 'EXPLORE';
}
async predictDirection(state) {
const result = await this.predict(state);
return typeof result === 'object' ? result.dirIdx : 0;
}
getStats() {
return {
...this.stats,
loaded: this.loaded,
modelPath: this._modelPath,
contextLen: this.context.length,
};
}
unload() {
this.session = null;
this.loaded = false;
this.context.reset();
log('🔴', 'نموذج AI: unloaded');
}
}
// Helper لتحويل biome name/id إلى رقم
function biomeAt(val) {
if (typeof val === 'number') return Math.min(val, 13);
const name = String(val || '').toLowerCase().replace('minecraft:', '');
return BIOME_MAP[name] ?? 0;
}
module.exports = { BotModel, ContextManager, FEATURES, ACTIONS, SEQ_LEN, INPUT_DIM, BLOCK_TYPES, FEATURE_MINMAX };