aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-08-21 17:26:35 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-21 17:26:35 +0200
commitaf168066174ee2e7172bd3acb16e316265c1189b (patch)
tree38dd563a5e4d15a9896af2774e0760aa9d4f0a50
parent14b14369c10abab2f0de2f7109735f95f1a4c94a (diff)
downloadmuhqs-game-af168066174ee2e7172bd3acb16e316265c1189b.tar.gz
muhqs-game-af168066174ee2e7172bd3acb16e316265c1189b.zip
add random map generation to the random challenge
-rw-r--r--go/game/challenge.go27
-rw-r--r--go/game/map.go180
-rw-r--r--go/ui/mapView.go8
3 files changed, 201 insertions, 14 deletions
diff --git a/go/game/challenge.go b/go/game/challenge.go
index 3b5b35cd..e12459fb 100644
--- a/go/game/challenge.go
+++ b/go/game/challenge.go
@@ -1,17 +1,14 @@
package game
import (
- "log"
"math/rand"
+
+ "muhq.space/muhqs-game/go/log"
)
-func prepState(playerName, mapName string) *LocalState {
+func prepState(playerName string, m *Map) *LocalState {
s := NewLocalState()
- if m, err := GetMap(mapName); err != nil {
- log.Panic(err)
- } else {
- s.SetMap(m)
- }
+ s.SetMap(m)
// Prepare the players
s.AddNewPlayer(playerName, NewDeck())
@@ -24,7 +21,17 @@ func prepState(playerName, mapName string) *LocalState {
func NewRandomChallenge(name string) *LocalState {
maps := []string{"2P-ring-street", "2P-river-king"}
- m := maps[rand.Intn(len(maps))]
+ var m *Map
+ n := rand.Intn(len(maps) + 1)
+ switch n {
+ case 0, 1:
+ m, _ = GetMap(maps[rand.Intn(len(maps))])
+ case 2:
+ seed := rand.Int63()
+ log.Info("create random map", "seed", seed)
+ m = RandomMap(seed, 2)
+ }
+
s := prepState(name, m)
switch rand.Intn(3) {
@@ -107,7 +114,7 @@ func prepCavArcherChallenge(s *LocalState) *LocalState {
func NewCavArcherChallenge(playerName string) *LocalState {
maps := []string{"2P-ring-street", "2P-river-king"}
- m := maps[rand.Intn(len(maps))]
+ m, _ := GetMap(maps[rand.Intn(len(maps))])
return prepCavArcherChallenge(prepState(playerName, m))
}
@@ -154,7 +161,7 @@ func prepMissionaryChallenge(s *LocalState) *LocalState {
func NewMissionaryChallenge(playerName string) *LocalState {
maps := []string{"2P-ring-street", "2P-river-king"}
- m := maps[rand.Intn(len(maps))]
+ m, _ := GetMap(maps[rand.Intn(len(maps))])
return prepMissionaryChallenge(prepState(playerName, m))
}
diff --git a/go/game/map.go b/go/game/map.go
index f2cf1c53..a83f4984 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path"
+ "strconv"
"strings"
"golang.org/x/exp/slices"
@@ -315,3 +316,182 @@ func (m *Map) HasStores() bool {
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{
+ Position{0, 0},
+ Position{def.x - 1, def.y - 1},
+ Position{0, def.y - 1},
+ Position{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{
+ Position{len(m.Tiles[0]) - 1, def.y - 1},
+ Position{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, tower, 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)
+}
diff --git a/go/ui/mapView.go b/go/ui/mapView.go
index 36455d8c..29b1ddd3 100644
--- a/go/ui/mapView.go
+++ b/go/ui/mapView.go
@@ -4,7 +4,6 @@ import (
"fmt"
"image"
"image/color"
- "log"
"math"
"strings"
"unicode"
@@ -13,6 +12,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector"
"muhq.space/muhqs-game/go/assets"
"muhq.space/muhqs-game/go/game"
+ "muhq.space/muhqs-game/go/log"
)
var (
@@ -101,7 +101,7 @@ func (vw *MapView) handleStreet(x, y int, op *ebiten.DrawImageOptions) *ebiten.I
// This street is not connected to anything. Seams odd!
if connections == 0 {
- log.Printf("Street at (%d, %d) is not connected", x, y)
+ log.Warn(fmt.Sprintf("Street at (%d, %d) is not connected", x, y))
img = assets.GetTile("street")
} else if connections == 1 ||
(connections == 2 && ((left && right) || (above && below))) {
@@ -149,7 +149,7 @@ func (vw *MapView) handleWall(x, y int, op *ebiten.DrawImageOptions) *ebiten.Ima
// This wall is not connected to anything. Seams odd!
if connections == 0 {
- log.Printf("Wall at (%d, %d) is not connected", x, y)
+ log.Warn(fmt.Sprintf("Wall at (%d, %d) is not connected", x, y))
img = assets.GetTile("wall")
} else if (connections == 1 ||
(connections == 2 && ((left && right) || (above && below)))) &&
@@ -259,7 +259,7 @@ func (vw *MapView) drawMapLayer(screen *ebiten.Image) {
tileImg = assets.GetTile(tile.Raw)
}
if tileImg == nil {
- log.Panic("failed to load tile", tile.Raw)
+ log.Panic("failed to load tile", "tile", tile.Raw)
}
op.GeoM.Translate(x_px, y_px)