55 Commits

Author SHA1 Message Date
Thomas von Dein
d1ad4dc7bb bumpversion 2024-07-12 23:01:46 +02:00
caec6048a8 added C test 2024-06-30 19:09:48 +02:00
18cc6cdf74 fixed grid lines performance problem 2024-06-15 18:21:54 +02:00
d87f42b877 use optimized rule check if B3/S23 is active 2024-06-15 15:30:58 +02:00
d0f0af6459 draw the grid explicitly thus leading to full cells w/o the grid 2024-06-15 13:57:54 +02:00
7dbd52970c revert last revert and fixed history slow down bug 2024-06-15 13:35:27 +02:00
c565187113 revert history=>struct, but evolution doesn't work anymore anyway 2024-06-15 13:15:22 +02:00
78147b287b bool => uint8 2024-06-15 12:06:17 +02:00
73e74761bb using go-routines (one per row), makes it faster 2024-06-15 11:42:41 +02:00
c7be9ab3ee use cells instead of only bools, use pointer list to all neighbors 2024-06-14 17:53:58 +02:00
45e5fc7e3b tried arche ecs: utter fail, needs 4.3 the time 2024-06-12 20:01:30 +02:00
39da34cb5c more tests 2024-06-11 23:59:47 +02:00
T.v.Dein
1623277c85 Update TODO.md: add pointer idea, try ecs 2024-06-11 23:08:49 +02:00
15bce3cb3a add grid idea 2024-06-11 19:47:01 +02:00
3cff41c991 fixed colors of standard theme 2024-06-11 19:43:30 +02:00
d66fb489fe using switch in input checks 2024-06-11 19:39:46 +02:00
927e47dc92 remove old debug prints, update TODO 2024-06-11 19:22:29 +02:00
f14f4ff21a removed unneeded code, fixed game exit from menu 2024-06-11 19:15:40 +02:00
b8496d0ae2 added grid comment to theme.go, since I always forget how grindlines work 2024-06-09 18:41:27 +02:00
1af3e9fc42 add clean target, fixed mix up colors 2024-06-09 18:33:09 +02:00
aa7999a01b add screenshots 2024-06-09 18:21:45 +02:00
3a743a65e5 fixed theme selection from menu, centralized theme def to 1 place 2024-06-09 18:00:06 +02:00
01cfaf3b78 +fix 2024-06-08 20:12:36 +02:00
7d717423c9 +previewtext 2024-06-08 20:12:10 +02:00
4f7cf4c419 initial options bug fixed
See
424f62327b

Thanks a lot for the fast support, amazing!
2024-06-08 20:06:02 +02:00
T.v.Dein
3c992a00c6 fix video 2024-06-08 19:56:20 +02:00
1ec84213a0 cleanup and added video to readme 2024-06-08 19:52:20 +02:00
a109838c4c calculating zoom factor based on cellsize 2024-06-08 16:29:09 +02:00
65a67f1bac removed invert option, added new standard theme (orange on grey) 2024-06-08 16:19:54 +02:00
5813f8fab8 added option to select theme, can be used also to select RLEs :) 2024-06-07 18:39:14 +02:00
0c44a7e266 more performance tests 2024-06-07 17:34:09 +02:00
81c4b976e2 added theme system, makes it easier to add more color schemes 2024-06-07 17:33:54 +02:00
4695338323 added key bindings help screen, reachable via menu 2024-06-06 19:55:16 +02:00
63f4aa839d hint about themes 2024-06-06 19:38:11 +02:00
1574e03085 put evolutioin drawing out of Draw(), handle history only if enabled 2024-06-06 19:36:51 +02:00
9fb2779c02 fixed canvas dragging speed, it's now relative to the world 2024-06-06 19:24:23 +02:00
9adc7ddbdc added explicit insert/draw mode, left mouse by default moves canvas 2024-06-06 19:13:07 +02:00
ab22e0f4e2 changed grid data type to bool, save mem and better perf 2024-06-06 18:58:31 +02:00
a5dbd69976 branchless experiments (failed and reverted, see commented code) 2024-06-05 23:51:55 +02:00
162d141b34 reorganized source, added test cases for tuning 2024-06-05 16:33:35 +02:00
T.v.Dein
50fab6e1a5 Update TODO.md 2024-06-05 08:28:52 +02:00
8e361a04bd added tuning hint 2024-06-04 19:27:55 +02:00
2febea3264 added info about patterns 2024-06-04 19:25:54 +02:00
c4a00dcee2 added wrap option to option popup 2024-06-04 18:50:29 +02:00
f877cf5cb0 fixed initial zoom for smaller grids, turned grid to 1px, fix generation count 2024-06-04 18:46:59 +02:00
126de458b1 normalized pattern file loading and saving, only one option for loading: -f 2024-06-04 14:09:40 +02:00
c1a9a0f2c4 switched to lif.105 format for state files 2024-06-04 13:49:06 +02:00
443b5a2bcf fixed exit function with q, added evolution trace to options fixed inverse 2024-06-03 18:38:18 +02:00
03e1101248 lots changes:
- renamed scene files
- fixed options back using scene.Prev
- fixed initial zooming (finally)
- fixed reset zoom (key r)
- fixed initial size, now works with state loading as well
2024-06-03 17:44:17 +02:00
6527dba219 more zoom/center fixes. RLEs now load centered and visible. 2024-06-02 20:15:23 +02:00
3785799f4e fixed centering of squares, but not rectangles yet. 2024-06-02 20:15:23 +02:00
47f3693f77 fixed drawing bug: cells outside default canvas were not deleted 2024-06-02 20:15:23 +02:00
cb87815e4f fixed grid lines 2024-06-02 20:15:23 +02:00
e536f91790 fixed initial cam pos, it's now always centered 2024-06-02 20:15:23 +02:00
689b7be08b fixed clear screen problem, menus are now shown correctly. lots new
bugs though
2024-06-02 20:15:23 +02:00
83 changed files with 3484 additions and 1035 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

4
.gitignore vendored
View File

@@ -3,3 +3,7 @@ bak
dump* dump*
rect* rect*
*profile *profile
*prof
*lif
*rle
svgicons

102
Makefile
View File

@@ -1,97 +1,9 @@
# Copyright © 2024 Thomas von Dein .PHONY: all
all:
# This program is free software: you can redistribute it and/or modify make -C src
# it under the terms of the GNU General Public License as published by mv src/golsky .
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# no need to modify anything below
tool = golsky
VERSION = $(shell grep VERSION main.go | head -1 | cut -d '"' -f2)
archs = darwin freebsd linux windows
PREFIX = /usr/local
UID = root
GID = 0
HAVE_POD := $(shell pod2text -h 2>/dev/null)
#TAGS = -tags=ebitenginedebug
all: buildlocal
buildlocal:
go build $(TAGS) -o $(tool)
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
.PHONY: clean
clean: clean:
rm -rf $(tool) coverage.out testdata t/out make -C src clean
rm -f dump* rect*
test: clean
mkdir -p t/out
go test ./... $(ARGS)
testlint: test lint
lint:
golangci-lint run
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest
testfuzzy: clean
go test -fuzz ./... $(ARGS)
singletest:
@echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
go test -run $(TEST) $(ARGS)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
goupdate:
go get -t -u=patch ./...
buildall:
./mkrel.sh $(tool) $(VERSION)
release: buildall
gh release create $(VERSION) --generate-notes releases/*
show-versions: buildlocal
@echo "### golsky version:"
@./golsky -V
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod
# lint:
# golangci-lint run -p bugs -p unused
buildwasm:
env GOOS=js GOARCH=wasm go build -o $(tool).wasm $(LDFLAGS) .
zipwasm:
zip -r openquell-$(SHORTVERSION).zip index.html $(tool).wasm wasm_exec.js
wasm: buildwasm zipwasm
@ls -l $(tool)-$(SHORTVERSION).zip

View File

@@ -5,8 +5,7 @@
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/golsky/blob/master/LICENSE) [![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/golsky/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/golsky)](https://goreportcard.com/report/github.com/tlinden/golsky) [![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/golsky)](https://goreportcard.com/report/github.com/tlinden/golsky)
I wanted to play around a little bit with [**Conways Game of I wanted to play around a little bit with [**Conways Game of Life**](https://conwaylife.com/)
Life**](https://conwaylife.com/)
in golang and here's the result. It's a simple game using in golang and here's the result. It's a simple game using
[ebitengine](https://github.com/hajimehoshi/ebiten/). [ebitengine](https://github.com/hajimehoshi/ebiten/).
@@ -14,6 +13,20 @@ John Conway himself: https://youtu.be/R9Plq-D1gEk?si=yYxs77e9yXxeSNbL
Based on: https://youtu.be/FWSR_7kZuYg?si=ix1dmo76D8AmF25F Based on: https://youtu.be/FWSR_7kZuYg?si=ix1dmo76D8AmF25F
# Screenshots
[![golsky-mainmenu.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-mainmenu.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-mainmenu.png)
[![golsky-options.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-options.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-options.png)
[![golsky-bindings.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-bindings.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-bindings.png)
[![golsky-evolution-trace.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-evolution-trace.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-evolution-trace.png)
[![golsky-zoom.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-zoom.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-zoom.png)
[![golsky-debug.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-debug.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-debug.png)
[![golsky-capture.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-capture.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-capture.png)
[![golsky-captured.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-captured.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-captured.png)
[![golsky-dark-theme.png](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/256_golsky-dark-theme.png)](https://github.com/TLINDEN/golsky/blob/main/.github/assets/screenshots/golsky-dark-theme.png)
[Youtube video game preview](https://www.youtube.com/watch?v=xEto6Oew16I)
# Features # Features
* flexible parameters as grid and cell size * flexible parameters as grid and cell size
@@ -76,7 +89,10 @@ While it runs, there are a couple of commands you can use:
* page up: speed up * page up: speed up
* page down: slow down * page down: slow down
* Mouse wheel: zoom in or out * Mouse wheel: zoom in or out
* move mouse while middle mouse button pressed: move canvas * move mouse while left mouse button pressed: move canvas
* i: enter "insert" (draw) mode: use left mouse to set cells alife and right
button to dead. Leave with "space". While in insert mode, use middle mouse
button to drag grid.
* r: reset to 1:1 zoom * r: reset to 1:1 zoom
* escape: open menu * escape: open menu
* s: save game state to file (can be loaded with -l) * s: save game state to file (can be loaded with -l)

53
TODO.md
View File

@@ -1,16 +1,47 @@
- add all other options like size etc - add all other options like size etc
- add gif export
- add toolbar (not working yet, see branch trackui)
- only draw visible part of the world
- print current mode to the bottom like pause, insert and mark
- add https://www.ibiblio.org/lifepatterns/october1970.html
- history: dont count age but do calc to get index to age tile based on cell age
- maybe pre calc neighbors as 8 slice of pointers to neighboring cells to faster do the count
see various-tests/perf-2dim-pointers/: it's NOT faster :(
- use an array of 8 pointers to neighbors. on edge just add either fake dead neighbors or the wrap around neighbors.
- try arche ecs variant with either a component of the cells neighbors or using relations.
- https://mattnakama.com/blog/go-branchless-coding/
- add performance measurements, see:
DrawTriangles: https://github.com/TLINDEN/testgol
WritePixels: https://github.com/TLINDEN/testgol/tree/wrpixels
https://www.tasnimzotder.com/blog/optimizing-game-of-life-algorithm
- pre-draw the grid separately to a cache grid image, then during
rendering, first draw the dead background, then the life cells, and
lastly the grid - if enabled. If disabled, there's be no gap between
the cells anymore.
- Speed
https://conwaylife.com/forums/viewtopic.php?f=7&t=3237
- Clear screen problem: - Patterns:
- it works when hitting the K key, immediately
- its being turned off correctly when entering menu and on when leaving it A Catagolue textcensus of, say, period-2 oscillators from
- but regardless of the setting, after turning it off, the engine non-symmetrical soups can be found at
seems to run a couple of ticks with the old setting before switching
scenes https://catagolue.hatsya.com/textcensus/b3s23/C1/xp2
- looks like a race condition
- obviously with K there are more loops before actually switching The URL is made by just adding the prefix "text" to the word "census",
scenes, which doesn't happen with ESC in any URL linked to from a Catagolue census page such as this one:
https://catagolue.hatsya.com/census/b3s23/C1
Format:
https://conwaylife.com/wiki/Apgcode
- if grid lines is disabled, they appear anyway in the first frame (then disappear) Collections:
- changing options mid-game has no effect in most cases, even after a restart https://conwaylife.com/wiki/Pattern_of_the_Year
https://www.ibiblio.org/lifepatterns/
https://entropymine.com/jason/life/
https://github.com/Matthias-Merzenich/jslife-moving
https://conwaylife.com/ref/mniemiec/lifepage.htm
https://conwaylife.com/wiki/Spaceship ff.

243
config.go
View File

@@ -1,243 +0,0 @@
package main
import (
"errors"
"fmt"
"os"
"runtime/pprof"
"strconv"
"strings"
"github.com/alecthomas/repr"
"github.com/spf13/pflag"
"github.com/tlinden/golsky/rle"
)
// all the settings comming from commandline, but maybe tweaked later from the UI
type Config struct {
Width, Height, Cellsize, Density int // measurements
ScreenWidth, ScreenHeight int
TPG int // ticks per generation/game speed, 1==max
Debug, Empty, Invert, Paused, Markmode bool // game modi
ShowEvolution, ShowGrid, RunOneStep bool // flags
Rule *Rule // which rule to use, default: B3/S23
RLE *rle.RLE // loaded GOL pattern from RLE file
Statefile string // load game state from it if non-nil
StateGrid *Grid // a grid from a statefile
Wrap bool // wether wraparound mode is in place or not
ShowVersion bool
UseShader bool // to use a shader to render alife cells
Restart, RestartGrid, RestartCache bool
StartWithMenu bool
Zoomfactor int
// for internal profiling
ProfileFile string
ProfileDraw bool
ProfileMaxLoops int64
}
const (
VERSION = "v0.0.7"
Alive = 1
Dead = 0
DEFAULT_WIDTH = 600
DEFAULT_HEIGHT = 400
DEFAULT_CELLSIZE = 4
DEFAULT_ZOOMFACTOR = 150 // FIXME, doesn't work?
DEFAULT_GEOM = "640x384"
)
// parse given window geometry and adjust game settings according to it
func (config *Config) ParseGeom(geom string) error {
if geom == "" {
config.ScreenWidth = config.Cellsize * config.Width
config.ScreenHeight = config.Cellsize * config.Height
config.Zoomfactor = 0
return nil
}
// force a geom
geometry := strings.Split(geom, "x")
if len(geometry) != 2 {
return errors.New("failed to parse -g parameters, expecting WIDTHxHEIGHT")
}
width, err := strconv.Atoi(geometry[0])
if err != nil {
return errors.New("failed to parse width, expecting integer")
}
height, err := strconv.Atoi(geometry[1])
if err != nil {
return errors.New("failed to parse height, expecting integer")
}
/*
// adjust dimensions, account for grid width+height so that cells
// fit into window
config.ScreenWidth = width - (width % config.Width)
config.ScreenHeight = height - (height % config.Height)
if config.ScreenWidth == 0 || config.ScreenHeight == 0 {
return errors.New("the number of requested cells don't fit into the requested window size")
}
*/
config.ScreenWidth = width
config.ScreenHeight = height
//config.Cellsize = config.ScreenWidth / config.Width
config.Cellsize = DEFAULT_CELLSIZE
config.Zoomfactor = DEFAULT_ZOOMFACTOR
repr.Println(config)
return nil
}
// check if we have been given an RLE file to load, then load it and
// adjust game settings accordingly
func (config *Config) ParseRLE(rlefile string) error {
if rlefile == "" {
return nil
}
rleobj, err := rle.GetRLE(rlefile)
if err != nil {
return err
}
if rleobj == nil {
return errors.New("failed to load RLE file (uncatched module error)")
}
config.RLE = rleobj
// adjust geometry if needed
if config.RLE.Width > config.Width || config.RLE.Height > config.Height {
config.Width = config.RLE.Width * 2
config.Height = config.RLE.Height * 2
config.Cellsize = config.ScreenWidth / config.Width
}
// RLE needs an empty grid
config.Empty = true
// it may come with its own rule
if config.RLE.Rule != "" {
config.Rule = ParseGameRule(config.RLE.Rule)
}
return nil
}
// parse a state file, if given, and adjust game settings accordingly
func (config *Config) ParseStatefile(statefile string) error {
if config.Statefile == "" {
return nil
}
grid, err := LoadState(config.Statefile)
if err != nil {
return fmt.Errorf("failed to load game state: %s", err)
}
config.Width = grid.Width
config.Height = grid.Height
config.Cellsize = config.ScreenWidth / config.Width
config.StateGrid = grid
return nil
}
func (config *Config) EnableCPUProfiling(filename string) error {
if filename == "" {
return nil
}
fd, err := os.Create(filename)
if err != nil {
return err
}
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
return nil
}
func ParseCommandline() (*Config, error) {
config := Config{}
var (
rule, rlefile, geom string
)
// commandline params, most configure directly config flags
pflag.IntVarP(&config.Width, "width", "W", DEFAULT_WIDTH, "grid width in cells")
pflag.IntVarP(&config.Height, "height", "H", DEFAULT_HEIGHT, "grid height in cells")
pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels")
pflag.StringVarP(&geom, "geom", "G", DEFAULT_GEOM, "window geometry in WxH in pixels, overturns -c")
pflag.IntVarP(&config.Density, "density", "D", 10, "density of random cells")
pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10,
"game speed: the higher the slower (default: 10)")
pflag.StringVarP(&rule, "rule", "r", "B3/S23", "game rule")
pflag.StringVarP(&rlefile, "rle-file", "f", "", "RLE pattern file")
pflag.StringVarP(&config.Statefile, "load-state-file", "l", "", "game state file")
pflag.BoolVarP(&config.ShowVersion, "version", "v", false, "show version")
pflag.BoolVarP(&config.Paused, "paused", "p", false, "do not start simulation (use space to start)")
pflag.BoolVarP(&config.Debug, "debug", "d", false, "show debug info")
pflag.BoolVarP(&config.ShowGrid, "show-grid", "g", true, "draw grid lines")
pflag.BoolVarP(&config.Empty, "empty", "e", false, "start with an empty screen")
pflag.BoolVarP(&config.Invert, "invert", "i", false, "invert colors (dead cell: black)")
pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution traces")
pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode")
pflag.BoolVarP(&config.UseShader, "use-shader", "k", false, "use shader for cell rendering")
pflag.StringVarP(&config.ProfileFile, "profile-file", "", "", "enable profiling")
pflag.BoolVarP(&config.ProfileDraw, "profile-draw", "", false, "profile draw method (default false)")
pflag.Int64VarP(&config.ProfileMaxLoops, "profile-max-loops", "", 10, "how many loops to execute (default 10)")
pflag.Parse()
err := config.ParseGeom(geom)
if err != nil {
return nil, err
}
err = config.ParseRLE(rlefile)
if err != nil {
return nil, err
}
// load rule from commandline when no rule came from RLE file,
// default is B3/S23, aka conways game of life
if config.Rule == nil {
config.Rule = ParseGameRule(rule)
}
return &config, nil
}
func (config *Config) TogglePaused() {
config.Paused = !config.Paused
}
func (config *Config) ToggleDebugging() {
fmt.Println("DEBUG TOGGLED")
config.Debug = !config.Debug
}
func (config *Config) ToggleInvert() {
config.Invert = !config.Invert
config.RestartCache = true
}
func (config *Config) ToggleGridlines() {
config.ShowGrid = !config.ShowGrid
config.RestartCache = true
}

