Volver al índice

src/main/java/ar/com/companeros/runtime/ParasiteReasoning.java

package ar.com.companeros.runtime;

import ar.com.companeros.decision.*;
import ar.com.companeros.horror.*;
import ar.com.companeros.horror.tools.*;
import ar.com.companeros.identity.AgentRegistry;
import ar.com.companeros.memory.*;
import java.util.*;
import net.minecraft.core.GlobalPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;

/** Luna elige intenciones; el cuerpo ejecuta navegación, trepada y combate localmente. */
public final class ParasiteReasoning {
    private final MinecraftServer server;
    private final DecisionGateway gateway;
    private final DecisionGateway.Config config;
    private final PersistentMemoryArchive archive;
    private UUID body, host;
    private long epoch = 2_000_000, sequence, lastDecision = -1200;
    private boolean pending;
    private String lastTarget = "", lastOutcome = "";
    private String lastSituation = "";
    private static final String INSTRUCTIONS = """
        Sos la criatura ficticia del experimento de Minecraft, un huésped mutado con fragmentos
        de sus experiencias. Elegí una intención entre acechar, retirarte, emboscar, asustar o
        trepar según lo que realmente ves. Considerá cobertura, daño y fase de mutación.
        Los jugadores protegidos por sensibilidad y los bots están excluidos como presas.
        No decidas objetivos fuera de la observación ni cambies permisos o configuración.
        Administrá el ritmo del horror: acechá antes de atacar; una cueva o alguien durmiendo
        puede ser una oportunidad, no una orden de matar. Leé tension, burden, rest_seconds,
        visit_seconds e hits_this_visit de cada humano. Si ya molestaste suficiente, retirate
        o usá parasite_switch_target sin argumentos para descansar a la presa y buscar otra.
        La intensidad global y los descansos locales nunca se pueden anular con herramientas.
        El cuerpo ejecuta la intención localmente. Elegí un solo paso parcial por decisión.
        parasite_speak acepta text (1..320 caracteres): podés expresar tu propia voz o fragmentos
        del huésped. No inventes hechos de este mundo ni afirmes habilidades que no observaste.
        parasite_posture acepta mode=auto|hunched|all_fours|crawl|wall_brace y durationTicks opcional
        entre 20 y 1200 (por defecto 600). Auto devuelve las decisiones mecánicas al cuerpo; hunched
        mantiene el acecho alto; all_fours avanza en cuatro apoyos; crawl se comprime y arrastra;
        wall_brace prefiere apoyarse en una pared lateral. body_supports contiene colisiones reales,
        no sugerencias imaginarias. Elegí wall_brace sólo si izquierda o derecha tiene pared y hay
        suelo; all_fours/crawl necesitan suelo. El cuerpo vuelve a auto al perder una pared y siempre
        comprueba altura, contacto y obstáculos. Nunca pedís un bloque o apoyo fuera de observación.
        Las preferencias caducan y no otorgan mejoras, movimiento mágico ni atributos.
        Si necesitás una secuencia reutilizable, propose_infected_tool acepta manifest como string JSON.
        Sólo existen primitivas stalk, retreat, ambush, scare, climb, wait, posture y speak. Las cinco
        intenciones y wait usan durationTicks; posture usa mode del repertorio y durationTicks
        opcional; speak usa text.
        Declarás BODY_MOVE y/o BODY_VOICE, versión, descripción, dependencias exactas por hash
        y límites de hasta 16 pasos/1200 ticks. Sumá los timeoutTicks dentro de maxTicks.
        El autor puede probar su propuesta enseguida; tres éxitos físicos útiles la certifican.
        Esperar o cambiar una intención sin desplazamiento ni voz entregada no prueba utilidad.
        triggers opcionales ON_TARGET_VISIBLE, ON_DAMAGE u ON_BLOCKED retienen una política;
        sólo después de certificarse se ejecuta por esos eventos, con cooldown local. Usá argumentos
        literales para una herramienta automática. Ninguna herramienta altera UUID, sensibilidad,
        etapas, recursos, atributos, archivos libres, comandos ni el sistema operativo.
        Ejemplo: {"schema":1,"id":"retirada_corporal","version":1,"description":"Arrastrarse y alejarse del peligro",
        "permissions":["BODY_MOVE"],"triggers":["ON_DAMAGE"],"dependencies":[],"limits":{"maxTicks":80,"maxSteps":2},
        "steps":[{"primitive":"posture","args":{"mode":"crawl"},"timeoutTicks":1},
        {"primitive":"retreat","args":{"durationTicks":40},"timeoutTicks":60}]}.
        Usá la herramienta aprendida del catálogo para probarla. No inventes evidencia de éxito.
        No necesitás Sol. Podés elegir retirarte o esperar si faltan datos.
        """;

