goids/main.go

303 lines
6.6 KiB
Go

package main
import (
"bytes"
"encoding/base64"
"fmt"
"image"
"image/color"
"image/png"
"io/ioutil"
"math"
"math/rand"
"os"
"sort"
"strconv"
"github.com/llgcode/draw2d/draw2dimg"
"gopkg.in/yaml.v2"
)
// parameters
var goidSize = 3
var goidColor = color.RGBA{200, 200, 100, 255} // gray, 50% transparency
var populationSize = 0
var loops = 3000
var numNeighbours = 5
var separationFactor = float64(goidSize * 5)
var coherenceFactor = 8
type configuration struct {
Steps []steps `yaml:"steps"`
ScreenSize ScreenSize `yaml:"screen_size"`
LastStep int `yaml:"last_step"`
InitAmount int `yaml:"init_amount"`
}
type ScreenSize struct {
Width int `yaml:"width"`
Height int `yaml:"height"`
}
type steps struct {
Step int `yaml:"step"`
Amount int `yaml:"amount"`
NumNeighbours int `yaml:"neighbours"`
}
type Coor struct {
X int `yaml:"x"`
Y int `yaml:"y"`
}
type OutPut struct {
Goid int `yaml:"goid"`
Loop int `yaml:"loop"`
Data Coor `yaml:"data"`
}
func fixGoids(gs []*Goid, actual int, wished int, field ScreenSize) ([]*Goid, int) {
if actual < wished {
g := createRandomGoid(field)
gs = append(gs, &g)
return gs, actual + 1
} else if wished < actual {
return gs[:len(gs)-1], actual - 1
} else {
return gs, actual
}
}
func main() {
fo, err := os.Create("/tmp/goids.yaml")
if err != nil {
panic(err)
}
conf, err := ioutil.ReadFile("config.yaml")
if err != nil {
panic(err)
}
config := configuration{}
err = yaml.Unmarshal(conf, &config)
steps := config.Steps
loops = config.LastStep
populationSize = config.InitAmount
defer func() {
if err := fo.Close(); err != nil {
panic(err)
}
}()
clearScreen()
hideCursor()
var goids []*Goid
for i := 0; i < populationSize; i++ {
g := createRandomGoid(config.ScreenSize)
goids = append(goids, &g)
}
current_stage := 0
for i := 0; i < loops; i++ {
goids, populationSize = fixGoids(goids, populationSize, steps[current_stage].Amount, config.ScreenSize)
if steps[current_stage].NumNeighbours >= populationSize {
numNeighbours = populationSize
} else {
numNeighbours = steps[current_stage].NumNeighbours
}
if i == steps[current_stage+1].Step {
current_stage += 1
}
move(goids, fo, i, config.ScreenSize)
frame := draw(goids, config.ScreenSize)
printImage(frame.SubImage(frame.Rect))
}
showCursor()
out, err := ioutil.ReadFile("/tmp/goids.yaml")
if err != nil {
panic(err)
}
output := []OutPut{}
err = yaml.Unmarshal(out, &output)
var total int = 0
for _, goid := range output {
if total < goid.Goid {
total = goid.Goid
}
}
outfile, err := os.Create("./goids.txt")
if err != nil {
panic(err)
}
for i := 0; i <= total; i++ {
var x []string
var y []string
outfile.Write([]byte(fmt.Sprintf("GOID: %d\n", i)))
for z := 0; z <= loops; z++ {
check := false
for _, g := range output {
if g.Goid == i && g.Loop == z {
check = true
x = append(x, strconv.Itoa(g.Data.X))
y = append(y, strconv.Itoa(g.Data.Y))
break
}
}
if !check {
x = append(x, "-")
y = append(y, "-")
}
}
outfile.Write([]byte(fmt.Sprintf("%v\n", x)))
outfile.Write([]byte(fmt.Sprintf("%v\n", y)))
}
}
// Goid represents a drawn goid
type Goid struct {
X int // position
Y int
Vx int // velocity
Vy int
R int // radius
Color color.Color
}
func createRandomGoid(field ScreenSize) (g Goid) {
g = Goid{
X: rand.Intn(field.Width),
Y: rand.Intn(field.Height),
Vx: rand.Intn(goidSize),
Vy: rand.Intn(goidSize),
R: goidSize,
Color: goidColor,
}
return
}
// find the nearest neighbours
func (g *Goid) nearestNeighbours(goids []*Goid) (neighbours []Goid) {
neighbours = make([]Goid, len(goids))
for _, goid := range goids {
neighbours = append(neighbours, *goid)
}
sort.SliceStable(neighbours, func(i, j int) bool {
return g.distance(neighbours[i]) < g.distance(neighbours[j])
})
return
}
// distance between 2 goids
func (g *Goid) distance(n Goid) float64 {
x := g.X - n.X
y := g.Y - n.Y
return math.Sqrt(float64(x*x + y*y))
}
// move the goids with the 3 classic boid rules
func move(goids []*Goid, file *os.File, loop int, field ScreenSize) {
for i, goid := range goids {
neighbours := goid.nearestNeighbours(goids)
separate(goid, neighbours)
align(goid, neighbours)
cohere(goid, neighbours)
position := fmt.Sprintf("- goid: %d\n loop: %d\n data:\n x: %d\n y: %d\n", i, loop, goid.X, goid.Y)
file.Write([]byte(position))
stayInWindow(goid, field)
}
}
// if goid goes out of the window frame it comes back on the other side
func stayInWindow(goid *Goid, field ScreenSize) {
if goid.X < 0 {
goid.X = field.Width - goid.X
} else if goid.X > field.Width {
goid.X = field.Width - goid.X
}
if goid.Y < 0 {
goid.Y = field.Height - goid.Y
} else if goid.Y > field.Height {
goid.Y = field.Height - goid.Y
}
}
// steer to avoid crowding local goids
func separate(g *Goid, neighbours []Goid) {
x, y := 0, 0
for _, n := range neighbours[0:numNeighbours] {
if g.distance(n) < separationFactor {
x += g.X - n.X
y += g.Y - n.Y
}
}
g.Vx = x
g.Vy = y
g.X += x
g.Y += y
}
// steer towards the average heading of local goids
func align(g *Goid, neighbours []Goid) {
x, y := 0, 0
for _, n := range neighbours[0:numNeighbours] {
x += n.Vx
y += n.Vy
}
dx, dy := x/numNeighbours, y/numNeighbours
g.Vx += dx
g.Vy += dy
g.X += dx
g.Y += dy
}
// steer to move toward the average position of local goids
func cohere(g *Goid, neighbours []Goid) {
x, y := 0, 0
for _, n := range neighbours[0:numNeighbours] {
x += n.X
y += n.Y
}
dx, dy := ((x/numNeighbours)-g.X)/coherenceFactor, ((y/numNeighbours)-g.Y)/coherenceFactor
g.Vx += dx
g.Vy += dy
g.X += dx
g.Y += dy
}
// draw the goids
func draw(goids []*Goid, field ScreenSize) *image.RGBA {
dest := image.NewRGBA(image.Rect(0, 0, field.Width, field.Height))
gc := draw2dimg.NewGraphicContext(dest)
for _, goid := range goids {
gc.SetFillColor(goid.Color)
gc.MoveTo(float64(goid.X), float64(goid.Y))
gc.ArcTo(float64(goid.X), float64(goid.Y), float64(goid.R), float64(goid.R), 0, -math.Pi*2)
gc.LineTo(float64(goid.X-goid.Vx), float64(goid.Y-goid.Vy))
gc.Close()
gc.Fill()
}
return dest
}
// ANSI escape sequence codes to perform action on terminal
func hideCursor() {
fmt.Print("\033[?25l")
}
func showCursor() {
fmt.Print("\x1b[?25h\n")
}
func clearScreen() {
fmt.Print("\x1b[2J")
}
// this only works for iTerm!
func printImage(img image.Image) {
var buf bytes.Buffer
png.Encode(&buf, img)
imgBase64Str := base64.StdEncoding.EncodeToString(buf.Bytes())
fmt.Printf("\x1b[2;0H\x1b]1337;File=inline=1:%s\a", imgBase64Str)
}