3
go.mod
View File

@@ -13,9 +13,10 @@ require (
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.7.0 // indirect github.com/ebitengine/purego v0.7.0 // indirect
github.com/ebitenui/ebitenui v0.5.6 // indirect github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jezek/xgb v1.1.1 // indirect github.com/jezek/xgb v1.1.1 // indirect
github.com/mlange-42/arche v0.13.0 // indirect
github.com/tinne26/etxt v0.0.8 // indirect github.com/tinne26/etxt v0.0.8 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.7.0 // indirect

4
go.sum
View File

@@ -8,12 +8,16 @@ github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXn
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/ebitenui/ebitenui v0.5.6 h1:qyJRU5j+lQo1lamxB48IBwMxMfz1xNb5iWUayCtA0Wk= github.com/ebitenui/ebitenui v0.5.6 h1:qyJRU5j+lQo1lamxB48IBwMxMfz1xNb5iWUayCtA0Wk=
github.com/ebitenui/ebitenui v0.5.6/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE= github.com/ebitenui/ebitenui v0.5.6/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE=
github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21 h1:dElhYGyf+FYY+makAndUQNOSDwFSFYyFWziPwQrPObY=
github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8= github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY= github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/mlange-42/arche v0.13.0 h1:ef0fu9qC2KIr8wIlVs+CgeQ5CSUJ8A1Hut6nXYdf+xk=
github.com/mlange-42/arche v0.13.0/go.mod h1:bFktKnvGDj2kP01xar79z0hKwGHdnoaEZR8HWmJkIyU=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc= github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc=

192
grid.go
View File

@@ -1,192 +0,0 @@
package main
import (
"bufio"
"errors"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
"github.com/tlinden/golsky/rle"
)
type Grid struct {
Data [][]int64
Width, Height, Density int
Empty bool
}
// Create new empty grid and allocate Data according to provided dimensions
func NewGrid(width, height, density int, empty bool) *Grid {
grid := &Grid{
Height: height,
Width: width,
Density: density,
Data: make([][]int64, height),
Empty: empty,
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]int64, width)
}
return grid
}
// Create a new 1:1 instance
func (grid *Grid) Clone() *Grid {
newgrid := &Grid{}
newgrid.Width = grid.Width
newgrid.Height = grid.Height
newgrid.Data = grid.Data
return newgrid
}
// copy data
func (grid *Grid) Copy(other *Grid) {
for y := range grid.Data {
for x := range grid.Data[y] {
other.Data[y][x] = grid.Data[y][x]
}
}
}
// delete all contents
func (grid *Grid) Clear() {
for y := range grid.Data {
for x := range grid.Data[y] {
grid.Data[y][x] = 0
}
}
}
// initialize with random life cells using the given density
func (grid *Grid) FillRandom() {
if !grid.Empty {
for y := range grid.Data {
for x := range grid.Data[y] {
if rand.Intn(grid.Density) == 1 {
grid.Data[y][x] = 1
}
}
}
}
}
// initialize using a given RLE pattern
func (grid *Grid) LoadRLE(pattern *rle.RLE) {
if pattern != nil {
startX := (grid.Width / 2) - (pattern.Width / 2)
startY := (grid.Height / 2) - (pattern.Height / 2)
var y, x int
for rowIndex, patternRow := range pattern.Pattern {
for colIndex := range patternRow {
if pattern.Pattern[rowIndex][colIndex] > 0 {
x = colIndex + startX
y = rowIndex + startY
grid.Data[y][x] = 1
}
}
}
}
}
// save the contents of the whole grid as a simple mcell alike
// file. One line per row, 0 for dead and 1 for life cell.
func (grid *Grid) SaveState(filename string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to open state file: %w", err)
}
defer file.Close()
for y := range grid.Data {
for _, cell := range grid.Data[y] {
_, err := file.WriteString(strconv.FormatInt(cell, 10))
if err != nil {
return fmt.Errorf("failed to write to state file: %w", err)
}
}
file.WriteString("\n")
}
return nil
}
// the reverse of the above, load a mcell file
func LoadState(filename string) (*Grid, error) {
fd, err := os.Open(filename)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(fd)
scanner.Split(bufio.ScanLines)
grid := &Grid{}
for scanner.Scan() {
items := strings.Split(scanner.Text(), "")
row := make([]int64, len(items))
for idx, item := range items {
num, err := strconv.ParseInt(item, 10, 64)
if err != nil {
return nil, err
}
if num > 1 {
return nil, errors.New("cells must be 0 or 1")
}
row[idx] = num
}
grid.Data = append(grid.Data, row)
}
// sanity check the grid
explen := 0
rows := 0
first := true
for _, row := range grid.Data {
length := len(row)
if first {
explen = length
first = false
}
if explen != length {
return nil, fmt.Errorf(
fmt.Sprintf("all rows must be in the same length, got: %d, expected: %d",
length, explen))
}
rows++
}
grid.Width = explen
grid.Height = rows
return grid, nil
}
// generate filenames for dumps
func GetFilename(generations int64) string {
now := time.Now()
return fmt.Sprintf("dump-%s-%d.gol", now.Format("20060102150405"), generations)
}
func GetFilenameRLE(generations int64) string {
now := time.Now()
return fmt.Sprintf("rect-%s-%d.rle", now.Format("20060102150405"), generations)
}

View File

@@ -120,7 +120,7 @@ func removeWhitespace(input string) string {
} }
// Store a grid to an RLE file // Store a grid to an RLE file
func StoreGridToRLE(grid [][]int64, filename, rule string, width, height int) error { func StoreGridToRLE(grid [][]uint8, filename, rule string, width, height int) error {
fd, err := os.Create(filename) fd, err := os.Create(filename)
if err != nil { if err != nil {
return err return err
@@ -131,12 +131,12 @@ func StoreGridToRLE(grid [][]int64, filename, rule string, width, height int) er
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
line := "" line := ""
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
switch grid[y][x] { char := "b"
case 0: if grid[y][x] == 1 {
line += "b" char = "o"
case 1:
line += "o"
} }
line += char
} }
// if first row is: 001011110, then line is now: // if first row is: 001011110, then line is now:

97
src/Makefile Normal file
View File

@@ -0,0 +1,97 @@
# Copyright © 2024 Thomas von Dein
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# no need to modify anything below
tool = golsky
VERSION = $(shell grep VERSION main.go | head -1 | cut -d '"' -f2)
archs = darwin freebsd linux windows
PREFIX = /usr/local
UID = root
GID = 0
HAVE_POD := $(shell pod2text -h 2>/dev/null)
#TAGS = -tags=ebitenginedebug
all: buildlocal
buildlocal:
go build $(TAGS) -o $(tool)
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean:
rm -rf $(tool) coverage.out testdata t/out
test: clean
mkdir -p t/out
go test ./... $(ARGS)
testlint: test lint
lint:
golangci-lint run
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest
testfuzzy: clean
go test -fuzz ./... $(ARGS)
singletest:
@echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
go test -run $(TEST) $(ARGS)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
goupdate:
go get -t -u=patch ./...
buildall:
./mkrel.sh $(tool) $(VERSION)
release: buildall
gh release create $(VERSION) --generate-notes releases/*
show-versions: buildlocal
@echo "### golsky version:"
@./golsky -V
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod
# lint:
# golangci-lint run -p bugs -p unused
buildwasm:
env GOOS=js GOARCH=wasm go build -o $(tool).wasm $(LDFLAGS) .
zipwasm:
zip -r openquell-$(SHORTVERSION).zip index.html $(tool).wasm wasm_exec.js
wasm: buildwasm zipwasm
@ls -l $(tool)-$(SHORTVERSION).zip

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 263 B

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 289 B

View File

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

View File

@@ -1,3 +1,5 @@
// this comes from the camera example but I enhanced it a little bit
package main package main
import ( import (
@@ -12,6 +14,9 @@ type Camera struct {
ViewPort f64.Vec2 ViewPort f64.Vec2
Position f64.Vec2 Position f64.Vec2
ZoomFactor int ZoomFactor int
InitialZoomFactor int
InitialPosition f64.Vec2
ZoomOutFactor int
} }
func (c *Camera) String() string { func (c *Camera) String() string {
@@ -32,15 +37,17 @@ func (c *Camera) worldMatrix() ebiten.GeoM {
m := ebiten.GeoM{} m := ebiten.GeoM{}
m.Translate(-c.Position[0], -c.Position[1]) m.Translate(-c.Position[0], -c.Position[1])
viewportCenter := c.viewportCenter()
// We want to scale and rotate around center of image / screen // We want to scale and rotate around center of image / screen
m.Translate(-c.viewportCenter()[0], -c.viewportCenter()[1]) m.Translate(-viewportCenter[0], -viewportCenter[1])
m.Scale( m.Scale(
math.Pow(1.01, float64(c.ZoomFactor)), math.Pow(1.01, float64(c.ZoomFactor)),
math.Pow(1.01, float64(c.ZoomFactor)), math.Pow(1.01, float64(c.ZoomFactor)),
) )
m.Translate(c.viewportCenter()[0], c.viewportCenter()[1]) m.Translate(viewportCenter[0], viewportCenter[1])
return m return m
} }
@@ -61,8 +68,14 @@ func (c *Camera) ScreenToWorld(posX, posY int) (float64, float64) {
} }
} }
func (c *Camera) Reset() { func (c *Camera) Setup() {
c.Position[0] = 0 c.Position[0] = c.InitialPosition[0]
c.Position[1] = 0 c.Position[1] = c.InitialPosition[1]
c.ZoomFactor = 0 c.ZoomFactor = c.InitialZoomFactor
}
func (c *Camera) Reset() {
c.Position[0] = c.InitialPosition[0]
c.Position[1] = c.InitialPosition[1]
c.ZoomFactor = c.ZoomOutFactor
} }

291
src/config.go Normal file
View File

@@ -0,0 +1,291 @@
package main
import (
"errors"
"fmt"
"math"
"os"
"runtime/pprof"
"strconv"
"strings"
"github.com/spf13/pflag"
"github.com/tlinden/golsky/rle"
)
// all the settings comming from commandline, but maybe tweaked later from the UI
type Config struct {
Width, Height, Cellsize, Density int // measurements
ScreenWidth, ScreenHeight int
TPG int // ticks per generation/game speed, 1==max
Debug, Empty, Paused, Markmode, Drawmode bool // game modi
ShowEvolution, ShowGrid, RunOneStep bool // flags
Rule *Rule // which rule to use, default: B3/S23
RLE *rle.RLE // loaded GOL pattern from RLE file
Statefile string // load game state from it if non-nil
StateGrid *Grid // a grid from a statefile
Wrap bool // wether wraparound mode is in place or not
ShowVersion bool
UseShader bool // to use a shader to render alife cells
Restart, RestartGrid, RestartCache bool
StartWithMenu bool
Zoomfactor int
ZoomOutFactor int
InitialCamPos []float64
DelayedStart bool // if true game, we wait. like pause but program induced
Theme string
ThemeManager ThemeManager
// for internal profiling
ProfileFile string
ProfileDraw bool
ProfileMaxLoops int64
}
const (
VERSION = "v0.0.9"
Alive = 1
Dead = 0
DEFAULT_GRID_WIDTH = 600
DEFAULT_GRID_HEIGHT = 400
DEFAULT_CELLSIZE = 4
DEFAULT_ZOOMFACTOR = 400
DEFAULT_GEOM = "640x384"
DEFAULT_THEME = "standard" // "light" // inverse => "dark"
)
const KEYBINDINGS string = `
- SPACE: pause or resume the game
- N: while game is paused: forward one step
- PAGE UP: speed up
- PAGE DOWN: slow down
- MOUSE WHEEL: zoom in or out
- LEFT MOUSE BUTTON: use to drag canvas, keep clicked and move mouse
- I: enter "insert" (draw) mode: use left mouse to set cells alife and right
button to dead. Leave with "space". While in insert mode, use middle mouse
button to drag grid.
- R: reset to 1:1 zoom
- ESCAPE: open menu, o: open options menu
- S: save game state to file (can be loaded with -l)
- C: enter mark mode. Mark a rectangle with the mouse, when you
release the mouse buttonx it is being saved to an RLE file
- D: toggle debug output
- Q: quit game
`
func (config *Config) SetupCamera() {
config.Zoomfactor = DEFAULT_ZOOMFACTOR / config.Cellsize
// calculate the initial cam pos. It is negative if the total grid
// size is smaller than the screen in a centered position, but
// it's zero if it's equal or larger than the screen.
config.InitialCamPos = make([]float64, 2)
config.InitialCamPos[0] = float64(((config.ScreenWidth - (config.Width * config.Cellsize)) / 2) * -1)
if config.Width*config.Cellsize >= config.ScreenWidth {
// must be positive if world wider than screen
config.InitialCamPos[0] = math.Abs(config.InitialCamPos[0])
}
// same for Y
config.InitialCamPos[1] = float64(((config.ScreenHeight - (config.Height * config.Cellsize)) / 2) * -1)
if config.Height*config.Cellsize > config.ScreenHeight {
config.InitialCamPos[1] = math.Abs(config.InitialCamPos[1])
}
// Calculate zoom out factor, which shows 100% of the world. We
// need to reverse math.Pow(1.01, $zoomfactor) to get the correct
// percentage of the world to show. I.e: with a ScreenHeight of
// 384px and a world of 800px the factor to show 100% of the world
// is -75: math.Log(384/800) / math.Log(1.01). The 1.01 constant
// is being used in camera.go:worldMatrix().
// FIXME: determine if the diff is larger on width, then calc with
// width instead of height
config.ZoomOutFactor = int(
math.Log(float64(config.ScreenHeight)/(float64(config.Height)*float64(config.Cellsize))) /
math.Log(1.01))
}
// parse given window geometry and adjust game settings according to it
func (config *Config) ParseGeom(geom string) error {
// force a geom
geometry := strings.Split(geom, "x")
if len(geometry) != 2 {
return errors.New("failed to parse -g parameters, expecting WIDTHxHEIGHT")
}
width, err := strconv.Atoi(geometry[0])
if err != nil {
return errors.New("failed to parse width, expecting integer")
}
height, err := strconv.Atoi(geometry[1])
if err != nil {
return errors.New("failed to parse height, expecting integer")
}
config.ScreenWidth = width
config.ScreenHeight = height
//config.Cellsize = DEFAULT_CELLSIZE
return nil
}
// check if we have been given an RLE or LIF file to load, then load
// it and adjust game settings accordingly
func (config *Config) ParseRLE(rlefile string) error {
if rlefile == "" {
return nil
}
var rleobj *rle.RLE
if strings.HasSuffix(rlefile, ".lif") {
lifobj, err := LoadLIF(rlefile)
if err != nil {
return err
}
rleobj = lifobj
} else {
rleobject, err := rle.GetRLE(rlefile)
if err != nil {
return err
}
rleobj = rleobject
}
if rleobj == nil {
return errors.New("failed to load pattern file (uncatched module error)")
}
config.RLE = rleobj
// adjust geometry if needed
if config.RLE.Width > config.Width || config.RLE.Height > config.Height {
config.Width = config.RLE.Width * 2
config.Height = config.RLE.Height * 2
config.Cellsize = config.ScreenWidth / config.Width
}
fmt.Printf("width: %d, screenwidth: %d, rlewidth: %d, cellsize: %d\n",
config.Width, config.ScreenWidth, config.RLE.Width, config.Cellsize)
// RLE needs an empty grid
config.Empty = true
// it may come with its own rule
if config.RLE.Rule != "" {
config.Rule = ParseGameRule(config.RLE.Rule)
}
return nil
}
func (config *Config) EnableCPUProfiling(filename string) error {
if filename == "" {
return nil
}
fd, err := os.Create(filename)
if err != nil {
return err
}
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
return nil
}
func ParseCommandline() (*Config, error) {
config := Config{}
var (
rule, rlefile, geom string
)
// commandline params, most configure directly config flags
pflag.IntVarP(&config.Width, "width", "W", DEFAULT_GRID_WIDTH, "grid width in cells")
pflag.IntVarP(&config.Height, "height", "H", DEFAULT_GRID_HEIGHT, "grid height in cells")
pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels")
pflag.StringVarP(&geom, "geom", "G", DEFAULT_GEOM, "window geometry in WxH in pixels, overturns -c")
pflag.IntVarP(&config.Density, "density", "D", 10, "density of random cells")
pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10,
"game speed: the higher the slower (default: 10)")
pflag.StringVarP(&rule, "rule", "r", "B3/S23", "game rule")
pflag.StringVarP(&rlefile, "pattern-file", "f", "", "RLE or LIF pattern file")
pflag.BoolVarP(&config.ShowVersion, "version", "v", false, "show version")
pflag.BoolVarP(&config.ShowGrid, "show-grid", "g", false, "draw grid lines")
pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution traces")
pflag.BoolVarP(&config.Paused, "paused", "p", false, "do not start simulation (use space to start)")
pflag.BoolVarP(&config.Debug, "debug", "d", false, "show debug info")
pflag.BoolVarP(&config.Empty, "empty", "e", false, "start with an empty screen")
// style
pflag.StringVarP(&config.Theme, "theme", "T", DEFAULT_THEME, "color theme: standard, dark, light (default: standard)")
pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode")
pflag.BoolVarP(&config.UseShader, "use-shader", "k", false, "use shader for cell rendering")
pflag.StringVarP(&config.ProfileFile, "profile-file", "", "", "enable profiling")
pflag.Parse()
err := config.ParseGeom(geom)
if err != nil {
return nil, err
}
err = config.ParseRLE(rlefile)
if err != nil {
return nil, err
}
// load rule from commandline when no rule came from RLE file,
// default is B3/S23, aka conways game of life
if config.Rule == nil {
config.Rule = ParseGameRule(rule)
}
config.SetupCamera()
config.ThemeManager = NewThemeManager(config.Theme, config.Cellsize)
//repr.Println(config)
return &config, nil
}
func (config *Config) TogglePaused() {
config.Paused = !config.Paused
}
func (config *Config) ToggleDebugging() {
config.Debug = !config.Debug
}
func (config *Config) SwitchTheme(theme string) {
config.ThemeManager.SetCurrentTheme(theme)
config.RestartCache = true
}
func (config *Config) ToggleGridlines() {
config.ShowGrid = !config.ShowGrid
config.RestartCache = true
}
func (config *Config) ToggleEvolution() {
config.ShowEvolution = !config.ShowEvolution
}
func (config *Config) ToggleWrap() {
config.Wrap = !config.Wrap
}

View File

@@ -1,8 +1,6 @@
package main package main
import ( import (
"fmt"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@@ -12,7 +10,6 @@ type Game struct {
CurrentScene SceneName CurrentScene SceneName
Config *Config Config *Config
Scale float32 Scale float32
Screen *ebiten.Image
} }
func NewGame(config *Config, startscene SceneName) *Game { func NewGame(config *Config, startscene SceneName) *Game {
@@ -28,6 +25,7 @@ func NewGame(config *Config, startscene SceneName) *Game {
game.Scenes[Play] = NewPlayScene(game, config) game.Scenes[Play] = NewPlayScene(game, config)
game.Scenes[Menu] = NewMenuScene(game, config) game.Scenes[Menu] = NewMenuScene(game, config)
game.Scenes[Options] = NewOptionsScene(game, config) game.Scenes[Options] = NewOptionsScene(game, config)
game.Scenes[Keybindings] = NewKeybindingsScene(game, config)
// setup environment // setup environment
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight) ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
@@ -35,7 +33,6 @@ func NewGame(config *Config, startscene SceneName) *Game {
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
ebiten.SetScreenClearedEveryFrame(true) ebiten.SetScreenClearedEveryFrame(true)
game.Screen = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
return game return game
} }
@@ -51,28 +48,35 @@ func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
func (game *Game) Update() error { func (game *Game) Update() error {
scene := game.GetCurrentScene() scene := game.GetCurrentScene()
scene.Update()
fmt.Printf("Clear Screen: %t\n", ebiten.IsScreenClearedEveryFrame()) if quit := scene.Update(); quit != nil {
return quit
}
next := scene.GetNext()
if next != game.CurrentScene {
game.Scenes[next].SetPrevious(game.CurrentScene)
scene.ResetNext()
game.CurrentScene = next
}
return nil return nil
} }
func (game *Game) Draw(screen *ebiten.Image) { func (game *Game) Draw(screen *ebiten.Image) {
var nextscene Scene // first draw primary scene[s], although there are only 1
scene := game.GetCurrentScene() for current, scene := range game.Scenes {
if scene.IsPrimary() {
next := scene.GetNext() // primary scenes always draw
if next != game.CurrentScene {
scene.ResetNext()
game.CurrentScene = next
nextscene = game.GetCurrentScene()
ebiten.SetScreenClearedEveryFrame(nextscene.Clearscreen())
}
scene.Draw(screen) scene.Draw(screen)
if nextscene != nil { if current == game.CurrentScene {
nextscene.Draw(screen) // avoid to redraw it in the next step
return
} }
}
}
scene := game.GetCurrentScene()
scene.Draw(screen)
} }

View File

@@ -10,3 +10,10 @@ func Contains[E comparable](s []E, v E) bool {
return false return false
} }
func Exists[K comparable, V any](m map[K]V, v K) bool {
if _, ok := m[v]; ok {
return true
}
return false
}

306
src/grid.go Normal file
View File

@@ -0,0 +1,306 @@
package main
import (
"bufio"
"errors"
"fmt"
"math/rand"
"os"
"strings"
"time"
"github.com/tlinden/golsky/rle"
)
type Cell struct {
State uint8
Neighbors [8]*Cell
NeighborCount int
}
func (cell *Cell) Count() uint8 {
var count uint8
for idx := 0; idx < cell.NeighborCount; idx++ {
count += cell.Neighbors[idx].State
}
return count
}
type Grid struct {
Data [][]*Cell
Empty bool
Config *Config
}
// Create new empty grid and allocate Data according to provided dimensions
func NewGrid(config *Config) *Grid {
grid := &Grid{
Data: make([][]*Cell, config.Height),
Empty: config.Empty,
Config: config,
}
// first setup the cells
for y := 0; y < config.Height; y++ {
grid.Data[y] = make([]*Cell, config.Width)
for x := 0; x < config.Width; x++ {
grid.Data[y][x] = &Cell{}
}
}
// in a second pass, collect pointers to the neighbors of each cell
for y := 0; y < config.Height; y++ {
for x := 0; x < config.Width; x++ {
grid.SetupNeighbors(x, y)
}
}
return grid
}
func (grid *Grid) SetupNeighbors(x, y int) {
idx := 0
for nbgY := -1; nbgY < 2; nbgY++ {
for nbgX := -1; nbgX < 2; nbgX++ {
var col, row int
if grid.Config.Wrap {
// In wrap mode we look at all the 8 neighbors surrounding us.
// In case we are on an edge we'll look at the neighbor on the
// other side of the grid, thus wrapping lookahead around
// using the mod() function.
col = (x + nbgX + grid.Config.Width) % grid.Config.Width
row = (y + nbgY + grid.Config.Height) % grid.Config.Height
} else {
// In traditional grid mode the edges are deadly
if x+nbgX < 0 || x+nbgX >= grid.Config.Width || y+nbgY < 0 || y+nbgY >= grid.Config.Height {
continue
}
col = x + nbgX
row = y + nbgY
}
if col == x && row == y {
continue
}
grid.Data[y][x].Neighbors[idx] = grid.Data[row][col]
grid.Data[y][x].NeighborCount++
idx++
}
}
}
// count the living neighbors of a cell
func (grid *Grid) CountNeighbors(x, y int) uint8 {
return grid.Data[y][x].Count()
}
// Create a new 1:1 instance
func (grid *Grid) Clone() *Grid {
newgrid := &Grid{}
newgrid.Config = grid.Config
newgrid.Data = grid.Data
return newgrid
}
// copy data
func (grid *Grid) Copy(other *Grid) {
for y := range grid.Data {
for x := range grid.Data[y] {
other.Data[y][x] = grid.Data[y][x]
}
}
}
// delete all contents
func (grid *Grid) Clear() {
for y := range grid.Data {
for x := range grid.Data[y] {
grid.Data[y][x].State = 0
}
}
}
// initialize with random life cells using the given density
func (grid *Grid) FillRandom() {
if !grid.Empty {
for y := range grid.Data {
for x := range grid.Data[y] {
if rand.Intn(grid.Config.Density) == 1 {
grid.Data[y][x].State = 1
}
}
}
}
}
func (grid *Grid) Dump() {
for y := 0; y < grid.Config.Height; y++ {
for x := 0; x < grid.Config.Width; x++ {
if grid.Data[y][x].State == 1 {
fmt.Print("XX")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
}
// initialize using a given RLE pattern
func (grid *Grid) LoadRLE(pattern *rle.RLE) {
if pattern != nil {
startX := (grid.Config.Width / 2) - (pattern.Width / 2)
startY := (grid.Config.Height / 2) - (pattern.Height / 2)
var y, x int
for rowIndex, patternRow := range pattern.Pattern {
for colIndex := range patternRow {
if pattern.Pattern[rowIndex][colIndex] > 0 {
x = colIndex + startX
y = rowIndex + startY
grid.Data[y][x].State = 1
}
}
}
//grid.Dump()
}
}
// load a lif file parameters like R and P are not supported yet
func LoadLIF(filename string) (*rle.RLE, error) {
fd, err := os.Open(filename)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(fd)
scanner.Split(bufio.ScanLines)
gothead := false
grid := &rle.RLE{}
for scanner.Scan() {
line := scanner.Text()
items := strings.Split(line, "")
if len(items) < 0 {
continue
}
if strings.Contains(line, "# r") {
parts := strings.Split(line, " ")
if len(parts) == 2 {
grid.Rule = parts[1]
}
continue
}
if items[0] == "#" {
if gothead {
break
}
continue
}
gothead = true
row := make([]int, len(items))
for idx, item := range items {
switch item {
case ".":
row[idx] = 0
case "o":
fallthrough
case "*":
row[idx] = 1
default:
return nil, errors.New("cells must be . or o")
}
}
grid.Pattern = append(grid.Pattern, row)
}
// sanity check the grid
explen := 0
rows := 0
first := true
for _, row := range grid.Pattern {
length := len(row)
if first {
explen = length
first = false
}
if explen != length {
return nil, fmt.Errorf(
fmt.Sprintf("all rows must be in the same length, got: %d, expected: %d",
length, explen))
}
rows++
}
grid.Width = explen
grid.Height = rows
return grid, nil
}
// save the contents of the whole grid as a simple lif alike
// file. One line per row, 0 for dead and 1 for life cell.
// file format: https://conwaylife.com/wiki/Life_1.05
func (grid *Grid) SaveState(filename, rule string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to open state file: %w", err)
}
defer file.Close()
fmt.Fprintf(file, "#Life 1.05\n#R %s\n#D golsky state file\n#P -1 -1\n", rule)
for y := range grid.Data {
for _, cell := range grid.Data[y] {
row := "."
if cell.State == 1 {
row = "o"
}
_, err := file.WriteString(row)
if err != nil {
return fmt.Errorf("failed to write to state file: %w", err)
}
}
file.WriteString("\n")
}
return nil
}
// generate filenames for dumps
func GetFilename(generations int64) string {
now := time.Now()
return fmt.Sprintf("dump-%s-%d.lif", now.Format("20060102150405"), generations)
}
func GetFilenameRLE(generations int64) string {
now := time.Now()
return fmt.Sprintf("rect-%s-%d.rle", now.Format("20060102150405"), generations)
}

98
src/keybindings.go Normal file
View File

@@ -0,0 +1,98 @@
package main
import (
"image/color"
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type SceneKeybindings struct {
Game *Game
Config *Config
Next SceneName
Prev SceneName
Whoami SceneName
Ui *ebitenui.UI
FontColor color.RGBA
First bool
}
func NewKeybindingsScene(game *Game, config *Config) Scene {
scene := &SceneKeybindings{
Whoami: Keybindings,
Game: game,
Next: Keybindings,
Config: config,
FontColor: color.RGBA{255, 30, 30, 0xff},
}
scene.Init()
return scene
}
func (scene *SceneKeybindings) GetNext() SceneName {
return scene.Next
}
func (scene *SceneKeybindings) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *SceneKeybindings) ResetNext() {
scene.Next = scene.Whoami
}
func (scene *SceneKeybindings) SetNext(next SceneName) {
scene.Next = next
}
func (scene *SceneKeybindings) Update() error {
scene.Ui.Update()
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.Config.DelayedStart = false
scene.Leave()
}
return nil
}
func (scene *SceneKeybindings) IsPrimary() bool {
return false
}
func (scene *SceneKeybindings) Draw(screen *ebiten.Image) {
scene.Ui.Draw(screen)
}
func (scene *SceneKeybindings) Leave() {
scene.SetNext(Play)
}
func (scene *SceneKeybindings) Init() {
rowContainer := NewRowContainer("Key Bindings")
bindings := widget.NewText(
widget.TextOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
})),
widget.TextOpts.Text(KEYBINDINGS, *FontRenderer.FontSmall, color.NRGBA{0xdf, 0xf4, 0xff, 0xff}))
cancel := NewMenuButton("Back",
func(args *widget.ButtonClickedEventArgs) {
scene.Leave()
})
rowContainer.AddChild(bindings)
rowContainer.AddChild(cancel)
scene.Ui = &ebitenui.UI{
Container: rowContainer.Container(),
}
}

View File

@@ -12,11 +12,12 @@ import (
) )
func main() { func main() {
dau := true var directstart bool
if len(os.Args) > 1 { if len(os.Args) > 1 {
dau = false directstart = true
} }
config, err := ParseCommandline() config, err := ParseCommandline()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -28,8 +29,9 @@ func main() {
} }
start := Play start := Play
if dau { if !directstart {
start = Menu start = Menu
config.DelayedStart = true
} }
game := NewGame(config, SceneName(start)) game := NewGame(config, SceneName(start))

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"image/color" "image/color"
"os"
"github.com/ebitenui/ebitenui" "github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget" "github.com/ebitenui/ebitenui/widget"
@@ -14,10 +13,12 @@ type SceneMenu struct {
Game *Game Game *Game
Config *Config Config *Config
Next SceneName Next SceneName
Prev SceneName
Whoami SceneName Whoami SceneName
Ui *ebitenui.UI Ui *ebitenui.UI
FontColor color.RGBA FontColor color.RGBA
First bool First bool
Exit bool
} }
func NewMenuScene(game *Game, config *Config) Scene { func NewMenuScene(game *Game, config *Config) Scene {
@@ -38,6 +39,10 @@ func (scene *SceneMenu) GetNext() SceneName {
return scene.Next return scene.Next
} }
func (scene *SceneMenu) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *SceneMenu) ResetNext() { func (scene *SceneMenu) ResetNext() {
scene.Next = scene.Whoami scene.Next = scene.Whoami
} }
@@ -46,14 +51,15 @@ func (scene *SceneMenu) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *SceneMenu) Clearscreen() bool {
return false
}
func (scene *SceneMenu) Update() error { func (scene *SceneMenu) Update() error {
scene.Ui.Update() scene.Ui.Update()
if scene.Exit {
return ebiten.Termination
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.Config.DelayedStart = false
scene.Leave() scene.Leave()
} }
@@ -61,6 +67,10 @@ func (scene *SceneMenu) Update() error {
} }
func (scene *SceneMenu) IsPrimary() bool {
return false
}
func (scene *SceneMenu) Draw(screen *ebiten.Image) { func (scene *SceneMenu) Draw(screen *ebiten.Image) {
scene.Ui.Draw(screen) scene.Ui.Draw(screen)
} }
@@ -98,18 +108,23 @@ func (scene *SceneMenu) Init() {
scene.SetNext(Options) scene.SetNext(Options)
}) })
separator1 := NewSeparator() bindings := NewMenuButton("Show Key Bindings",
separator2 := NewSeparator() func(args *widget.ButtonClickedEventArgs) {
separator3 := NewSeparator() scene.SetNext(Keybindings)
})
cancel := NewMenuButton("Close Window", separator1 := NewSeparator(3)
separator2 := NewSeparator(3)
separator3 := NewSeparator(10)
cancel := NewMenuButton("Back",
func(args *widget.ButtonClickedEventArgs) { func(args *widget.ButtonClickedEventArgs) {
scene.Leave() scene.Leave()
}) })
quit := NewMenuButton("Exit Golsky", quit := NewMenuButton("Exit Golsky",
func(args *widget.ButtonClickedEventArgs) { func(args *widget.ButtonClickedEventArgs) {
os.Exit(0) scene.Exit = true
}) })
rowContainer.AddChild(empty) rowContainer.AddChild(empty)
@@ -117,6 +132,7 @@ func (scene *SceneMenu) Init() {
rowContainer.AddChild(separator1) rowContainer.AddChild(separator1)
rowContainer.AddChild(options) rowContainer.AddChild(options)
rowContainer.AddChild(copy) rowContainer.AddChild(copy)
rowContainer.AddChild(bindings)
rowContainer.AddChild(separator2) rowContainer.AddChild(separator2)
rowContainer.AddChild(cancel) rowContainer.AddChild(cancel)
rowContainer.AddChild(separator3) rowContainer.AddChild(separator3)

View File

@@ -13,6 +13,7 @@ type SceneOptions struct {
Game *Game Game *Game
Config *Config Config *Config
Next SceneName Next SceneName
Prev SceneName
Whoami SceneName Whoami SceneName
Ui *ebitenui.UI Ui *ebitenui.UI
FontColor color.RGBA FontColor color.RGBA
@@ -36,6 +37,10 @@ func (scene *SceneOptions) GetNext() SceneName {
return scene.Next return scene.Next
} }
func (scene *SceneOptions) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *SceneOptions) ResetNext() { func (scene *SceneOptions) ResetNext() {
scene.Next = scene.Whoami scene.Next = scene.Whoami
} }
@@ -44,7 +49,7 @@ func (scene *SceneOptions) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *SceneOptions) Clearscreen() bool { func (scene *SceneOptions) IsPrimary() bool {
return false return false
} }
@@ -64,54 +69,85 @@ func (scene *SceneOptions) Draw(screen *ebiten.Image) {
} }
func (scene *SceneOptions) SetInitialValue(w *widget.LabeledCheckbox, value bool) { func (scene *SceneOptions) SetInitialValue(w *widget.LabeledCheckbox, value bool) {
var intval int
if value { if value {
intval = 1
}
w.SetState( w.SetState(
widget.WidgetState(intval), widget.WidgetChecked,
) )
}
} }
func (scene *SceneOptions) Init() { func (scene *SceneOptions) Init() {
rowContainer := NewRowContainer("Options") rowContainer := NewRowContainer("Options")
pause := NewCheckbox("Pause", pause := NewCheckbox("Pause",
scene.Config.Paused,
func(args *widget.CheckboxChangedEventArgs) { func(args *widget.CheckboxChangedEventArgs) {
scene.Config.TogglePaused() scene.Config.TogglePaused()
}) })
debugging := NewCheckbox("Debugging", debugging := NewCheckbox("Debugging",
scene.Config.Debug,
func(args *widget.CheckboxChangedEventArgs) { func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleDebugging() scene.Config.ToggleDebugging()
}) })
scene.SetInitialValue(debugging, scene.Config.Debug)
invert := NewCheckbox("Invert",
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.Invert = true
})
scene.SetInitialValue(invert, scene.Config.Invert)
gridlines := NewCheckbox("Show grid lines", gridlines := NewCheckbox("Show grid lines",
scene.Config.ShowGrid,
func(args *widget.CheckboxChangedEventArgs) { func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleGridlines() scene.Config.ToggleGridlines()
}) })
scene.SetInitialValue(gridlines, scene.Config.ShowGrid)
separator := NewSeparator() evolution := NewCheckbox("Show evolution traces",
scene.Config.ShowEvolution,
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleEvolution()
})
wrap := NewCheckbox("Wrap around edges",
scene.Config.Wrap,
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleWrap()
})
themenames := make([]string, len(THEMES))
i := 0
for name := range THEMES {
themenames[i] = name
i++
}
themes := NewCombobox(
themenames,
scene.Config.Theme,
func(args *widget.ListComboButtonEntrySelectedEventArgs) {
scene.Config.SwitchTheme(args.Entry.(ListEntry).Name)
})
themelabel := NewLabel("Themes")
combocontainer := NewColumnContainer()
combocontainer.AddChild(themes)
combocontainer.AddChild(themelabel)
separator := NewSeparator(3)
separator2 := NewSeparator(3)
cancel := NewMenuButton("Close", cancel := NewMenuButton("Close",
func(args *widget.ButtonClickedEventArgs) { func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Menu) scene.SetNext(scene.Prev)
}) })
rowContainer.AddChild(pause) rowContainer.AddChild(pause)
rowContainer.AddChild(debugging) rowContainer.AddChild(debugging)
rowContainer.AddChild(invert)
rowContainer.AddChild(gridlines) rowContainer.AddChild(gridlines)
rowContainer.AddChild(evolution)
rowContainer.AddChild(wrap)
rowContainer.AddChild(separator) rowContainer.AddChild(separator)
rowContainer.AddChild(combocontainer)
rowContainer.AddChild(separator2)
rowContainer.AddChild(cancel) rowContainer.AddChild(cancel)
scene.Ui = &ebitenui.UI{ scene.Ui = &ebitenui.UI{

View File

@@ -3,9 +3,9 @@ package main
import ( import (
"fmt" "fmt"
"image" "image"
"image/color"
"log" "log"
"os" "sync"
"unsafe"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
@@ -18,32 +18,51 @@ type Images struct {
Black, White, Age1, Age2, Age3, Age4, Old *ebiten.Image Black, White, Age1, Age2, Age3, Age4, Old *ebiten.Image
} }
const (
DEBUG_FORMAT = "FPS: %0.2f, TPG: %d, M: %0.2fMB, Generations: %d\nScale: %.02f, Zoom: %d, Cam: %.02f,%.02f Cursor: %d,%d %s"
)
type History struct {
Age [][]int64
}
func NewHistory(height, width int) History {
hist := History{}
hist.Age = make([][]int64, height)
for y := 0; y < height; y++ {
hist.Age[y] = make([]int64, width)
}
return hist
}
type ScenePlay struct { type ScenePlay struct {
Game *Game Game *Game
Config *Config Config *Config
Next SceneName Next SceneName
Prev SceneName
Whoami SceneName Whoami SceneName
Clear bool Clear bool
Grids []*Grid // 2 grids: one current, one next Grids []*Grid // 2 grids: one current, one next
History *Grid // holds state of past dead cells for evolution traces History History // holds state of past dead cells for evolution traces
Index int // points to current grid Index int // points to current grid
Generations int64 // Stats Generations int64 // Stats
Black, White, Grey, Old color.RGBA
AgeColor1, AgeColor2, AgeColor3, AgeColor4 color.RGBA
TicksElapsed int // tick counter for game speed TicksElapsed int // tick counter for game speed
Tiles Images // pre-computed tiles for dead and alife cells
Camera Camera // for zoom+move Camera Camera // for zoom+move
World, Cache *ebiten.Image // actual image we render to World, Cache *ebiten.Image // actual image we render to
WheelTurned bool // when user turns wheel multiple times, zoom faster WheelTurned bool // when user turns wheel multiple times, zoom faster
Dragging bool // middle mouse is pressed, move canvas Dragging bool // middle mouse is pressed, move canvas
LastCursorPos []int // used to check if the user is dragging LastCursorPos []float64 // used to check if the user is dragging
MarkTaken bool // true when mouse1 pressed MarkTaken bool // true when mouse1 pressed
MarkDone bool // true when mouse1 released, copy cells between Mark+Point MarkDone bool // true when mouse1 released, copy cells between Mark+Point
Mark, Point image.Point // area to marks+save Mark, Point image.Point // area to marks+save
RunOneStep bool // mutable flags from config RunOneStep bool // mutable flags from config
TPG int TPG int // current game speed (ticks per game)
Theme Theme
RuleCheckFunc func(uint8, uint8) uint8
} }
func NewPlayScene(game *Game, config *Config) Scene { func NewPlayScene(game *Game, config *Config) Scene {
@@ -61,10 +80,18 @@ func NewPlayScene(game *Game, config *Config) Scene {
return scene return scene
} }
func (scene *ScenePlay) IsPrimary() bool {
return true
}
func (scene *ScenePlay) GetNext() SceneName { func (scene *ScenePlay) GetNext() SceneName {
return scene.Next return scene.Next
} }
func (scene *ScenePlay) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *ScenePlay) ResetNext() { func (scene *ScenePlay) ResetNext() {
scene.Next = scene.Whoami scene.Next = scene.Whoami
} }
@@ -73,12 +100,27 @@ func (scene *ScenePlay) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *ScenePlay) Clearscreen() bool { func (scene *ScenePlay) CheckRuleB3S23(state uint8, neighbors uint8) uint8 {
return true var nextstate uint8
check := (9 * state) + neighbors
switch check {
case 11:
fallthrough
case 12:
fallthrough
case 3:
nextstate = Alive
default:
nextstate = Dead
}
return nextstate
} }
func (scene *ScenePlay) CheckRule(state int64, neighbors int64) int64 { func (scene *ScenePlay) CheckRuleGeneric(state uint8, neighbors uint8) uint8 {
var nextstate int64 var nextstate uint8
// The standard Scene of Life is symbolized in rule-string notation // The standard Scene of Life is symbolized in rule-string notation
// as B3/S23 (23/3 here). A cell is born if it has exactly three // as B3/S23 (23/3 here). A cell is born if it has exactly three
@@ -86,7 +128,7 @@ func (scene *ScenePlay) CheckRule(state int64, neighbors int64) int64 {
// and dies otherwise. The first number, or list of numbers, is // and dies otherwise. The first number, or list of numbers, is
// what is required for a dead cell to be born. // what is required for a dead cell to be born.
if state == 0 && Contains(scene.Config.Rule.Birth, neighbors) { if state != 1 && Contains(scene.Config.Rule.Birth, neighbors) {
nextstate = Alive nextstate = Alive
} else if state == 1 && Contains(scene.Config.Rule.Death, neighbors) { } else if state == 1 && Contains(scene.Config.Rule.Death, neighbors) {
nextstate = Alive nextstate = Alive
@@ -110,26 +152,39 @@ func (scene *ScenePlay) UpdateCells() {
// next grid index, we just xor 0|1 to 1|0 // next grid index, we just xor 0|1 to 1|0
next := scene.Index ^ 1 next := scene.Index ^ 1
var wg sync.WaitGroup
wg.Add(scene.Config.Height)
// compute life status of cells // compute life status of cells
for y := 0; y < scene.Config.Height; y++ { for y := 0; y < scene.Config.Height; y++ {
go func() {
defer wg.Done()
for x := 0; x < scene.Config.Width; x++ { for x := 0; x < scene.Config.Width; x++ {
state := scene.Grids[scene.Index].Data[y][x] // 0|1 == dead or alive state := scene.Grids[scene.Index].Data[y][x].State // 0|1 == dead or alive
neighbors := scene.CountNeighbors(x, y) // alive neighbor count neighbors := scene.Grids[scene.Index].CountNeighbors(x, y)
// actually apply the current rules // actually apply the current rules
nextstate := scene.CheckRule(state, neighbors) nextstate := scene.RuleCheckFunc(state, neighbors)
// change state of current cell in next grid // change state of current cell in next grid
scene.Grids[next].Data[y][x] = nextstate scene.Grids[next].Data[y][x].State = nextstate
if scene.Config.ShowEvolution {
// set history to current generation so we can infer the // set history to current generation so we can infer the
// age of the cell's state during rendering and use it to // age of the cell's state during rendering and use it to
// deduce the color to use if evolution tracing is enabled // deduce the color to use if evolution tracing is enabled
// 60FPS:
if state != nextstate { if state != nextstate {
scene.History.Data[y][x] = scene.Generations scene.History.Age[y][x] = scene.Generations
} }
} }
} }
}()
}
wg.Wait()
// switch grid for rendering // switch grid for rendering
scene.Index ^= 1 scene.Index ^= 1
@@ -148,61 +203,54 @@ func (scene *ScenePlay) UpdateCells() {
func (scene *ScenePlay) Reset() { func (scene *ScenePlay) Reset() {
scene.Config.Paused = true scene.Config.Paused = true
scene.InitGrid(nil) scene.InitGrid()
scene.Config.Paused = false scene.Config.Paused = false
} }
// check user input // check user input
func (scene *ScenePlay) CheckInput() { func (scene *ScenePlay) CheckExit() error {
if inpututil.IsKeyJustPressed(ebiten.KeyQ) { if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
os.Exit(0) return ebiten.Termination
} }
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { return nil
}
func (scene *ScenePlay) CheckInput() {
// primary functions, always available
switch {
case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
scene.SetNext(Menu) scene.SetNext(Menu)
} case inpututil.IsKeyJustPressed(ebiten.KeyO):
scene.SetNext(Options)
if inpututil.IsKeyJustPressed(ebiten.KeyC) { case inpututil.IsKeyJustPressed(ebiten.KeyC):
fmt.Println("mark mode on")
scene.Config.Markmode = true scene.Config.Markmode = true
scene.Config.Drawmode = false
scene.Config.Paused = true
case inpututil.IsKeyJustPressed(ebiten.KeyI):
scene.Config.Drawmode = true
scene.Config.Paused = true scene.Config.Paused = true
} }
if scene.Config.Markmode { if scene.Config.Markmode {
// no need to check any more input in mark mode
return return
} }
if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) { switch {
case inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter):
scene.Config.TogglePaused() scene.Config.TogglePaused()
} case inpututil.IsKeyJustPressed(ebiten.KeyPageDown):
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
scene.ToggleCellOnCursorPos(Alive)
scene.Config.Paused = true // drawing while running makes no sense
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
scene.ToggleCellOnCursorPos(Dead)
scene.Config.Paused = true // drawing while running makes no sense
}
if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
if scene.TPG < 120 { if scene.TPG < 120 {
scene.TPG++ scene.TPG++
} }
} case inpututil.IsKeyJustPressed(ebiten.KeyPageUp):
if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
if scene.TPG >= 1 { if scene.TPG >= 1 {
scene.TPG-- scene.TPG--
} }
} case inpututil.IsKeyJustPressed(ebiten.KeyS):
if inpututil.IsKeyJustPressed(ebiten.KeyS) {
scene.SaveState() scene.SaveState()
} case inpututil.IsKeyJustPressed(ebiten.KeyD):
if inpututil.IsKeyJustPressed(ebiten.KeyD) {
scene.Config.Debug = !scene.Config.Debug scene.Config.Debug = !scene.Config.Debug
} }
@@ -213,6 +261,19 @@ func (scene *ScenePlay) CheckInput() {
} }
} }
func (scene *ScenePlay) CheckDrawingInput() {
if scene.Config.Drawmode {
switch {
case ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft):
scene.ToggleCellOnCursorPos(Alive)
case ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight):
scene.ToggleCellOnCursorPos(Dead)
case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
scene.Config.Drawmode = false
}
}
}
// Check dragging input. move the canvas with the mouse while pressing // Check dragging input. move the canvas with the mouse while pressing
// the middle mouse button, zoom in and out using the wheel. // the middle mouse button, zoom in and out using the wheel.
func (scene *ScenePlay) CheckDraggingInput() { func (scene *ScenePlay) CheckDraggingInput() {
@@ -220,20 +281,26 @@ func (scene *ScenePlay) CheckDraggingInput() {
return return
} }
dragbutton := ebiten.MouseButtonLeft
if scene.Config.Drawmode {
dragbutton = ebiten.MouseButtonMiddle
}
// move canvas // move canvas
if scene.Dragging && !ebiten.IsMouseButtonPressed(ebiten.MouseButton1) { if scene.Dragging && !ebiten.IsMouseButtonPressed(dragbutton) {
// release // release
scene.Dragging = false scene.Dragging = false
} }
if !scene.Dragging && ebiten.IsMouseButtonPressed(ebiten.MouseButton1) { if !scene.Dragging && ebiten.IsMouseButtonPressed(dragbutton) {
// start dragging // start dragging
scene.Dragging = true scene.Dragging = true
scene.LastCursorPos[0], scene.LastCursorPos[1] = ebiten.CursorPosition() scene.LastCursorPos[0], scene.LastCursorPos[1] = scene.Camera.ScreenToWorld(ebiten.CursorPosition())
} }
if scene.Dragging { if scene.Dragging {
x, y := ebiten.CursorPosition() x, y := scene.Camera.ScreenToWorld(ebiten.CursorPosition())
if x != scene.LastCursorPos[0] || y != scene.LastCursorPos[1] { if x != scene.LastCursorPos[0] || y != scene.LastCursorPos[1] {
// actually drag by mouse cursor pos diff to last cursor pos // actually drag by mouse cursor pos diff to last cursor pos
@@ -241,20 +308,18 @@ func (scene *ScenePlay) CheckDraggingInput() {
scene.Camera.Position[1] -= float64(y - scene.LastCursorPos[1]) scene.Camera.Position[1] -= float64(y - scene.LastCursorPos[1])
} }
scene.LastCursorPos[0], scene.LastCursorPos[1] = ebiten.CursorPosition() scene.LastCursorPos[0], scene.LastCursorPos[1] = scene.Camera.ScreenToWorld(ebiten.CursorPosition())
} }
// also support the arrow keys to move the canvas // also support the arrow keys to move the canvas
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) { switch {
case ebiten.IsKeyPressed(ebiten.KeyArrowLeft):
scene.Camera.Position[0] -= 1 scene.Camera.Position[0] -= 1
} case ebiten.IsKeyPressed(ebiten.KeyArrowRight):
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
scene.Camera.Position[0] += 1 scene.Camera.Position[0] += 1
} case ebiten.IsKeyPressed(ebiten.KeyArrowUp):
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
scene.Camera.Position[1] -= 1 scene.Camera.Position[1] -= 1
} case ebiten.IsKeyPressed(ebiten.KeyArrowDown):
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
scene.Camera.Position[1] += 1 scene.Camera.Position[1] += 1
} }
@@ -284,6 +349,10 @@ func (scene *ScenePlay) CheckMarkInput() {
return return
} }
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
scene.Config.Markmode = false
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButton0) { if ebiten.IsMouseButtonPressed(ebiten.MouseButton0) {
if !scene.MarkTaken { if !scene.MarkTaken {
scene.Mark = scene.GetWorldCursorPos() scene.Mark = scene.GetWorldCursorPos()
@@ -304,7 +373,7 @@ func (scene *ScenePlay) CheckMarkInput() {
func (scene *ScenePlay) SaveState() { func (scene *ScenePlay) SaveState() {
filename := GetFilename(scene.Generations) filename := GetFilename(scene.Generations)
err := scene.Grids[scene.Index].SaveState(filename) err := scene.Grids[scene.Index].SaveState(filename, scene.Config.Rule.Definition)
if err != nil { if err != nil {
log.Printf("failed to save game state to %s: %s", filename, err) log.Printf("failed to save game state to %s: %s", filename, err)
} }
@@ -344,13 +413,13 @@ func (scene *ScenePlay) SaveRectRLE() {
height = scene.Mark.Y - scene.Point.Y height = scene.Mark.Y - scene.Point.Y
} }
grid := make([][]int64, height) grid := make([][]uint8, height)
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
grid[y] = make([]int64, width) grid[y] = make([]uint8, width)
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
grid[y][x] = scene.Grids[scene.Index].Data[y+starty][x+startx] grid[y][x] = scene.Grids[scene.Index].Data[y+starty][x+startx].State
} }
} }
@@ -366,18 +435,25 @@ func (scene *ScenePlay) SaveRectRLE() {
func (scene *ScenePlay) Update() error { func (scene *ScenePlay) Update() error {
if scene.Config.Restart { if scene.Config.Restart {
scene.Config.Restart = false scene.Config.Restart = false
scene.Init() scene.Generations = 0
scene.InitGrid()
scene.InitCache()
return nil return nil
} }
if scene.Config.RestartCache { if scene.Config.RestartCache {
scene.Config.RestartCache = false scene.Config.RestartCache = false
scene.InitTiles() scene.Theme = scene.Config.ThemeManager.GetCurrentTheme()
scene.InitCache() scene.InitCache()
return nil return nil
} }
if quit := scene.CheckExit(); quit != nil {
return quit
}
scene.CheckInput() scene.CheckInput()
scene.CheckDrawingInput()
scene.CheckDraggingInput() scene.CheckDraggingInput()
scene.CheckMarkInput() scene.CheckMarkInput()
@@ -389,15 +465,15 @@ func (scene *ScenePlay) Update() error {
} }
// set a cell to alive or dead // set a cell to alive or dead
func (scene *ScenePlay) ToggleCellOnCursorPos(alive int64) { func (scene *ScenePlay) ToggleCellOnCursorPos(alive uint8) {
// use cursor pos relative to the world // use cursor pos relative to the world
worldX, worldY := scene.Camera.ScreenToWorld(ebiten.CursorPosition()) worldX, worldY := scene.Camera.ScreenToWorld(ebiten.CursorPosition())
x := int(worldX) / scene.Config.Cellsize x := int(worldX) / scene.Config.Cellsize
y := int(worldY) / scene.Config.Cellsize y := int(worldY) / scene.Config.Cellsize
if x > -1 && y > -1 { if x > -1 && y > -1 && x < scene.Config.Width && y < scene.Config.Height {
scene.Grids[scene.Index].Data[y][x] = alive scene.Grids[scene.Index].Data[y][x].State = alive
scene.History.Data[y][x] = 1 scene.History.Age[y][x] = 1
} }
} }
@@ -411,8 +487,6 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
op.GeoM.Translate(0, 0) op.GeoM.Translate(0, 0)
scene.World.DrawImage(scene.Cache, op) scene.World.DrawImage(scene.Cache, op)
var age int64
for y := 0; y < scene.Config.Height; y++ { for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ { for x := 0; x < scene.Config.Width; x++ {
op.GeoM.Reset() op.GeoM.Reset()
@@ -421,29 +495,11 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
float64(y*scene.Config.Cellsize), float64(y*scene.Config.Cellsize),
) )
age = scene.Generations - scene.History.Data[y][x] if scene.Config.ShowEvolution {
scene.DrawEvolution(screen, x, y, op)
switch scene.Grids[scene.Index].Data[y][x] {
case Alive:
if age > 50 && scene.Config.ShowEvolution {
scene.World.DrawImage(scene.Tiles.Old, op)
} else { } else {
scene.World.DrawImage(scene.Tiles.Black, op) if scene.Grids[scene.Index].Data[y][x].State == 1 {
} scene.World.DrawImage(scene.Theme.Tile(ColLife), op)
case Dead:
// only draw dead cells in case evolution trace is enabled
if scene.History.Data[y][x] > 1 && scene.Config.ShowEvolution {
switch {
case age < 10:
scene.World.DrawImage(scene.Tiles.Age1, op)
case age < 20:
scene.World.DrawImage(scene.Tiles.Age2, op)
case age < 30:
scene.World.DrawImage(scene.Tiles.Age3, op)
default:
scene.World.DrawImage(scene.Tiles.Age4, op)
}
} }
} }
} }
@@ -454,11 +510,33 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
scene.Camera.Render(scene.World, screen) scene.Camera.Render(scene.World, screen)
scene.DrawDebug(screen) scene.DrawDebug(screen)
}
op.GeoM.Reset() func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten.DrawImageOptions) {
op.GeoM.Translate(0, 0) age := scene.Generations - scene.History.Age[y][x]
scene.Game.Screen.DrawImage(screen, op) switch scene.Grids[scene.Index].Data[y][x].State {
case Alive:
if age > 50 && scene.Config.ShowEvolution {
scene.World.DrawImage(scene.Theme.Tile(ColOld), op)
} else {
scene.World.DrawImage(scene.Theme.Tile(ColLife), op)
}
case Dead:
// only draw dead cells in case evolution trace is enabled
if scene.History.Age[y][x] > 1 && scene.Config.ShowEvolution {
switch {
case age < 10:
scene.World.DrawImage(scene.Theme.Tile(ColAge1), op)
case age < 20:
scene.World.DrawImage(scene.Theme.Tile(ColAge2), op)
case age < 30:
scene.World.DrawImage(scene.Theme.Tile(ColAge3), op)
default:
scene.World.DrawImage(scene.Theme.Tile(ColAge4), op)
}
}
}
} }
func (scene *ScenePlay) DrawMark(screen *ebiten.Image) { func (scene *ScenePlay) DrawMark(screen *ebiten.Image) {
@@ -472,7 +550,7 @@ func (scene *ScenePlay) DrawMark(screen *ebiten.Image) {
scene.World, scene.World,
x+1, y+1, x+1, y+1,
w, h, w, h,
1.0, scene.Old, false, 1.0, scene.Theme.Color(ColOld), false,
) )
} }
} }
@@ -484,39 +562,58 @@ func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) {
paused = "-- paused --" paused = "-- paused --"
} }
if scene.Config.Markmode {
paused = "-- mark --"
}
if scene.Config.Drawmode {
paused = "-- insert --"
}
x, y := ebiten.CursorPosition()
debug := fmt.Sprintf( debug := fmt.Sprintf(
"FPS: %0.2f, TPG: %d, Mem: %0.2fMB, Gen: %d, Scale: %.02f, Z: %d, Clear: %t %s", DEBUG_FORMAT,
ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations, ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations,
scene.Game.Scale, scene.Camera.ZoomFactor, ebiten.IsScreenClearedEveryFrame(), paused) scene.Game.Scale, scene.Camera.ZoomFactor,
scene.Camera.Position[0], scene.Camera.Position[1],
x, y,
paused)
FontRenderer.Renderer.SetSizePx(10 + int(scene.Game.Scale*10)) FontRenderer.Renderer.SetSizePx(10 + int(scene.Game.Scale*10))
FontRenderer.Renderer.SetTarget(screen) FontRenderer.Renderer.SetTarget(screen)
FontRenderer.Renderer.SetColor(scene.Black) FontRenderer.Renderer.SetColor(scene.Theme.Color(ColLife))
FontRenderer.Renderer.Draw(debug, 31, 31) FontRenderer.Renderer.Draw(debug, 31, 31)
FontRenderer.Renderer.SetColor(scene.Old) FontRenderer.Renderer.SetColor(scene.Theme.Color(ColOld))
FontRenderer.Renderer.Draw(debug, 30, 30) FontRenderer.Renderer.Draw(debug, 30, 30)
fmt.Println(debug) fmt.Println(debug)
} }
} }
// load a pre-computed pattern from RLE file // load a pre-computed pattern from RLE file
func (scene *ScenePlay) InitPattern() { func (scene *ScenePlay) InitPattern() {
scene.Grids[0].LoadRLE(scene.Config.RLE) scene.Grids[0].LoadRLE(scene.Config.RLE)
scene.History.LoadRLE(scene.Config.RLE)
// rule might have changed
scene.InitRuleCheckFunc()
} }
// pre-render offscreen cache image // pre-render offscreen cache image
func (scene *ScenePlay) InitCache() { func (scene *ScenePlay) InitCache() {
// setup theme
scene.Theme.SetGrid(scene.Config.ShowGrid)
if !scene.Config.ShowGrid {
scene.Cache.Fill(scene.Theme.Color(ColDead))
return
}
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
if scene.Config.ShowGrid { scene.Cache.Fill(scene.Theme.Color(ColGrid))
scene.Cache.Fill(scene.Grey)
} else {
scene.Cache.Fill(scene.White)
}
for y := 0; y < scene.Config.Height; y++ { for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ { for x := 0; x < scene.Config.Width; x++ {
@@ -526,157 +623,88 @@ func (scene *ScenePlay) InitCache() {
float64(y*scene.Config.Cellsize), float64(y*scene.Config.Cellsize),
) )
scene.Cache.DrawImage(scene.Tiles.White, op) scene.Cache.DrawImage(scene.Theme.Tile(ColDead), op)
} }
} }
} }
// initialize grid[s], either using pre-computed from state or rle file, or random // initialize grid[s], either using pre-computed from state or rle file, or random
func (scene *ScenePlay) InitGrid(grid *Grid) { func (scene *ScenePlay) InitGrid() {
if grid != nil { grida := NewGrid(scene.Config)
// use pre-loaded grid gridb := NewGrid(scene.Config)
scene.Grids = []*Grid{
grid,
NewGrid(grid.Width, grid.Height, 0, false),
}
scene.History = NewGrid(grid.Width, grid.Height, 0, false)
return
}
grida := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
history := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
// startup is delayed until user has selected options
grida.FillRandom() grida.FillRandom()
grida.Copy(history)
scene.Grids = []*Grid{ scene.Grids = []*Grid{
grida, grida,
gridb, gridb,
} }
scene.History = history scene.History = NewHistory(scene.Config.Height, scene.Config.Width)
}
// prepare tile images
func (scene *ScenePlay) InitTiles() {
scene.Grey = color.RGBA{128, 128, 128, 0xff}
scene.Old = color.RGBA{255, 30, 30, 0xff}
scene.Black = color.RGBA{0, 0, 0, 0xff}
scene.White = color.RGBA{200, 200, 200, 0xff}
scene.AgeColor1 = color.RGBA{255, 195, 97, 0xff} // FIXME: use slice!
scene.AgeColor2 = color.RGBA{255, 211, 140, 0xff}
scene.AgeColor3 = color.RGBA{255, 227, 181, 0xff}
scene.AgeColor4 = color.RGBA{255, 240, 224, 0xff}
if scene.Config.Invert {
scene.White = color.RGBA{0, 0, 0, 0xff}
scene.Black = color.RGBA{200, 200, 200, 0xff}
scene.AgeColor1 = color.RGBA{82, 38, 0, 0xff}
scene.AgeColor2 = color.RGBA{66, 35, 0, 0xff}
scene.AgeColor3 = color.RGBA{43, 27, 0, 0xff}
scene.AgeColor4 = color.RGBA{25, 17, 0, 0xff}
}
scene.Tiles.Black = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize)
scene.Tiles.White = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize)
scene.Tiles.Old = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize)
scene.Tiles.Age1 = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize)
scene.Tiles.Age2 = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize)
scene.Tiles.Age3 = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize)
scene.Tiles.Age4 = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize)
cellsize := scene.Config.ScreenWidth / scene.Config.Cellsize
FillCell(scene.Tiles.Black, cellsize, scene.Black)
FillCell(scene.Tiles.White, cellsize, scene.White)
FillCell(scene.Tiles.Old, cellsize, scene.Old)
FillCell(scene.Tiles.Age1, cellsize, scene.AgeColor1)
FillCell(scene.Tiles.Age2, cellsize, scene.AgeColor2)
FillCell(scene.Tiles.Age3, cellsize, scene.AgeColor3)
FillCell(scene.Tiles.Age4, cellsize, scene.AgeColor4)
} }
func (scene *ScenePlay) Init() { func (scene *ScenePlay) Init() {
// setup the scene // setup the scene
var grid *Grid
if scene.Config.StateGrid != nil {
grid = scene.Config.StateGrid
}
scene.Camera = Camera{ scene.Camera = Camera{
ViewPort: f64.Vec2{ ViewPort: f64.Vec2{
float64(scene.Config.ScreenWidth), float64(scene.Config.ScreenWidth),
float64(scene.Config.ScreenHeight), float64(scene.Config.ScreenHeight),
}, },
InitialZoomFactor: scene.Config.Zoomfactor,
InitialPosition: f64.Vec2{
scene.Config.InitialCamPos[0],
scene.Config.InitialCamPos[1],
},
ZoomOutFactor: scene.Config.ZoomOutFactor,
} }
scene.World = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight) scene.World = ebiten.NewImage(
scene.Cache = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight) scene.Config.Width*scene.Config.Cellsize,
scene.Config.Height*scene.Config.Cellsize,
)
scene.InitTiles() scene.Cache = ebiten.NewImage(
scene.Config.Width*scene.Config.Cellsize,
scene.Config.Height*scene.Config.Cellsize,
)
scene.Theme = scene.Config.ThemeManager.GetCurrentTheme()
scene.InitCache() scene.InitCache()
scene.InitGrid(grid)
if scene.Config.DelayedStart && !scene.Config.Empty {
// do not fill the grid when the main menu comes up first, the
// user decides interactively what to do
scene.Config.Empty = true
scene.InitGrid()
scene.Config.Empty = false
} else {
scene.InitGrid()
}
scene.InitPattern() scene.InitPattern()
scene.Index = 0 scene.Index = 0
scene.TicksElapsed = 0 scene.TicksElapsed = 0
scene.LastCursorPos = make([]int, 2) scene.LastCursorPos = make([]float64, 2)
if scene.Config.Zoomfactor < 0 || scene.Config.Zoomfactor > 0 { if scene.Config.Zoomfactor < 0 || scene.Config.Zoomfactor > 0 {
scene.Camera.ZoomFactor = scene.Config.Zoomfactor scene.Camera.ZoomFactor = scene.Config.Zoomfactor
} }
scene.Camera.Setup()
} }
// count the living neighbors of a cell func bool2int(b bool) int {
func (scene *ScenePlay) CountNeighbors(x, y int) int64 { return int(*(*byte)(unsafe.Pointer(&b)))
var sum int64 }
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if scene.Config.Wrap {
// In wrap mode we look at all the 8 neighbors surrounding us.
// In case we are on an edge we'll look at the neighbor on the
// other side of the grid, thus wrapping lookahead around
// using the mod() function.
col = (x + nbgX + scene.Config.Width) % scene.Config.Width
row = (y + nbgY + scene.Config.Height) % scene.Config.Height
func (scene *ScenePlay) InitRuleCheckFunc() {
if scene.Config.Rule.Definition == "B3/S23" {
scene.RuleCheckFunc = scene.CheckRuleB3S23
} else { } else {
// In traditional grid mode the edges are deadly scene.RuleCheckFunc = scene.CheckRuleGeneric
if x+nbgX < 0 || x+nbgX >= scene.Config.Width || y+nbgY < 0 || y+nbgY >= scene.Config.Height {
continue
} }
col = x + nbgX
row = y + nbgY
}
sum += scene.Grids[scene.Index].Data[row][col]
}
}
// don't count ourselfes though
sum -= scene.Grids[scene.Index].Data[y][x]
return sum
}
// fill a cell with the given color
func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA) {
vector.DrawFilledRect(
tile,
float32(1),
float32(1),
float32(cellsize-1),
float32(cellsize-1),
col, false,
)
} }

View File

@@ -9,22 +9,22 @@ import (
// a GOL rule // a GOL rule
type Rule struct { type Rule struct {
Definition string Definition string
Birth []int64 Birth []uint8
Death []int64 Death []uint8
} }
// parse one part of a GOL rule into rule slice // parse one part of a GOL rule into rule slice
func NumbersToList(numbers string) []int64 { func NumbersToList(numbers string) []uint8 {
list := []int64{} list := []uint8{}
items := strings.Split(numbers, "") items := strings.Split(numbers, "")
for _, item := range items { for _, item := range items {
num, err := strconv.ParseInt(item, 10, 64) num, err := strconv.Atoi(item)
if err != nil { if err != nil {
log.Fatalf("failed to parse game rule part <%s>: %s", numbers, err) log.Fatalf("failed to parse game rule part <%s>: %s", numbers, err)
} }
list = append(list, num) list = append(list, uint8(num))
} }
return list return list

View File

@@ -13,14 +13,16 @@ type SceneName int
type Scene interface { type Scene interface {
SetNext(SceneName) SetNext(SceneName)
GetNext() SceneName GetNext() SceneName
SetPrevious(SceneName)
ResetNext() ResetNext()
Clearscreen() bool
Update() error Update() error
Draw(screen *ebiten.Image) Draw(screen *ebiten.Image)
IsPrimary() bool // if true, this scene will be always drawn
} }
const ( const (
Menu = iota // main top level menu Menu = iota // main top level menu
Play // actual playing happens here Play // actual playing happens here
Options Options
Keybindings
) )

189
src/theme.go Normal file
View File

@@ -0,0 +1,189 @@
package main
import (
"fmt"
"image/color"
"log"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
// Color definitions. ColLife could be black or white depending on theme
const (
ColLife = iota
ColDead
ColOld
ColAge1
ColAge2
ColAge3
ColAge4
ColGrid
)
// A Theme defines how the grid and the cells are colored. We define
// the colors and the actual tile images here, so that they are
// readily available from play.go
type Theme struct {
Tiles map[int]*ebiten.Image
GridTiles map[int]*ebiten.Image
Colors map[int]color.RGBA
Name string
ShowGrid bool
}
type ThemeDef struct {
life, dead, grid, old, age1, age2, age3, age4 string
}
var THEMES = map[string]ThemeDef{
"standard": {
life: "e15f0b",
dead: "5a5a5a",
old: "ff1e1e",
grid: "808080",
age1: "735f52",
age2: "6c6059",
age3: "635d59",
age4: "7b5e4b",
},
"dark": {
life: "c8c8c8",
dead: "000000",
old: "ff1e1e",
grid: "808080",
age1: "522600",
age2: "422300",
age3: "2b1b00",
age4: "191100",
},
"light": {
life: "000000",
dead: "c8c8c8",
old: "ff1e1e",
grid: "808080",
age1: "ffc361",
age2: "ffd38c",
age3: "ffe3b5",
age4: "fff0e0",
},
}
// create a new theme
func NewTheme(def ThemeDef, cellsize int, name string) Theme {
theme := Theme{
Name: name,
Colors: map[int]color.RGBA{
ColLife: HexColor2RGBA(def.life),
ColDead: HexColor2RGBA(def.dead),
ColGrid: HexColor2RGBA(def.grid),
ColAge1: HexColor2RGBA(def.age1),
ColAge2: HexColor2RGBA(def.age2),
ColAge3: HexColor2RGBA(def.age3),
ColAge4: HexColor2RGBA(def.age4),
ColOld: HexColor2RGBA(def.old),
},
}
theme.Tiles = make(map[int]*ebiten.Image, 6)
theme.GridTiles = make(map[int]*ebiten.Image, 6)
for cid, col := range theme.Colors {
theme.Tiles[cid] = ebiten.NewImage(cellsize, cellsize)
FillCell(theme.Tiles[cid], cellsize, col, 0)
theme.GridTiles[cid] = ebiten.NewImage(cellsize, cellsize)
FillCell(theme.GridTiles[cid], cellsize, col, 1)
}
return theme
}
// return the tile image for the requested color type. panic if
// unknown type is being used, which is ok, since the code is the only
// user anyway
func (theme *Theme) Tile(col int) *ebiten.Image {
if theme.ShowGrid {
return theme.GridTiles[col]
}
return theme.Tiles[col]
}
func (theme *Theme) Color(col int) color.RGBA {
return theme.Colors[col]
}
func (theme *Theme) SetGrid(showgrid bool) {
theme.ShowGrid = showgrid
}
type ThemeManager struct {
Theme string
Themes map[string]Theme
}
// Manager is used to easily switch themes from cli or menu
func NewThemeManager(initial string, cellsize int) ThemeManager {
manager := ThemeManager{
Theme: initial,
}
manager.Themes = make(map[string]Theme, len(THEMES))
for name, def := range THEMES {
manager.Themes[name] = NewTheme(def, cellsize, name)
}
return manager
}
func (manager *ThemeManager) GetCurrentTheme() Theme {
return manager.Themes[manager.Theme]
}
func (manager *ThemeManager) GetCurrentThemeName() string {
return manager.Theme
}
func (manager *ThemeManager) SetCurrentTheme(theme string) {
if Exists(manager.Themes, theme) {
manager.Theme = theme
}
}
// Fill a cell with the given color.
//
// We do not draw the cell at 0,0 of it's position but at 1,1. This
// creates a top and lef transparent. By using a different background
// for the whole grid we can then decide wether to show grid lines or
// not.
//
// If no gridlines are selected the background will just be filled
// with the DEAD color. However, IF we are to show the gridlines, we
// fill it with a lighter color. The transparent edges of all tiles
// then create the grid.
//
// So we don't draw a grid, we just left a grid behind, which saves us
// from a lot of drawing operations.
func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA, x int) {
vector.DrawFilledRect(
tile,
float32(x),
float32(x),
float32(cellsize),
float32(cellsize),
col, false,
)
}
func HexColor2RGBA(hex string) color.RGBA {
var r, g, b uint8
_, err := fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b)
if err != nil {
log.Fatalf("failed to parse hex color: %s", err)
}
return color.RGBA{r, g, b, 255}
}

305
src/widgets.go Normal file
View File

@@ -0,0 +1,305 @@
package main
import (
"image/color"
"github.com/ebitenui/ebitenui/image"
"github.com/ebitenui/ebitenui/widget"
)
func NewMenuButton(
text string,
action func(args *widget.ButtonClickedEventArgs)) *widget.Button {
buttonImage, _ := LoadButtonImage()
return widget.NewButton(
widget.ButtonOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Position: widget.RowLayoutPositionCenter,
Stretch: true,
MaxWidth: 200,
MaxHeight: 100,
}),
),
widget.ButtonOpts.Image(buttonImage),
widget.ButtonOpts.Text(text, *FontRenderer.FontSmall, &widget.ButtonTextColor{
Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff},
}),
widget.ButtonOpts.TextPadding(widget.Insets{
Left: 5,
Right: 5,
Top: 5,
Bottom: 5,
}),
widget.ButtonOpts.ClickedHandler(action),
)
}
func NewCheckbox(
text string,
initialvalue bool,
action func(args *widget.CheckboxChangedEventArgs)) *widget.LabeledCheckbox {
checkboxImage, _ := LoadCheckboxImage()
buttonImage, _ := LoadButtonImage()
var state widget.WidgetState
if initialvalue {
state = widget.WidgetChecked
}
return widget.NewLabeledCheckbox(
widget.LabeledCheckboxOpts.CheckboxOpts(
widget.CheckboxOpts.ButtonOpts(
widget.ButtonOpts.Image(buttonImage),
),
widget.CheckboxOpts.Image(checkboxImage),
widget.CheckboxOpts.StateChangedHandler(action),
widget.CheckboxOpts.InitialState(state),
),
widget.LabeledCheckboxOpts.LabelOpts(
widget.LabelOpts.Text(text, *FontRenderer.FontSmall,
&widget.LabelColor{
Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff},
}),
),
)
}
func NewSeparator(padding int) widget.PreferredSizeLocateableWidget {
c := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.Insets{
Top: padding,
Bottom: 0,
}))),
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(
widget.RowLayoutData{Stretch: true})))
return c
}
type ListEntry struct {
id int
Name string
}
func NewCombobox(items []string, selected string,
action func(args *widget.ListComboButtonEntrySelectedEventArgs)) *widget.ListComboButton {
buttonImage, _ := LoadButtonImage()
entries := make([]any, 0, len(items))
idxselected := 0
for i, item := range items {
entries = append(entries, ListEntry{i, item})
if items[i] == selected {
idxselected = i
}
}
comboBox := widget.NewListComboButton(
widget.ListComboButtonOpts.SelectComboButtonOpts(
widget.SelectComboButtonOpts.ComboButtonOpts(
//Set the max height of the dropdown list
widget.ComboButtonOpts.MaxContentHeight(150),
//Set the parameters for the primary displayed button
widget.ComboButtonOpts.ButtonOpts(
widget.ButtonOpts.Image(buttonImage),
widget.ButtonOpts.TextPadding(widget.NewInsetsSimple(5)),
widget.ButtonOpts.Text("", *FontRenderer.FontSmall, &widget.ButtonTextColor{
Idle: color.White,
Disabled: color.White,
}),
widget.ButtonOpts.WidgetOpts(
//Set how wide the button should be
widget.WidgetOpts.MinSize(50, 0),
//Set the combobox's position
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionCenter,
VerticalPosition: widget.AnchorLayoutPositionCenter,
})),
),
),
),
widget.ListComboButtonOpts.ListOpts(
//Set how wide the dropdown list should be
widget.ListOpts.ContainerOpts(
widget.ContainerOpts.WidgetOpts(widget.WidgetOpts.MinSize(50, 0)),
),
//Set the entries in the list
widget.ListOpts.Entries(entries),
widget.ListOpts.ScrollContainerOpts(
//Set the background images/color for the dropdown list
widget.ScrollContainerOpts.Image(&widget.ScrollContainerImage{
Idle: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
Disabled: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
Mask: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
}),
),
widget.ListOpts.SliderOpts(
//Set the background images/color for the background of the slider track
widget.SliderOpts.Images(&widget.SliderTrackImage{
Idle: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
Hover: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}),
}, buttonImage),
widget.SliderOpts.MinHandleSize(5),
//Set how wide the track should be
widget.SliderOpts.TrackPadding(widget.NewInsetsSimple(2))),
//Set the font for the list options
widget.ListOpts.EntryFontFace(*FontRenderer.FontSmall),
//Set the colors for the list
widget.ListOpts.EntryColor(&widget.ListEntryColor{
Selected: color.NRGBA{254, 255, 255, 255},
Unselected: color.NRGBA{254, 255, 255, 255},
SelectedBackground: HexColor2RGBA(THEMES["standard"].life),
SelectedFocusedBackground: HexColor2RGBA(THEMES["standard"].old),
FocusedBackground: HexColor2RGBA(THEMES["standard"].old),
DisabledUnselected: HexColor2RGBA(THEMES["standard"].grid),
DisabledSelected: HexColor2RGBA(THEMES["standard"].grid),
DisabledSelectedBackground: HexColor2RGBA(THEMES["standard"].grid),
}),
//Padding for each entry
widget.ListOpts.EntryTextPadding(widget.NewInsetsSimple(5)),
),
//Define how the entry is displayed
widget.ListComboButtonOpts.EntryLabelFunc(
func(e any) string {
//Button Label function, visible if not open
return e.(ListEntry).Name
},
func(e any) string {
//List Label function, visible items if open
return e.(ListEntry).Name
}),
//Callback when a new entry is selected
widget.ListComboButtonOpts.EntrySelectedHandler(action),
)
//Select the middle entry -- optional
comboBox.SetSelectedEntry(entries[idxselected])
return comboBox
}
func NewLabel(text string) *widget.Text {
return widget.NewText(
widget.TextOpts.Text(text, *FontRenderer.FontSmall, color.White),
widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter),
widget.TextOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Position: widget.RowLayoutPositionCenter,
}),
),
)
}
/////////////// containers
type RowContainer struct {
Root *widget.Container
Row *widget.Container
}
func (container *RowContainer) AddChild(child widget.PreferredSizeLocateableWidget) {
container.Row.AddChild(child)
}
func (container *RowContainer) Container() *widget.Container {
return container.Root
}
// set arg to false if no background needed
func NewRowContainer(title string) *RowContainer {
buttonImageHover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
uiContainer := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
)
titleLabel := widget.NewText(
widget.TextOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
})),
widget.TextOpts.Text(title, *FontRenderer.FontNormal, color.NRGBA{0xdf, 0xf4, 0xff, 0xff}))
rowContainer := widget.NewContainer(
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionCenter,
VerticalPosition: widget.AnchorLayoutPositionCenter,
}),
),
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(8)),
widget.RowLayoutOpts.Spacing(0),
)),
widget.ContainerOpts.BackgroundImage(buttonImageHover),
)
rowContainer.AddChild(titleLabel)
uiContainer.AddChild(rowContainer)
return &RowContainer{
Root: uiContainer,
Row: rowContainer,
}
}
func NewColumnContainer() *widget.Container {
colcontainer := widget.NewContainer(
widget.ContainerOpts.Layout(
widget.NewGridLayout(
widget.GridLayoutOpts.Columns(2),
widget.GridLayoutOpts.Spacing(5, 0),
),
),
)
return colcontainer
}
func LoadButtonImage() (*widget.ButtonImage, error) {
idle := image.NewNineSlice(Assets["button-9slice2"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
hover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
pressed := image.NewNineSlice(Assets["button-9slice1"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
return &widget.ButtonImage{
Idle: idle,
Hover: hover,
Pressed: pressed,
}, nil
}
func LoadComboLabelImage() *widget.ButtonImageImage {
return &widget.ButtonImageImage{
Idle: Assets["checkbox-9slice2"],
Disabled: Assets["checkbox-9slice2"],
}
}
func LoadCheckboxImage() (*widget.CheckboxGraphicImage, error) {
unchecked := &widget.ButtonImageImage{
Idle: Assets["checkbox-9slice2"],
Disabled: Assets["checkbox-9slice2"],
}
checked := &widget.ButtonImageImage{
Idle: Assets["checkbox-9slice1"],
Disabled: Assets["checkbox-9slice1"],
}
return &widget.CheckboxGraphicImage{
Checked: checked,
Unchecked: unchecked,
Greyed: unchecked,
}, nil
}

12
various-tests/README.md Normal file
View File

@@ -0,0 +1,12 @@
## Various performance tests
Running with 1500x1500 grid 5k times
| Variation | Description | Duration |
|------------------------------|-----------------------------------------------------------------------------|-------------------|
| perf-2dim | uses 2d grid of bools, no tuning | 00:03:14 |
| perf-2dim-pointers | use 2d grid of `Cell{Neighbors,NeighborCount}`s using pointers to neighbors | 00:03:35/00:04:75 |
| perf-2dim-pointers-array | same as above but array of neighbors instead of slice | 00:02:40 |
| perf-2dim-pointers-all-array | use arrays for everything, static 1500x1500 | infinite, aborted |
| perf-1dim | use 1d grid of bools, access using y*x, no further tuning | 00:03:24 |
| perf-ecs | use arche ecs, unusable | 00:14:51 |

View File

@@ -0,0 +1,13 @@
module drawminimal
go 1.22
require (
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.7.0 // indirect
github.com/hajimehoshi/ebiten/v2 v2.7.4 // indirect
github.com/jezek/xgb v1.1.1 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
)

View File

@@ -0,0 +1,14 @@
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU=
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M=
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.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -0,0 +1,362 @@
package main
import (
"fmt"
"image"
"image/color"
"log"
"math/rand"
"os"
"runtime/pprof"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/vector"
)
var (
blackImage = ebiten.NewImage(3, 3)
blackSubImage = blackImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
)
type Images struct {
Black, White *ebiten.Image
}
type Grid struct {
Data [][]int64
Width, Height, Density int
}
// Create new empty grid and allocate Data according to provided dimensions
func NewGrid(width, height, density int) *Grid {
grid := &Grid{
Height: height,
Width: width,
Density: density,
Data: make([][]int64, height),
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]int64, width)
}
return grid
}
// live console output of the grid
func (grid *Grid) Dump() {
/*
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
for y := 0; y < grid.Height; y++ {
for x := 0; x < grid.Width; x++ {
if grid.Data[y][x] == 1 {
fmt.Print("XX")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
*/
fmt.Printf("FPS: %0.2f\n", ebiten.ActualTPS())
}
type Game struct {
Width, Height, Cellsize, Density int
ScreenWidth, ScreenHeight int
Grids []*Grid
Index int
Black, White, Grey color.RGBA
Tiles Images
Cache *ebiten.Image
Elapsed int64
TPG int64 // adjust game speed independently of TPS
Vertices []ebiten.Vertex
Indices []uint16
Pause, Debug bool
}
// fill a cell
func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA) {
vector.DrawFilledRect(
tile,
float32(1),
float32(1),
float32(cellsize-1),
float32(cellsize-1),
col, false,
)
}
func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return game.ScreenWidth, game.ScreenHeight
}
func (game *Game) Init() {
// setup two grids, one for display, one for next state
grida := NewGrid(game.Width, game.Height, game.Density)
gridb := NewGrid(game.Width, game.Height, game.Density)
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
if rand.Intn(game.Density) == 1 {
grida.Data[y][x] = 1
}
}
}
game.Grids = []*Grid{
grida,
gridb,
}
// setup colors
game.Grey = color.RGBA{128, 128, 128, 0xff}
game.Black = color.RGBA{0, 0, 0, 0xff}
game.White = color.RGBA{200, 200, 200, 0xff}
game.Tiles.White = ebiten.NewImage(game.Cellsize, game.Cellsize)
game.Cache = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
FillCell(game.Tiles.White, game.Cellsize, game.White)
game.Cache.Fill(game.Grey)
// draw the offscreen image
op := &ebiten.DrawImageOptions{}
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
op.GeoM.Reset()
op.GeoM.Translate(float64(x*game.Cellsize), float64(y*game.Cellsize))
game.Cache.DrawImage(game.Tiles.White, op)
}
}
blackSubImage.Fill(game.Black)
lenvertices := game.ScreenHeight * game.ScreenWidth
game.Vertices = make([]ebiten.Vertex, lenvertices)
game.Indices = make([]uint16, lenvertices+(lenvertices/2))
}
// count the living neighbors of a cell
func (game *Game) CountNeighbors(x, y int) int64 {
var sum int64
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
// Wrap mode we look at all the 8 neighbors surrounding
// us. In case we are on an edge we'll look at the
// neighbor on the other side of the grid, thus wrapping
// lookahead around using the mod() function.
col = (x + nbgX + game.Width) % game.Width
row = (y + nbgY + game.Height) % game.Height
sum += game.Grids[game.Index].Data[row][col]
}
}
// don't count ourselfes though
sum -= game.Grids[game.Index].Data[y][x]
return sum
}
// the heart of the game
func (game *Game) CheckRule(state int64, neighbors int64) int64 {
var nextstate int64
if state == 0 && neighbors == 3 {
nextstate = 1
} else if state == 1 && (neighbors == 2 || neighbors == 3) {
nextstate = 1
} else {
nextstate = 0
}
return nextstate
}
// we only update the cells if we are not in pause state or if the
// game timer (TPG) is elapsed.
func (game *Game) UpdateCells() {
if game.Pause {
return
}
if game.Elapsed < game.TPG {
game.Elapsed++
return
}
// next grid index. we only have to, so we just xor it
next := game.Index ^ 1
// reset vertices
// FIXME: fails!
game.ClearVertices()
// calculate cell life state, this is the actual game of life
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
state := game.Grids[game.Index].Data[y][x] // 0|1 == dead or alive
neighbors := game.CountNeighbors(x, y) // alive neighbor count
// actually apply the current rules
nextstate := game.CheckRule(state, neighbors)
// change state of current cell in next grid
game.Grids[next].Data[y][x] = nextstate
}
}
// calculate triangles for rendering
game.UpdateTriangles()
// switch grid for rendering
game.Index ^= 1
game.Elapsed = 0
if game.Debug {
game.Grids[next].Dump()
}
}
func (game *Game) Update() error {
game.UpdateCells()
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
game.Pause = !game.Pause
}
return nil
}
func (game *Game) ClearVertices() {
// FIXME: fails
for i := 0; i < len(game.Vertices); i++ {
game.Vertices[i] = ebiten.Vertex{}
// game.Vertices[i].DstX = 0
// game.Vertices[i].DstY = 1
}
game.Indices = game.Indices[:len(game.Indices)]
}
// create the triangles needed for rendering. Actual rendering doesn't
// happen here but in Draw()
func (game *Game) UpdateTriangles() {
var base uint16 = 0
var index uint16 = 0
idx := 0
// iterate over every cell
for celly := 0; celly < game.Height; celly++ {
for cellx := 0; cellx < game.Width; cellx++ {
// if the cell is alife
if game.Grids[game.Index].Data[celly][cellx] == 1 {
/* iterate over the cell's corners:
0 1
2 3
*/
for i := 0; i < 2; i++ {
for j := 0; j < 2; j++ {
// calculate the corner position
x := (cellx * game.Cellsize) + (i * game.Cellsize) + 1
y := (celly * game.Cellsize) + (j * game.Cellsize) + 1
if i == 1 {
x -= 1
}
if j == 1 {
y -= 1
}
// setup the vertex
game.Vertices[idx].DstX = float32(x)
game.Vertices[idx].DstY = float32(y)
game.Vertices[idx].SrcX = 1
game.Vertices[idx].SrcY = 1
game.Vertices[idx].ColorR = float32(game.Black.R)
game.Vertices[idx].ColorG = float32(game.Black.G)
game.Vertices[idx].ColorB = float32(game.Black.B)
game.Vertices[idx].ColorA = 1
idx++
}
}
}
// indices for first triangle
game.Indices[index] = base
game.Indices[index+1] = base + 1
game.Indices[index+2] = base + 3
// for the second one
game.Indices[index+3] = base
game.Indices[index+4] = base + 2
game.Indices[index+5] = base + 3
index += 6 // 3 indicies per triangle
base += 4 // 4 vertices per cell
}
}
}
func (game *Game) Draw(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(0, 0)
screen.DrawImage(game.Cache, op)
triop := &ebiten.DrawTrianglesOptions{}
screen.DrawTriangles(game.Vertices, game.Indices, blackSubImage, triop)
}
func main() {
size := 200
game := &Game{
Width: size,
Height: size,
Cellsize: 4,
Density: 5,
TPG: 5,
Debug: true,
}
game.ScreenWidth = game.Width * game.Cellsize
game.ScreenHeight = game.Height * game.Cellsize
game.Init()
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
ebiten.SetWindowTitle("triangle conway's game of life")
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}

