feat(index-builder): add map library deployment per plan §3.8
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
8639e44ef4
commit
b6000fd889
2 changed files with 233 additions and 0 deletions
183
scripts/generate-maps-index.go
Normal file
183
scripts/generate-maps-index.go
Normal file
|
|
@ -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 <maps-dir> <output-dir>\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)
|
||||
}
|
||||
|
|
@ -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}, ...]
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue