ai-code-battle/cmd/acb-evolver/internal/prompt/convert_test.go
jedarden 0813e36297 fix(evolver): wire Nash mixture and meta weaknesses into LLM prompts, fix 4-D diversity
- Add NashMixture and MetaWeaknesses fields to meta.Description and
  compute them from island population proportions (§10.2 PSRO)
- Update behaviorDistance to support N-D vectors for 4-D MAP-Elites
  grid (aggression, economy, exploration, formation)
- Wire NashMixture/MetaWeaknesses through FromMetaDescription converter
  so they actually reach the LLM prompt (was dead code before)
- Align LLM prompt with plan §15.1/§15.5: correct combat rules
  (focus-fire), fog of war, HTTP protocol section, Nash mixture target
- Fix diversity normalization from sqrt(2) (2-D) to 2.0 (4-D max)
- Rename handleUIFeedback to handleCreateFeedback (§13.6 naming)
- Update tests for new fields and corrected prompt text

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 01:22:19 -04:00

269 lines
7.5 KiB
Go

package prompt
import (
"testing"
evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db"
"github.com/aicodebattle/acb/cmd/acb-evolver/internal/meta"
"github.com/aicodebattle/acb/cmd/acb-evolver/internal/replay"
)
func TestFromReplayAnalysis_Nil(t *testing.T) {
got := FromReplayAnalysis(nil)
if got.MatchID != "" || got.WinnerName != "" {
t.Errorf("expected empty MatchSummary for nil input, got %+v", got)
}
}
func TestFromReplayAnalysis_Full(t *testing.T) {
analysis := &replay.Analysis{
MatchID: "match-123",
WinnerName: "Winner",
LoserName: "Loser",
Condition: "elimination",
TurnCount: 100,
Scores: []int{50, 20},
KeyMoments: []string{"moment1", "moment2"},
Strategies: []string{"strategy1"},
Weaknesses: []string{"weakness1"},
}
got := FromReplayAnalysis(analysis)
if got.MatchID != "match-123" {
t.Errorf("expected MatchID 'match-123', got %q", got.MatchID)
}
if got.WinnerName != "Winner" {
t.Errorf("expected WinnerName 'Winner', got %q", got.WinnerName)
}
if got.LoserName != "Loser" {
t.Errorf("expected LoserName 'Loser', got %q", got.LoserName)
}
if got.Condition != "elimination" {
t.Errorf("expected Condition 'elimination', got %q", got.Condition)
}
if got.TurnCount != 100 {
t.Errorf("expected TurnCount 100, got %d", got.TurnCount)
}
if len(got.Scores) != 2 || got.Scores[0] != 50 || got.Scores[1] != 20 {
t.Errorf("expected Scores [50, 20], got %v", got.Scores)
}
if len(got.KeyMoments) != 2 {
t.Errorf("expected 2 KeyMoments, got %d", len(got.KeyMoments))
}
if len(got.Strategies) != 1 {
t.Errorf("expected 1 Strategy, got %d", len(got.Strategies))
}
if len(got.Weaknesses) != 1 {
t.Errorf("expected 1 Weakness, got %d", len(got.Weaknesses))
}
}
func TestFromReplayAnalysis_SliceCopy(t *testing.T) {
analysis := &replay.Analysis{
Scores: []int{1, 2, 3},
KeyMoments: []string{"a", "b"},
}
got := FromReplayAnalysis(analysis)
// Modify original slices to ensure we have a copy
analysis.Scores[0] = 999
analysis.KeyMoments[0] = "modified"
if got.Scores[0] == 999 {
t.Error("expected Scores to be a copy, not a reference")
}
if got.KeyMoments[0] == "modified" {
t.Error("expected KeyMoments to be a copy, not a reference")
}
}
func TestFromReplayAnalyses_Nil(t *testing.T) {
got := FromReplayAnalyses(nil)
if got != nil {
t.Errorf("expected nil for nil input, got %v", got)
}
}
func TestFromReplayAnalyses_Empty(t *testing.T) {
got := FromReplayAnalyses([]*replay.Analysis{})
if got != nil {
t.Errorf("expected nil for empty input, got %v", got)
}
}
func TestFromReplayAnalyses_Multiple(t *testing.T) {
analyses := []*replay.Analysis{
{MatchID: "m1", WinnerName: "w1"},
{MatchID: "m2", WinnerName: "w2"},
}
got := FromReplayAnalyses(analyses)
if len(got) != 2 {
t.Fatalf("expected 2 summaries, got %d", len(got))
}
if got[0].MatchID != "m1" || got[1].MatchID != "m2" {
t.Errorf("expected match IDs m1, m2, got %v", got)
}
}
func TestFromMetaDescription_Nil(t *testing.T) {
got := FromMetaDescription(nil)
if got.TotalBots != 0 || got.DominantStrategy != "" {
t.Errorf("expected empty MetaDescription for nil input, got %+v", got)
}
}
func TestFromMetaDescription_Full(t *testing.T) {
desc := &meta.Description{
TotalBots: 42,
DominantStrategy: "aggressive",
NashMixture: "60% aggressive, 40% economy",
MetaWeaknesses: []string{"No bots exploring defense", "Low diversity in alpha"},
TopBots: []meta.BotInfo{
{Name: "bot1", Rating: 1600, Island: "alpha", Evolved: true},
{Name: "bot2", Rating: 1500, Island: "beta", Evolved: false},
},
IslandStats: map[string]meta.IslandStats{
"alpha": {Count: 10, AvgFitness: 0.5, TopFitness: 0.9, Diversity: 0.8},
},
}
got := FromMetaDescription(desc)
if got.TotalBots != 42 {
t.Errorf("expected TotalBots 42, got %d", got.TotalBots)
}
if got.DominantStrategy != "aggressive" {
t.Errorf("expected DominantStrategy 'aggressive', got %q", got.DominantStrategy)
}
if got.NashMixture != "60% aggressive, 40% economy" {
t.Errorf("expected NashMixture, got %q", got.NashMixture)
}
if len(got.MetaWeaknesses) != 2 || got.MetaWeaknesses[0] != "No bots exploring defense" {
t.Errorf("expected 2 MetaWeaknesses, got %v", got.MetaWeaknesses)
}
if len(got.TopBots) != 2 {
t.Errorf("expected 2 TopBots, got %d", len(got.TopBots))
}
if got.TopBots[0].Name != "bot1" || got.TopBots[0].Rating != 1600 {
t.Errorf("expected bot1 with rating 1600, got %+v", got.TopBots[0])
}
if len(got.IslandStats) != 1 {
t.Errorf("expected 1 IslandStats entry, got %d", len(got.IslandStats))
}
if stat, ok := got.IslandStats["alpha"]; !ok {
t.Error("expected alpha in IslandStats")
} else if stat.Count != 10 || stat.AvgFitness != 0.5 {
t.Errorf("expected Count=10, AvgFitness=0.5, got %+v", stat)
}
}
func TestFromBotInfos_Nil(t *testing.T) {
got := FromBotInfos(nil)
if got != nil {
t.Errorf("expected nil for nil input, got %v", got)
}
}
func TestFromBotInfos_Empty(t *testing.T) {
got := FromBotInfos([]meta.BotInfo{})
if got != nil {
t.Errorf("expected nil for empty input, got %v", got)
}
}
func TestFromBotInfos_Multiple(t *testing.T) {
bots := []meta.BotInfo{
{Name: "a", Rating: 100, Island: "x", Evolved: true},
{Name: "b", Rating: 200, Island: "y", Evolved: false},
}
got := FromBotInfos(bots)
if len(got) != 2 {
t.Fatalf("expected 2 bots, got %d", len(got))
}
if got[0].Name != "a" || got[0].Rating != 100 {
t.Errorf("expected a/100, got %+v", got[0])
}
if got[1].Name != "b" || got[1].Rating != 200 {
t.Errorf("expected b/200, got %+v", got[1])
}
}
func TestFromIslandStatsMap_Nil(t *testing.T) {
got := FromIslandStatsMap(nil)
if got != nil {
t.Errorf("expected nil for nil input, got %v", got)
}
}
func TestFromIslandStatsMap_Multiple(t *testing.T) {
stats := map[string]meta.IslandStats{
"alpha": {Count: 5, AvgFitness: 0.6, TopFitness: 0.95},
"beta": {Count: 3, AvgFitness: 0.4, TopFitness: 0.8},
}
got := FromIslandStatsMap(stats)
if len(got) != 2 {
t.Fatalf("expected 2 entries, got %d", len(got))
}
if got["alpha"].Count != 5 {
t.Errorf("expected alpha.Count=5, got %d", got["alpha"].Count)
}
if got["beta"].AvgFitness != 0.4 {
t.Errorf("expected beta.AvgFitness=0.4, got %f", got["beta"].AvgFitness)
}
}
func TestBuildRequest_Full(t *testing.T) {
parents := []*evolverdb.Program{
{ID: 1, Code: "code1", Language: "go", Fitness: 0.8},
}
analyses := []*replay.Analysis{
{MatchID: "m1", WinnerName: "w1"},
}
metaDesc := &meta.Description{
TotalBots: 10,
DominantStrategy: "aggressive",
}
req := BuildRequest(parents, analyses, metaDesc, "alpha", "go", 5)
if len(req.Parents) != 1 {
t.Errorf("expected 1 parent, got %d", len(req.Parents))
}
if len(req.Replays) != 1 {
t.Errorf("expected 1 replay, got %d", len(req.Replays))
}
if req.Meta.TotalBots != 10 {
t.Errorf("expected Meta.TotalBots=10, got %d", req.Meta.TotalBots)
}
if req.Island != "alpha" {
t.Errorf("expected Island 'alpha', got %q", req.Island)
}
if req.TargetLang != "go" {
t.Errorf("expected TargetLang 'go', got %q", req.TargetLang)
}
if req.Generation != 5 {
t.Errorf("expected Generation 5, got %d", req.Generation)
}
}
func TestBuildRequest_NilInputs(t *testing.T) {
req := BuildRequest(nil, nil, nil, "beta", "python", 0)
if req.Parents != nil {
t.Errorf("expected nil Parents, got %v", req.Parents)
}
if req.Replays != nil {
t.Errorf("expected nil Replays, got %v", req.Replays)
}
if req.Meta.TotalBots != 0 {
t.Errorf("expected empty Meta, got %+v", req.Meta)
}
}