Bots really play: shared understanding for headless bots, social layer (bot-to-bot talk, squads, invites), castle sieges, economy (sell/buy/professions/jewelry/buffs), telemetry, brain (loot, fight back, leash, spoil/sweep), 43 combat profiles, zones rebuilt from real spawns, tools (harness, data checks, zone fixer, build/deploy)
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.l2jmobius.gameserver.managers.FakePlayerBotContext;
|
||||
import org.l2jmobius.gameserver.managers.FakePlayerChatLines;
|
||||
import org.l2jmobius.gameserver.managers.FakePlayerHeardManager;
|
||||
import org.l2jmobius.gameserver.managers.FakePlayerIdiolect;
|
||||
import org.l2jmobius.gameserver.managers.FakePlayerIntentParser;
|
||||
|
||||
/**
|
||||
* Offline harness for the bot chat engine: runs the real server classes
|
||||
* (FakePlayerChatLines, FakePlayerIntentParser, FakePlayerIdiolect,
|
||||
* FakePlayerBotContext, FakePlayerHeardManager) against the real XML data
|
||||
* without a game server. Run from a directory that contains data/ (see
|
||||
* tools/run_harness.sh). Exit code 1 on any failed check.
|
||||
*/
|
||||
public class ChatHarness
|
||||
{
|
||||
private static int failures = 0;
|
||||
private static int checks = 0;
|
||||
|
||||
private static void check(boolean condition, String what)
|
||||
{
|
||||
checks++;
|
||||
if (!condition)
|
||||
{
|
||||
failures++;
|
||||
System.out.println(" FAIL: " + what);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
final FakePlayerChatLines lines = FakePlayerChatLines.getInstance();
|
||||
final FakePlayerIntentParser intents = FakePlayerIntentParser.getInstance();
|
||||
|
||||
// 1. Every seam the code uses must exist and produce a line with full slots.
|
||||
final String[] codeSeams =
|
||||
{
|
||||
"greeting", "whisper_default", "reply_where", "reply_price", "reply_help_yes", "reply_help_no", "reply_party_no", "party_accept", "reply_insult_soft", "reply_insult_aggro", "reply_thanks", "reply_botcheck", "reply_duel", "reply_rumor_none", "reply_bye", "reply_yes_ok", "reply_how", "reply_caps", "reply_again", "overheard_q", "reply_level", "gank_start", "pkk_start", "war_start", "killed_by", "revenge_meet", "friendly_meet", "ambient", "levelup", "grats", "migrate", "raid_start", "trade_shout", "invite_player", "call_help", "lfp", "overheard", "assist", "companion_done", "store_title", "bot_talk", "reply_drop", "siege_start", "siege_defend", "siege_gate", "siege_engrave", "siege_win", "siege_lose", "profession", "died_mob", "loot_brag", "reply_clan", "reply_solo", "reply_long", "reply_pk"
|
||||
};
|
||||
final Map<String, String> slots = new HashMap<>();
|
||||
slots.put("player", "Vanger");
|
||||
slots.put("target", "Vanger");
|
||||
slots.put("level", "42");
|
||||
slots.put("boss", "Core");
|
||||
slots.put("zone", "Cruma");
|
||||
slots.put("town", "Giran");
|
||||
slots.put("msg", "продам дроп");
|
||||
slots.put("prev", "не пойду");
|
||||
slots.put("enemyclan", "RedCrew");
|
||||
slots.put("mob", "Cruma Marshlands Traitor");
|
||||
slots.put("castle", "Giran");
|
||||
slots.put("clan", "IronPact");
|
||||
slots.put("class", "Gladiator");
|
||||
slots.put("drop", "Oriharukon Ore");
|
||||
slots.put("item", "соски D");
|
||||
slots.put("price", "50к");
|
||||
slots.put("crowd", "6");
|
||||
System.out.println("== seams used by code (" + codeSeams.length + ") ==");
|
||||
final Map<String, String> samples = new TreeMap<>();
|
||||
for (String seam : codeSeams)
|
||||
{
|
||||
String ok = null;
|
||||
for (int botId = 1; (botId <= 12) && (ok == null); botId++)
|
||||
{
|
||||
final Map<String, String> facts = new HashMap<>();
|
||||
facts.put("personality", (botId % 4 == 0) ? "ganker" : (botId % 4 == 1) ? "helper" : (botId % 4 == 2) ? "pkk" : "neutral");
|
||||
facts.put("asker", (botId % 3 == 0) ? "stranger" : (botId % 3 == 1) ? "clanmate" : "killed_me");
|
||||
ok = lines.speak(botId, seam, facts, slots, false, null);
|
||||
}
|
||||
check(ok != null, "seam produces nothing: " + seam);
|
||||
if (ok != null)
|
||||
{
|
||||
samples.put(seam, ok);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, String> entry : samples.entrySet())
|
||||
{
|
||||
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
|
||||
}
|
||||
|
||||
// 2. Context facts change what a bot says (plan / event / grounding slots).
|
||||
System.out.println("== context ==");
|
||||
FakePlayerBotContext.setPlan(77, "siege", 60000);
|
||||
FakePlayerBotContext.setSlot(77, "castle", "Aden");
|
||||
int siegeMentions = 0;
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
final String line = lines.speak(77, "ambient", Map.of("personality", "neutral"), null, false, null);
|
||||
if ((line != null) && (line.contains("осад") || line.contains("Aden")))
|
||||
{
|
||||
siegeMentions++;
|
||||
}
|
||||
}
|
||||
System.out.println(" plan=siege -> siege mentions in 40 ambient lines: " + siegeMentions);
|
||||
check(siegeMentions >= 8, "plan=siege rule should fire regularly (got " + siegeMentions + ")");
|
||||
FakePlayerBotContext.setPlan(78, "farm", 0);
|
||||
FakePlayerBotContext.setEvent(78, "profession");
|
||||
int professionMentions = 0;
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
final String line = lines.speak(78, "ambient", Map.of("personality", "neutral"), null, false, null);
|
||||
if ((line != null) && line.contains("проф"))
|
||||
{
|
||||
professionMentions++;
|
||||
}
|
||||
}
|
||||
System.out.println(" event=profession -> mentions in 40 ambient lines: " + professionMentions);
|
||||
check(professionMentions >= 2, "event=profession rule should fire (got " + professionMentions + ")");
|
||||
FakePlayerBotContext.setSlot(79, "drop", "Adamantite Nugget");
|
||||
FakePlayerBotContext.setSlot(79, "mob", "Dion Grizzly");
|
||||
final String dropLine = lines.speak(79, "reply_drop", Map.of("personality", "neutral"), Map.of("player", "Vanger"), false, null);
|
||||
System.out.println(" reply_drop with grounding slots: " + dropLine);
|
||||
check(dropLine != null, "reply_drop with slots from context");
|
||||
final String noDrop = lines.speak(80, "reply_drop", Map.of("personality", "neutral"), Map.of("player", "Vanger"), false, null);
|
||||
System.out.println(" reply_drop without drop slot: " + noDrop);
|
||||
check(noDrop != null, "reply_drop must have slot-free lines");
|
||||
|
||||
// 3. Bot to bot openers are understood by the listener (intent) or fall back to overheard.
|
||||
System.out.println("== bot_talk -> intents ==");
|
||||
final Map<String, Integer> distribution = new LinkedHashMap<>();
|
||||
int understood = 0;
|
||||
final int openers = 200;
|
||||
for (int i = 0; i < openers; i++)
|
||||
{
|
||||
final int speaker = 100 + (i % 20);
|
||||
final String opener = lines.speak(speaker, "bot_talk", Map.of("personality", "neutral"), Map.of("player", "Kolyan", "level", "35"), false, null);
|
||||
if (opener == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
final FakePlayerIntentParser.Intent intent = intents.parse(opener);
|
||||
final String key = (intent != null) ? intent.key : (FakePlayerHeardManager.isQuestionLine(opener) ? "(overheard_q)" : "(overheard)");
|
||||
distribution.merge(key, 1, Integer::sum);
|
||||
if (intent != null)
|
||||
{
|
||||
understood++;
|
||||
}
|
||||
if (i < 12)
|
||||
{
|
||||
System.out.println(" \"" + opener + "\" -> " + key);
|
||||
}
|
||||
}
|
||||
System.out.println(" intents: " + distribution);
|
||||
check(understood >= (openers / 2), "at least half of bot_talk openers should map to an intent (got " + understood + "/" + openers + ")");
|
||||
|
||||
// 4. Sample dialogue as the social layer would run it (opener -> reply seam of the intent).
|
||||
System.out.println("== sample dialogues ==");
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
final int a = 300 + i;
|
||||
final int b = 400 + i;
|
||||
final String opener = lines.speak(a, "bot_talk", Map.of("personality", "neutral"), Map.of("player", "Zheka"), false, null);
|
||||
final FakePlayerIntentParser.Intent intent = (opener != null) ? intents.parse(opener) : null;
|
||||
String reply;
|
||||
if ((intent != null) && !intent.seam.isEmpty())
|
||||
{
|
||||
final Map<String, String> replySlots = new HashMap<>(slots);
|
||||
replySlots.put("player", "Vovan");
|
||||
reply = lines.speak(b, intent.seam, Map.of("personality", "helper", "asker", "clanmate"), replySlots, false, FakePlayerIdiolect.Mirror.of(opener));
|
||||
}
|
||||
else
|
||||
{
|
||||
reply = lines.speak(b, (opener != null) && FakePlayerHeardManager.isQuestionLine(opener) ? "overheard_q" : "overheard", Map.of("personality", "helper"), Map.of("msg", FakePlayerHeardManager.echoOf(opener != null ? opener : ""), "player", "Vovan"), true, null);
|
||||
}
|
||||
System.out.println(" Vovan: " + opener);
|
||||
System.out.println(" Zheka: " + reply + " [" + ((intent != null) ? intent.key : "overheard") + "]");
|
||||
}
|
||||
|
||||
// 5. Idiolect: two bots never sound the same.
|
||||
System.out.println("== idiolect ==");
|
||||
final FakePlayerIdiolect one = FakePlayerIdiolect.of(1001);
|
||||
final FakePlayerIdiolect two = FakePlayerIdiolect.of(1002);
|
||||
check(!(one.laugh.equals(two.laugh) && one.filler.equals(two.filler) && (one.chattiness == two.chattiness) && (one.slang == two.slang)), "idiolects should differ");
|
||||
System.out.println(" 1001: laugh=" + one.laugh + " filler=" + one.filler + " slang=" + one.slang + " mat=" + one.traitMat);
|
||||
System.out.println(" 1002: laugh=" + two.laugh + " filler=" + two.filler + " slang=" + two.slang + " mat=" + two.traitMat);
|
||||
|
||||
// 6. Heard classification helpers.
|
||||
check(FakePlayerHeardManager.isQuestionLine("кто знает где рб"), "cyrillic question detection");
|
||||
check(!FakePlayerHeardManager.isQuestionLine("фарм идет норм"), "statement is not a question");
|
||||
check(FakePlayerHeardManager.isCommercialLine("продам соски дешево"), "commercial detection");
|
||||
check("продам соски дешево".startsWith(FakePlayerHeardManager.echoOf("продам соски дешево налетай")), "echo takes first words");
|
||||
|
||||
// 7. Intents that sieges / economy rely on.
|
||||
final FakePlayerIntentParser.Intent drop = intents.parse("дроп есть?");
|
||||
check((drop != null) && "ask_drop".equals(drop.key), "ask_drop intent");
|
||||
final FakePlayerIntentParser.Intent level = intents.parse("какой лвл");
|
||||
check((level != null) && "ask_level".equals(level.key), "ask_level intent");
|
||||
final FakePlayerIntentParser.Intent party = intents.parse("го в пати");
|
||||
check((party != null) && "ask_party".equals(party.key), "ask_party intent");
|
||||
final FakePlayerIntentParser.Intent how = intents.parse("как фарм?");
|
||||
check((how != null) && "how_are_you".equals(how.key), "how_are_you intent");
|
||||
final String[][] regression =
|
||||
{
|
||||
{ "го пвп", "duel" }, { "помгите", "ask_help" }, { "дуель?", "duel" }, { "ты бот?", "botcheck" }, { "спс бро", "thanks" },
|
||||
{ "лох", "insult" }, { "где ты", "ask_where" }, { "что нового", "ask_rumors" }, { "пока", "bye" }, { "прив", "greeting" },
|
||||
{ "ок", "yes_ok" }, { "сколько стоит", "ask_price" }, { "клан есть?", "ask_clan" }, { "ты соло?", "ask_solo" },
|
||||
{ "давно тут стоишь?", "ask_long" }, { "пк ходят?", "ask_pk" }, { "здарова, как жизнь молодая?", "how_are_you" },
|
||||
{ "где качаться думаешь", "ask_where" }
|
||||
};
|
||||
for (String[] pair : regression)
|
||||
{
|
||||
final FakePlayerIntentParser.Intent parsed = intents.parse(pair[0]);
|
||||
final String got = (parsed != null) ? parsed.key : "null";
|
||||
check(got.equals(pair[1]), "intent of \"" + pair[0] + "\": expected " + pair[1] + ", got " + got);
|
||||
}
|
||||
final String[] mustBeNull = { "думаешь тут фармить?", "молодая", "стоишь тут" };
|
||||
for (String text : mustBeNull)
|
||||
{
|
||||
final FakePlayerIntentParser.Intent parsed = intents.parse(text);
|
||||
check((parsed == null) || "ask_long".equals(parsed.key) || "ask_where".equals(parsed.key), "no false positive for \"" + text + "\" (got " + ((parsed != null) ? parsed.key : "null") + ")");
|
||||
}
|
||||
|
||||
System.out.println("== " + (checks - failures) + "/" + checks + " checks passed ==");
|
||||
System.exit(failures == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user