kartograph-maps/pkg/generator/world.go

252 lines
5.0 KiB
Go
Raw Normal View History

2021-03-09 11:29:46 +01:00
package generator
import (
"encoding/json"
2021-03-09 11:29:46 +01:00
"errors"
"fmt"
"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
2021-03-09 11:29:46 +01:00
}
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"`
2021-03-09 11:29:46 +01:00
World []Tile `json:"tiles"`
2021-03-09 11:29:46 +01:00
}
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)
}
2021-03-09 11:29:46 +01:00
func (w World) ExportToPDF(filename string) error {
// TODO: Draw the PDF
return nil
}
func New(size int, numWastelands int, numMountains int, numRuins int) World {
rand.Seed(time.Now().UnixNano())
w := World{
Size: size,
Wastelands: numWastelands,
Mountains: numMountains,
Ruins: numRuins,
2021-03-09 11:29:46 +01:00
}
// 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
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)
}
}
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)
}
}
w.place(RuinsTerritory, randomItem(candidates))
}
return w
}
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}
}