227 lines
4.4 KiB
Go
227 lines
4.4 KiB
Go
|
package generator
|
||
|
|
||
|
import (
|
||
|
"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
|
||
|
}
|
||
|
|
||
|
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
|
||
|
Wastelands int
|
||
|
Ruins int
|
||
|
Mountains int
|
||
|
|
||
|
World []Tile
|
||
|
}
|
||
|
|
||
|
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) 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,
|
||
|
}
|
||
|
|
||
|
// 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}
|
||
|
}
|