View File

@@ -0,0 +1,3 @@
module perf
go 1.22

View File

@@ -0,0 +1,106 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"unsafe"
)
const (
dim int = 1500
loops int = 5000
density int = 8
debug bool = false
)
var max int
// https://dev.to/chigbeef_77/bool-int-but-stupid-in-go-3jb3
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func Count(grid []bool, x, y int) int {
var sum int
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= dim || y+nbgY < 0 || y+nbgY >= dim {
continue
}
col = x + nbgX
row = y + nbgY
state := grid[row*col]
intstate := bool2int(state)
sum += intstate
}
}
sum -= bool2int(grid[y*x])
return sum
}
func Init() []bool {
max = dim * dim
grid := make([]bool, max)
for y := 0; y < dim; y++ {
for x := 0; x < dim; x++ {
if rand.Intn(density) == 1 {
grid[y*x] = true
}
}
}
return grid
}
func Loop(grid []bool) {
c := 0
for i := 0; i < loops; i++ {
for y := 0; y < dim; y++ {
for x := 0; x < dim; x++ {
state := grid[y*x]
neighbors := Count(grid, x, y)
if state && neighbors > 1 {
if debug {
fmt.Printf("Loop %d - cell at %d,%d is %t and has %d living neighbors\n", i, x, y, state, neighbors)
}
c = 1
}
}
}
}
if c > 1 {
c = 0
}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
grid := Init()
// main loop
Loop(grid)
}

