package game import ( "fmt" "io" "math/rand" "net/http" "os" "path" "slices" "strconv" "strings" "github.com/goccy/go-yaml" "muhq.space/muhqs-game/go/assets" "muhq.space/muhqs-game/go/log" ) const ( MAP_URL_PART string = "html/build/maps" DEFAULT_RESOURCE_GAIN int = 5 DEFAULT_START_DECK string = "3 misc/farmer" ) type MapYml struct { Map string `yaml:"map"` Symbols map[string]string `yaml:"symbols"` StartDeckList string `yaml:"start_deck_list"` Kings [][]int `yaml:"kings,omitempty"` } type Map struct { // Tile slices of rows containing the individual tiles. Tiles [][]Tile symbols map[string]string ResourceGain int StartDeckList string WinCondition WinCondition Stores map[Position]*Store Prepare func(*LocalState) } func readMap(r io.Reader) (*Map, error) { data, err := io.ReadAll(r) if err != nil { return nil, err } return ParseMap(data) } // ParseMap constructs a Map from a yaml definition. func ParseMap(data []byte) (*Map, error) { mapYml := MapYml{} err := yaml.Unmarshal(data, &mapYml) if err != nil { return nil, err } mapYml.Symbols[" "] = "neutral" m := &Map{ Tiles: [][]Tile{}, symbols: mapYml.Symbols, ResourceGain: DEFAULT_RESOURCE_GAIN, StartDeckList: DEFAULT_START_DECK, WinCondition: DummyWinCondition, Stores: make(map[Position]*Store), } if mapYml.StartDeckList != "" { m.StartDeckList = mapYml.StartDeckList } // Trim trailing newlines mapDef := strings.TrimRight(mapYml.Map, "\n") rows := strings.Split(mapDef, "\n") for y, row := range rows { m.Tiles = append(m.Tiles, []Tile{}) for x := range len(rows[y]) { pos := Position{x, y} s := string(row[x]) tile, err := NewTileFromString(mapYml.Symbols[s], pos) if err != nil { return nil, err } if tile.Type == TileTypes.Store { m.Stores[pos] = NewStore() } m.Tiles[y] = append(m.Tiles[y], tile) } } m.selectStreets() if len(mapYml.Kings) > 0 { m.WinCondition = KingGame m.Prepare = func(s *LocalState) { for i, kingPos := range mapYml.Kings { card := NewCard("misc/king") owner := s.PlayerById(i + 1) // TODO: Fix position index base. pos := Position{kingPos[0] - 1, kingPos[1] - 1} if m.TileAt(pos) == nil { log.Panic("invalid king pos", pos, len(m.Tiles[0]), "x", len(m.Tiles), "map") } s.addNewUnit(card, pos, owner) } } } return m, nil } func (m *Map) selectStreets() { for y := range len(m.Tiles) { for x := range len(m.Tiles[y]) { tile := &m.Tiles[y][x] if tile.Type != TileTypes.Street { continue } connections, left, right, above, below := m.FindStreetConnections(x, y) // Decide if curve or straight if connections == 2 && (left || right) && (above || below) { tile.Raw = "street 2" } if connections > 2 { tile.Raw = fmt.Sprintf("street %d", connections) } } } } func (m *Map) FindNeighbours(x, y int) (left, right, above, below *Tile) { left, right, above, below = nil, nil, nil, nil if y > 0 { above = &m.Tiles[y-1][x] } if y < len(m.Tiles)-1 { below = &m.Tiles[y+1][x] } if x > 0 { left = &m.Tiles[y][x-1] } if x < len(m.Tiles[y])-1 { right = &m.Tiles[y][x+1] } return left, right, above, below } func (m *Map) FindConnections(x, y int, isConnection func(TileType) bool) (connections int, left, right, above, below bool) { leftT, rightT, aboveT, belowT := m.FindNeighbours(x, y) connections = 0 if leftT != nil && isConnection(leftT.Type) { left = true connections += 1 } if rightT != nil && isConnection(rightT.Type) { right = true connections += 1 } if aboveT != nil && isConnection(aboveT.Type) { above = true connections += 1 } if belowT != nil && isConnection(belowT.Type) { below = true connections += 1 } return } func (m *Map) FindAnyConnections(x, y int) (connections int, left, right, above, below bool) { return m.FindConnections(x, y, func(t TileType) bool { return t != TileTypes.Neutral }) } func (m *Map) FindStreetConnections(x int, y int) (connections int, left, right, above, below bool) { return m.FindConnections(x, y, func(t TileType) bool { return t == TileTypes.Street }) } func (m *Map) FindFortificationConnections(x int, y int) (connections int, left, right, above, below bool) { return m.FindConnections(x, y, func(t TileType) bool { return t.IsFortification() }) } // GetMap creates a new map from the definition retrieved for the map's name. func GetMap(name string) (*Map, error) { url := path.Join(assets.BASE_URL, MAP_URL_PART, name+".yml") log.Info("loading map", "url", url) resp, err := http.Get(assets.PROTOCOL + url) if err != nil { log.Fatal(err) } defer resp.Body.Close() return readMap(resp.Body) } // LoadMap creates a new map from a definition stored in path. func LoadMap(path string) (*Map, error) { f, err := os.Open(path) if err != nil { log.Fatal("Opening map ", path, " failed: ", err) } defer f.Close() return readMap(f) } // TileAt returns the tile at a certain position. // If p is an invalid position, nil is returned. func (m *Map) TileAt(p Position) *Tile { if !p.Valid() || p.Y >= len(m.Tiles) || p.X >= len(m.Tiles[p.Y]) { return nil } return &m.Tiles[p.Y][p.X] } // RandomTile returns the position of a randomly selected tile. // The selected tile's type is included in the types input slice. func (m *Map) RandomTile(r *rand.Rand, types []TileType) Position { candidates := []Position{} for y := range len(m.Tiles) { for x := range len(m.Tiles[y]) { candidate := m.Tiles[y][x] for slices.Contains(types, candidate.Type) { candidates = append(candidates, candidate.Position) } } } randIdx := r.Intn(len(candidates)) return candidates[randIdx] } // RandomTileFromSymbols returns the position of a randomly selected tile. // The selected tile's type is represented in the symbols input slice. func (m *Map) RandomTileFromSymbols(r *rand.Rand, symbols []string) (Position, error) { types := make([]TileType, 0, len(symbols)) for _, symbol := range symbols { tileString, ok := m.symbols[symbol] if !ok { return INVALID_POSITION(), fmt.Errorf("%s", fmt.Sprintf("symbol %s not in the map symbols %v", symbol, m.symbols)) } tileType, ok := TileNames[tileString] if !ok { return INVALID_POSITION(), ErrInvalidTileName } types = append(types, tileType) } return m.RandomTile(r, types), nil } // FilterTiles returns a slice of tiles matching the filter. // A tile is included in the result if the filter functions returns true for the given tile. func (m *Map) FilterTiles(filter func(t *Tile) bool) []*Tile { tiles := []*Tile{} for y := range len(m.Tiles) { for x := range len(m.Tiles[y]) { tile := &m.Tiles[y][x] if filter(tile) { tiles = append(tiles, tile) } } } return tiles } // AllTiles returns all map tiles as flattened slice. func (m *Map) AllTiles() []*Tile { return m.FilterTiles(func(*Tile) bool { return true }) } func (m *Map) FreeTiles() []*Tile { return m.FilterTiles(func(t *Tile) bool { return t.IsFree() }) } func (m *Map) AvailableTilesFor(c *Card) []*Tile { return m.FilterTiles(func(t *Tile) bool { return t.IsAvailableForCard(c) }) } func (m *Map) distributeStoreCards(cards PileOfCards, rand *rand.Rand) { nStores := len(m.Stores) stores := make([]*Store, 0, nStores) for _, store := range m.Stores { stores = append(stores, store) } d := NewDeckFrom(cards) d.Shuffle(rand) nCards := d.Size() for i := range nCards { c := d.DrawOne() stores[i%nStores].AddCard(c) } } func (m *Map) HasStores() bool { return len(m.Stores) > 0 } func (m *Map) StoreOn(p Position) *Store { return m.Stores[p] } type randomMapDef struct { types []TileType stores int farms int players int x, y int } var mapGens = []func(*rand.Rand, randomMapDef) *Map{ randomMapGen, dotSymRandomMapGen, } func randomMapGen(r *rand.Rand, def randomMapDef) *Map { log.Debug("random map gen", "def", def) m := &Map{ Tiles: make([][]Tile, 0, def.y), ResourceGain: DEFAULT_RESOURCE_GAIN, StartDeckList: DEFAULT_START_DECK, WinCondition: KingGame, Stores: make(map[Position]*Store), } m.Prepare = func(s *LocalState) { for _, t := range m.AllTiles() { if t.Type != spawn { continue } card := NewCard("misc/king") tokens := strings.Split(t.Raw, " ") id, _ := strconv.Atoi(tokens[len(tokens)-1]) owner := s.PlayerById(id) // TODO: Fix position index base. s.addNewUnit(card, t.Position, owner) } } for y := range def.y { m.Tiles = append(m.Tiles, make([]Tile, 0, def.x)) for x := range def.x { s := def.types[rand.Intn(len(def.types))].String() t, _ := NewTileFromString(s, Position{x, y}) m.Tiles[y] = append(m.Tiles[y], t) } } spawns := []Position{ {0, 0}, {def.x - 1, def.y - 1}, {0, def.y - 1}, {def.x - 1, 0}, } // Prepare spawn tiles for n := range def.players { pos := spawns[n] log.Debug("add spawn", "pos", pos) t, _ := NewTileFromString(fmt.Sprintf("spawn player %d", n+1), pos) m.Tiles[pos.Y][pos.X] = t } // Place stores on non spawn tiles if def.stores > 0 { for range def.stores { var pos Position for { pos = Position{r.Intn(def.x), r.Intn(def.y)} if m.TileAt(pos).Type != spawn { break } } log.Debug("place store at", "pos", pos) t, _ := NewTileFromString("store", pos) m.Tiles[pos.Y][pos.X] = t m.Stores[pos] = NewStore() } } // Place stores on non spawn tiles if def.farms > 0 { for range def.farms { var pos Position for { pos = Position{r.Intn(def.x), r.Intn(def.y)} tt := m.TileAt(pos).Type if tt != spawn && tt != store { break } } log.Debug("place farm at", "pos", pos) t, _ := NewTileFromString("farm", pos) m.Tiles[pos.Y][pos.X] = t } } return m } func dotSymRandomMapGen(r *rand.Rand, def randomMapDef) *Map { def.x = def.x / 2 // We place the stores symmetrically def.stores = 0 m := randomMapGen(r, def) log.Debug("random map has dimensions", "x", len(m.Tiles[0]), "y", len(m.Tiles)) // point reflection // max tiles to reflect in this row mY := len(m.Tiles) - 1 mX := len(m.Tiles[0]) - 1 for y := mY; y >= 0; y = y - 1 { for x := mX; x >= 0; x = x - 1 { t := m.Tiles[y][x] // Remove spawns at edge of reflection if x == mX && t.Type == spawn { t, _ = NewTileFromString("neutral", t.Position) m.Tiles[y][x] = t } rY := mY - y rRow := m.Tiles[rY] rX := len(rRow) rp := Position{rX, rY} log.Debug("reflect", "o", t.Position, "r", rp) rt, _ := NewTileFromString(t.Raw, rp) m.Tiles[rY] = append(rRow, rt) } } // prepare spawns spawns := []Position{ {len(m.Tiles[0]) - 1, def.y - 1}, {len(m.Tiles[0]) - 1, 0}, } for i := 2; i <= def.players; i = i + 2 { pos := spawns[i/2-1] spawn, _ := NewTileFromString(fmt.Sprintf("spawn player %d", i), pos) m.Tiles[pos.Y][pos.X] = spawn } // TODO: prepare stores // centerX := def.x return m } func RandomMapFromDef(seed int64, def randomMapDef) *Map { r := rand.New(rand.NewSource(seed)) n := rand.Intn(len(mapGens)) return mapGens[n](r, def) } func RandomMap(seed int64, players int) *Map { n := rand.Intn(10) + 3 types := []TileType{neutral, neutral} exclude := []TileType{spawn, docks, gate, wall, farm} for range n { for { t := TileType(rand.Intn(9) + 1) if !slices.Contains(exclude, t) { types = append(types, t) break } } } def := randomMapDef{ types: types, players: players, stores: rand.Intn(2), farms: rand.Intn(2) * players, x: rand.Intn(6) + 6, y: rand.Intn(6) + 6, } return RandomMapFromDef(seed, def) }