package main import ( "fmt" "math/rand" "testing" "time" ) func TestGenerateMapID(t *testing.T) { rng := rand.New(rand.NewSource(42)) id1 := generateMapID(rng) id2 := generateMapID(rng) // Check format if len(id1) != 12 { // "map_" + 8 chars t.Errorf("Expected ID length 12, got %d", len(id1)) } if id1[:4] != "map_" { t.Errorf("Expected ID to start with 'map_', got %s", id1[:4]) } // Check uniqueness if id1 == id2 { t.Error("Expected different IDs") } } func TestSelectWeighted(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2}, rng: rand.New(rand.NewSource(time.Now().UnixNano())), } parents := []*ParentMap{ {Engagement: 10.0, VoteMult: 1.0}, {Engagement: 5.0, VoteMult: 1.0}, {Engagement: 2.0, VoteMult: 1.0}, } // Run selection many times and count counts := make(map[int]int) for i := 0; i < 1000; i++ { selected := evolver.selectWeighted(parents) for idx, p := range parents { if selected == p { counts[idx]++ break } } } // Parent 0 should be selected most often (highest weight) if counts[0] < counts[1] || counts[0] < counts[2] { t.Errorf("Expected parent 0 to be selected most often, got counts: %v", counts) } } func TestCrossover(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2}, rng: rand.New(rand.NewSource(42)), } p1 := &Map{ Players: 2, Rows: 40, Cols: 40, WallDensity: 0.15, Walls: []Position{ {Row: 10, Col: 10}, {Row: 10, Col: 11}, {Row: 10, Col: 12}, }, Cores: []Core{ {Position: Position{Row: 10, Col: 20}, Owner: 0}, {Position: Position{Row: 30, Col: 20}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 15, Col: 15}, {Row: 25, Col: 25}, }, } p2 := &Map{ Players: 2, Rows: 40, Cols: 40, WallDensity: 0.20, Walls: []Position{ {Row: 20, Col: 20}, {Row: 20, Col: 21}, }, Cores: []Core{ {Position: Position{Row: 10, Col: 20}, Owner: 0}, {Position: Position{Row: 30, Col: 20}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 18, Col: 18}, {Row: 22, Col: 22}, }, } child := evolver.crossover(p1, p2) if child == nil { t.Fatal("Expected child map, got nil") } if child.Rows != p1.Rows { t.Errorf("Expected rows %d, got %d", p1.Rows, child.Rows) } if child.Cols != p1.Cols { t.Errorf("Expected cols %d, got %d", p1.Cols, child.Cols) } if len(child.Cores) != len(p1.Cores) { t.Errorf("Expected %d cores, got %d", len(p1.Cores), len(child.Cores)) } } func TestValidate(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2}, rng: rand.New(rand.NewSource(42)), } // Valid map validMap := &Map{ Players: 2, Rows: 60, Cols: 60, WallDensity: 0.15, Walls: []Position{ {Row: 10, Col: 10}, // Some walls far from cores {Row: 10, Col: 11}, }, Cores: []Core{ {Position: Position{Row: 15, Col: 30}, Owner: 0}, {Position: Position{Row: 45, Col: 30}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 20, Col: 25}, {Row: 20, Col: 35}, {Row: 40, Col: 25}, {Row: 40, Col: 35}, {Row: 30, Col: 30}, {Row: 30, Col: 31}, }, } if !evolver.validate(validMap) { t.Error("Expected valid map to pass validation") } // Invalid map: wall density too high invalidMap := &Map{ Players: 2, Rows: 60, Cols: 60, WallDensity: 0.50, // Too high Walls: make([]Position, 1800), // 50% density Cores: []Core{ {Position: Position{Row: 15, Col: 30}, Owner: 0}, {Position: Position{Row: 45, Col: 30}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 20, Col: 25}, {Row: 40, Col: 35}, }, } if evolver.validate(invalidMap) { t.Error("Expected invalid map (high density) to fail validation") } } func TestCheckConnectivity(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2}, rng: rand.New(rand.NewSource(42)), } // Connected map connected := &Map{ Players: 2, Rows: 20, Cols: 20, Walls: []Position{}, Cores: []Core{ {Position: Position{Row: 5, Col: 10}, Owner: 0}, {Position: Position{Row: 15, Col: 10}, Owner: 1}, }, } if !evolver.checkConnectivity(connected) { t.Error("Expected connected map to pass connectivity check") } // Disconnected map (walls blocking) disconnected := &Map{ Players: 2, Rows: 20, Cols: 20, Walls: []Position{}, Cores: []Core{ {Position: Position{Row: 0, Col: 0}, Owner: 0}, {Position: Position{Row: 10, Col: 10}, Owner: 1}, }, } // Add a ring of walls around position (5,5) for d := 0; d < 20; d++ { // Top/bottom walls if d > 0 && d < 19 { disconnected.Walls = append(disconnected.Walls, Position{Row: 4, Col: d}) disconnected.Walls = append(disconnected.Walls, Position{Row: 6, Col: d}) } // Left/right walls disconnected.Walls = append(disconnected.Walls, Position{Row: d, Col: 4}) disconnected.Walls = append(disconnected.Walls, Position{Row: d, Col: 6}) } // This test is tricky because toroidal wrapping means all positions are reachable // For a proper disconnected test, we'd need to fill most of the grid with walls // Skip this test for now since toroidal grids are inherently connected t.Log("Skipping disconnected test - toroidal grids are inherently connected") } func TestCountReachableEnergyNodes(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2}, rng: rand.New(rand.NewSource(42)), } m := &Map{ Players: 2, Rows: 20, Cols: 20, Walls: []Position{}, Cores: []Core{ {Position: Position{Row: 5, Col: 10}, Owner: 0}, }, EnergyNodes: []Position{ {Row: 6, Col: 10}, // 1 step away {Row: 7, Col: 10}, // 2 steps away {Row: 8, Col: 10}, // 3 steps away {Row: 15, Col: 15}, // Far away }, } count := evolver.countReachableEnergyNodes(m, m.Cores[0].Position) if count != 4 { t.Errorf("Expected 4 reachable energy nodes, got %d", count) } } func TestCanReach(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2}, rng: rand.New(rand.NewSource(42)), } m := &Map{ Players: 2, Rows: 20, Cols: 20, Walls: []Position{}, } start := Position{Row: 0, Col: 0} end := Position{Row: 19, Col: 19} if !evolver.canReach(m, start, end) { t.Error("Expected positions to be reachable on empty grid") } // With a wall blocking (toroidal, so nothing truly blocks) m.Walls = []Position{{Row: 10, Col: 10}} if !evolver.canReach(m, start, end) { t.Error("Expected positions to still be reachable around wall") } } func TestSmokeTest(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2}, rng: rand.New(rand.NewSource(42)), } // Good map goodMap := &Map{ Players: 2, Rows: 60, Cols: 60, WallDensity: 0.10, Walls: []Position{}, Cores: []Core{ {Position: Position{Row: 15, Col: 30}, Owner: 0}, {Position: Position{Row: 45, Col: 30}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 20, Col: 20}, {Row: 20, Col: 40}, {Row: 40, Col: 20}, {Row: 40, Col: 40}, {Row: 30, Col: 30}, {Row: 30, Col: 31}, {Row: 30, Col: 29}, }, } if !evolver.smokeTest(goodMap) { t.Error("Expected good map to pass smoke test") } // Bad map: not enough energy nodes badMap := &Map{ Players: 2, Rows: 60, Cols: 60, WallDensity: 0.10, Walls: []Position{}, Cores: []Core{ {Position: Position{Row: 15, Col: 30}, Owner: 0}, {Position: Position{Row: 45, Col: 30}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 20, Col: 20}, }, } if evolver.smokeTest(badMap) { t.Error("Expected bad map (few energy nodes) to fail smoke test") } } func TestMutate(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2}, rng: rand.New(rand.NewSource(42)), } // Create a map with symmetric walls in sector 0 // For 2 players, sector 0 is the right half (angle -π/2 to π/2 from center) // Center is at (20, 20). Sector 0 is cols >= 20. walls := make([]Position, 0) rows := 40 cols := 40 // Add walls in sector 0 (right side), then mirror to sector 1 (left side) for row := 5; row < 35; row++ { for col := 21; col < 35; col++ { // Start from col 21 (right of center) // Skip positions near cores and energy nodes if (row >= 7 && row <= 13 && col >= 17 && col <= 23) || // Near core 0 (row >= 27 && row <= 33 && col >= 17 && col <= 23) { // Near core 1 continue } if (row >= 13 && row <= 17 && col >= 13 && col <= 17) || // Near energy 1 (row >= 13 && row <= 17 && col >= 23 && col <= 27) || // Near energy 2 (row >= 23 && row <= 27 && col >= 13 && col <= 17) || // Near energy 3 (row >= 23 && row <= 27 && col >= 23 && col <= 27) { // Near energy 4 continue } // Only add ~30% of possible positions to get reasonable density if (row+col)%3 == 0 { // Add wall in sector 0 walls = append(walls, Position{Row: row, Col: col}) // Mirror to sector 1 (180 degree rotation) mirrorRow := (rows - row) % rows mirrorCol := (cols - col) % cols if mirrorCol != col || mirrorRow != row { // Don't duplicate center positions walls = append(walls, Position{Row: mirrorRow, Col: mirrorCol}) } } } } original := &Map{ Players: 2, Rows: rows, Cols: cols, WallDensity: float64(len(walls)) / float64(rows*cols), Walls: walls, Cores: []Core{ {Position: Position{Row: 10, Col: 20}, Owner: 0}, {Position: Position{Row: 30, Col: 20}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 15, Col: 15}, {Row: 15, Col: 25}, {Row: 25, Col: 15}, {Row: 25, Col: 25}, }, } originalWallCount := len(original.Walls) t.Logf("Initial walls: %d, density: %.3f", originalWallCount, original.WallDensity) evolver.mutate(original) t.Logf("After mutation walls: %d, density: %.3f", len(original.Walls), original.WallDensity) // After mutation, verify the map structure is valid // Verify walls exist if len(original.Walls) == 0 { t.Error("Expected some walls to remain after mutation") } } func TestBreed(t *testing.T) { evolver := &MapEvolver{ cfg: &Config{PlayerCount: 2, NumOffspring: 5, MaxAttempts: 10}, rng: rand.New(rand.NewSource(42)), } parents := []*ParentMap{ { Map: &Map{ Players: 2, Rows: 40, Cols: 40, WallDensity: 0.10, Walls: []Position{}, Cores: []Core{ {Position: Position{Row: 10, Col: 20}, Owner: 0}, {Position: Position{Row: 30, Col: 20}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 15, Col: 15}, {Row: 25, Col: 25}, }, }, Engagement: 10.0, VoteMult: 1.0, }, { Map: &Map{ Players: 2, Rows: 40, Cols: 40, WallDensity: 0.10, Walls: []Position{}, Cores: []Core{ {Position: Position{Row: 10, Col: 20}, Owner: 0}, {Position: Position{Row: 30, Col: 20}, Owner: 1}, }, EnergyNodes: []Position{ {Row: 18, Col: 18}, {Row: 22, Col: 22}, }, }, Engagement: 8.0, VoteMult: 1.0, }, } child := evolver.breed(parents) if child == nil { t.Fatal("Expected child map, got nil") } if child.Players != 2 { t.Errorf("Expected 2 players, got %d", child.Players) } if child.ID == "" { t.Error("Expected child to have an ID") } if child.ID[:4] != "map_" { t.Errorf("Expected ID to start with 'map_', got %s", child.ID) } } func TestNextScheduledTime(t *testing.T) { // Create a fixed time for testing: Monday, May 12, 2025 at 10:00 UTC baseTime := time.Date(2025, 5, 12, 10, 0, 0, 0, time.UTC) // Monday tests := []struct { name string now time.Time schedule WeeklySchedule wantScheduled string // Expected scheduled time in RFC3339 wantHour int wantMinute int wantWeekday time.Weekday }{ { name: "Sunday 03:00 when current time is Monday 10:00", now: baseTime, // Monday 10:00 schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, wantScheduled: "2025-05-18T03:00:00Z", // Next Sunday wantHour: 3, wantMinute: 0, wantWeekday: time.Sunday, }, { name: "Monday 03:00 when current time is Monday 10:00 (should schedule next Monday)", now: baseTime, // Monday 10:00 schedule: WeeklySchedule{Weekday: time.Monday, Hour: 3, Minute: 0}, wantScheduled: "2025-05-19T03:00:00Z", // Next Monday (passed today) wantHour: 3, wantMinute: 0, wantWeekday: time.Monday, }, { name: "Monday 15:00 when current time is Monday 10:00 (same day, future)", now: baseTime, // Monday 10:00 schedule: WeeklySchedule{Weekday: time.Monday, Hour: 15, Minute: 0}, wantScheduled: "2025-05-12T15:00:00Z", // Same day wantHour: 15, wantMinute: 0, wantWeekday: time.Monday, }, { name: "Wednesday 12:30 when current time is Monday 10:00", now: baseTime, // Monday 10:00 schedule: WeeklySchedule{Weekday: time.Wednesday, Hour: 12, Minute: 30}, wantScheduled: "2025-05-14T12:30:00Z", // 2 days from now wantHour: 12, wantMinute: 30, wantWeekday: time.Wednesday, }, { name: "Saturday 23:59 when current time is Monday 10:00", now: baseTime, // Monday 10:00 schedule: WeeklySchedule{Weekday: time.Saturday, Hour: 23, Minute: 59}, wantScheduled: "2025-05-17T23:59:00Z", // 5 days from now wantHour: 23, wantMinute: 59, wantWeekday: time.Saturday, }, { name: "Default schedule (Sunday 03:00) on Saturday before midnight", now: time.Date(2025, 5, 10, 23, 0, 0, 0, time.UTC), // Saturday 23:00 schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, wantScheduled: "2025-05-11T03:00:00Z", // Next day (Sunday) wantHour: 3, wantMinute: 0, wantWeekday: time.Sunday, }, { name: "Default schedule (Sunday 03:00) on Sunday after 03:00", now: time.Date(2025, 5, 11, 5, 0, 0, 0, time.UTC), // Sunday 05:00 schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, wantScheduled: "2025-05-18T03:00:00Z", // Next week (passed today) wantHour: 3, wantMinute: 0, wantWeekday: time.Sunday, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Calculate expected result manually (same logic as nextScheduledTime) scheduled := time.Date(tt.now.Year(), tt.now.Month(), tt.now.Day(), tt.schedule.Hour, tt.schedule.Minute, 0, 0, time.UTC) // Add days until correct weekday daysUntil := int(tt.schedule.Weekday) - int(tt.now.Weekday()) if daysUntil < 0 { daysUntil += 7 } scheduled = scheduled.AddDate(0, 0, daysUntil) // If scheduled time has passed, move to next week if scheduled.Before(tt.now) || scheduled.Equal(tt.now) { scheduled = scheduled.Add(7 * 24 * time.Hour) } // Verify the scheduled time matches expected gotScheduled := scheduled.Format(time.RFC3339) if gotScheduled != tt.wantScheduled { t.Errorf("Expected scheduled time %s, got %s (from now: %s)", tt.wantScheduled, gotScheduled, tt.now.Format(time.RFC3339)) } if scheduled.Hour() != tt.wantHour { t.Errorf("Expected hour %d, got %d", tt.wantHour, scheduled.Hour()) } if scheduled.Minute() != tt.wantMinute { t.Errorf("Expected minute %d, got %d", tt.wantMinute, scheduled.Minute()) } // Verify weekday matches if scheduled.Weekday() != tt.wantWeekday { t.Errorf("Expected weekday %v, got %v", tt.wantWeekday, scheduled.Weekday()) } }) } } func TestWeeklyScheduleEnvParsing(t *testing.T) { // Test that environment variable parsing works correctly // Format: "WEEKDAY:HH:MM" e.g., "0:03:00" for Sunday 03:00 tests := []struct { name string envValue string wantParseError bool // Whether Sscanf itself should fail wantValid bool // Whether the parsed values are valid for use wantDay time.Weekday wantHour int wantMinute int }{ { name: "Valid schedule Sunday 03:00", envValue: "0:03:00", wantParseError: false, wantValid: true, wantDay: time.Sunday, wantHour: 3, wantMinute: 0, }, { name: "Valid schedule Wednesday 15:30", envValue: "3:15:30", wantParseError: false, wantValid: true, wantDay: time.Wednesday, wantHour: 15, wantMinute: 30, }, { name: "Valid schedule Saturday 23:59", envValue: "6:23:59", wantParseError: false, wantValid: true, wantDay: time.Saturday, wantHour: 23, wantMinute: 59, }, { name: "Invalid weekday 7 (out of range 0-6)", envValue: "7:03:00", wantParseError: false, // Sscanf parses it fine wantValid: false, // But validation rejects it }, { name: "Invalid hour 24 (out of range 0-23)", envValue: "0:24:00", wantParseError: false, // Sscanf parses it fine wantValid: false, // But validation rejects it }, { name: "Invalid minute 60 (out of range 0-59)", envValue: "0:03:60", wantParseError: false, // Sscanf parses it fine wantValid: false, // But validation rejects it }, { name: "Invalid format", envValue: "invalid", wantParseError: true, // Sscanf fails wantValid: false, }, { name: "Incomplete format", envValue: "0:03", wantParseError: true, // Sscanf returns n < 3 wantValid: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var weekday, hour, minute int n, err := fmt.Sscanf(tt.envValue, "%d:%d:%d", &weekday, &hour, &minute) hasParseError := (err != nil) || (n != 3) // Check parse error expectation if hasParseError != tt.wantParseError { if tt.wantParseError { t.Errorf("Expected parse error, but got: weekday=%d, hour=%d, minute=%d (n=%d, err=%v)", weekday, hour, minute, n, err) } else { t.Errorf("Unexpected parse error: %v (n=%d)", err, n) } return } // If parse succeeded, check validation if !hasParseError { isValid := (weekday >= 0 && weekday <= 6 && hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) if isValid != tt.wantValid { if tt.wantValid { t.Errorf("Expected values to be valid, but validation would reject: weekday=%d (range 0-6), hour=%d (range 0-23), minute=%d (range 0-59)", weekday, hour, minute) } else { t.Errorf("Expected values to be invalid, but validation would accept: weekday=%d, hour=%d, minute=%d", weekday, hour, minute) } return } // If valid, check the parsed values match expectations if tt.wantValid { gotDay := time.Weekday(weekday) if gotDay != tt.wantDay { t.Errorf("Expected weekday %v, got %v", tt.wantDay, gotDay) } if hour != tt.wantHour { t.Errorf("Expected hour %d, got %d", tt.wantHour, hour) } if minute != tt.wantMinute { t.Errorf("Expected minute %d, got %d", tt.wantMinute, minute) } } } }) } }