package world import ( "crypto/sha1" "encoding/hex" "encoding/json" "fmt" "hash/fnv" "math/rand" "strings" ) const mapVersion = "v1" // MapRef is a lightweight map descriptor returned by hero init. type MapRef struct { MapID string `json:"mapId"` MapVersion string `json:"mapVersion"` ETag string `json:"etag"` Biome string `json:"biome"` RecommendedLevelMin int `json:"recommendedLevelMin"` RecommendedLevelMax int `json:"recommendedLevelMax"` } // Tile is a single ground cell in the map grid. type Tile struct { X int `json:"x"` Y int `json:"y"` Terrain string `json:"terrain"` } // Object is a world object rendered on top of tiles. type Object struct { ID string `json:"id"` Type string `json:"type"` X int `json:"x"` Y int `json:"y"` } // SpawnPoint defines where entities can be spawned. type SpawnPoint struct { ID string `json:"id"` Kind string `json:"kind"` X int `json:"x"` Y int `json:"y"` } // ServerMap is the full map payload returned by /maps/{mapId}. type ServerMap struct { MapID string `json:"mapId"` MapVersion string `json:"mapVersion"` Biome string `json:"biome"` RecommendedLevelMin int `json:"recommendedLevelMin"` RecommendedLevelMax int `json:"recommendedLevelMax"` Width int `json:"width"` Height int `json:"height"` Tiles []Tile `json:"tiles"` Objects []Object `json:"objects"` SpawnPoints []SpawnPoint `json:"spawnPoints"` } type levelBand struct { min int max int biome string } var levelBands = []levelBand{ {min: 1, max: 10, biome: "meadow"}, {min: 11, max: 20, biome: "forest"}, {min: 21, max: 35, biome: "ruins"}, {min: 36, max: 50, biome: "canyon"}, {min: 51, max: 70, biome: "swamp"}, {min: 71, max: 100, biome: "volcanic"}, {min: 101, max: 999, biome: "astral"}, } type mapData struct { ref MapRef data ServerMap } // Service provides deterministic map refs and payloads for MVP. type Service struct { byID map[string]mapData } // NewService creates a map service with precomputed deterministic maps. func NewService() *Service { s := &Service{ byID: make(map[string]mapData, len(levelBands)), } for _, band := range levelBands { mapID := fmt.Sprintf("%s-%d-%d", band.biome, band.min, band.max) serverMap := generateMap(mapID, band) etag := computeETag(serverMap) s.byID[mapID] = mapData{ ref: MapRef{ MapID: mapID, MapVersion: mapVersion, ETag: etag, Biome: band.biome, RecommendedLevelMin: band.min, RecommendedLevelMax: band.max, }, data: serverMap, } } return s } // RefForLevel returns a deterministic map reference for hero level. func (s *Service) RefForLevel(level int) MapRef { band := bandForLevel(level) mapID := fmt.Sprintf("%s-%d-%d", band.biome, band.min, band.max) if entry, ok := s.byID[mapID]; ok { return entry.ref } // Fallback should not happen in normal flow. return MapRef{ MapID: mapID, MapVersion: mapVersion, Biome: band.biome, RecommendedLevelMin: band.min, RecommendedLevelMax: band.max, } } // GetMap returns map payload and ETag by map ID. func (s *Service) GetMap(mapID string) (*ServerMap, string, bool) { entry, ok := s.byID[strings.TrimSpace(mapID)] if !ok { return nil, "", false } m := entry.data return &m, entry.ref.ETag, true } func bandForLevel(level int) levelBand { if level < 1 { level = 1 } for _, b := range levelBands { if level >= b.min && level <= b.max { return b } } return levelBands[len(levelBands)-1] } func generateMap(mapID string, band levelBand) ServerMap { const ( width = 24 height = 24 ) seed := hashSeed(mapID, band.biome, band.min, band.max) rng := rand.New(rand.NewSource(seed)) tiles := make([]Tile, 0, width*height) isRoad := make(map[[2]int]struct{}, width*2) roadY := clamp(9+rng.Intn(6), 2, height-3) currentY := roadY for x := 0; x < width; x++ { if x > 0 && x%4 == 0 { currentY = clamp(currentY+rng.Intn(3)-1, 2, height-3) } isRoad[[2]int{x, currentY}] = struct{}{} isRoad[[2]int{x, currentY + 1}] = struct{}{} } baseTerrain := biomeBaseTerrain(band.biome) for y := 0; y < height; y++ { for x := 0; x < width; x++ { terrain := baseTerrain if _, ok := isRoad[[2]int{x, y}]; ok { terrain = "road" } tiles = append(tiles, Tile{ X: x, Y: y, Terrain: terrain, }) } } objects := make([]Object, 0, 96) objID := 1 for y := 1; y < height-1; y++ { for x := 1; x < width-1; x++ { if _, ok := isRoad[[2]int{x, y}]; ok { continue } roll := rng.Intn(1000) switch { case roll < 70: objects = append(objects, Object{ ID: fmt.Sprintf("obj-%d", objID), Type: "tree", X: x, Y: y, }) objID++ case roll < 120: objects = append(objects, Object{ ID: fmt.Sprintf("obj-%d", objID), Type: "bush", X: x, Y: y, }) objID++ } } } spawnPoints := []SpawnPoint{ {ID: "hero-start", Kind: "hero", X: 1, Y: roadY}, {ID: "enemy-main", Kind: "enemy", X: width - 3, Y: roadY}, {ID: "enemy-alt", Kind: "enemy", X: width - 5, Y: clamp(roadY+1, 1, height-2)}, } return ServerMap{ MapID: mapID, MapVersion: mapVersion, Biome: band.biome, RecommendedLevelMin: band.min, RecommendedLevelMax: band.max, Width: width, Height: height, Tiles: tiles, Objects: objects, SpawnPoints: spawnPoints, } } func biomeBaseTerrain(biome string) string { switch biome { case "forest", "meadow": return "grass" case "canyon": return "dirt" case "ruins": return "stone" case "swamp": return "mud" case "volcanic": return "ash" case "astral": return "ether" default: return "grass" } } func computeETag(m ServerMap) string { payload, _ := json.Marshal(m) sum := sha1.Sum(payload) return `"` + hex.EncodeToString(sum[:]) + `"` } func hashSeed(parts ...any) int64 { h := fnv.New64a() for _, p := range parts { fmt.Fprint(h, p, "|") } return int64(h.Sum64() & 0x7fffffffffffffff) } func clamp(v, min, max int) int { if v < min { return min } if v > max { return max } return v }