| '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 = [ |
| |
| 'health','food','y_level','xp_level','time_of_day', |
| |
| 'biome','dimension','light_level','is_raining','can_see_sky', |
| 'is_in_water','is_on_lava', |
| |
| 'block_below','block_above','is_on_ladder','is_in_shelter', |
| 'is_in_danger', |
| |
| 'mobs_nearby','danger_mobs','items_nearby', |
| |
| 'inventory_pct','has_weapon','has_pickaxe','has_axe', |
| 'has_shield','has_bow', |
| |
| 'has_chestplate','has_boots','has_food','has_bed','has_torches', |
| |
| 'has_logs','has_stone','has_iron_raw','has_diamond_raw','has_building_blocks', |
| |
| 'player_nearby','bot_count','is_sleeping','is_sprinting', |
| |
| 'block_north','block_south','block_east','block_west', |
| 'block_north2','block_south2','block_east2','block_west2', |
| |
| 'count_logs','count_stone','count_iron','count_gold','count_diamond','count_coal','count_food_items','count_torches', |
| |
| 'pickaxe_level','axe_level','sword_level','shovel_level', |
| |
| 'total_armor','has_extra_shield', |
| |
| 'terrain_flatness','elevation_change', |
| |
| 'near_water','near_lava', |
| |
| 'time_since_hurt','is_thundering', |
| |
| 'portal_nearby','structure_nearby','y_relative_surface', |
| |
| 'has_obsidian','has_flint_steel','has_ender_pearl','has_blaze_rod', |
| |
| 'animals_nearby','villagers_nearby', |
| |
| 'explored_radius','trees_nearby','flowers_nearby', |
| ]; |
|
|
| const ACTIONS = [ |
| |
| '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', |
| |
| 'BUILD_PATH','BUILD_BRIDGE','BUILD_WALL','PLACE_DOOR','PLACE_LADDER', |
| |
| 'MINE_IRON','MINE_GOLD','MINE_DIAMOND','MINE_OBSIDIAN', |
| |
| 'CRAFT_ENCHANT_TABLE','CRAFT_ANVIL','CRAFT_BREWING_STAND','CRAFT_BED','CRAFT_BOAT', |
| |
| 'ENTER_NETHER','EXPLORE_NETHER','FIGHT_BLAZE', |
| |
| 'ENTER_END','FIGHT_DRAGON', |
| |
| 'PLANT_CROP','HARVEST_CROP','FISH','SHEAR','MILK', |
| |
| 'SLEEP_AT_VILLAGE', |
| |
| 'CHAT_GREETING','CHAT_STATUS','CHAT_THANK','CHAT_QUESTION','CHAT_ALERT', |
| |
| '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], |
| |
| '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], |
| |
| '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], |
| |
| 'pickaxe_level':[0,5],'axe_level':[0,5],'sword_level':[0,5],'shovel_level':[0,5], |
| |
| 'total_armor':[0,20],'has_extra_shield':[0,1], |
| |
| 'terrain_flatness':[0,10],'elevation_change':[-10,10], |
| |
| 'near_water':[0,1],'near_lava':[0,1], |
| |
| 'time_since_hurt':[0,1000],'is_thundering':[0,1], |
| |
| 'portal_nearby':[0,1],'structure_nearby':[0,5],'y_relative_surface':[-64,100], |
| |
| 'has_obsidian':[0,1],'has_flint_steel':[0,1],'has_ender_pearl':[0,1],'has_blaze_rod':[0,1], |
| |
| 'animals_nearby':[0,5],'villagers_nearby':[0,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; |
| 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); |
|
|
| |
| 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; |
|
|
| |
| 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'); |
| } |
| } |
|
|
| |
| 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 }; |
|
|