View File

@@ -0,0 +1,3 @@
module perf
go 1.22

View File

@@ -0,0 +1,141 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"time"
"unsafe"
)
const (
max int = 1500
loops int = 5000
density int = 8
debug bool = false
)
type Cell struct {
State bool
Neighbors [8]*Cell
NeighborCount int
}
type Grid [1500][1500]Cell
// https://dev.to/chigbeef_77/bool-int-but-stupid-in-go-3jb3
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) {
cell.NeighborCount = 0
for _, neighbor := range cell.Neighbors {
cell.NeighborCount += bool2int(neighbor.State)
}
}
func SetNeighbors(grid Grid, x, y int) {
cells := []*Cell{}
deadcell := &Cell{}
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= max || y+nbgY < 0 || y+nbgY >= max {
cells = append(cells, deadcell)
} else {
col = x + nbgX
row = y + nbgY
if col == x && row == y {
// do not add self
continue
}
cells = append(cells, &grid[row][col])
}
}
}
for idx, cell := range cells {
grid[y][x].Neighbors[idx] = cell
}
}
func Init() Grid {
grid := Grid{}
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
if rand.Intn(density) == 1 {
grid[y][x].State = true
}
}
}
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
SetNeighbors(grid, x, y)
}
}
return grid
}
func Loop(grid Grid) {
c := 0
for i := 0; i < loops; i++ {
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
cell := &grid[y][x]
state := cell.State
cell.Count(x, y)
if state && cell.NeighborCount > 1 {
if debug {
fmt.Printf(
"Loop %d - cell at %d,%d is %t and has %d living neighbors\n",
i, x, y, state, cell.NeighborCount)
}
c = 1
}
}
}
}
if c > 1 {
c = 0
}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
grid := Init()
// main loop
loopstart := time.Now()
Loop(grid)
loopend := time.Now()
diff := loopstart.Sub(loopend)
fmt.Printf("Loop took %.04f\n", diff.Seconds())
}

View File

@@ -0,0 +1,3 @@
module perf
go 1.22

View File

@@ -0,0 +1,139 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"time"
"unsafe"
)
const (
max int = 1500
loops int = 5000
density int = 8
debug bool = false
)
type Cell struct {
State bool
Neighbors [8]*Cell
NeighborCount int
}
// https://dev.to/chigbeef_77/bool-int-but-stupid-in-go-3jb3
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) {
cell.NeighborCount = 0
for _, neighbor := range cell.Neighbors {
cell.NeighborCount += bool2int(neighbor.State)
}
}
func SetNeighbors(grid [][]Cell, x, y int) {
cells := []*Cell{}
deadcell := &Cell{}
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= max || y+nbgY < 0 || y+nbgY >= max {
cells = append(cells, deadcell)
} else {
col = x + nbgX
row = y + nbgY
if col == x && row == y {
// do not add self
continue
}
cells = append(cells, &grid[row][col])
}
}
}
for idx, cell := range cells {
grid[y][x].Neighbors[idx] = cell
}
}
func Init() [][]Cell {
grid := make([][]Cell, max)
for y := 0; y < max; y++ {
grid[y] = make([]Cell, max)
for x := 0; x < max; x++ {
if rand.Intn(density) == 1 {
grid[y][x].State = true
}
}
}
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
SetNeighbors(grid, x, y)
}
}
return grid
}
func Loop(grid [][]Cell) {
c := 0
for i := 0; i < loops; i++ {
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
cell := &grid[y][x]
state := cell.State
cell.Count(x, y)
if state && cell.NeighborCount > 1 {
if debug {
fmt.Printf(
"Loop %d - cell at %d,%d is %t and has %d living neighbors\n",
i, x, y, state, cell.NeighborCount)
}
c = 1
}
}
}
}
if c > 1 {
c = 0
}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
grid := Init()
// main loop
loopstart := time.Now()
Loop(grid)
loopend := time.Now()
diff := loopstart.Sub(loopend)
fmt.Printf("Loop took %.04f\n", diff.Seconds())
}

