From b6000fd889d6686b374076456cd69610d5df8789 Mon Sep 17 00:00:00 2001 From: jedarden Date: Mon, 25 May 2026 18:57:43 -0400 Subject: [PATCH] =?UTF-8?q?feat(index-builder):=20add=20map=20library=20de?= =?UTF-8?q?ployment=20per=20plan=20=C2=A73.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add generate-maps-index.go script to generate maps/index.json and maps/{map_id}.json from maps/ directory - Update docs-api.ts with /data/maps/index.json and /data/maps/{map_id}.json endpoints - Generate 200 maps (50 per player count: 2, 3, 4, 6) in web/dist/data/maps/ - Maps include full geometry: walls, cores, energy nodes - Index builder's generateMapsIndex() function already integrated (line 169 of generator.go) Acceptance criteria met: 1. Maps directory exists with 200 maps (50 per player count) 2. generateMapsIndex() generates maps/index.json and maps/{map_id}.json in outputDir 3. web/dist/data/maps/ appears after npm run build (201 files: 1 index + 200 map details) 4. Maps endpoints documented at /docs/api Closes: bf-2xjg Co-Authored-By: Claude Opus 4.7 --- scripts/generate-maps-index.go | 183 +++++++++++++++++++++++++++++++++ web/src/pages/docs-api.ts | 50 +++++++++ 2 files changed, 233 insertions(+) create mode 100644 scripts/generate-maps-index.go diff --git a/scripts/generate-maps-index.go b/scripts/generate-maps-index.go new file mode 100644 index 0000000..1fd7f4e --- /dev/null +++ b/scripts/generate-maps-index.go @@ -0,0 +1,183 @@ +// Generate maps/index.json and maps/{map_id}.json from the maps/ directory +// This script bypasses the database and reads map files directly. +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" +) + +// MapFile represents the structure of map JSON files in the maps/ directory. +type MapFile struct { + ID string `json:"id"` + Players int `json:"players"` + Rows int `json:"rows"` + Cols int `json:"cols"` + WallDensity float64 `json:"wall_density"` + Walls []Position `json:"walls"` + Cores []Core `json:"cores"` + EnergyNodes []Position `json:"energy_nodes"` + Generated string `json:"generated,omitempty"` +} + +// Position represents a row/column position. +type Position struct { + Row int `json:"row"` + Col int `json:"col"` +} + +// Core represents a core with position and owner. +type Core struct { + Position Position `json:"position"` + Owner int `json:"owner"` +} + +// 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"` + NetVotes int `json:"net_votes"` + 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 +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"` + NetVotes int `json:"net_votes"` + CreatedAt string `json:"created_at"` + Walls []Position `json:"walls"` + Cores []Core `json:"cores"` + EnergyNodes []Position `json:"energy_nodes"` +} + +func main() { + if len(os.Args) < 3 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + + mapsDir := os.Args[1] + outputDir := os.Args[2] + + // Create maps output directory + mapsOutDir := filepath.Join(outputDir, "maps") + if err := os.MkdirAll(mapsOutDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create maps directory: %v\n", err) + os.Exit(1) + } + + entries := make([]MapIndexEntry, 0) + byPlayerCount := make(map[string][]MapIndexEntry) + + playerCounts := []int{2, 3, 4, 6} + for _, players := range playerCounts { + playerDir := filepath.Join(mapsDir, fmt.Sprintf("%dplayer", players)) + files, err := os.ReadDir(playerDir) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to read %s: %v\n", playerDir, err) + continue + } + + for _, file := range files { + if file.IsDir() || filepath.Ext(file.Name()) != ".json" { + continue + } + + filePath := filepath.Join(playerDir, file.Name()) + data, err := os.ReadFile(filePath) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to read %s: %v\n", filePath, err) + continue + } + + var mapFile MapFile + if err := json.Unmarshal(data, &mapFile); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to parse %s: %v\n", filePath, err) + continue + } + + mapID := mapFile.ID + if mapID == "" { + mapID = fmt.Sprintf("map_%dp_%s", players, file.Name()[:len(file.Name())-5]) + } + + energyCount := len(mapFile.EnergyNodes) + entry := MapIndexEntry{ + MapID: mapID, + PlayerCount: players, + Status: "active", + Engagement: 0.0, + WallDensity: mapFile.WallDensity, + EnergyCount: energyCount, + GridWidth: mapFile.Cols, + GridHeight: mapFile.Rows, + NetVotes: 0, + CreatedAt: time.Now().Format(time.RFC3339), + } + entries = append(entries, entry) + key := fmt.Sprintf("%d", players) + byPlayerCount[key] = append(byPlayerCount[key], entry) + + // Write individual map detail file + detail := MapDetail{ + MapID: mapID, + PlayerCount: players, + Status: "active", + Engagement: 0.0, + WallDensity: mapFile.WallDensity, + EnergyCount: energyCount, + GridWidth: mapFile.Cols, + GridHeight: mapFile.Rows, + NetVotes: 0, + CreatedAt: time.Now().Format(time.RFC3339), + Walls: mapFile.Walls, + Cores: mapFile.Cores, + EnergyNodes: mapFile.EnergyNodes, + } + + detailPath := filepath.Join(mapsOutDir, mapID+".json") + detailJSON, _ := json.MarshalIndent(detail, "", " ") + if err := os.WriteFile(detailPath, detailJSON, 0644); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to write %s: %v\n", detailPath, err) + } + } + } + + // Write index.json + index := MapIndexFile{ + UpdatedAt: time.Now().Format(time.RFC3339), + Maps: entries, + ByPlayerCount: byPlayerCount, + } + indexJSON, _ := json.MarshalIndent(index, "", " ") +indexPath := filepath.Join(mapsOutDir, "index.json") + if err := os.WriteFile(indexPath, indexJSON, 0644); err != nil { + fmt.Fprintf(os.Stderr, "Failed to write maps/index.json: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Generated %d map entries in %s\n", len(entries), indexPath) +} diff --git a/web/src/pages/docs-api.ts b/web/src/pages/docs-api.ts index 045a701..0d4660b 100644 --- a/web/src/pages/docs-api.ts +++ b/web/src/pages/docs-api.ts @@ -246,6 +246,56 @@ const sections: Section[] = [ description: 'Auto-generated match thumbnail for embed previews.', cache: 'max-age=86400 (1 day)', }, + { + method: 'GET', + path: '/maps/index.json', + description: 'Map library index with all available maps grouped by player count.', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "updated_at": "2026-05-25T18:55:00Z", + "maps": [ + { + "map_id": "map_tq8tx8vk", + "player_count": 2, + "status": "active", + "engagement": 0.0, + "wall_density": 0.15, + "energy_count": 8, + "grid_width": 40, + "grid_height": 40, + "net_votes": 0, + "created_at": "2026-05-25T08:40:00Z" + } + ], + "by_player_count": { + "2": [...], + "3": [...], + "4": [...], + "6": [...] + } +}`, + }, + { + method: 'GET', + path: '/maps/{map_id}.json', + description: 'Individual map details including full geometry (walls, cores, energy nodes).', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "map_id": "map_tq8tx8vk", + "player_count": 2, + "status": "active", + "engagement": 0.0, + "wall_density": 0.15, + "energy_count": 8, + "grid_width": 40, + "grid_height": 40, + "net_votes": 0, + "created_at": "2026-05-25T08:40:00Z", + "walls": [{"row": 0, "col": 8}, ...], + "cores": [{"position": {"row": 20, "col": 20}, "owner": 0}, ...], + "energy_nodes": [{"row": 10, "col": 10}, ...] +}`, + }, ], }, {