aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-07-03 21:48:58 -0400
committerFlorian Fischer <florian.fischer@muhq.space>2025-07-06 11:49:19 -0400
commitd4e6d843dc71b68a34f1aa2eed84e5fe20af3589 (patch)
treeee25b7ef22ee4408325642c65d26784646f5ff37
parentfa5c66a13d7c182f44c9fa52c216e4d0f455ea29 (diff)
downloadmuhqs-game-d4e6d843dc71b68a34f1aa2eed84e5fe20af3589.tar.gz
muhqs-game-d4e6d843dc71b68a34f1aa2eed84e5fe20af3589.zip
support king games
-rw-r--r--go/game/map.go69
-rw-r--r--go/game/map_test.go30
-rw-r--r--go/game/state.go4
-rw-r--r--go/game/tile.go5
-rw-r--r--go/game/winCondition.go9
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() {