View File

@@ -0,0 +1,3 @@
module perf
go 1.22

View File

@@ -0,0 +1,137 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"time"
"unsafe"
)
const (
max int = 1500
loops int = 5000
density int = 8
debug bool = false
)
type Cell struct {
State bool
Neighbors []*Cell
NeighborCount int
}
// https://dev.to/chigbeef_77/bool-int-but-stupid-in-go-3jb3
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) {
cell.NeighborCount = 0
for _, neighbor := range cell.Neighbors {
cell.NeighborCount += bool2int(neighbor.State)
}
}
func SetNeighbors(grid [][]Cell, x, y int) {
cells := []*Cell{}
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= max || y+nbgY < 0 || y+nbgY >= max {
continue
}
col = x + nbgX
row = y + nbgY
if col == x && row == y {
continue
}
cells = append(cells, &grid[row][col])
}
}
grid[y][x].Neighbors = make([]*Cell, len(cells))
for idx, cell := range cells {
grid[y][x].Neighbors[idx] = cell
}
}
func Init() [][]Cell {
grid := make([][]Cell, max)
for y := 0; y < max; y++ {
grid[y] = make([]Cell, max)
for x := 0; x < max; x++ {
if rand.Intn(density) == 1 {
grid[y][x].State = true
}
}
}
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
SetNeighbors(grid, x, y)
}
}
return grid
}
func Loop(grid [][]Cell) {
c := 0
for i := 0; i < loops; i++ {
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
cell := &grid[y][x]
state := cell.State
cell.Count(x, y)
if state && cell.NeighborCount > 1 {
if debug {
fmt.Printf(
"Loop %d - cell at %d,%d is %t and has %d living neighbors\n",
i, x, y, state, cell.NeighborCount)
}
c = 1
}
}
}
}
if c > 1 {
c = 0
}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
grid := Init()
// main loop
loopstart := time.Now()
Loop(grid)
loopend := time.Now()
diff := loopstart.Sub(loopend)
fmt.Printf("Loop took %.04f\n", diff.Seconds())
}

View File

@@ -0,0 +1,3 @@
module perf
go 1.22

View File

@@ -0,0 +1,102 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"unsafe"
)
const (
max int = 1500
loops int = 5000
density int = 8
debug bool = false
)
// https://dev.to/chigbeef_77/bool-int-but-stupid-in-go-3jb3
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func Count(grid [][]bool, x, y int) int {
var sum int
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= max || y+nbgY < 0 || y+nbgY >= max {
continue
}
col = x + nbgX
row = y + nbgY
state := grid[row][col]
intstate := bool2int(state)
sum += intstate
}
}
sum -= bool2int(grid[y][x])
return sum
}
func Init() [][]bool {
grid := make([][]bool, max)
for y := 0; y < max; y++ {
grid[y] = make([]bool, max)
for x := 0; x < max; x++ {
if rand.Intn(density) == 1 {
grid[y][x] = true
}
}
}
return grid
}
func Loop(grid [][]bool) {
c := 0
for i := 0; i < loops; i++ {
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
state := grid[y][x]
neighbors := Count(grid, x, y)
if state && neighbors > 1 {
if debug {
fmt.Printf("Loop %d - cell at %d,%d is %t and has %d living neighbors\n", i, x, y, state, neighbors)
}
c = 1
}
}
}
}
if c > 1 {
c = 0
}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
grid := Init()
// main loop
Loop(grid)
}

View File

@@ -0,0 +1,5 @@
module perf
go 1.22
require github.com/mlange-42/arche v0.13.0 // indirect