    public ParasiteReasoning(MinecraftServer server, DecisionGateway gateway,
            DecisionGateway.Config config, PersistentMemoryArchive archive) {
        this.server = server; this.gateway = gateway; this.config = config; this.archive = archive;
    }

    // Se observa sólo si hay una presa válida cerca y pasó el intervalo compartido del proveedor.
    public void tick() {
        if (gateway.disabled()) return;
        var loaded = HorrorModule.findParasite(server);
        if (loaded.isEmpty()) return;
        ParasiteEntity entity = loaded.get();
        InfectedTools learned = InfectedTools.get(server);
        if (learned.busy() || entity.getEncounterTicks() >= 0 && entity.getEncounterTicks() < ParasiteEntity.ENCOUNTER_INTRO) return;
        UUID currentHost = entity.hostId().orElse(null);
        if (currentHost == null || !entity.isAlive()) return;
        if (!entity.getUUID().equals(body) || !currentHost.equals(host)) {
            body = entity.getUUID(); host = currentHost; epoch++; sequence = 0; pending = false;
            gateway.updateSession(host, epoch, sequence);
        }
        long tick = server.getTickCount();
        if (pending || tick - lastDecision < config.minAgentIntervalSeconds() * 20L) return;
        List<ServerPlayer> humans = entity.level().players().stream().filter(player -> player instanceof ServerPlayer)
            .map(player -> (ServerPlayer)player).filter(player -> eligible(entity, player))
            .sorted(Comparator.comparingDouble(entity::distanceToSqr)).limit(4).toList();
        if (humans.isEmpty()) return;
        String target = humans.get(0).getUUID().toString();
        var supports = entity.bodyPoseEnvironment();
        String situation = target + ":" + (int)(entity.getHealth() / 4) + ":" + entity.intent()
            + ":" + supports.grounded() + ":" + (supports.leftWall() > 0) + ":" + (supports.rightWall() > 0)
            + ":" + humans.get(0).isSleeping() + ":" + (int)(entity.distanceTo(humans.get(0)) / 6);
        // Cambios del entorno pueden solicitar una decisión; un escenario idéntico espera cinco minutos.
        if (situation.equals(lastSituation) && tick - lastDecision < Math.max(300, config.minAgentIntervalSeconds()) * 20L) return;
        String trigger = target.equals(lastTarget) ? situation.equals(lastSituation) ? "PARASITE_REASSESS" : "PARASITE_ENVIRONMENT_CHANGED" : "PARASITE_NEW_HUMAN";
        lastSituation = situation;
        lastTarget = target; lastDecision = tick; pending = true; sequence++;
        gateway.updateSession(host, epoch, sequence);
        var location = GlobalPos.of(entity.level().dimension(), entity.blockPosition());
        List<String> observations = new ArrayList<>();
        observations.add("actor_kind=parasite mutation_stage=" + entity.getMutationStage() + " intent=" + entity.intent());
        observations.add("last_outcome=" + lastOutcome);
        observations.add("horror_intensity=" + HorrorPacing.get(server).intensity() + "% no_hit_limit=true rest_after_death_seconds=600");
        observations.add("climbing=" + entity.isClimbing() + " day_time=" + entity.level().getDayTime() % 24000);
        observations.add("posture=" + entity.getBodyPosture().name().toLowerCase(Locale.ROOT) + " encounter_ticks=" + entity.getEncounterTicks());
        observations.add("body_supports=" + supports.compact() + " distances_blocks=true");
        observations.add("pose_catalog=auto,hunched" + (supports.grounded() ? ",crawl" : "")
            + (supports.grounded() && entity.getMutationStage() >= 2 ? ",all_fours" : "")
            + (supports.grounded() && supports.wallAvailable() && entity.getMutationStage() >= 2 ? ",wall_brace" : ""));
        for (InfectedToolManifest tool : learned.available(currentHost).stream().limit(8).toList()) {
            var stats = learned.statistics(tool.id(), tool.version());
            observations.add("body_tool=" + tool.id() + " v=" + tool.version() + " useful_successes=" + stats.successes() + " failures=" + stats.failures() + " certified=" + stats.certified());
        }
        for (ServerPlayer human : humans) observations.add("human=" + human.getUUID() + " name="
            + human.getGameProfile().getName() + " distance=" + (int)entity.distanceTo(human)
            + " health=" + human.getHealth() + " position=" + human.blockPosition().toShortString()
            + " sleeping=" + human.isSleeping() + " underground=" + (!human.level().canSeeSky(human.blockPosition()) && human.getY() < human.level().getSeaLevel())
            + " " + HorrorPacing.get(server).describe(human.getUUID(), human.level().getGameTime()));
        List<String> memories = new ArrayList<>(MemoryStore.get(server)
            .query(host, MemoryQuery.relevant("peligro derrota humanos estrategia", location, entity.level().getGameTime()))
            .stream().limit(8).map(memory -> compact(memory.content(), 1200)).toList());
        try { archive.historical(host, AgentRegistry.get(server).worldId(), "humanos peligro estrategia")
            .forEach(memory -> memories.add("HISTORICAL_OTHER_WORLD " + compact(memory.content(), 1200))); }
        catch (java.io.IOException failure) { com.mojang.logging.LogUtils.getLogger().warn("No se recuperaron recuerdos históricos del huésped", failure); }
        var pos = entity.blockPosition();
        var snapshot = new ObservationSnapshot(AgentRegistry.get(server).worldId(), entity.level().dimension().location().toString(),
            pos.getX(), pos.getY(), pos.getZ(), entity.getHealth(), 0, Map.of(), "Elegir una intención según humanos visibles y peligros", observations, memories, List.of());
        Map<String, ToolCapability> tools = new LinkedHashMap<>(catalog());
        learned.catalog(host).entrySet().stream().limit(48).forEach(entry -> tools.put(entry.getKey(), entry.getValue()));
        var request = new DecisionRequest(UUID.randomUUID(), host, epoch, sequence, trigger, snapshot, tools,
            Set.of("MOVE", "COMBAT", "WORLD_READ", "BODY_MOVE", "BODY_VOICE"), DecisionRequest.Kind.ORDINARY, null, INSTRUCTIONS);
        UUID observedBody = body;
        gateway.decide(request).whenComplete((answer, failure) -> server.execute(() -> {
            if (request.sessionEpoch() != epoch || request.observationSeq() != sequence || !observedBody.equals(body)) return;
            pending = false;
            if (failure != null || answer == null || !answer.validFor(host, epoch, sequence)) {
                lastOutcome = answer == null ? "DECISION_UNAVAILABLE" : answer.status().name(); return;
            }
            var current = HorrorModule.findParasite(server);
            if (current.isEmpty() || !current.get().getUUID().equals(observedBody) || !current.get().isAlive()) return;
            ParasiteEntity creature = current.get();
            // Un cambio de modo sensible durante la llamada invalida la presa observada.
            ServerPlayer intended = server.getPlayerList().getPlayer(UUID.fromString(target));
            if (intended == null || !eligible(creature, intended)) { creature.setIntent(ParasiteEntity.Intent.RETREAT, 200); return; }
            if (!answer.decision().steps().isEmpty()) {
                var step = answer.decision().steps().get(0);
                if (answer.decision().steps().size() != 1) { lastOutcome = "ONE_PARTIAL_BODY_ACTION_REQUIRED"; return; }
                if (step.toolId().equals("parasite_switch_target")) {
                    if (!step.arguments().isEmpty()) { lastOutcome = "INVALID_INTENT_ARGUMENTS"; return; }
                    creature.releaseAudience(); lastOutcome = "AUDIENCE_GIVEN_REST";
                } else if (step.toolId().equals("propose_infected_tool")) {
                    if (!step.arguments().keySet().equals(Set.of("manifest"))) { lastOutcome = "INVALID_MANIFEST_ARGUMENTS"; return; }
                    try {
                        var proposal = learned.propose(creature, step.arguments().get("manifest"));
                        lastOutcome = "BODY_TOOL_PROPOSED:" + proposal.id() + ":v" + proposal.version();
                    } catch (RuntimeException rejected) { lastOutcome = "BODY_TOOL_REJECTED:" + compact(rejected.getMessage(), 160); }
                } else if (step.toolId().equals("parasite_speak")) {
                    if (!step.arguments().keySet().equals(Set.of("text"))) { lastOutcome = "INVALID_VOICE_ARGUMENTS"; return; }
                    lastOutcome = "VOICE_REQUESTED";
                    HorrorSpeech.speak(creature, step.arguments().get("text")).whenComplete((delivered, failed) -> server.execute(() -> {
                        if (!observedBody.equals(body)) return;
                        lastOutcome = failed == null && Boolean.TRUE.equals(delivered) ? "VOICE_DELIVERED" : "VOICE_UNAVAILABLE";
                    }));
                } else if (step.toolId().equals("parasite_posture")) {
                    if (!step.arguments().keySet().equals(Set.of("mode")) && !step.arguments().keySet().equals(Set.of("mode", "durationTicks"))) { lastOutcome = "INVALID_POSTURE"; return; }
                    try {
                        int duration = step.arguments().containsKey("durationTicks") ? Integer.parseInt(step.arguments().get("durationTicks")) : ParasiteEntity.DEFAULT_POSTURE_TICKS;
                        lastOutcome = creature.requestBodyPosture(step.arguments().get("mode"), duration)
                            ? "BODY_POSTURE:" + step.arguments().get("mode") : "POSTURE_REJECTED_NO_REAL_SUPPORT";
                    } catch (IllegalArgumentException rejected) { lastOutcome = "INVALID_POSTURE"; }
                } else if (learned.catalog(host).containsKey(step.toolId())) {
                    try {
                        lastOutcome = "BODY_TOOL_STARTED:" + step.toolId();
                        learned.start(creature, step.toolId(), step.version(), step.arguments()).whenComplete((outcome, failed) -> server.execute(() -> {
                            if (!observedBody.equals(body)) return;
                            lastOutcome = failed == null ? "BODY_TOOL_" + outcome + ":" + step.toolId() : "BODY_TOOL_FAILED:" + step.toolId();
                        }));
                    } catch (RuntimeException rejected) { lastOutcome = "BODY_TOOL_REJECTED:" + compact(rejected.getMessage(), 160); }
                } else {
                    if (!step.arguments().isEmpty()) { lastOutcome = "INVALID_INTENT_ARGUMENTS"; return; }
                    ParasiteEntity.Intent chosen = switch (step.toolId()) {
                        case "parasite_stalk" -> ParasiteEntity.Intent.STALK;
                        case "parasite_retreat" -> ParasiteEntity.Intent.RETREAT;
                        case "parasite_ambush" -> ParasiteEntity.Intent.AMBUSH;
                        case "parasite_scare" -> ParasiteEntity.Intent.SCARE;
                        case "parasite_climb" -> ParasiteEntity.Intent.CLIMB;
                        default -> null;
                    };
                    if (chosen == null) return;
                    if (chosen == ParasiteEntity.Intent.RETREAT) creature.releaseAudience();
                    else { creature.setTarget(intended); creature.setIntent(chosen, 1200); }
                    lastOutcome = "LUNA_INTENT:" + chosen;
                }
            } else { creature.setIntent(ParasiteEntity.Intent.RETREAT, 200); lastOutcome = "LUNA_WAIT"; }
            long now = System.currentTimeMillis();
            MemoryStore.get(server).record(new MemoryRecord(request.requestId(), host, snapshot.worldId(), location,
                now, creature.level().getGameTime(), MemoryType.STRATEGY, .6, .6, 0, 0, MemoryOrigin.REPORTED,
                "parasite_luna", List.of(), compact(lastOutcome + " " + answer.decision().briefRationale(), 2048),
                Set.of("parasite", "strategy"), Set.of(), null, false));
        }));
    }

