Initial commit

implemented the game of life variation with hexagonal grid
This commit is contained in:
2025-12-16 11:33:00 +00:00
commit ea5b5c4e75
17 changed files with 1229 additions and 0 deletions

25
ui/colors.go Normal file
View 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
View 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
View 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
View 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
}