diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-07-03 21:48:58 -0400 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-07-06 11:49:19 -0400 |
| commit | d4e6d843dc71b68a34f1aa2eed84e5fe20af3589 (patch) | |
| tree | ee25b7ef22ee4408325642c65d26784646f5ff37 | |
| parent | fa5c66a13d7c182f44c9fa52c216e4d0f455ea29 (diff) | |
| download | muhqs-game-d4e6d843dc71b68a34f1aa2eed84e5fe20af3589.tar.gz muhqs-game-d4e6d843dc71b68a34f1aa2eed84e5fe20af3589.zip | |
support king games
| -rw-r--r-- | go/game/map.go | 69 | ||||
| -rw-r--r-- | go/game/map_test.go | 30 | ||||
| -rw-r--r-- | go/game/state.go | 4 | ||||
| -rw-r--r-- | go/game/tile.go | 5 | ||||
| -rw-r--r-- | go/game/winCondition.go | 9 |
5 files changed, 91 insertions, 26 deletions
diff --git a/go/game/map.go b/go/game/map.go index fb67ebbb..b3203bd4 100644 --- a/go/game/map.go +++ b/go/game/map.go @@ -1,7 +1,6 @@ package game import ( - "errors" "fmt" "io" "log" @@ -11,6 +10,8 @@ import ( "path" "strings" + "golang.org/x/exp/slices" + "gopkg.in/yaml.v3" ) @@ -27,10 +28,7 @@ type MapYml struct { Map string `yaml:"map"` Symbols map[string]string `yaml:"symbols"` StartDeckList string `yaml:"start_deck_list"` -} - -func DummyWinCondition(*LocalState) []*Player { - return []*Player{} + Kings [][]int `yaml:"omitemptt"` } type Map struct { @@ -38,8 +36,9 @@ type Map struct { symbols map[string]string ResourceGain int StartDeckList string - WinCondition func(*LocalState) []*Player + WinCondition WinCondition Stores map[Position]*Store + Prepare func(*LocalState) } func readMap(r io.Reader) (*Map, error) { @@ -51,6 +50,7 @@ func readMap(r io.Reader) (*Map, error) { return ParseMap(data) } +// ParseMap constructs a Map from a yaml definition. func ParseMap(data []byte) (*Map, error) { mapYml := MapYml{} err := yaml.Unmarshal(data, &mapYml) @@ -74,10 +74,10 @@ func ParseMap(data []byte) (*Map, error) { } rows := strings.Split(mapYml.Map, "\n") - for y := 0; y < len(rows); y++ { + for y := range len(rows) { row := rows[y] m.Tiles = append(m.Tiles, []Tile{}) - for x := 0; x < len(rows[y]); x++ { + for x := range len(rows[y]) { pos := Position{x, y} s := string(row[x]) tile, err := NewTileFromString(mapYml.Symbols[s], pos) @@ -95,12 +95,23 @@ func ParseMap(data []byte) (*Map, error) { m.selectStreets() + if len(mapYml.Kings) == 0 { + m.WinCondition = KingGame + m.Prepare = func(s *LocalState) { + k := NewCard("misc/king") + for i, kingPos := range mapYml.Kings { + p := s.PlayerById(i + 1) + s.addNewUnit(k, Position{kingPos[0], kingPos[1]}, p) + } + } + } + return m, nil } func (m *Map) selectStreets() { - for y := 0; y < len(m.Tiles); y++ { - for x := 0; x < len(m.Tiles[y]); x++ { + for y := range len(m.Tiles) { + for x := range len(m.Tiles[y]) { tile := &m.Tiles[y][x] if tile.Type != TileTypes.Street { continue @@ -175,6 +186,7 @@ func (m *Map) FindFortificationConnections(x int, y int) (connections int, left, 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) @@ -186,33 +198,35 @@ func GetMap(name string) (*Map, error) { return readMap(resp.Body) } -func LoadMap(file string) (*Map, error) { - f, err := os.Open(file) +// 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 ", file, " failed: ", err) + log.Fatal("Opening map ", path, " failed: ", err) } defer f.Close() return readMap(f) } +// TileAt returns the tile at a certain position. +// Id p is an invalid position, nil is returned. func (m *Map) TileAt(p Position) *Tile { - if p.Y >= len(m.Tiles) || p.X >= len(m.Tiles[p.Y]) { + if p.Y < 0 || p.Y >= len(m.Tiles) || p.X < 0 || 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 := 0; y < len(m.Tiles); y++ { - for x := 0; x < len(m.Tiles[y]); x++ { + for y := range len(m.Tiles) { + for x := range len(m.Tiles[y]) { candidate := m.Tiles[y][x] - for _, t := range types { - if candidate.Type == t { - candidates = append(candidates, candidate.Position) - break - } + for slices.Contains(types, candidate.Type) { + candidates = append(candidates, candidate.Position) } } } @@ -221,6 +235,8 @@ func (m *Map) RandomTile(r *rand.Rand, types []TileType) Position { 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 { @@ -231,7 +247,7 @@ func (m *Map) RandomTileFromSymbols(r *rand.Rand, symbols []string) (Position, e tileType, ok := TileNames[tileString] if !ok { - return INVALID_POSITION(), errors.New("tile type unknown") + return INVALID_POSITION(), ErrInvalidTileName } types = append(types, tileType) @@ -239,10 +255,12 @@ func (m *Map) RandomTileFromSymbols(r *rand.Rand, symbols []string) (Position, e 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 := 0; y < len(m.Tiles); y++ { - for x := 0; x < len(m.Tiles[y]); x++ { + 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) @@ -252,6 +270,7 @@ func (m *Map) FilterTiles(filter func(t *Tile) bool) []*Tile { return tiles } +// AllTiles returns all map tiles as flattened slice. func (m *Map) AllTiles() []*Tile { return m.FilterTiles(func(*Tile) bool { return true }) } @@ -271,7 +290,7 @@ func (m *Map) distributeStoreCards(cards PileOfCards, rand *rand.Rand) { d.Shuffle(rand) nCards := d.Size() - for i := 0; i < nCards; i++ { + for i := range nCards { c := d.DrawOne() stores[i%nStores].AddCard(c) } diff --git a/go/game/map_test.go b/go/game/map_test.go index 6972f867..e950747b 100644 --- a/go/game/map_test.go +++ b/go/game/map_test.go @@ -1,6 +1,7 @@ package game import ( + "reflect" "strings" "testing" ) @@ -71,6 +72,10 @@ kings: if m.ResourceGain != DEFAULT_RESOURCE_GAIN { t.Fatalf("resource gain is not the default one") } + + if reflect.ValueOf(m.WinCondition).Pointer() != reflect.ValueOf(KingGame).Pointer() { + t.Fatalf("win conditions is not KingGame") + } } func TestLoadingAllMaps(t *testing.T) { @@ -83,3 +88,28 @@ func TestLoadingAllMaps(t *testing.T) { } } } + +func TestTileAt(t *testing.T) { + mapDef := `map: |1- + HS + HS + +symbols: + H: house + S: street +` + + m := parseMapString(t, mapDef) + if m.TileAt(INVALID_POSITION()) != nil { + t.Fatal("INVALID_POSITION returned valid rile") + } + if m.TileAt(Position{3, 0}) != nil { + t.Fatal("INVALID_POSITION returned valid rile") + } + if m.TileAt(Position{0, 3}) != nil { + t.Fatal("INVALID_POSITION returned valid rile") + } + if m.TileAt(Position{0, 0}).Type != TileTypes.House { + t.Fatal("Tile at (0,0) is not a house") + } +} diff --git a/go/game/state.go b/go/game/state.go index f6fdd2d0..6fa8b4c3 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -131,7 +131,8 @@ func (s *LocalState) IsActivePlayer(p *Player) bool { } func (s *LocalState) Loop() []*Player { - winners := []*Player{} + // Prepare the map + s._map.Prepare(s) if s._map.HasStores() { poc := NewPileOfCards() @@ -145,6 +146,7 @@ func (s *LocalState) Loop() []*Player { s._map.distributeStoreCards(poc, s.Rand) } + winners := []*Player{} for len(winners) == 0 { for _, p := range s.players { s.activePlayerId = p.Id diff --git a/go/game/tile.go b/go/game/tile.go index 048768f5..06770ff3 100644 --- a/go/game/tile.go +++ b/go/game/tile.go @@ -1,6 +1,7 @@ package game import ( + "errors" "fmt" "strings" ) @@ -113,6 +114,10 @@ func INVALID_TILE() Tile { return Tile{Position: INVALID_POSITION()} } +var ( + ErrInvalidTileName = errors.New("unknown tile name") +) + func NewTileFromString(raw string, pos Position) (Tile, error) { tile := strings.ToLower(raw) tokens := strings.Split(tile, " ") diff --git a/go/game/winCondition.go b/go/game/winCondition.go index 4204ccb1..d08f4135 100644 --- a/go/game/winCondition.go +++ b/go/game/winCondition.go @@ -4,6 +4,15 @@ import ( "golang.org/x/exp/slices" ) +// A WinCondition determines if there are winners using the current game state. +type WinCondition func(*LocalState) []*Player + +// DummyWinCondition always return an empty winner slice. +func DummyWinCondition(*LocalState) []*Player { + return []*Player{} +} + +// KingGame reports the winners of a game using kings. func KingGame(s *LocalState) []*Player { foundKings := map[*Player]struct{}{} for _, u := range s.Units() { |
