286 lines
5.7 KiB
Go
286 lines
5.7 KiB
Go
package generator
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"math/rand"
|
|
"time"
|
|
)
|
|
|
|
type TerritoryType int
|
|
|
|
const (
|
|
EmptyTerritory TerritoryType = iota
|
|
WastelandTerritory
|
|
MountainTerritory
|
|
RuinsTerritory
|
|
)
|
|
|
|
const (
|
|
UpDirection int = iota
|
|
RightDirection
|
|
DownDirection
|
|
LeftDirection
|
|
)
|
|
|
|
type Tile struct {
|
|
Territory TerritoryType `json:"territory"`
|
|
}
|
|
|
|
func (t Tile) MarshalJSON() ([]byte, error) {
|
|
var s string
|
|
switch t.Territory {
|
|
case EmptyTerritory:
|
|
s = "empty"
|
|
case WastelandTerritory:
|
|
s = "wasteland"
|
|
case MountainTerritory:
|
|
s = "mountain"
|
|
case RuinsTerritory:
|
|
s = "ruin"
|
|
}
|
|
|
|
return []byte("\"" + s + "\""), nil
|
|
}
|
|
|
|
func (t Tile) Plot() string {
|
|
switch t.Territory {
|
|
case EmptyTerritory:
|
|
return " "
|
|
case WastelandTerritory:
|
|
return "#"
|
|
case MountainTerritory:
|
|
return "M"
|
|
case RuinsTerritory:
|
|
return "I"
|
|
}
|
|
|
|
return "?"
|
|
}
|
|
|
|
type World struct {
|
|
Size int `json:"size"`
|
|
Wastelands int `json:"wastelands"`
|
|
Ruins int `json:"ruins"`
|
|
Mountains int `json:"mountains"`
|
|
|
|
World []Tile `json:"tiles"`
|
|
Seed string
|
|
}
|
|
|
|
func (w World) Plot() string {
|
|
var board string
|
|
for i := 0; i < w.Size*w.Size; i++ {
|
|
board += fmt.Sprintf("[%v]", w.World[i].Plot())
|
|
|
|
if (i+1)%w.Size == 0 {
|
|
board += fmt.Sprintf("\n")
|
|
}
|
|
}
|
|
|
|
return board
|
|
}
|
|
|
|
func (w World) JSON() string {
|
|
output, _ := json.MarshalIndent(w, "", " ")
|
|
return string(output)
|
|
}
|
|
|
|
func New(size int, numWastelands int, numMountains int, numRuins int, seed string) (World, error) {
|
|
InitSeed(seed)
|
|
|
|
w := World{
|
|
Size: size,
|
|
Wastelands: numWastelands,
|
|
Mountains: numMountains,
|
|
Ruins: numRuins,
|
|
Seed: seed,
|
|
}
|
|
|
|
if size < 3 || size > 25 {
|
|
return World{}, errors.New("Spielfeldgröße nicht im Bereich 3..25.")
|
|
}
|
|
|
|
// All empty for start
|
|
for i := 0; i < w.Size*w.Size; i++ {
|
|
w.World = append(w.World, Tile{Territory: EmptyTerritory})
|
|
}
|
|
|
|
// Place wasteland area by finding a suitable place to start
|
|
// and surround it with 6 more wastelands
|
|
var wastelands []int
|
|
|
|
if numWastelands > 0 {
|
|
if numWastelands > size*size {
|
|
return World{}, errors.New("Zu viele Ödlandfelder.")
|
|
}
|
|
|
|
startPos := roll(w.Size-1) + roll(w.Size-1)*w.Size
|
|
w.place(WastelandTerritory, startPos)
|
|
wastelands = append(wastelands, startPos)
|
|
|
|
for i := 0; i < numWastelands-1; i++ {
|
|
var candidates []int
|
|
|
|
// Find all possible candidates (top, left, bottom, right),
|
|
// from all already places wastelands, then choose one at
|
|
// random and place it.
|
|
for _, wl := range wastelands {
|
|
for _, free := range w.neighboursOfType(wl, EmptyTerritory) {
|
|
if !contains(candidates, free) {
|
|
candidates = append(candidates, free)
|
|
}
|
|
}
|
|
}
|
|
|
|
candidate := randomItem(candidates)
|
|
w.place(WastelandTerritory, candidate)
|
|
wastelands = append(wastelands, candidate)
|
|
}
|
|
}
|
|
|
|
// Place 5 mountains on free tiles. We need to make sure that
|
|
// a mountain do not touch another mountain.
|
|
for i := 0; i < numMountains; i++ {
|
|
var candidates []int
|
|
|
|
// Start with all free fields
|
|
for pos, tile := range w.World {
|
|
if tile.Territory == EmptyTerritory &&
|
|
len(w.neighboursOfType(pos, MountainTerritory)) == 0 {
|
|
candidates = append(candidates, pos)
|
|
}
|
|
}
|
|
|
|
if len(candidates) < 1 {
|
|
return World{}, errors.New("Zu viele Berge.")
|
|
}
|
|
w.place(MountainTerritory, randomItem(candidates))
|
|
}
|
|
|
|
// Place 6 ruins. Same constraint as mountains apply here (no
|
|
// two ruins should not touch each other)
|
|
for i := 0; i < numRuins; i++ {
|
|
var candidates []int
|
|
|
|
// Start with all free fields
|
|
for pos, tile := range w.World {
|
|
if tile.Territory == EmptyTerritory &&
|
|
len(w.neighboursOfType(pos, RuinsTerritory)) == 0 {
|
|
candidates = append(candidates, pos)
|
|
}
|
|
}
|
|
if len(candidates) < 1 {
|
|
return World{}, errors.New("Zu viele Ruinen.")
|
|
}
|
|
w.place(RuinsTerritory, randomItem(candidates))
|
|
}
|
|
|
|
return w, nil
|
|
}
|
|
|
|
func InitSeed(seed string) {
|
|
h := md5.New()
|
|
io.WriteString(h, seed)
|
|
var intSeed uint64 = binary.BigEndian.Uint64(h.Sum(nil))
|
|
rand.Seed(int64(intSeed))
|
|
}
|
|
|
|
func RandomSeed() string {
|
|
InitSeed(time.Now().String())
|
|
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
var seed []byte
|
|
for i := 0; i < 10; i++ {
|
|
seed = append(seed, chars[roll(25)])
|
|
}
|
|
|
|
return string(seed)
|
|
}
|
|
|
|
func (w World) neighbour(direction int, pos int) (int, error) {
|
|
switch direction {
|
|
case UpDirection:
|
|
if pos < w.Size {
|
|
return 0, errors.New("Out of bounds")
|
|
}
|
|
return pos - w.Size, nil
|
|
case RightDirection:
|
|
if pos%w.Size == w.Size-1 ||
|
|
pos >= w.Size*w.Size-1 {
|
|
return 0, errors.New("Out of bounds")
|
|
}
|
|
return pos + 1, nil
|
|
case DownDirection:
|
|
if pos >= ((w.Size-1)*w.Size)-1 {
|
|
return 0, errors.New("Out of bounds")
|
|
}
|
|
return pos + w.Size, nil
|
|
case LeftDirection:
|
|
if pos%w.Size == 0 ||
|
|
pos <= 0 {
|
|
return 0, errors.New("Out of bounds")
|
|
}
|
|
return pos - 1, nil
|
|
}
|
|
|
|
return 0, errors.New("Wrong direction")
|
|
}
|
|
|
|
func (w World) neighbours(pos int) []int {
|
|
var neighbours []int
|
|
|
|
for i := 0; i < 4; i++ {
|
|
n, err := w.neighbour(i, pos)
|
|
if err == nil {
|
|
neighbours = append(neighbours, n)
|
|
}
|
|
}
|
|
|
|
return neighbours
|
|
}
|
|
|
|
func (w World) neighboursOfType(pos int, territory TerritoryType) []int {
|
|
var neighbours []int
|
|
|
|
for _, n := range w.neighbours(pos) {
|
|
if w.World[n].Territory == territory {
|
|
neighbours = append(neighbours, n)
|
|
}
|
|
}
|
|
|
|
return neighbours
|
|
}
|
|
|
|
func (w World) ToXY(pos int) (int, int) {
|
|
return pos % w.Size, int(math.Floor((float64(pos) / float64(w.Size))))
|
|
}
|
|
|
|
func roll(w int) int {
|
|
return rand.Intn(w)
|
|
}
|
|
|
|
func contains(s []int, e int) bool {
|
|
for _, a := range s {
|
|
if a == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func randomItem(c []int) int {
|
|
rand.Shuffle(len(c), func(i, j int) {
|
|
c[i], c[j] = c[j], c[i]
|
|
})
|
|
|
|
return c[0]
|
|
}
|
|
|
|
func (w World) place(territory TerritoryType, pos int) {
|
|
w.World[pos] = Tile{Territory: territory}
|
|
}
|