feat(index): implement §7.2/§15.2 maps/index.json and maps/{map_id}.json generation
Adds generateMapsIndex to acb-index-builder, writing:
- maps/index.json — active/probation/classic maps grouped by player count
- maps/{map_id}.json — full map definition with walls, cores, energy_nodes
Queries maps.map_json column for geometry; adds RawJSON field to MapData
and updates fetchMaps to select it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
89560e5ec4
commit
b4a975f5bf
3 changed files with 466 additions and 21 deletions
|
|
@ -158,29 +158,47 @@ type PredictorStats struct {
|
|||
|
||||
// MapData represents a map for the index
|
||||
type MapData struct {
|
||||
MapID string `json:"map_id"`
|
||||
PlayerCount int `json:"player_count"`
|
||||
Status string `json:"status"`
|
||||
Engagement float64 `json:"engagement"`
|
||||
WallDensity float64 `json:"wall_density"`
|
||||
EnergyCount int `json:"energy_count"`
|
||||
GridWidth int `json:"grid_width"`
|
||||
GridHeight int `json:"grid_height"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
MapID string `json:"map_id"`
|
||||
PlayerCount int `json:"player_count"`
|
||||
Status string `json:"status"`
|
||||
Engagement float64 `json:"engagement"`
|
||||
WallDensity float64 `json:"wall_density"`
|
||||
EnergyCount int `json:"energy_count"`
|
||||
GridWidth int `json:"grid_width"`
|
||||
GridHeight int `json:"grid_height"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
RawJSON json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// OpenPredictionMatch represents a pending match open for predictions
|
||||
type OpenPredictionMatch struct {
|
||||
MatchID string `json:"match_id"`
|
||||
BotAID string `json:"bot_a"`
|
||||
BotBID string `json:"bot_b"`
|
||||
BotAName string `json:"bot_a_name"`
|
||||
BotBName string `json:"bot_b_name"`
|
||||
ARating float64 `json:"a_rating"`
|
||||
BRating float64 `json:"b_rating"`
|
||||
AEvolved bool `json:"a_evolved"`
|
||||
BEvolved bool `json:"b_evolved"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
IsSeriesMatch bool `json:"is_series_match"`
|
||||
HeadToHeadRecord *string `json:"head_to_head_record,omitempty"`
|
||||
}
|
||||
|
||||
// IndexData contains all data needed for index generation
|
||||
type IndexData struct {
|
||||
GeneratedAt time.Time
|
||||
Bots []BotData
|
||||
Matches []MatchData
|
||||
RatingHistory []RatingHistoryEntry
|
||||
Series []SeriesData
|
||||
Seasons []SeasonData
|
||||
Predictions []PredictionData
|
||||
PredictorStats []PredictorStats
|
||||
Maps []MapData
|
||||
TopPredictors []PredictorStats
|
||||
GeneratedAt time.Time
|
||||
Bots []BotData
|
||||
Matches []MatchData
|
||||
RatingHistory []RatingHistoryEntry
|
||||
Series []SeriesData
|
||||
Seasons []SeasonData
|
||||
Predictions []PredictionData
|
||||
PredictorStats []PredictorStats
|
||||
Maps []MapData
|
||||
TopPredictors []PredictorStats
|
||||
OpenPredictionMatches []OpenPredictionMatch
|
||||
}
|
||||
|
||||
// fetchAllData retrieves all data from PostgreSQL for index generation
|
||||
|
|
@ -214,6 +232,9 @@ func fetchAllData(ctx context.Context, db *sql.DB) (*IndexData, error) {
|
|||
if data.Maps, err = fetchMaps(ctx, db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data.OpenPredictionMatches, err = fetchOpenPredictions(ctx, db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data.TopPredictors = computeTopPredictors(data.PredictorStats)
|
||||
|
||||
|
|
@ -715,7 +736,7 @@ func fetchPredictorStats(ctx context.Context, db *sql.DB) ([]PredictorStats, err
|
|||
func fetchMaps(ctx context.Context, db *sql.DB) ([]MapData, error) {
|
||||
query := `
|
||||
SELECT map_id, player_count, status, engagement, wall_density,
|
||||
energy_count, grid_width, grid_height, created_at
|
||||
energy_count, grid_width, grid_height, created_at, map_json
|
||||
FROM maps
|
||||
WHERE status IN ('active', 'probation', 'classic')
|
||||
ORDER BY engagement DESC
|
||||
|
|
@ -732,7 +753,7 @@ func fetchMaps(ctx context.Context, db *sql.DB) ([]MapData, error) {
|
|||
var m MapData
|
||||
if err := rows.Scan(
|
||||
&m.MapID, &m.PlayerCount, &m.Status, &m.Engagement, &m.WallDensity,
|
||||
&m.EnergyCount, &m.GridWidth, &m.GridHeight, &m.CreatedAt,
|
||||
&m.EnergyCount, &m.GridWidth, &m.GridHeight, &m.CreatedAt, &m.RawJSON,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -742,6 +763,187 @@ func fetchMaps(ctx context.Context, db *sql.DB) ([]MapData, error) {
|
|||
return maps, nil
|
||||
}
|
||||
|
||||
// fetchOpenPredictions retrieves pending matches that are "predictable":
|
||||
// - Both bots are in the top 20
|
||||
// - It's a rivalry match (at least 3 previous h2h matches)
|
||||
// - It's a series match
|
||||
// - An evolved bot faces a top-10 human-written bot
|
||||
func fetchOpenPredictions(ctx context.Context, db *sql.DB) ([]OpenPredictionMatch, error) {
|
||||
// Get all pending matches with their participants
|
||||
query := `
|
||||
SELECT m.match_id, m.created_at,
|
||||
mp1.bot_id as bot_a_id, b1.name as bot_a_name,
|
||||
(b1.rating_mu - 2*b1.rating_phi) as bot_a_rating,
|
||||
b1.evolved as bot_a_evolved,
|
||||
mp2.bot_id as bot_b_id, b2.name as bot_b_name,
|
||||
(b2.rating_mu - 2*b2.rating_phi) as bot_b_rating,
|
||||
b2.evolved as bot_b_evolved,
|
||||
COALESCE(EXISTS(
|
||||
SELECT 1 FROM series_games sg
|
||||
WHERE sg.match_id = m.match_id
|
||||
), false) as is_series_match
|
||||
FROM matches m
|
||||
JOIN match_participants mp1 ON m.match_id = mp1.match_id AND mp1.player_slot = 0
|
||||
JOIN match_participants mp2 ON m.match_id = mp2.match_id AND mp2.player_slot = 1
|
||||
JOIN bots b1 ON mp1.bot_id = b1.bot_id
|
||||
JOIN bots b2 ON mp2.bot_id = b2.bot_id
|
||||
WHERE m.status = 'pending'
|
||||
ORDER BY m.created_at ASC
|
||||
LIMIT 50
|
||||
`
|
||||
|
||||
rows, err := db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query pending matches: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var allMatches []OpenPredictionMatch
|
||||
for rows.Next() {
|
||||
var m OpenPredictionMatch
|
||||
var isSeries bool
|
||||
err := rows.Scan(
|
||||
&m.MatchID, &m.CreatedAt,
|
||||
&m.BotAID, &m.BotAName, &m.ARating, &m.AEvolved,
|
||||
&m.BotBID, &m.BotBName, &m.BRating, &m.BEvolved,
|
||||
&isSeries,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan pending match: %w", err)
|
||||
}
|
||||
m.IsSeriesMatch = isSeries
|
||||
allMatches = append(allMatches, m)
|
||||
}
|
||||
|
||||
if len(allMatches) == 0 {
|
||||
return []OpenPredictionMatch{}, nil
|
||||
}
|
||||
|
||||
// Get top 20 bot IDs for top-20 vs top-20 check
|
||||
topBotIDs := make(map[string]bool)
|
||||
topRows, err := db.QueryContext(ctx, `
|
||||
SELECT bot_id FROM bots
|
||||
WHERE status = 'active'
|
||||
ORDER BY rating_mu DESC
|
||||
LIMIT 20
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query top bots: %w", err)
|
||||
}
|
||||
defer topRows.Close()
|
||||
for topRows.Next() {
|
||||
var botID string
|
||||
if err := topRows.Scan(&botID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
topBotIDs[botID] = true
|
||||
}
|
||||
|
||||
// Get top 10 bot IDs for evolved vs top-10 check
|
||||
top10BotIDs := make(map[string]bool)
|
||||
top10Rows, err := db.QueryContext(ctx, `
|
||||
SELECT bot_id FROM bots
|
||||
WHERE status = 'active' AND evolved = false
|
||||
ORDER BY rating_mu DESC
|
||||
LIMIT 10
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query top 10 bots: %w", err)
|
||||
}
|
||||
defer top10Rows.Close()
|
||||
for top10Rows.Next() {
|
||||
var botID string
|
||||
if err := top10Rows.Scan(&botID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
top10BotIDs[botID] = true
|
||||
}
|
||||
|
||||
// Build pair frequency map for rivalry detection (count completed h2h matches)
|
||||
pairFrequency := make(map[string]int)
|
||||
freqRows, err := db.QueryContext(ctx, `
|
||||
SELECT mp1.bot_id, mp2.bot_id, COUNT(*)
|
||||
FROM matches m
|
||||
JOIN match_participants mp1 ON m.match_id = mp1.match_id AND mp1.player_slot = 0
|
||||
JOIN match_participants mp2 ON m.match_id = mp2.match_id AND mp2.player_slot = 1
|
||||
WHERE m.status = 'completed'
|
||||
GROUP BY mp1.bot_id, mp2.bot_id
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query pair frequency: %w", err)
|
||||
}
|
||||
defer freqRows.Close()
|
||||
for freqRows.Next() {
|
||||
var botA, botB string
|
||||
var count int
|
||||
if err := freqRows.Scan(&botA, &botB, &count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pairFrequency[botA+":"+botB] = count
|
||||
}
|
||||
|
||||
// Filter matches that are "predictable"
|
||||
var predictableMatches []OpenPredictionMatch
|
||||
for _, m := range allMatches {
|
||||
isPredictable := false
|
||||
|
||||
// Check: both bots in top 20
|
||||
if topBotIDs[m.BotAID] && topBotIDs[m.BotBID] {
|
||||
isPredictable = true
|
||||
}
|
||||
|
||||
// Check: rivalry match (at least 3 previous h2h matches)
|
||||
if freq, ok := pairFrequency[m.BotAID+":"+m.BotBID]; ok && freq >= 3 {
|
||||
isPredictable = true
|
||||
}
|
||||
|
||||
// Check: series match
|
||||
if m.IsSeriesMatch {
|
||||
isPredictable = true
|
||||
}
|
||||
|
||||
// Check: evolved bot vs top-10 human-written bot
|
||||
if m.AEvolved && top10BotIDs[m.BotBID] {
|
||||
isPredictable = true
|
||||
}
|
||||
if m.BEvolved && top10BotIDs[m.BotAID] {
|
||||
isPredictable = true
|
||||
}
|
||||
|
||||
if isPredictable {
|
||||
// Calculate head-to-head record
|
||||
h2hRecord := computeHeadToHeadRecord(ctx, db, m.BotAID, m.BotBID)
|
||||
m.HeadToHeadRecord = &h2hRecord
|
||||
predictableMatches = append(predictableMatches, m)
|
||||
}
|
||||
|
||||
// Limit to next 10 matches
|
||||
if len(predictableMatches) >= 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return predictableMatches, nil
|
||||
}
|
||||
|
||||
// computeHeadToHeadRecord returns the head-to-head record between two bots
|
||||
func computeHeadToHeadRecord(ctx context.Context, db *sql.DB, botAID, botBID string) string {
|
||||
var aWins, bWins int
|
||||
err := db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE m.winner = 0) as a_wins,
|
||||
COUNT(*) FILTER (WHERE m.winner = 1) as b_wins
|
||||
FROM matches m
|
||||
JOIN match_participants mp1 ON m.match_id = mp1.match_id AND mp1.player_slot = 0
|
||||
JOIN match_participants mp2 ON m.match_id = mp2.match_id AND mp2.player_slot = 1
|
||||
WHERE mp1.bot_id = $1 AND mp2.bot_id = $2 AND m.status = 'completed'
|
||||
`, botAID, botBID).Scan(&aWins, &bWins)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%d-%d", aWins, bWins)
|
||||
}
|
||||
|
||||
func computeTopPredictors(stats []PredictorStats) []PredictorStats {
|
||||
if len(stats) > 50 {
|
||||
return stats[:50]
|
||||
|
|
|
|||
|
|
@ -154,6 +154,11 @@ func generateAllIndexes(data *IndexData, outputDir string, db *sql.DB) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Generate maps/index.json and maps/{map_id}.json
|
||||
if err := generateMapsIndex(data, outputDir); err != nil {
|
||||
return fmt.Errorf("maps index: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1502,6 +1507,131 @@ func generateRivalriesIndex(rivalries []RivalryEntry, outputDir string) error {
|
|||
return writeJSON(filepath.Join(metaDir, "rivalries.json"), index)
|
||||
}
|
||||
|
||||
// mapPosition is a grid coordinate used in map geometry output.
|
||||
type mapPosition struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
}
|
||||
|
||||
// mapCore is a spawn/core point in a map.
|
||||
type mapCore struct {
|
||||
Position mapPosition `json:"position"`
|
||||
Owner int `json:"owner"`
|
||||
}
|
||||
|
||||
// mapGeometryJSON mirrors the map_json column structure for geometry extraction.
|
||||
type mapGeometryJSON struct {
|
||||
Walls []mapPosition `json:"walls"`
|
||||
Cores []mapCore `json:"cores"`
|
||||
EnergyNodes []mapPosition `json:"energy_nodes"`
|
||||
}
|
||||
|
||||
// MapIndexEntry is the per-map summary in maps/index.json.
|
||||
type MapIndexEntry struct {
|
||||
MapID string `json:"map_id"`
|
||||
PlayerCount int `json:"player_count"`
|
||||
Status string `json:"status"`
|
||||
Engagement float64 `json:"engagement"`
|
||||
WallDensity float64 `json:"wall_density"`
|
||||
EnergyCount int `json:"energy_count"`
|
||||
GridWidth int `json:"grid_width"`
|
||||
GridHeight int `json:"grid_height"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// MapIndexFile represents maps/index.json.
|
||||
type MapIndexFile struct {
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Maps []MapIndexEntry `json:"maps"`
|
||||
ByPlayerCount map[string][]MapIndexEntry `json:"by_player_count"`
|
||||
}
|
||||
|
||||
// MapDetail represents maps/{map_id}.json — summary metadata plus full geometry.
|
||||
type MapDetail struct {
|
||||
MapID string `json:"map_id"`
|
||||
PlayerCount int `json:"player_count"`
|
||||
Status string `json:"status"`
|
||||
Engagement float64 `json:"engagement"`
|
||||
WallDensity float64 `json:"wall_density"`
|
||||
EnergyCount int `json:"energy_count"`
|
||||
GridWidth int `json:"grid_width"`
|
||||
GridHeight int `json:"grid_height"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Walls []mapPosition `json:"walls"`
|
||||
Cores []mapCore `json:"cores"`
|
||||
EnergyNodes []mapPosition `json:"energy_nodes"`
|
||||
}
|
||||
|
||||
func generateMapsIndex(data *IndexData, outputDir string) error {
|
||||
mapsDir := filepath.Join(outputDir, "maps")
|
||||
if err := os.MkdirAll(mapsDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries := make([]MapIndexEntry, 0, len(data.Maps))
|
||||
byPlayerCount := make(map[string][]MapIndexEntry)
|
||||
|
||||
for _, m := range data.Maps {
|
||||
entry := MapIndexEntry{
|
||||
MapID: m.MapID,
|
||||
PlayerCount: m.PlayerCount,
|
||||
Status: m.Status,
|
||||
Engagement: m.Engagement,
|
||||
WallDensity: m.WallDensity,
|
||||
EnergyCount: m.EnergyCount,
|
||||
GridWidth: m.GridWidth,
|
||||
GridHeight: m.GridHeight,
|
||||
CreatedAt: m.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
key := fmt.Sprintf("%d", m.PlayerCount)
|
||||
byPlayerCount[key] = append(byPlayerCount[key], entry)
|
||||
|
||||
var geo mapGeometryJSON
|
||||
if len(m.RawJSON) > 0 {
|
||||
if err := json.Unmarshal(m.RawJSON, &geo); err != nil {
|
||||
return fmt.Errorf("parse map_json for %s: %w", m.MapID, err)
|
||||
}
|
||||
}
|
||||
|
||||
detail := MapDetail{
|
||||
MapID: m.MapID,
|
||||
PlayerCount: m.PlayerCount,
|
||||
Status: m.Status,
|
||||
Engagement: m.Engagement,
|
||||
WallDensity: m.WallDensity,
|
||||
EnergyCount: m.EnergyCount,
|
||||
GridWidth: m.GridWidth,
|
||||
GridHeight: m.GridHeight,
|
||||
CreatedAt: m.CreatedAt.Format(time.RFC3339),
|
||||
Walls: geo.Walls,
|
||||
Cores: geo.Cores,
|
||||
EnergyNodes: geo.EnergyNodes,
|
||||
}
|
||||
if detail.Walls == nil {
|
||||
detail.Walls = []mapPosition{}
|
||||
}
|
||||
if detail.Cores == nil {
|
||||
detail.Cores = []mapCore{}
|
||||
}
|
||||
if detail.EnergyNodes == nil {
|
||||
detail.EnergyNodes = []mapPosition{}
|
||||
}
|
||||
|
||||
if err := writeJSON(filepath.Join(mapsDir, m.MapID+".json"), detail); err != nil {
|
||||
return fmt.Errorf("write map %s: %w", m.MapID, err)
|
||||
}
|
||||
}
|
||||
|
||||
index := MapIndexFile{
|
||||
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
|
||||
Maps: entries,
|
||||
ByPlayerCount: byPlayerCount,
|
||||
}
|
||||
|
||||
return writeJSON(filepath.Join(mapsDir, "index.json"), index)
|
||||
}
|
||||
|
||||
func writeJSON(path string, data interface{}) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1433,3 +1433,116 @@ func TestGeneratePlaylistsWithFastLookups(t *testing.T) {
|
|||
t.Errorf("rivalry-classics should have 3 matches for bot1:bot2 (count=3), got %d", len(rivalryPlaylist.Matches))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMapsIndex(t *testing.T) {
|
||||
wallsJSON := `[{"row":10,"col":10},{"row":10,"col":11}]`
|
||||
coresJSON := `[{"position":{"row":5,"col":5},"owner":0},{"position":{"row":55,"col":55},"owner":1}]`
|
||||
energyJSON := `[{"row":20,"col":25}]`
|
||||
mapJSON := []byte(`{"walls":` + wallsJSON + `,"cores":` + coresJSON + `,"energy_nodes":` + energyJSON + `}`)
|
||||
|
||||
createdAt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
data := &IndexData{
|
||||
GeneratedAt: time.Date(2026, 4, 1, 12, 0, 0, 0, time.UTC),
|
||||
Maps: []MapData{
|
||||
{
|
||||
MapID: "map_abc123",
|
||||
PlayerCount: 2,
|
||||
Status: "active",
|
||||
Engagement: 0.85,
|
||||
WallDensity: 0.15,
|
||||
EnergyCount: 1,
|
||||
GridWidth: 60,
|
||||
GridHeight: 60,
|
||||
CreatedAt: createdAt,
|
||||
RawJSON: mapJSON,
|
||||
},
|
||||
{
|
||||
MapID: "map_def456",
|
||||
PlayerCount: 4,
|
||||
Status: "probation",
|
||||
Engagement: 0.40,
|
||||
WallDensity: 0.20,
|
||||
EnergyCount: 0,
|
||||
GridWidth: 80,
|
||||
GridHeight: 80,
|
||||
CreatedAt: createdAt,
|
||||
RawJSON: json.RawMessage(`{}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
if err := generateMapsIndex(data, tmpDir); err != nil {
|
||||
t.Fatalf("generateMapsIndex failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify maps/index.json
|
||||
indexContent, err := os.ReadFile(filepath.Join(tmpDir, "maps", "index.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read maps/index.json: %v", err)
|
||||
}
|
||||
var index MapIndexFile
|
||||
if err := json.Unmarshal(indexContent, &index); err != nil {
|
||||
t.Fatalf("Failed to parse maps/index.json: %v", err)
|
||||
}
|
||||
if len(index.Maps) != 2 {
|
||||
t.Errorf("Expected 2 maps in index, got %d", len(index.Maps))
|
||||
}
|
||||
if index.UpdatedAt == "" {
|
||||
t.Error("UpdatedAt should not be empty")
|
||||
}
|
||||
if index.Maps[0].MapID != "map_abc123" {
|
||||
t.Errorf("First map_id: got %q, want %q", index.Maps[0].MapID, "map_abc123")
|
||||
}
|
||||
if len(index.ByPlayerCount["2"]) != 1 {
|
||||
t.Errorf("by_player_count[\"2\"]: expected 1, got %d", len(index.ByPlayerCount["2"]))
|
||||
}
|
||||
if len(index.ByPlayerCount["4"]) != 1 {
|
||||
t.Errorf("by_player_count[\"4\"]: expected 1, got %d", len(index.ByPlayerCount["4"]))
|
||||
}
|
||||
|
||||
// Verify maps/map_abc123.json has geometry
|
||||
detailContent, err := os.ReadFile(filepath.Join(tmpDir, "maps", "map_abc123.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read maps/map_abc123.json: %v", err)
|
||||
}
|
||||
var detail MapDetail
|
||||
if err := json.Unmarshal(detailContent, &detail); err != nil {
|
||||
t.Fatalf("Failed to parse maps/map_abc123.json: %v", err)
|
||||
}
|
||||
if detail.MapID != "map_abc123" {
|
||||
t.Errorf("map_id: got %q, want %q", detail.MapID, "map_abc123")
|
||||
}
|
||||
if len(detail.Walls) != 2 {
|
||||
t.Errorf("Expected 2 walls, got %d", len(detail.Walls))
|
||||
}
|
||||
if len(detail.Cores) != 2 {
|
||||
t.Errorf("Expected 2 cores, got %d", len(detail.Cores))
|
||||
}
|
||||
if len(detail.EnergyNodes) != 1 {
|
||||
t.Errorf("Expected 1 energy node, got %d", len(detail.EnergyNodes))
|
||||
}
|
||||
if detail.Walls[0].Row != 10 || detail.Walls[0].Col != 10 {
|
||||
t.Errorf("First wall: got {%d,%d}, want {10,10}", detail.Walls[0].Row, detail.Walls[0].Col)
|
||||
}
|
||||
if detail.Cores[0].Owner != 0 || detail.Cores[0].Position.Row != 5 {
|
||||
t.Errorf("First core: got owner=%d row=%d, want owner=0 row=5", detail.Cores[0].Owner, detail.Cores[0].Position.Row)
|
||||
}
|
||||
|
||||
// Verify maps/map_def456.json has empty slices (no geometry in RawJSON)
|
||||
detail2Content, err := os.ReadFile(filepath.Join(tmpDir, "maps", "map_def456.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read maps/map_def456.json: %v", err)
|
||||
}
|
||||
var detail2 MapDetail
|
||||
if err := json.Unmarshal(detail2Content, &detail2); err != nil {
|
||||
t.Fatalf("Failed to parse maps/map_def456.json: %v", err)
|
||||
}
|
||||
if len(detail2.Walls) != 0 {
|
||||
t.Errorf("Expected 0 walls for map_def456, got %d", len(detail2.Walls))
|
||||
}
|
||||
if len(detail2.Cores) != 0 {
|
||||
t.Errorf("Expected 0 cores for map_def456, got %d", len(detail2.Cores))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue