Ground bot intentions in live game knowledge and verify outcomes
Core tests / core (push) Successful in 28s

This commit is contained in:
Codex
2026-09-25 13:28:03 +08:00
parent 20ccd0fced
commit 502cb1e4fd
17 changed files with 251 additions and 24 deletions
+20 -2
View File
@@ -1,3 +1,4 @@
import {observeIntentChecks} from './intent-evidence';
import type {Bot,State,StateStore,SocialEntry} from './types';
import {luaString} from './ale';
import {queueAleCommand} from './ale-store';
@@ -24,7 +25,8 @@ export function observePresence(s:State,b:Bot,p:Record<string,unknown>){
if(typeof p.team!=='number'||typeof p.x!=='number'||typeof p.y!=='number')return;
const old=b.world;
const stationary=old&&old.mapId===p.mapId&&Math.hypot(old.x-p.x,old.y-p.y)<3;
b.world={trainingPending:typeof p.trainingPending==='number'?p.trainingPending:undefined,trainingLevel:typeof p.trainingLevel==='number'?p.trainingLevel:undefined,trainingTrainer:typeof p.trainingTrainer==='string'?p.trainingTrainer:undefined,trainingStatus:typeof p.trainingStatus==='string'?p.trainingStatus:undefined,trainingAt:typeof p.trainingAt==='number'?p.trainingAt:undefined,trainingLearned:typeof p.trainingLearned==='number'?p.trainingLearned:undefined,lastTrainingAt:typeof p.lastTrainingAt==='number'?p.lastTrainingAt:undefined,minDurabilityPct:typeof p.minDurabilityPct==='number'?p.minDurabilityPct:undefined,survivalSince:typeof p.survivalSince==='number'?p.survivalSince:undefined,isDead:typeof p.isDead==='boolean'?p.isDead:undefined,isGhost:typeof p.isGhost==='boolean'?p.isGhost:undefined,manaPct:typeof p.manaPct==='number'?p.manaPct:undefined,leaderName:typeof p.leaderName==='string'?p.leaderName:undefined,brokenItems:typeof p.brokenItems==='number'?p.brokenItems:undefined,freeTalents:typeof p.freeTalents==='number'?p.freeTalents:undefined,foodCount:typeof p.foodCount==='number'?p.foodCount:undefined,drinkCount:typeof p.drinkCount==='number'?p.drinkCount:undefined,readinessAt:typeof p.readinessAt==='number'?p.readinessAt:undefined,deathMemoryId:typeof p.deathMemoryId==='number'?p.deathMemoryId:undefined,deathMemoryAt:typeof p.deathMemoryAt==='number'?p.deathMemoryAt:undefined,deathMemoryKiller:typeof p.deathMemoryKiller==='string'?p.deathMemoryKiller:undefined,deathMemoryMap:typeof p.deathMemoryMap==='number'?p.deathMemoryMap:undefined,deathMemoryX:typeof p.deathMemoryX==='number'?p.deathMemoryX:undefined,deathMemoryY:typeof p.deathMemoryY==='number'?p.deathMemoryY:undefined,deathMemoryLevel:typeof p.deathMemoryLevel==='number'?p.deathMemoryLevel:undefined,quests:Array.isArray(p.quests)?p.quests.filter((q):q is number=>typeof q==='number'&&q>0).slice(0,25):undefined,dangerAt:typeof p.dangerAt==='number'?p.dangerAt:old?.dangerAt,survivalAction:typeof p.survivalAction==='string'?p.survivalAction:undefined,survivalAt:typeof p.survivalAt==='number'?p.survivalAt:undefined,combatRole:typeof p.combatRole==='string'?p.combatRole:undefined,casting:p.casting===true,taxi:p.taxi===true,humanControlled:p.humanControlled===true,movementType:typeof p.movementType==='number'?p.movementType:undefined,healthPct:typeof p.healthPct==='number'?p.healthPct:undefined,at,team:p.team,guildId:Number(p.guildId)||0,guildName:String(p.guildName||''),guildRank:Number(p.guildRank)||0,groupId:String(p.groupId||''),groupNames:String(p.groupNames||'').split(',').filter(Boolean),nearby:String(p.nearbyBots||'').split(',').filter(Boolean),inCombat:p.inCombat===true,x:p.x,y:p.y,mapId:Number(p.mapId)||0,stationarySince:stationary?(old.stationarySince??old.at):at};
b.world={xp:typeof p.xp==='number'&&Number.isFinite(p.xp)?p.xp:undefined,questBrief:typeof p.questBrief==='string'?p.questBrief.slice(0,1800):undefined,threatBrief:typeof p.threatBrief==='string'?p.threatBrief.slice(0,600):undefined,trainingPending:typeof p.trainingPending==='number'?p.trainingPending:undefined,trainingLevel:typeof p.trainingLevel==='number'?p.trainingLevel:undefined,trainingTrainer:typeof p.trainingTrainer==='string'?p.trainingTrainer:undefined,trainingStatus:typeof p.trainingStatus==='string'?p.trainingStatus:undefined,trainingAt:typeof p.trainingAt==='number'?p.trainingAt:undefined,trainingLearned:typeof p.trainingLearned==='number'?p.trainingLearned:undefined,lastTrainingAt:typeof p.lastTrainingAt==='number'?p.lastTrainingAt:undefined,minDurabilityPct:typeof p.minDurabilityPct==='number'?p.minDurabilityPct:undefined,survivalSince:typeof p.survivalSince==='number'?p.survivalSince:undefined,isDead:typeof p.isDead==='boolean'?p.isDead:undefined,isGhost:typeof p.isGhost==='boolean'?p.isGhost:undefined,manaPct:typeof p.manaPct==='number'?p.manaPct:undefined,leaderName:typeof p.leaderName==='string'?p.leaderName:undefined,brokenItems:typeof p.brokenItems==='number'?p.brokenItems:undefined,freeTalents:typeof p.freeTalents==='number'?p.freeTalents:undefined,foodCount:typeof p.foodCount==='number'?p.foodCount:undefined,drinkCount:typeof p.drinkCount==='number'?p.drinkCount:undefined,readinessAt:typeof p.readinessAt==='number'?p.readinessAt:undefined,deathMemoryId:typeof p.deathMemoryId==='number'?p.deathMemoryId:undefined,deathMemoryAt:typeof p.deathMemoryAt==='number'?p.deathMemoryAt:undefined,deathMemoryKiller:typeof p.deathMemoryKiller==='string'?p.deathMemoryKiller:undefined,deathMemoryMap:typeof p.deathMemoryMap==='number'?p.deathMemoryMap:undefined,deathMemoryX:typeof p.deathMemoryX==='number'?p.deathMemoryX:undefined,deathMemoryY:typeof p.deathMemoryY==='number'?p.deathMemoryY:undefined,deathMemoryLevel:typeof p.deathMemoryLevel==='number'?p.deathMemoryLevel:undefined,quests:Array.isArray(p.quests)?p.quests.filter((q):q is number=>typeof q==='number'&&q>0).slice(0,25):undefined,dangerAt:typeof p.dangerAt==='number'?p.dangerAt:old?.dangerAt,survivalAction:typeof p.survivalAction==='string'?p.survivalAction:undefined,survivalAt:typeof p.survivalAt==='number'?p.survivalAt:undefined,combatRole:typeof p.combatRole==='string'?p.combatRole:undefined,casting:p.casting===true,taxi:p.taxi===true,humanControlled:p.humanControlled===true,movementType:typeof p.movementType==='number'?p.movementType:undefined,healthPct:typeof p.healthPct==='number'?p.healthPct:undefined,at,team:p.team,guildId:Number(p.guildId)||0,guildName:String(p.guildName||''),guildRank:Number(p.guildRank)||0,groupId:String(p.groupId||''),groupNames:String(p.groupNames||'').split(',').filter(Boolean),nearby:String(p.nearbyBots||'').split(',').filter(Boolean),inCombat:p.inCombat===true,x:p.x,y:p.y,mapId:Number(p.mapId)||0,stationarySince:stationary?(old.stationarySince??old.at):at};
observeIntentChecks(b);
if(b.world.deathMemoryId && b.world.deathMemoryId>(b.deathMemorySeenId??0)){
b.deathMemorySeenId=b.world.deathMemoryId;
b.memories.push({id:`native-death:${b.world.deathMemoryId}`,text:`I died to ${b.world.deathMemoryKiller??'an unknown enemy'} at level ${b.world.deathMemoryLevel??b.level}, map ${b.world.deathMemoryMap}, near (${Math.round(b.world.deathMemoryX??0)}, ${Math.round(b.world.deathMemoryY??0)}). Approach similar enemies there cautiously; recover before fighting again.`,importance:10,source:'observation',at:new Date((b.world.deathMemoryAt??at)*1000).toISOString()});
@@ -58,13 +60,29 @@ export function companionScore(a:Bot,b:Bot){
const shared=(a.world?.quests??[]).filter(q=>b.world?.quests?.includes(q)).length;
return Math.min(40,shared*15)+(recentDanger(b)?30:0)+roleBonus+Math.min(20,Math.max(0,affinity(a,b)))+Math.max(0,10-Math.abs(a.level-b.level));
}
const fresh=(b:Bot)=>b.status==='online'&&b.autonomy?.managed&&b.world&&Date.now()/1000-b.world.at<90&&!b.world.inCombat&&!b.world.humanControlled&&!b.world.taxi&&!b.world.casting;
const fresh=(b:Bot)=>b.status==='online'&&b.autonomy?.managed&&b.world&&Date.now()/1000-b.world.at<90&&!b.world.isDead&&!b.world.isGhost&&!b.world.inCombat&&!b.world.humanControlled&&!b.world.taxi&&!b.world.casting;
export function partyDecision(a:Bot,b:Bot){
if(!fresh(a)||!fresh(b)||a.id===b.id||a.world!.team!==b.world!.team||Math.abs(a.level-b.level)>5||!a.world!.nearby.includes(b.name)||!b.world!.nearby.includes(a.name))return {accepted:false,reason:'No compatible nearby pair'};
const danger=recentDanger(a)||recentDanger(b);
const willing=(x:Bot,y:Bot)=>affinity(x,y)>=0&&(danger||x.personality.sociability>=35||affinity(x,y)>=5)&&x.personality.activities.some(v=>v==='quest'||v==='explore');
return {accepted:willing(a,b)&&willing(b,a),reason:willing(a,b)&&willing(b,a)?danger?'Seeking safety together after recent danger; compatible faction, levels, and non-hostile companions.':'Both accepted: compatible levels, shared adventure interests, and no hostile bond.':'One character prefers to adventure alone or does not trust the other.'};
}
export function intentCompanions(s:State,a:Bot){
if(!s.community?.partiesEnabled)return [];
const groupSafe=(p:Bot)=>!p.world?.groupId||(p.world.groupNames.length<5&&p.world.groupNames.length>0&&p.world.groupNames.every(n=>s.bots.some(m=>m.name===n&&fresh(m)&&m.world!.groupId===p.world!.groupId)));
if(!groupSafe(a))return [];
return s.bots.filter(b=>!b.world?.groupId&&partyDecision(a,b).accepted).sort((x,y)=>companionScore(a,y)-companionScore(a,x));
}
export async function partyIntentBusy(db:D1Database,a:Bot,b:Bot){
await ensure(db);
const row=await db.prepare("SELECT id FROM community_actions WHERE (a IN (?,?) OR b IN (?,?)) AND (status='pending' OR created_at>?) LIMIT 1").bind(a.id,b.id,a.id,b.id,new Date(Date.now()-120000).toISOString()).first();
return !!row;
}
export async function recordPartyIntent(db:D1Database,s:State,id:string,a:Bot,b:Bot){
const reason='Seeking a nearby compatible adventuring companion; awaiting native confirmation.';
await db.prepare('INSERT INTO community_actions VALUES (?,?,?,?,?,?,?,?)').bind(id,new Date().toISOString(),'party',a.id,b.id,id,'pending',reason).run();
socialEntry(s,{id:id+':proposal',at:new Date().toISOString(),kind:'invitation',names:[a.name,b.name],text:reason,status:'pending'});
}
export function compileCommunityAction(kind:'party'|'leave'|'recruit',a:Bot,b?:Bot){
if(kind==='party'){
if(!b||!a.autonomy?.accountId||!b.autonomy?.accountId)throw new Error('Verified native accounts required');