goids/main.go
2023-02-26 18:00:34 +01:00

347 lines
8.7 KiB
Go

package main
import (
"bytes"
"encoding/base64"
"fmt"
"html/template"
"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
const OM_TEMPLATE = `; OM File Header - Saved 2023/01/22 08:12:44
; (7.01 :patc (om-make-point 10 10) (om-make-point 915 795) (om-make-point 845 307) "" 183 0 "2023/01/22 08:11:39" "2023/01/22 08:12:44")
; End File Header
(in-package :om)(load-lib-for (quote nil))(setf *om-current-persistent* (om-load-patch1 "Patch" (quote ({{range $val := .BPC}}(let ((box (om-load-editor-box1 "BPC" (quote bpc) (quote ((om-load-inputfun (quote input-funbox) "object" "self" nil) (om-load-inputfun (quote input-funbox) "X coordinates (list)" "x-points" (list{{ range $x := .X }} {{ $x }}{{ end }})) (om-load-inputfun (quote input-funbox) "Y coordinates (list)" "y-points" (list{{ range $y:= .Y }} {{ $y }}{{ end }})) (om-load-inputfun (quote input-funbox) "precision (integer) [0 - 10]" "decimals" 0))) (om-make-point {{ .MakePoint.X }} {{ .MakePoint.Y }}) (om-make-point 50 50) (let ((newobj (when (find-class (quote bpc) nil) (let ((newbpf (simple-bpf-from-list (quote ({{ range $x := .X }} {{ $x }}{{ end }})) (quote ({{ range $y := .Y }} {{ $y }}{{ end }})) (quote bpc) 0))) (setf (bpfcolor newbpf) (om-make-color 0 0 0)) (set-name newbpf "BPC") newbpf)))) (when newobj) newobj) "&" nil (pairlis (quote (picture winpos winsize)) (list nil (om-make-point 20 20)(om-make-point 20 20))) nil nil nil nil))) (when (fboundp (quote set-active)) (set-active box nil)) box){{ end }}))(quote nil) nil 7.01))
`
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 BPC struct {
BPC []Coords
}
type Coords struct {
X []int
Y []int
MakePoint Coor
}
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)
}
bpc := BPC{}
mX := 0
mY := 0
for i := 0; i <= total; i++ {
mX += 70
if i%10 == 0 {
mX = 70
mY += 70
}
coords := Coords{}
makePoint := Coor{X: mX, Y: mY}
coords.MakePoint = makePoint
var x []string
var y []string
outfile.Write([]byte(fmt.Sprintf("GOID: %d\n", i)))
for z := 0; z <= loops; z++ {
cX := 0
cY := 0
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))
coords.X = append(coords.X, g.Data.X)
coords.Y = append(coords.Y, g.Data.Y)
cX = g.Data.X
cY = g.Data.Y
break
}
}
if !check {
x = append(x, "-")
y = append(y, "-")
coords.X = append(coords.X, cX)
coords.Y = append(coords.Y, cY)
}
}
bpc.BPC = append(bpc.BPC, coords)
outfile.Write([]byte(fmt.Sprintf("%v\n", x)))
outfile.Write([]byte(fmt.Sprintf("%v\n", y)))
}
t, err := template.New("todos").Parse(OM_TEMPLATE)
if err != nil {
panic(err)
}
file, _ := os.Create("Patch.omp")
defer file.Close()
err = t.Execute(file, bpc)
if err != nil {
panic(err)
}
}
// 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)
}