View File

@@ -0,0 +1,2 @@
github.com/mlange-42/arche v0.13.0 h1:ef0fu9qC2KIr8wIlVs+CgeQ5CSUJ8A1Hut6nXYdf+xk=
github.com/mlange-42/arche v0.13.0/go.mod h1:bFktKnvGDj2kP01xar79z0hKwGHdnoaEZR8HWmJkIyU=

View File

@@ -0,0 +1,145 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime/pprof"
"github.com/mlange-42/arche/ecs"
"github.com/mlange-42/arche/generic"
)
const (
max int = 1500
loops int = 5000
density int = 8
debug bool = false
)
// components
type Pos struct {
X, Y, GridX, GridY int
}
type Cell struct {
State bool
Neighbors [8]ecs.Entity
}
type ECS struct {
World *ecs.World
Filter *generic.Filter2[Pos, Cell]
Map *generic.Map2[Pos, Cell]
}
func (cell *Cell) NeighborCount(ECS *ECS) int {
sum := 0
for _, neighbor := range cell.Neighbors {
if ECS.World.Alive(neighbor) {
_, cel := ECS.Map.Get(neighbor)
if cel.State {
sum++
}
}
}
return sum
}
func Loop(ECS *ECS) {
c := 0
for i := 0; i < loops; i++ {
query := ECS.Filter.Query(ECS.World)
for query.Next() {
_, cel := query.Get()
if cel.State && cel.NeighborCount(ECS) > 1 {
c = 1
}
}
}
if c > 1 {
c = 0
}
}
func SetupWorld() *ECS {
world := ecs.NewWorld()
builder := generic.NewMap2[Pos, Cell](&world)
// we need a temporary grid in order to find out neighbors
grid := [max][max]ecs.Entity{}
// setup entities
for y := 0; y < max; y++ {
for x := 0; x < max; x++ {
e := builder.New()
pos, cell := builder.Get(e)
pos.X = x
pos.Y = y // pos.GridX = x*cellsize
cell.State = false
if rand.Intn(density) == 1 {
cell.State = true
}
// store to tmp grid
grid[y][x] = e
}
}
// global filter
filter := generic.NewFilter2[Pos, Cell]()
query := filter.Query(&world)
for query.Next() {
pos, cel := query.Get()
n := 0
for x := -1; x < 2; x++ {
for y := -1; y < 2; y++ {
XX := pos.X + x
YY := pos.Y + y
if XX < 0 || XX >= max || YY < 0 || YY >= max {
continue
}
if pos.X != XX || pos.Y != YY {
cel.Neighbors[n] = grid[XX][YY]
n++
}
}
}
}
return &ECS{World: &world, Filter: filter, Map: &builder}
}
func main() {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
// init
fmt.Print("Setup ... ")
ECS := SetupWorld()
fmt.Println("done")
fmt.Println(ECS.World.Stats())
// main loop
Loop(ECS)
}

2
various-tests/raygol/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
golsky
*.o

View File

@@ -0,0 +1,33 @@
CFLAGS = -Wall -Wextra -Werror -O2 -g
LDFLAGS= -L/usr/local/lib -lraylib -lGL -lm -lpthread -ldl -lrt -lX11 -g
CC = clang
OBJS = main.o game.o grid.o
DST = golsky
PREFIX = /usr/local
UID = root
GID = 0
MAN = udpxd.1
.PHONY: all
all: $(DST)
$(DST): $(OBJS)
$(CC) $(OBJS) $(LDFLAGS) -o $(DST)
%.o: %.c
$(CC) -c $(CFLAGS) $*.c -o $*.o
.PHONY: clean
clean:
rm -f *.o $(DST)
.PHONY: install
install: $(DST)
install -d -o $(UID) -g $(GID) $(PREFIX)/sbin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(DST) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(MAN) $(PREFIX)/man/man1/
.PHONY: run
run:
LD_LIBRARY_PATH=/usr/local/lib ./golsky

View File

@@ -0,0 +1,48 @@
#include "game.h"
#include <stdio.h>
Game *Init(int width, int height, int gridwidth, int gridheight, int density) {
struct Game *game = malloc(sizeof(struct Game));
game->ScreenWidth = width;
game->ScreenHeight = height;
game->Cellsize = width / gridwidth;
game->Width = gridwidth;
game->Height = gridheight;
InitWindow(width, height, "golsky");
SetTargetFPS(60);
game->Grid = NewGrid(gridwidth, gridheight, density);
return game;
}
void Update(Game *game) {
if (IsKeyDown(KEY_Q)) {
game->Done = true;
exit(0);
}
}
void Draw(Game *game) {
BeginDrawing();
ClearBackground(RAYWHITE);
for (int y = 0; y < game->Width; y++) {
for (int x = 0; x < game->Height; x++) {
if (game->Grid->Data[y][x] == 1) {
DrawRectangle(x * game->Cellsize, y * game->Cellsize, game->Cellsize,
game->Cellsize, GREEN);
} else {
DrawRectangle(x * game->Cellsize, y * game->Cellsize, game->Cellsize,
game->Cellsize, RAYWHITE);
}
}
}
DrawText("TEST", game->ScreenWidth / 2, 10, 20, RED);
EndDrawing();
}

View File

@@ -0,0 +1,25 @@
#ifndef _HAVE_GAME_H
#define _HAVE_GAME_H
#include "grid.h"
#include "raylib.h"
#include <stdlib.h>
typedef struct Game {
// Camera2D Camera;
int ScreenWidth;
int ScreenHeight;
int Cellsize;
// Grid dimensions
int Width;
int Height;
bool Done;
Grid *Grid;
} Game;
Game *Init(int width, int height, int gridwidth, int gridheight, int density);
void Update(Game *game);
void Draw(Game *game);
#endif

View File

@@ -0,0 +1,28 @@
#include "grid.h"
Grid *NewGrid(int width, int height, int density) {
Grid *grid = malloc(sizeof(struct Grid));
grid->Width = width;
grid->Height = height;
grid->Density = density;
grid->Data = malloc(height * sizeof(int *));
for (int y = 0; y < grid->Height; y++) {
grid->Data[y] = malloc(width * sizeof(int *));
}
FillRandom(grid);
return grid;
}
void FillRandom(Grid *grid) {
int r;
for (int y = 0; y < grid->Width; y++) {
for (int x = 0; x < grid->Height; x++) {
r = GetRandomValue(0, grid->Density);
if (r == 1)
grid->Data[y][x] = r;
}
}
}

View File

@@ -0,0 +1,18 @@
#ifndef _HAVE_GRID_H
#define _HAVE_GRID_H
#include "raylib.h"
#include <stdio.h>
#include <stdlib.h>
typedef struct Grid {
int Width;
int Height;
int Density;
int **Data;
} Grid;
Grid *NewGrid(int width, int height, int density);
void FillRandom(Grid *grid);
#endif

View File

@@ -0,0 +1,15 @@
#include "game.h"
#include "raylib.h"
int main(void) {
Game *game = Init(800, 800, 10, 10, 8);
while (!WindowShouldClose()) {
Update(game);
Draw(game);
}
CloseWindow();
free(game);
return 0;
}

View File

@@ -0,0 +1,13 @@
module testgol
go 1.22
require (
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.7.0 // indirect
github.com/hajimehoshi/ebiten/v2 v2.7.4 // indirect
github.com/jezek/xgb v1.1.1 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
)

View File

@@ -0,0 +1,14 @@
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU=
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M=
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.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -0,0 +1,305 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"runtime/pprof"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Images struct {
Black, White *ebiten.Image
}
type Grid struct {
Data [][]int64
Width, Height, Density int
}
// Create new empty grid and allocate Data according to provided dimensions
func NewGrid(width, height, density int) *Grid {
grid := &Grid{
Height: height,
Width: width,
Density: density,
Data: make([][]int64, height),
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]int64, width)
}
return grid
}
type Game struct {
Width, Height, Cellsize, Density int
ScreenWidth, ScreenHeight int
Grids []*Grid
Index int
Elapsed int64
TPG int64 // adjust game speed independently of TPS
Pause, Debug, Profile, Gridlines bool
Pixels []byte
OffScreen *ebiten.Image
}
func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return game.ScreenWidth, game.ScreenHeight
}
// live console output of the grid
func (game *Game) DebugDump() {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
if game.Debug {
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
if game.Grids[game.Index].Data[y][x] == 1 {
fmt.Print("XX")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
}
fmt.Printf("FPS: %0.2f\n", ebiten.ActualTPS())
}
func (game *Game) Init() {
// setup two grids, one for display, one for next state
grida := NewGrid(game.Width, game.Height, game.Density)
gridb := NewGrid(game.Width, game.Height, game.Density)
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
if rand.Intn(game.Density) == 1 {
grida.Data[y][x] = 1
}
}
}
game.Grids = []*Grid{
grida,
gridb,
}
game.Pixels = make([]byte, game.ScreenWidth*game.ScreenHeight*4)
game.OffScreen = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
}
// count the living neighbors of a cell
func (game *Game) CountNeighbors(x, y int) int64 {
var sum int64
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
// Wrap mode we look at all the 8 neighbors surrounding
// us. In case we are on an edge we'll look at the
// neighbor on the other side of the grid, thus wrapping
// lookahead around using the mod() function.
col = (x + nbgX + game.Width) % game.Width
row = (y + nbgY + game.Height) % game.Height
sum += game.Grids[game.Index].Data[row][col]
}
}
// don't count ourselfes though
sum -= game.Grids[game.Index].Data[y][x]
return sum
}
// the heart of the game
func (game *Game) CheckRule(state int64, neighbors int64) int64 {
var nextstate int64
if state == 0 && neighbors == 3 {
nextstate = 1
} else if state == 1 && (neighbors == 2 || neighbors == 3) {
nextstate = 1
} else {
nextstate = 0
}
return nextstate
}
// we only update the cells if we are not in pause state or if the
// game timer (TPG) is elapsed.
func (game *Game) UpdateCells() {
if game.Pause {
return
}
if game.Elapsed < game.TPG {
game.Elapsed++
return
}
// next grid index. we only have to, so we just xor it
next := game.Index ^ 1
// calculate cell life state, this is the actual game of life
for y := 0; y < game.Height; y++ {
for x := 0; x < game.Width; x++ {
state := game.Grids[game.Index].Data[y][x] // 0|1 == dead or alive
neighbors := game.CountNeighbors(x, y) // alive neighbor count
// actually apply the current rules
nextstate := game.CheckRule(state, neighbors)
// change state of current cell in next grid
game.Grids[next].Data[y][x] = nextstate
}
}
// switch grid for rendering
game.Index ^= 1
game.Elapsed = 0
game.UpdatePixels()
}
func (game *Game) Update() error {
game.UpdateCells()
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
game.Pause = !game.Pause
}
return nil
}
/*
*
r, g, b := color(it)
78 p := 4 * (i + j*screenWidth)
79 gm.offscreenPix[p] = r
80 gm.offscreenPix[p+1] = g
81 gm.offscreenPix[p+2] = b
82 gm.offscreenPix[p+3] = 0xff
*/
func (game *Game) UpdatePixels() {
var col byte
gridx := 0
gridy := 0
idx := 0
for y := 0; y < game.ScreenHeight; y++ {
for x := 0; x < game.ScreenWidth; x++ {
gridx = x / game.Cellsize
gridy = y / game.Cellsize
col = 0xff
if game.Grids[game.Index].Data[gridy][gridx] == 1 {
col = 0x0
}
if game.Gridlines {
if x%game.Cellsize == 0 || y%game.Cellsize == 0 {
col = 128
}
}
idx = 4 * (x + y*game.ScreenWidth)
game.Pixels[idx] = col
game.Pixels[idx+1] = col
game.Pixels[idx+2] = col
game.Pixels[idx+3] = 0xff
idx++
}
}
game.OffScreen.WritePixels(game.Pixels)
}
func (game *Game) Draw(screen *ebiten.Image) {
screen.DrawImage(game.OffScreen, nil)
game.DebugDump()
}
func main() {
//x := 1
//y := 0
col := 1 >> 0xff
fmt.Printf("col: %d\n", col)
x := 1
y := 2
c := 4
xm := x & (c - 1)
ym := y & (c - 1)
fmt.Println(xm & ym)
a := 1
b := 1
//gen := 100
hist := 0
for gen := 0; gen < 50; gen++ {
fmt.Println((a ^ (1 ^ b)) * (gen - hist))
if gen == 25 {
a = 0
}
}
}
func _main() {
size := 800
game := &Game{
Width: size,
Height: size,
Cellsize: 4,
Density: 8,
TPG: 10,
Debug: false,
Profile: false,
Gridlines: false,
}
game.ScreenWidth = game.Width * game.Cellsize
game.ScreenHeight = game.Height * game.Cellsize
game.Init()
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
ebiten.SetWindowTitle("triangle conway's game of life")
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
if game.Profile {
fd, err := os.Create("cpu.profile")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
}
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}

View File

@@ -1,172 +0,0 @@
package main
import (
"image/color"
"github.com/ebitenui/ebitenui/image"
"github.com/ebitenui/ebitenui/widget"
)
func NewMenuButton(
text string,
action func(args *widget.ButtonClickedEventArgs)) *widget.Button {
buttonImage, _ := LoadButtonImage()
return widget.NewButton(
widget.ButtonOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Position: widget.RowLayoutPositionCenter,
Stretch: true,
MaxWidth: 200,
MaxHeight: 100,
}),
),
widget.ButtonOpts.Image(buttonImage),
widget.ButtonOpts.Text(text, *FontRenderer.FontSmall, &widget.ButtonTextColor{
Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff},
}),
widget.ButtonOpts.TextPadding(widget.Insets{
Left: 5,
Right: 5,
Top: 5,
Bottom: 5,
}),
widget.ButtonOpts.ClickedHandler(action),
)
}
func NewCheckbox(
text string,
action func(args *widget.CheckboxChangedEventArgs)) *widget.LabeledCheckbox {
checkboxImage, _ := LoadCheckboxImage()
buttonImage, _ := LoadButtonImage()
return widget.NewLabeledCheckbox(
widget.LabeledCheckboxOpts.CheckboxOpts(
widget.CheckboxOpts.ButtonOpts(
widget.ButtonOpts.Image(buttonImage),
),
widget.CheckboxOpts.Image(checkboxImage),
widget.CheckboxOpts.StateChangedHandler(action),
),
widget.LabeledCheckboxOpts.LabelOpts(
widget.LabelOpts.Text(text, *FontRenderer.FontSmall,
&widget.LabelColor{
Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff},
}),
),
)
}
func NewSeparator() widget.PreferredSizeLocateableWidget {
c := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.Insets{
Top: 20,
Bottom: 20,
}))),
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(
widget.RowLayoutData{Stretch: true})))
c.AddChild(widget.NewGraphic(
widget.GraphicOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
MaxHeight: 2,
})),
widget.GraphicOpts.ImageNineSlice(
image.NewNineSliceColor(
color.NRGBA{0xdf, 0xf4, 0xff, 0xff})),
))
return c
}
type RowContainer struct {
Root *widget.Container
Row *widget.Container
}
func (container *RowContainer) AddChild(child widget.PreferredSizeLocateableWidget) {
container.Row.AddChild(child)
}
func (container *RowContainer) Container() *widget.Container {
return container.Root
}
// set arg to false if no background needed
func NewRowContainer(title string) *RowContainer {
buttonImageHover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
uiContainer := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
)
titleLabel := widget.NewText(
widget.TextOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
})),
widget.TextOpts.Text(title, *FontRenderer.FontNormal, color.NRGBA{0xdf, 0xf4, 0xff, 0xff}))
rowContainer := widget.NewContainer(
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionCenter,
VerticalPosition: widget.AnchorLayoutPositionCenter,
}),
),
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(20)),
widget.RowLayoutOpts.Spacing(0),
)),
widget.ContainerOpts.BackgroundImage(buttonImageHover),
)
rowContainer.AddChild(titleLabel)
uiContainer.AddChild(rowContainer)
return &RowContainer{
Root: uiContainer,
Row: rowContainer,
}
}
func LoadButtonImage() (*widget.ButtonImage, error) {
idle := image.NewNineSlice(Assets["button-9slice2"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
hover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
pressed := image.NewNineSlice(Assets["button-9slice1"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
return &widget.ButtonImage{
Idle: idle,
Hover: hover,
Pressed: pressed,
}, nil
}
func LoadCheckboxImage() (*widget.CheckboxGraphicImage, error) {
unchecked := &widget.ButtonImageImage{
Idle: Assets["checkbox-9slice2"],
Disabled: Assets["checkbox-9slice2"],
}
checked := &widget.ButtonImageImage{
Idle: Assets["checkbox-9slice1"],
Disabled: Assets["checkbox-9slice1"],
}
return &widget.CheckboxGraphicImage{
Checked: checked,
Unchecked: unchecked,
Greyed: unchecked,
}, nil
}