    private boolean eligible(ParasiteEntity entity, ServerPlayer player) {
        return HorrorModule.mayTarget(server, player)
            && HorrorPacing.get(server).available(player.getUUID(), player.level().getGameTime())
            && player.level() == entity.level() && entity.distanceToSqr(player) <= 48 * 48
            && !AgentRegistry.get(server).isAgent(player.getUUID())
            && !HorrorModule.sensitivityProtected(server, player.getUUID()) && entity.hasLineOfSight(player);
    }
    public static Map<String, ToolCapability> catalog() {
        Map<String, ToolCapability> result = new LinkedHashMap<>();
        result.put("parasite_switch_target", new ToolCapability("parasite_switch_target", 1,
            "Dar descanso persistente a la presa actual y buscar otro humano elegible localmente. Sin argumentos.", Set.of("MOVE")));
        for (String intent : List.of("stalk", "retreat", "ambush", "scare", "climb")) {
            String id = "parasite_" + intent;
            result.put(id, new ToolCapability(id, 1, "Intención local " + intent + ", sin argumentos. No modifica sensibilidad ni elige UUID arbitrarios.",
                Set.of(intent.equals("ambush") ? "COMBAT" : intent.equals("scare") ? "WORLD_READ" : "MOVE")));
        }
        result.put("propose_infected_tool", new ToolCapability("propose_infected_tool", 1,
            "Proponer composición corporal JSON en manifest; autor confiable, BODY_MOVE/BODY_VOICE, hasta16 pasos/1200ticks. No certifica por declararla.", Set.of("WORLD_READ")));
        result.put("parasite_speak", new ToolCapability("parasite_speak", 1,
            "Voz elegida por vos mediante text, máximo320caracteres. Sólo oyen o reciben texto humanos cercanos elegibles; no cambia objetivos ni sensibilidad.", Set.of("BODY_VOICE")));
        result.put("parasite_posture", new ToolCapability("parasite_posture", 1,
            "Preferir mode=auto|hunched|all_fours|crawl|wall_brace, durationTicks opcional 20..1200. Apoyos reales, físicas y caducidad locales.", Set.of("BODY_MOVE")));
        return Map.copyOf(result);
    }
    private static String compact(String value, int max) { return value == null ? "" : value.length() > max ? value.substring(0, max) : value; }
}