package game import ( "fmt" "io" "log" "math/rand" "net/http" "os" "path" "strings" "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) const ( PROTOCOL string = "http://" BASE_URL string = "localhost:8000/" 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 } rows := strings.Split(mapYml.Map, "\n") for y := range len(rows) { row := rows[y] 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(BASE_URL, MAP_URL_PART, name+".yml") log.Printf("loading map from %s\n", url) resp, err := http.Get(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] }