Initial commit
implemented the game of life variation with hexagonal grid
This commit is contained in:
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Game of life with hexagons
|
||||||
|
|
||||||
|
Just a small experiment with Ebitengine and hexagonal grids.
|
||||||
|
|
||||||
|
The grid implementation was heavily inspired by Amit Patel blog post: [Hexagonal Grids](https://www.redblobgames.com/grids/hexagons/).
|
||||||
|
|
||||||
|
# Running
|
||||||
|
|
||||||
|
Pull the repo and then
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
or build and run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build .
|
||||||
|
./game-of-life
|
||||||
|
```
|
||||||
25
go.mod
Normal file
25
go.mod
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module github.com/nmoniz/game-of-life
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
require github.com/hajimehoshi/ebiten/v2 v2.9.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8 // indirect
|
||||||
|
github.com/go-text/typesetting v0.3.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
|
golang.org/x/image v0.31.0 // indirect
|
||||||
|
golang.org/x/text v0.29.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
|
||||||
|
github.com/ebitengine/hideconsole v1.0.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.9.0 // indirect
|
||||||
|
github.com/ebitenui/ebitenui v0.7.2
|
||||||
|
github.com/jezek/xgb v1.1.1 // indirect
|
||||||
|
github.com/nmoniz/gubgub v0.0.0-20251120170732-570e6317c0fa
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
)
|
||||||
40
go.sum
Normal file
40
go.sum
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 h1:+kz5iTT3L7uU+VhlMfTb8hHcxLO3TlaELlX8wa4XjA0=
|
||||||
|
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1/go.mod h1:lKJoeixeJwnFmYsBny4vvCJGVFc3aYDalhuDsfZzWHI=
|
||||||
|
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
|
||||||
|
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
|
||||||
|
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||||
|
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/ebitenui/ebitenui v0.7.2 h1:gSMiKvgJbrbYo57hrYeI3vRzE12kIFDNq4X09WLgM/o=
|
||||||
|
github.com/ebitenui/ebitenui v0.7.2/go.mod h1:QiJoDflkWoBv4V/LKErS3cgzTZHrXDQyqajef7IA8vM=
|
||||||
|
github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8 h1:sdIsYe6Vv7KIWZWp8KqSeTl+XlF17d+wHCC4lbxFcYs=
|
||||||
|
github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8/go.mod h1:0QBxkXxN+o4FyZgLI9FHY/oUizheze3+bNY/kgCKL+4=
|
||||||
|
github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
|
||||||
|
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.9.4 h1:IlPJpwtksylmmvNhQjv4W2bmCFWXtjY7Z10Esise1bk=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.9.4/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM=
|
||||||
|
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||||
|
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||||
|
github.com/nmoniz/gubgub v0.0.0-20251120170732-570e6317c0fa h1:oXNcFTkvGRVHk2wYrVauFnihBQFLxFHrw7HeUBjq3CU=
|
||||||
|
github.com/nmoniz/gubgub v0.0.0-20251120170732-570e6317c0fa/go.mod h1:bCxDGZD//WQAsr6yDyytOhd5tfx79MJJ+/nENzHO6yM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
|
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||||
|
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
183
grid/hex.go
Normal file
183
grid/hex.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package grid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nmoniz/game-of-life/math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HexLayout struct {
|
||||||
|
Orientation Orientation
|
||||||
|
Origin math.Pair[int]
|
||||||
|
Radius math.Pair[int]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l HexLayout) HexToPix(h Hex) math.Pair[int] {
|
||||||
|
m := l.Orientation.forward
|
||||||
|
|
||||||
|
x := float64(l.Radius.X) * (m[0]*float64(h.q) + m[1]*float64(h.r))
|
||||||
|
y := float64(l.Radius.Y) * (m[2]*float64(h.q) + m[3]*float64(h.r))
|
||||||
|
|
||||||
|
return math.Pair[int]{
|
||||||
|
X: int(x) + l.Origin.X,
|
||||||
|
Y: int(y) + l.Origin.Y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l HexLayout) PixToHex(c math.Pair[int]) Hex {
|
||||||
|
relC := math.Pair[float64]{
|
||||||
|
X: float64(c.X-l.Origin.X) / float64(l.Radius.X),
|
||||||
|
Y: float64(c.Y-l.Origin.Y) / float64(l.Radius.Y),
|
||||||
|
}
|
||||||
|
|
||||||
|
m := l.Orientation.inverse
|
||||||
|
|
||||||
|
return NewHexRounded(
|
||||||
|
m[0]*relC.X+m[1]*relC.Y,
|
||||||
|
m[2]*relC.X+m[3]*relC.Y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hex struct {
|
||||||
|
q, r, s int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHex(q, r int) Hex {
|
||||||
|
return Hex{
|
||||||
|
q: q,
|
||||||
|
r: r,
|
||||||
|
s: -q - r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHexRounded(q, r float64) Hex {
|
||||||
|
var (
|
||||||
|
s = -q - r
|
||||||
|
qi = math.RoundInt(q)
|
||||||
|
ri = math.RoundInt(r)
|
||||||
|
si = math.RoundInt(s)
|
||||||
|
)
|
||||||
|
|
||||||
|
if qi+ri+si == 0 {
|
||||||
|
return Hex{
|
||||||
|
q: qi,
|
||||||
|
r: ri,
|
||||||
|
s: si,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
qdiff = math.Abs(float64(qi) - q)
|
||||||
|
rdiff = math.Abs(float64(ri) - r)
|
||||||
|
sdiff = math.Abs(float64(si) - s)
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case qdiff > rdiff && qdiff > sdiff:
|
||||||
|
qi = -ri - si
|
||||||
|
case rdiff > sdiff:
|
||||||
|
ri = -qi - si
|
||||||
|
default:
|
||||||
|
si = -qi - ri
|
||||||
|
}
|
||||||
|
|
||||||
|
return Hex{
|
||||||
|
q: qi,
|
||||||
|
r: ri,
|
||||||
|
s: si,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) Q() int {
|
||||||
|
return h.q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) R() int {
|
||||||
|
return h.r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) S() int {
|
||||||
|
return h.s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) Add(o Hex) Hex {
|
||||||
|
return Hex{
|
||||||
|
q: h.q + o.q,
|
||||||
|
r: h.r + o.r,
|
||||||
|
s: h.s + o.s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) Sub(o Hex) Hex {
|
||||||
|
return Hex{
|
||||||
|
q: h.q - o.q,
|
||||||
|
r: h.r - o.r,
|
||||||
|
s: h.s - o.s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) Mul(o Hex) Hex {
|
||||||
|
return Hex{
|
||||||
|
q: h.q * o.q,
|
||||||
|
r: h.r * o.r,
|
||||||
|
s: h.s * o.s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) Equal(other Hex) bool {
|
||||||
|
return h.q == other.q && h.r == other.r && h.s == other.s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) Length() int {
|
||||||
|
return (math.Abs(h.q) + math.Abs(h.r) + math.Abs(h.s)) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) Distance(o Hex) int {
|
||||||
|
return h.Sub(o).Length()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) AllNeighbors() []Hex {
|
||||||
|
return []Hex{
|
||||||
|
NewHex(h.q+1, h.r),
|
||||||
|
NewHex(h.q, h.r+1),
|
||||||
|
NewHex(h.q-1, h.r+1),
|
||||||
|
NewHex(h.q-1, h.r),
|
||||||
|
NewHex(h.q, h.r-1),
|
||||||
|
NewHex(h.q+1, h.r-1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) NeighborAt(direction int) Hex {
|
||||||
|
neighbors := h.AllNeighbors()
|
||||||
|
|
||||||
|
direction = direction % 6
|
||||||
|
|
||||||
|
if direction < 0 {
|
||||||
|
direction += 6
|
||||||
|
}
|
||||||
|
|
||||||
|
return neighbors[direction]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hex) String() string {
|
||||||
|
return fmt.Sprintf("(%d,%d,%d)", h.Q(), h.R(), h.S())
|
||||||
|
}
|
||||||
|
|
||||||
|
type Orientation struct {
|
||||||
|
forward []float64
|
||||||
|
inverse []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func PointyTopLayout() Orientation {
|
||||||
|
return Orientation{
|
||||||
|
forward: []float64{math.Sqrt(3.0), math.Sqrt(3.0) / 2.0, 0.0, 3.0 / 2.0},
|
||||||
|
inverse: []float64{math.Sqrt(3.0) / 3.0, -1.0 / 3.0, 0.0, 2.0 / 3.0},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlatTopLayout() Orientation {
|
||||||
|
return Orientation{
|
||||||
|
forward: []float64{3.0 / 2.0, 0.0, math.Sqrt(3.0) / 2.0, math.Sqrt(3.0)},
|
||||||
|
inverse: []float64{2.0 / 3.0, 0.0, -1.0 / 3.0, math.Sqrt(3.0) / 3.0},
|
||||||
|
}
|
||||||
|
}
|
||||||
83
grid/hex_test.go
Normal file
83
grid/hex_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package grid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHex_NeighborAt(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
hex Hex
|
||||||
|
direction int
|
||||||
|
want Hex
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero hex zero direction",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: 0,
|
||||||
|
want: NewHex(1, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex direction 1",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: 1,
|
||||||
|
want: NewHex(0, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex direction 2",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: 2,
|
||||||
|
want: NewHex(-1, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex direction 3",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: 3,
|
||||||
|
want: NewHex(-1, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex direction 4",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: 4,
|
||||||
|
want: NewHex(0, -1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex zero direction 5",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: 5,
|
||||||
|
want: NewHex(1, -1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex zero direction overflow",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: 6,
|
||||||
|
want: NewHex(1, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex zero direction overflow twice",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: 12,
|
||||||
|
want: NewHex(1, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex zero direction underflow",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: -1,
|
||||||
|
want: NewHex(1, -1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero hex zero direction underflow twice",
|
||||||
|
hex: Hex{},
|
||||||
|
direction: -7,
|
||||||
|
want: NewHex(1, -1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := tc.hex.NeighborAt(tc.direction)
|
||||||
|
if tc.want != got {
|
||||||
|
t.Fatalf("want %v but got %v", tc.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
169
main.go
Normal file
169
main.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nmoniz/game-of-life/grid"
|
||||||
|
"github.com/nmoniz/game-of-life/math"
|
||||||
|
"github.com/nmoniz/game-of-life/ui"
|
||||||
|
|
||||||
|
"github.com/ebitenui/ebitenui"
|
||||||
|
"github.com/ebitenui/ebitenui/input"
|
||||||
|
"github.com/ebitenui/ebitenui/widget"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
ui *ebitenui.UI
|
||||||
|
|
||||||
|
Grid grid.HexLayout
|
||||||
|
Alive map[grid.Hex]struct{}
|
||||||
|
Dead map[grid.Hex]struct{}
|
||||||
|
Ticker Ticker
|
||||||
|
Speed int
|
||||||
|
Paused bool
|
||||||
|
|
||||||
|
UnderPopThreshold int
|
||||||
|
OverPopThreshold int
|
||||||
|
ReproductionTarget int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
ui: &ebitenui.UI{Container: ui.Root()},
|
||||||
|
|
||||||
|
Grid: grid.HexLayout{
|
||||||
|
Orientation: grid.PointyTopLayout(),
|
||||||
|
Origin: math.Pair[int]{X: 4, Y: 4},
|
||||||
|
Radius: math.Pair[int]{X: 8, Y: 8},
|
||||||
|
},
|
||||||
|
Alive: make(map[grid.Hex]struct{}),
|
||||||
|
Dead: make(map[grid.Hex]struct{}),
|
||||||
|
|
||||||
|
Ticker: *NewTicker(1, time.Second),
|
||||||
|
Speed: 0,
|
||||||
|
|
||||||
|
OverPopThreshold: 5,
|
||||||
|
UnderPopThreshold: 2,
|
||||||
|
ReproductionTarget: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ui.EventBus().Listen(ui.SpeedSliderEvent, func(e ui.Event) {
|
||||||
|
args, ok := e.Data().(*widget.SliderChangedEventArgs)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Speed = args.Current
|
||||||
|
|
||||||
|
if g.Speed > 0 {
|
||||||
|
g.Ticker.Reset(g.Speed, time.Second)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = ui.EventBus().Listen(ui.OverPopulationSliderEvent, func(e ui.Event) {
|
||||||
|
args, ok := e.Data().(*widget.SliderChangedEventArgs)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g.OverPopThreshold = args.Current
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = ui.EventBus().Listen(ui.UnderPopulationSliderEvent, func(e ui.Event) {
|
||||||
|
args, ok := e.Data().(*widget.SliderChangedEventArgs)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g.UnderPopThreshold = args.Current
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = ui.EventBus().Listen(ui.ReproductionTargetEvent, func(e ui.Event) {
|
||||||
|
args, ok := e.Data().(*widget.SliderChangedEventArgs)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g.ReproductionTarget = args.Current
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = ui.EventBus().Listen(ui.ResetButtonEvent, func(ui.Event) {
|
||||||
|
g.Alive = make(map[grid.Hex]struct{})
|
||||||
|
g.Dead = make(map[grid.Hex]struct{})
|
||||||
|
})
|
||||||
|
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Update() error {
|
||||||
|
g.ui.Update()
|
||||||
|
|
||||||
|
if !input.UIHovered && input.MouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
||||||
|
x, y := ebiten.CursorPosition()
|
||||||
|
hexPos := g.Grid.PixToHex(math.NewPair(x, y))
|
||||||
|
if _, ok := g.Alive[hexPos]; ok {
|
||||||
|
delete(g.Alive, hexPos)
|
||||||
|
} else {
|
||||||
|
g.Alive[hexPos] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Speed == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.Alive) > 0 && g.Ticker.HasTicked() {
|
||||||
|
g.Dead = make(map[grid.Hex]struct{})
|
||||||
|
|
||||||
|
neighbourhood := make(map[grid.Hex]int)
|
||||||
|
for hex := range g.Alive {
|
||||||
|
neighbourhood[hex] = 0
|
||||||
|
for _, neighbourHex := range hex.AllNeighbors() {
|
||||||
|
neighbourhood[neighbourHex]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for hex, count := range neighbourhood {
|
||||||
|
if _, ok := g.Alive[hex]; ok {
|
||||||
|
if count < g.UnderPopThreshold || count > g.OverPopThreshold {
|
||||||
|
g.Dead[hex] = struct{}{}
|
||||||
|
delete(g.Alive, hex)
|
||||||
|
}
|
||||||
|
} else if count == g.ReproductionTarget {
|
||||||
|
g.Alive[hex] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
|
for k := range g.Dead {
|
||||||
|
pixPosition := g.Grid.HexToPix(k)
|
||||||
|
vector.FillCircle(screen, float32(pixPosition.X), float32(pixPosition.Y), 5, color.RGBA{128, 128, 128, 0}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range g.Alive {
|
||||||
|
pixPosition := g.Grid.HexToPix(k)
|
||||||
|
vector.FillCircle(screen, float32(pixPosition.X), float32(pixPosition.Y), 5, color.RGBA{0, 255, 0, 0}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.ui.Draw(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
|
||||||
|
return 800, 600
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ebiten.SetWindowSize(800, 600)
|
||||||
|
ebiten.SetWindowTitle("Game of Life with hexagons")
|
||||||
|
if err := ebiten.RunGame(NewGame()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
37
math/comp.go
Normal file
37
math/comp.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package math
|
||||||
|
|
||||||
|
func Max[T SignedNumber](nLst ...T) T {
|
||||||
|
switch len(nLst) {
|
||||||
|
case 0:
|
||||||
|
var zeroT T
|
||||||
|
return zeroT
|
||||||
|
case 1:
|
||||||
|
return nLst[0]
|
||||||
|
default:
|
||||||
|
m := nLst[0]
|
||||||
|
for _, n := range nLst[1:] {
|
||||||
|
if m < n {
|
||||||
|
m = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Min[T SignedNumber](nLst ...T) T {
|
||||||
|
switch len(nLst) {
|
||||||
|
case 0:
|
||||||
|
var zeroT T
|
||||||
|
return zeroT
|
||||||
|
case 1:
|
||||||
|
return nLst[0]
|
||||||
|
default:
|
||||||
|
m := nLst[0]
|
||||||
|
for _, n := range nLst[1:] {
|
||||||
|
if m > n {
|
||||||
|
m = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
75
math/comp_test.go
Normal file
75
math/comp_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package math
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMax(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
values []int
|
||||||
|
exp int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multiple unique",
|
||||||
|
values: []int{3, 2, 4, 1},
|
||||||
|
exp: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single",
|
||||||
|
values: []int{5},
|
||||||
|
exp: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple duplicates",
|
||||||
|
values: []int{2, 2, 2, 1},
|
||||||
|
exp: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
act := Max(tc.values...)
|
||||||
|
if tc.exp != act {
|
||||||
|
t.Fatalf("want %v but got %v", tc.exp, act)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMin(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
values []int
|
||||||
|
exp int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multiple unique",
|
||||||
|
values: []int{3, 2, 4, 1},
|
||||||
|
exp: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single",
|
||||||
|
values: []int{5},
|
||||||
|
exp: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple duplicates",
|
||||||
|
values: []int{2, 1, 1, 1},
|
||||||
|
exp: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
act := Min(tc.values...)
|
||||||
|
if tc.exp != act {
|
||||||
|
t.Fatalf("want %v but got %v", tc.exp, act)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
math/const.go
Normal file
5
math/const.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package math
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
const Pi = math.Pi
|
||||||
39
math/ops.go
Normal file
39
math/ops.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package math
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SignedNumber interface {
|
||||||
|
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Float interface {
|
||||||
|
~float32 | ~float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func Abs[T SignedNumber](i T) T {
|
||||||
|
var zero T
|
||||||
|
if i < zero {
|
||||||
|
return -i
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func Round[T Float](n T, precision int) T {
|
||||||
|
if precision == 0 {
|
||||||
|
return T(math.Round(float64(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio := math.Pow(10, float64(precision))
|
||||||
|
f := math.Round(float64(n)*ratio) / ratio
|
||||||
|
return T(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoundInt[T Float](n T) int {
|
||||||
|
return int(Round(n, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sqrt[T Float](n T) T {
|
||||||
|
return T(math.Sqrt(float64(n)))
|
||||||
|
}
|
||||||
47
math/ops_test.go
Normal file
47
math/ops_test.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package math
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAbs(t *testing.T) {
|
||||||
|
assertEqual(t, 0, Abs(0))
|
||||||
|
assertEqual(t, 1, Abs(1))
|
||||||
|
assertEqual(t, 2, Abs(-2))
|
||||||
|
|
||||||
|
assertEqual(t, 0.0, Abs(0.0))
|
||||||
|
assertEqual(t, 1.5, Abs(1.5))
|
||||||
|
assertEqual(t, 2.75, Abs(-2.75))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoundPositive(t *testing.T) {
|
||||||
|
posf64 := float64(0.987654321)
|
||||||
|
posf64 = Round(posf64, 2)
|
||||||
|
assertEqual(t, float64(0.99), posf64)
|
||||||
|
|
||||||
|
posf32 := float32(0.987654321)
|
||||||
|
posf32 = Round(posf32, 5)
|
||||||
|
assertEqual(t, float32(0.98765), posf32)
|
||||||
|
|
||||||
|
negf64 := float64(-0.987654321)
|
||||||
|
negf64 = Round(negf64, 2)
|
||||||
|
assertEqual(t, float64(-0.99), negf64)
|
||||||
|
|
||||||
|
negf32 := float32(-0.987654321)
|
||||||
|
negf32 = Round(negf32, 5)
|
||||||
|
assertEqual(t, float32(-0.98765), negf32)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoundInt(t *testing.T) {
|
||||||
|
i := RoundInt(float64(0.5))
|
||||||
|
assertEqual(t, 1, i)
|
||||||
|
|
||||||
|
i = RoundInt(float32(1.4999999))
|
||||||
|
assertEqual(t, 1, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEqual[T comparable](t testing.TB, want, got T) {
|
||||||
|
if want != got {
|
||||||
|
t.Fatalf("want %v but got %v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
math/pair.go
Normal file
19
math/pair.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package math
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Pair[T SignedNumber] struct {
|
||||||
|
X, Y T
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPair[T SignedNumber](x, y T) Pair[T] {
|
||||||
|
return Pair[T]{X: x, Y: y}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Pair[T]) Equal(other Pair[T]) bool {
|
||||||
|
return p.X == other.X && p.Y == other.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Pair[T]) String() string {
|
||||||
|
return fmt.Sprintf("(%v,%v)", c.X, c.Y)
|
||||||
|
}
|
||||||
30
ticker.go
Normal file
30
ticker.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ticker struct {
|
||||||
|
*time.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTicker(rate int, interval time.Duration) *Ticker {
|
||||||
|
t := &Ticker{
|
||||||
|
Ticker: time.NewTicker(interval / time.Duration(rate)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Ticker) Reset(rate int, interval time.Duration) {
|
||||||
|
t.Ticker.Reset(interval / time.Duration(rate))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Ticker) HasTicked() bool {
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
25
ui/colors.go
Normal file
25
ui/colors.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mixColors(a, b color.Color, percent float64) color.Color {
|
||||||
|
rgba := func(c color.Color) (r, g, b, a uint8) {
|
||||||
|
r16, g16, b16, a16 := c.RGBA()
|
||||||
|
return uint8(r16 >> 8), uint8(g16 >> 8), uint8(b16 >> 8), uint8(a16 >> 8)
|
||||||
|
}
|
||||||
|
lerp := func(x, y uint8) uint8 {
|
||||||
|
return uint8(math.Round(float64(x) + percent*(float64(y)-float64(x))))
|
||||||
|
}
|
||||||
|
r1, g1, b1, a1 := rgba(a)
|
||||||
|
r2, g2, b2, a2 := rgba(b)
|
||||||
|
|
||||||
|
return color.RGBA{
|
||||||
|
R: lerp(r1, r2),
|
||||||
|
G: lerp(g1, g2),
|
||||||
|
B: lerp(b1, b2),
|
||||||
|
A: lerp(a1, a2),
|
||||||
|
}
|
||||||
|
}
|
||||||
58
ui/event_bus.go
Normal file
58
ui/event_bus.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/nmoniz/gubgub"
|
||||||
|
)
|
||||||
|
|
||||||
|
var eventBusGetter = sync.OnceValue(func() *eventBus {
|
||||||
|
return NewSyncEventBus()
|
||||||
|
})
|
||||||
|
|
||||||
|
func EventBus() *eventBus {
|
||||||
|
return eventBusGetter()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event interface {
|
||||||
|
Name() string
|
||||||
|
Data() any
|
||||||
|
}
|
||||||
|
|
||||||
|
type eventBus struct {
|
||||||
|
topics map[string]gubgub.Topic[Event]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSyncEventBus() *eventBus {
|
||||||
|
return &eventBus{
|
||||||
|
topics: make(map[string]gubgub.Topic[Event]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *eventBus) Emit(e Event) error {
|
||||||
|
topic, ok := eb.topics[e.Name()]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := topic.Publish(e)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("emit event: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *eventBus) Listen(name string, handler func(Event)) error {
|
||||||
|
topic, ok := eb.topics[name]
|
||||||
|
if !ok {
|
||||||
|
topic = gubgub.NewSyncTopic[Event]()
|
||||||
|
eb.topics[name] = topic
|
||||||
|
}
|
||||||
|
|
||||||
|
err := topic.Subscribe(gubgub.Forever(handler))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen for event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
9
ui/events.go
Normal file
9
ui/events.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResetButtonEvent = "reset_button"
|
||||||
|
SpeedSliderEvent = "speed_slider"
|
||||||
|
ReproductionTargetEvent = "reproduction_target_slider"
|
||||||
|
OverPopulationSliderEvent = "over_population_slider"
|
||||||
|
UnderPopulationSliderEvent = "under_population_slider"
|
||||||
|
)
|
||||||
365
ui/root.go
Normal file
365
ui/root.go
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/ebitenui/ebitenui/image"
|
||||||
|
"github.com/ebitenui/ebitenui/widget"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
|
"golang.org/x/image/colornames"
|
||||||
|
"golang.org/x/image/font/gofont/goregular"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Root() *widget.Container {
|
||||||
|
face, err := loadFont(12)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slidersTrack := &widget.SliderTrackImage{
|
||||||
|
Idle: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
|
||||||
|
Hover: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
|
||||||
|
}
|
||||||
|
|
||||||
|
slidersButton := &widget.ButtonImage{
|
||||||
|
Idle: image.NewNineSliceColor(color.NRGBA{255, 100, 100, 255}),
|
||||||
|
Hover: image.NewNineSliceColor(color.NRGBA{255, 100, 100, 255}),
|
||||||
|
Pressed: image.NewNineSliceColor(color.NRGBA{255, 100, 100, 255}),
|
||||||
|
}
|
||||||
|
|
||||||
|
root := widget.NewContainer(
|
||||||
|
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
|
||||||
|
)
|
||||||
|
|
||||||
|
topLeft := widget.NewContainer(
|
||||||
|
// widget.ContainerOpts.BackgroundImage(
|
||||||
|
// image.NewNineSliceColor(colornames.Indigo),
|
||||||
|
// ),
|
||||||
|
widget.ContainerOpts.Layout(widget.NewRowLayout(
|
||||||
|
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
|
||||||
|
widget.RowLayoutOpts.Spacing(5),
|
||||||
|
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(5)),
|
||||||
|
)),
|
||||||
|
widget.ContainerOpts.WidgetOpts(
|
||||||
|
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||||
|
VerticalPosition: widget.AnchorLayoutPositionStart,
|
||||||
|
HorizontalPosition: widget.AnchorLayoutPositionStart,
|
||||||
|
StretchVertical: true,
|
||||||
|
}),
|
||||||
|
widget.WidgetOpts.MinSize(80, 50),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
root.AddChild(topLeft)
|
||||||
|
|
||||||
|
overPopLabel := widget.NewText(
|
||||||
|
widget.TextOpts.Text(fmt.Sprintf("Over Population Threshold: >%d", 5), &face, color.White),
|
||||||
|
widget.TextOpts.Position(widget.TextPositionStart, widget.TextPositionCenter),
|
||||||
|
widget.TextOpts.WidgetOpts(
|
||||||
|
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||||
|
Position: widget.RowLayoutPositionCenter,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
topLeft.AddChild(overPopLabel)
|
||||||
|
|
||||||
|
overPopSlider := widget.NewSlider(
|
||||||
|
// Set the slider orientation - n/s vs e/w
|
||||||
|
widget.SliderOpts.Orientation(widget.DirectionHorizontal),
|
||||||
|
// Set the minimum and maximum value for the slider
|
||||||
|
widget.SliderOpts.MinMax(0, 6),
|
||||||
|
// Set the current value of the slider, without triggering a change event
|
||||||
|
widget.SliderOpts.InitialCurrent(5),
|
||||||
|
widget.SliderOpts.WidgetOpts(
|
||||||
|
// Set the Widget to layout in the center on the screen
|
||||||
|
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||||
|
HorizontalPosition: widget.AnchorLayoutPositionCenter,
|
||||||
|
VerticalPosition: widget.AnchorLayoutPositionCenter,
|
||||||
|
}),
|
||||||
|
// Set the widget's dimensions
|
||||||
|
widget.WidgetOpts.MinSize(100, 6),
|
||||||
|
),
|
||||||
|
widget.SliderOpts.Images(
|
||||||
|
slidersTrack,
|
||||||
|
slidersButton,
|
||||||
|
),
|
||||||
|
// Set the size of the handle
|
||||||
|
widget.SliderOpts.FixedHandleSize(6),
|
||||||
|
// Set the offset to display the track
|
||||||
|
widget.SliderOpts.TrackOffset(0),
|
||||||
|
// Set the size to move the handle
|
||||||
|
widget.SliderOpts.PageSizeFunc(func() int {
|
||||||
|
return 1
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
topLeft.AddChild(overPopSlider)
|
||||||
|
|
||||||
|
underPopLabel := widget.NewText(
|
||||||
|
widget.TextOpts.Text(fmt.Sprintf("Under Population Threshold: <%d", 5), &face, color.White),
|
||||||
|
widget.TextOpts.Position(widget.TextPositionStart, widget.TextPositionCenter),
|
||||||
|
widget.TextOpts.WidgetOpts(
|
||||||
|
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||||
|
Position: widget.RowLayoutPositionCenter,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
topLeft.AddChild(underPopLabel)
|
||||||
|
|
||||||
|
underPopSlider := widget.NewSlider(
|
||||||
|
// Set the slider orientation - n/s vs e/w
|
||||||
|
widget.SliderOpts.Orientation(widget.DirectionHorizontal),
|
||||||
|
// Set the minimum and maximum value for the slider
|
||||||
|
widget.SliderOpts.MinMax(0, 6),
|
||||||
|
// Set the current value of the slider, without triggering a change event
|
||||||
|
widget.SliderOpts.InitialCurrent(2),
|
||||||
|
widget.SliderOpts.WidgetOpts(
|
||||||
|
// Set the Widget to layout in the center on the screen
|
||||||
|
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||||
|
HorizontalPosition: widget.AnchorLayoutPositionCenter,
|
||||||
|
VerticalPosition: widget.AnchorLayoutPositionCenter,
|
||||||
|
}),
|
||||||
|
// Set the widget's dimensions
|
||||||
|
widget.WidgetOpts.MinSize(100, 6),
|
||||||
|
),
|
||||||
|
widget.SliderOpts.Images(
|
||||||
|
slidersTrack,
|
||||||
|
slidersButton,
|
||||||
|
),
|
||||||
|
// Set the size of the handle
|
||||||
|
widget.SliderOpts.FixedHandleSize(6),
|
||||||
|
// Set the offset to display the track
|
||||||
|
widget.SliderOpts.TrackOffset(0),
|
||||||
|
// Set the size to move the handle
|
||||||
|
widget.SliderOpts.PageSizeFunc(func() int {
|
||||||
|
return 1
|
||||||
|
}),
|
||||||
|
// Set the callback to call when the slider value is changed
|
||||||
|
widget.SliderOpts.ChangedHandler(func(args *widget.SliderChangedEventArgs) {
|
||||||
|
if args.Current == 0 {
|
||||||
|
underPopLabel.Label = "Under Population Threshold: (disabled)"
|
||||||
|
} else {
|
||||||
|
underPopLabel.Label = fmt.Sprintf("Under Population Threshold: <%d", args.Current)
|
||||||
|
}
|
||||||
|
if args.Current > overPopSlider.Current {
|
||||||
|
overPopSlider.Current = args.Current
|
||||||
|
}
|
||||||
|
_ = EventBus().Emit(SliderEvent{data: args, name: UnderPopulationSliderEvent})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
overPopSlider.ChangedEvent.AddHandler(func(argsRaw any) {
|
||||||
|
args, ok := argsRaw.(*widget.SliderChangedEventArgs)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Current == 6 {
|
||||||
|
overPopLabel.Label = "Over Population Threshold: (disabled)"
|
||||||
|
} else {
|
||||||
|
overPopLabel.Label = fmt.Sprintf("Over Population Threshold: >%d ", args.Current)
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Current < underPopSlider.Current {
|
||||||
|
underPopSlider.Current = args.Current
|
||||||
|
}
|
||||||
|
_ = EventBus().Emit(SliderEvent{data: args, name: OverPopulationSliderEvent})
|
||||||
|
})
|
||||||
|
|
||||||
|
topLeft.AddChild(underPopSlider)
|
||||||
|
|
||||||
|
reprodTartgeLabel := widget.NewText(
|
||||||
|
widget.TextOpts.Text(fmt.Sprintf("Reproduction Target: =%d", 2), &face, color.White),
|
||||||
|
widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter),
|
||||||
|
widget.TextOpts.WidgetOpts(
|
||||||
|
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||||
|
Position: widget.RowLayoutPositionStart,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
topLeft.AddChild(reprodTartgeLabel)
|
||||||
|
|
||||||
|
reprodTargetSlider := widget.NewSlider(
|
||||||
|
// Set the slider orientation - n/s vs e/w
|
||||||
|
widget.SliderOpts.Orientation(widget.DirectionHorizontal),
|
||||||
|
// Set the minimum and maximum value for the slider
|
||||||
|
widget.SliderOpts.MinMax(0, 6),
|
||||||
|
// Set the current value of the slider, without triggering a change event
|
||||||
|
widget.SliderOpts.InitialCurrent(2),
|
||||||
|
widget.SliderOpts.WidgetOpts(
|
||||||
|
// Set the Widget to layout in the center on the screen
|
||||||
|
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||||
|
HorizontalPosition: widget.AnchorLayoutPositionStart,
|
||||||
|
VerticalPosition: widget.AnchorLayoutPositionCenter,
|
||||||
|
}),
|
||||||
|
// Set the widget's dimensions
|
||||||
|
widget.WidgetOpts.MinSize(100, 6),
|
||||||
|
),
|
||||||
|
widget.SliderOpts.Images(
|
||||||
|
slidersTrack,
|
||||||
|
slidersButton,
|
||||||
|
),
|
||||||
|
// Set the size of the handle
|
||||||
|
widget.SliderOpts.FixedHandleSize(6),
|
||||||
|
// Set the offset to display the track
|
||||||
|
//
|
||||||
|
widget.SliderOpts.TrackOffset(0),
|
||||||
|
// Set the size to move the handle
|
||||||
|
widget.SliderOpts.PageSizeFunc(func() int {
|
||||||
|
return 1
|
||||||
|
}),
|
||||||
|
// Set the callback to call when the slider value is changed
|
||||||
|
widget.SliderOpts.ChangedHandler(func(args *widget.SliderChangedEventArgs) {
|
||||||
|
if args.Current > overPopSlider.Current || args.Current < underPopSlider.Current {
|
||||||
|
reprodTartgeLabel.Label = "Reproduction target: (disabled due to other parameters)"
|
||||||
|
} else {
|
||||||
|
reprodTartgeLabel.Label = fmt.Sprintf("Reproduction target: =%d", args.Current)
|
||||||
|
}
|
||||||
|
_ = EventBus().Emit(SliderEvent{data: args, name: ReproductionTargetEvent})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
topLeft.AddChild(reprodTargetSlider)
|
||||||
|
|
||||||
|
bottomRight := widget.NewContainer(
|
||||||
|
widget.ContainerOpts.Layout(widget.NewRowLayout(
|
||||||
|
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
|
||||||
|
widget.RowLayoutOpts.Spacing(5),
|
||||||
|
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(5)),
|
||||||
|
)),
|
||||||
|
widget.ContainerOpts.WidgetOpts(
|
||||||
|
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||||
|
VerticalPosition: widget.AnchorLayoutPositionEnd,
|
||||||
|
HorizontalPosition: widget.AnchorLayoutPositionEnd,
|
||||||
|
}),
|
||||||
|
widget.WidgetOpts.MinSize(80, 50),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
root.AddChild(bottomRight)
|
||||||
|
|
||||||
|
speedLabel := widget.NewText(
|
||||||
|
widget.TextOpts.Text("Speed: 0 ticks/second", &face, color.White),
|
||||||
|
widget.TextOpts.Position(widget.TextPositionStart, widget.TextPositionCenter),
|
||||||
|
widget.TextOpts.WidgetOpts(
|
||||||
|
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||||
|
Position: widget.RowLayoutPositionCenter,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
bottomRight.AddChild(speedLabel)
|
||||||
|
|
||||||
|
speedSlider := widget.NewSlider(
|
||||||
|
// Set the slider orientation - n/s vs e/w
|
||||||
|
widget.SliderOpts.Orientation(widget.DirectionHorizontal),
|
||||||
|
// Set the minimum and maximum value for the slider
|
||||||
|
widget.SliderOpts.MinMax(0, 10),
|
||||||
|
// Set the current value of the slider, without triggering a change event
|
||||||
|
widget.SliderOpts.InitialCurrent(1),
|
||||||
|
widget.SliderOpts.WidgetOpts(
|
||||||
|
// Set the Widget to layout in the center on the screen
|
||||||
|
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
|
||||||
|
HorizontalPosition: widget.AnchorLayoutPositionCenter,
|
||||||
|
VerticalPosition: widget.AnchorLayoutPositionCenter,
|
||||||
|
}),
|
||||||
|
// Set the widget's dimensions
|
||||||
|
widget.WidgetOpts.MinSize(100, 6),
|
||||||
|
),
|
||||||
|
widget.SliderOpts.Images(
|
||||||
|
slidersTrack,
|
||||||
|
slidersButton,
|
||||||
|
),
|
||||||
|
// Set the size of the handle
|
||||||
|
widget.SliderOpts.FixedHandleSize(6),
|
||||||
|
// Set the offset to display the track
|
||||||
|
widget.SliderOpts.TrackOffset(0),
|
||||||
|
// Set the size to move the handle
|
||||||
|
widget.SliderOpts.PageSizeFunc(func() int {
|
||||||
|
return 1
|
||||||
|
}),
|
||||||
|
// Set the callback to call when the slider value is changed
|
||||||
|
widget.SliderOpts.ChangedHandler(func(args *widget.SliderChangedEventArgs) {
|
||||||
|
speedLabel.Label = fmt.Sprintf("Speed: %d ticks/second", args.Current)
|
||||||
|
_ = EventBus().Emit(SliderEvent{data: args, name: SpeedSliderEvent})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
bottomRight.AddChild(speedSlider)
|
||||||
|
|
||||||
|
baseColor := colornames.Aliceblue
|
||||||
|
resetButton := widget.NewButton(
|
||||||
|
widget.ButtonOpts.Image(&widget.ButtonImage{
|
||||||
|
Idle: image.NewBorderedNineSliceColor(baseColor, colornames.Black, 1),
|
||||||
|
Hover: image.NewBorderedNineSliceColor(mixColors(baseColor, colornames.Gainsboro, 0.2), colornames.Black, 1),
|
||||||
|
Pressed: image.NewBorderedNineSliceColor(mixColors(baseColor, colornames.Darkgray, 0.2), colornames.Black, 1),
|
||||||
|
}),
|
||||||
|
widget.ButtonOpts.Text("Reset", &face, &widget.ButtonTextColor{
|
||||||
|
Idle: colornames.Purple,
|
||||||
|
}),
|
||||||
|
widget.ButtonOpts.WidgetOpts(
|
||||||
|
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
|
||||||
|
Stretch: true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
widget.ButtonOpts.TextPosition(
|
||||||
|
widget.TextPositionCenter,
|
||||||
|
widget.TextPositionCenter,
|
||||||
|
),
|
||||||
|
widget.ButtonOpts.ClickedHandler(func(args *widget.ButtonClickedEventArgs) {
|
||||||
|
_ = EventBus().Emit(ButtonEvent{data: args, name: ResetButtonEvent})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
bottomRight.AddChild(resetButton)
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
type SliderEvent struct {
|
||||||
|
name string
|
||||||
|
data *widget.SliderChangedEventArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data implements Event.
|
||||||
|
func (s SliderEvent) Data() any {
|
||||||
|
return s.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements Event.
|
||||||
|
func (s SliderEvent) Name() string {
|
||||||
|
return s.name
|
||||||
|
}
|
||||||
|
|
||||||
|
type ButtonEvent struct {
|
||||||
|
name string
|
||||||
|
data *widget.ButtonClickedEventArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data implements Event.
|
||||||
|
func (b ButtonEvent) Data() any {
|
||||||
|
return b.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements Event.
|
||||||
|
func (b ButtonEvent) Name() string {
|
||||||
|
return b.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFont(size float64) (text.Face, error) {
|
||||||
|
s, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &text.GoTextFace{
|
||||||
|
Source: s,
|
||||||
|
Size: size,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user