- 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>
356 lines
8.7 KiB
Go
356 lines
8.7 KiB
Go
package prompt
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db"
|
|
)
|
|
|
|
func TestAssemble_containsGameRules(t *testing.T) {
|
|
r := Request{
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
for _, want := range []string{"toroidal", "energy", "spawn", "focus-fire"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("expected prompt to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssemble_islandContext(t *testing.T) {
|
|
tests := []struct {
|
|
island string
|
|
keyword string
|
|
}{
|
|
{evolverdb.IslandAlpha, "aggressive"},
|
|
{evolverdb.IslandBeta, "energy-focused"},
|
|
{evolverdb.IslandGamma, "defensive"},
|
|
{evolverdb.IslandDelta, "experimental"},
|
|
}
|
|
for _, tc := range tests {
|
|
r := Request{Island: tc.island, TargetLang: "go", Generation: 2}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, tc.keyword) {
|
|
t.Errorf("island %s: expected %q in prompt", tc.island, tc.keyword)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssemble_targetLanguageAppears(t *testing.T) {
|
|
for _, lang := range []string{"go", "python", "rust", "typescript", "java", "php"} {
|
|
r := Request{Island: evolverdb.IslandDelta, TargetLang: lang, Generation: 0}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "```"+lang) {
|
|
t.Errorf("lang %s: expected fenced block in prompt", lang)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssemble_parentCodeEmbedded(t *testing.T) {
|
|
parents := []*evolverdb.Program{
|
|
{
|
|
ID: 42,
|
|
Code: "func main() { /* gatherer */ }",
|
|
Language: "go",
|
|
Fitness: 0.75,
|
|
BehaviorVector: []float64{0.1, 0.9},
|
|
},
|
|
}
|
|
r := Request{
|
|
Parents: parents,
|
|
Island: evolverdb.IslandBeta,
|
|
TargetLang: "go",
|
|
Generation: 3,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "func main() { /* gatherer */ }") {
|
|
t.Error("expected parent code to be embedded in the prompt")
|
|
}
|
|
if !strings.Contains(got, "fitness: 0.750") {
|
|
t.Error("expected parent fitness to appear in the prompt")
|
|
}
|
|
if !strings.Contains(got, "aggression=0.10") {
|
|
t.Error("expected behavior vector to appear in the prompt")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_replayAnalysis(t *testing.T) {
|
|
replays := []MatchSummary{
|
|
{
|
|
MatchID: "match-001",
|
|
WinnerName: "rusher",
|
|
LoserName: "gatherer",
|
|
Condition: "elimination",
|
|
TurnCount: 123,
|
|
Scores: []int{42, 10},
|
|
Strategies: []string{"core rush", "aggressive spawn"},
|
|
Weaknesses: []string{"exposed energy lines", "slow response"},
|
|
KeyMoments: []string{"Turn 50: rusher surrounded gatherer core"},
|
|
},
|
|
}
|
|
r := Request{
|
|
Replays: replays,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "rust",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "match-001") {
|
|
t.Error("expected match ID in prompt")
|
|
}
|
|
if !strings.Contains(got, "rusher defeated gatherer") {
|
|
t.Error("expected match result in prompt")
|
|
}
|
|
if !strings.Contains(got, "core rush") {
|
|
t.Error("expected strategies in prompt")
|
|
}
|
|
if !strings.Contains(got, "Turn 50: rusher surrounded gatherer core") {
|
|
t.Error("expected key moment in prompt")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_metaDescription(t *testing.T) {
|
|
meta := MetaDescription{
|
|
TotalBots: 12,
|
|
DominantStrategy: "energy-focused economy",
|
|
TopBots: []BotSummary{
|
|
{Name: "gatherer", Rating: 1600, Island: "beta", Evolved: false},
|
|
{Name: "evo-001", Rating: 1550, Island: "alpha", Evolved: true},
|
|
},
|
|
IslandStats: map[string]IslandStat{
|
|
"alpha": {Count: 3, AvgFitness: 0.5, TopFitness: 0.9},
|
|
},
|
|
}
|
|
r := Request{
|
|
Meta: meta,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 5,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "12") {
|
|
t.Error("expected total bot count in prompt")
|
|
}
|
|
if !strings.Contains(got, "energy-focused economy") {
|
|
t.Error("expected dominant strategy in prompt")
|
|
}
|
|
if !strings.Contains(got, "gatherer") {
|
|
t.Error("expected top bot name in prompt")
|
|
}
|
|
if !strings.Contains(got, "evolved") {
|
|
t.Error("expected evolved flag for evo-001 in prompt")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_emptyMeta_noMetaSection(t *testing.T) {
|
|
r := Request{
|
|
Island: evolverdb.IslandDelta,
|
|
TargetLang: "python",
|
|
Generation: 0,
|
|
}
|
|
got := Assemble(r)
|
|
// Meta section heading should not appear when meta is empty.
|
|
if strings.Contains(got, "## Current Meta") {
|
|
t.Error("expected no meta section when meta is empty")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_generationAppearsInIslandContext(t *testing.T) {
|
|
r := Request{
|
|
Island: evolverdb.IslandGamma,
|
|
TargetLang: "java",
|
|
Generation: 7,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "generation 7") {
|
|
t.Error("expected generation number in island context")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_emptyParents_noParentSection(t *testing.T) {
|
|
r := Request{
|
|
Parents: nil,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
if strings.Contains(got, "## Parent Programs") {
|
|
t.Error("expected no parent section when parents is nil")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_emptyReplays_noReplaySection(t *testing.T) {
|
|
r := Request{
|
|
Replays: nil,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
if strings.Contains(got, "## Recent Match Analysis") {
|
|
t.Error("expected no replay section when replays is nil")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_multipleReplays(t *testing.T) {
|
|
replays := []MatchSummary{
|
|
{MatchID: "m1", WinnerName: "w1", Condition: "elimination", TurnCount: 100},
|
|
{MatchID: "m2", WinnerName: "w2", Condition: "dominance", TurnCount: 200},
|
|
{MatchID: "m3", WinnerName: "w3", Condition: "turns", TurnCount: 500},
|
|
}
|
|
r := Request{
|
|
Replays: replays,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
|
|
for _, id := range []string{"m1", "m2", "m3"} {
|
|
if !strings.Contains(got, id) {
|
|
t.Errorf("expected match ID %s in prompt", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssemble_drawResult(t *testing.T) {
|
|
replays := []MatchSummary{
|
|
{MatchID: "draw-match", Condition: "draw", TurnCount: 500},
|
|
}
|
|
r := Request{
|
|
Replays: replays,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "Draw") {
|
|
t.Error("expected Draw in prompt for draw condition")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_allIslandsHaveContext(t *testing.T) {
|
|
for _, island := range evolverdb.AllIslands {
|
|
r := Request{
|
|
Island: island,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, island) {
|
|
t.Errorf("expected island %s in prompt", island)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssemble_behaviorVectorDisplay(t *testing.T) {
|
|
parents := []*evolverdb.Program{
|
|
{
|
|
ID: 1,
|
|
Code: "code",
|
|
Language: "go",
|
|
Fitness: 0.5,
|
|
BehaviorVector: []float64{0.25, 0.75},
|
|
},
|
|
}
|
|
r := Request{
|
|
Parents: parents,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "aggression=0.25") {
|
|
t.Error("expected aggression value in prompt")
|
|
}
|
|
if !strings.Contains(got, "economy=0.75") {
|
|
t.Error("expected economy value in prompt")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_parentWithoutBehaviorVector(t *testing.T) {
|
|
parents := []*evolverdb.Program{
|
|
{
|
|
ID: 1,
|
|
Code: "code",
|
|
Language: "go",
|
|
Fitness: 0.5,
|
|
BehaviorVector: nil, // No behavior vector
|
|
},
|
|
}
|
|
r := Request{
|
|
Parents: parents,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
// Should still include the parent, just without behavior info
|
|
if !strings.Contains(got, "code") {
|
|
t.Error("expected parent code in prompt even without behavior vector")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_codeBlockLanguage(t *testing.T) {
|
|
parents := []*evolverdb.Program{
|
|
{Code: "code", Language: "python", Fitness: 0.5},
|
|
}
|
|
r := Request{
|
|
Parents: parents,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "python",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "```python") {
|
|
t.Error("expected python code block in prompt")
|
|
}
|
|
}
|
|
|
|
func TestAssemble_scoresDisplay(t *testing.T) {
|
|
replays := []MatchSummary{
|
|
{
|
|
MatchID: "m1",
|
|
Scores: []int{100, 50, 25},
|
|
Condition: "turns",
|
|
TurnCount: 100,
|
|
},
|
|
}
|
|
r := Request{
|
|
Replays: replays,
|
|
Island: evolverdb.IslandAlpha,
|
|
TargetLang: "go",
|
|
Generation: 1,
|
|
}
|
|
got := Assemble(r)
|
|
if !strings.Contains(got, "[100 50 25]") {
|
|
t.Error("expected scores to be displayed")
|
|
}
|
|
}
|
|
|
|
func TestLangDisplayName(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"go", "Go"},
|
|
{"python", "Python"},
|
|
{"rust", "Rust"},
|
|
{"typescript", "TypeScript"},
|
|
{"java", "Java"},
|
|
{"php", "PHP"},
|
|
{"unknown", "unknown"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
got := langDisplayName(tc.input)
|
|
if got != tc.expected {
|
|
t.Errorf("langDisplayName(%q) = %q, want %q", tc.input, got, tc.expected)
|
|
}
|
|
}
|
|
}
|