diff --git a/Makefile b/Makefile
index c3beded..9f212ba 100644
--- a/Makefile
+++ b/Makefile
@@ -1,97 +1,4 @@
-# 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 .
-
-
-#
-# 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
+.PHONY: all
+all:
+ make -C src
+ mv src/golsky .
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..c3beded
--- /dev/null
+++ b/src/Makefile
@@ -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 .
+
+
+#
+# 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
diff --git a/assets/fonts/NotoSans-Regular.ttf b/src/assets/fonts/NotoSans-Regular.ttf
similarity index 100%
rename from assets/fonts/NotoSans-Regular.ttf
rename to src/assets/fonts/NotoSans-Regular.ttf
diff --git a/assets/shaders/row.kg b/src/assets/shaders/row.kg
similarity index 100%
rename from assets/shaders/row.kg
rename to src/assets/shaders/row.kg
diff --git a/assets/sprites/button-9slice1.png b/src/assets/sprites/button-9slice1.png
similarity index 100%
rename from assets/sprites/button-9slice1.png
rename to src/assets/sprites/button-9slice1.png
diff --git a/assets/sprites/button-9slice2.png b/src/assets/sprites/button-9slice2.png
similarity index 100%
rename from assets/sprites/button-9slice2.png
rename to src/assets/sprites/button-9slice2.png
diff --git a/assets/sprites/button-9slice3.png b/src/assets/sprites/button-9slice3.png
similarity index 100%
rename from assets/sprites/button-9slice3.png
rename to src/assets/sprites/button-9slice3.png
diff --git a/assets/sprites/checkbox-9slice1.png b/src/assets/sprites/checkbox-9slice1.png
similarity index 100%
rename from assets/sprites/checkbox-9slice1.png
rename to src/assets/sprites/checkbox-9slice1.png
diff --git a/assets/sprites/checkbox-9slice2.png b/src/assets/sprites/checkbox-9slice2.png
similarity index 100%
rename from assets/sprites/checkbox-9slice2.png
rename to src/assets/sprites/checkbox-9slice2.png
diff --git a/assets/src/button-9slice.ase b/src/assets/src/button-9slice.ase
similarity index 100%
rename from assets/src/button-9slice.ase
rename to src/assets/src/button-9slice.ase
diff --git a/assets/src/checkbox-9slice.ase b/src/assets/src/checkbox-9slice.ase
similarity index 100%
rename from assets/src/checkbox-9slice.ase
rename to src/assets/src/checkbox-9slice.ase
diff --git a/camera.go b/src/camera.go
similarity index 100%
rename from camera.go
rename to src/camera.go
diff --git a/config.go b/src/config.go
similarity index 100%
rename from config.go
rename to src/config.go
diff --git a/game.go b/src/game.go
similarity index 100%
rename from game.go
rename to src/game.go
diff --git a/generics.go b/src/generics.go
similarity index 100%
rename from generics.go
rename to src/generics.go
diff --git a/grid.go b/src/grid.go
similarity index 100%
rename from grid.go
rename to src/grid.go
diff --git a/loader-fonts.go b/src/loader-fonts.go
similarity index 100%
rename from loader-fonts.go
rename to src/loader-fonts.go
diff --git a/loader-shaders.go b/src/loader-shaders.go
similarity index 100%
rename from loader-shaders.go
rename to src/loader-shaders.go
diff --git a/loader-sprites.go b/src/loader-sprites.go
similarity index 100%
rename from loader-sprites.go
rename to src/loader-sprites.go
diff --git a/main.go b/src/main.go
similarity index 100%
rename from main.go
rename to src/main.go
diff --git a/menu.go b/src/menu.go
similarity index 100%
rename from menu.go
rename to src/menu.go
diff --git a/options.go b/src/options.go
similarity index 100%
rename from options.go
rename to src/options.go
diff --git a/play.go b/src/play.go
similarity index 100%
rename from play.go
rename to src/play.go
diff --git a/rule.go b/src/rule.go
similarity index 100%
rename from rule.go
rename to src/rule.go
diff --git a/scene.go b/src/scene.go
similarity index 100%
rename from scene.go
rename to src/scene.go
diff --git a/system.go b/src/system.go
similarity index 100%
rename from system.go
rename to src/system.go
diff --git a/widgets.go b/src/widgets.go
similarity index 100%
rename from widgets.go
rename to src/widgets.go
diff --git a/various-tests/drawtriangles/go.mod b/various-tests/drawtriangles/go.mod
new file mode 100644
index 0000000..7a41db3
--- /dev/null
+++ b/various-tests/drawtriangles/go.mod
@@ -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
+)
diff --git a/various-tests/drawtriangles/go.sum b/various-tests/drawtriangles/go.sum
new file mode 100644
index 0000000..d3c38a0
--- /dev/null
+++ b/various-tests/drawtriangles/go.sum
@@ -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=
diff --git a/various-tests/drawtriangles/main.go b/various-tests/drawtriangles/main.go
new file mode 100644
index 0000000..259ef74
--- /dev/null
+++ b/various-tests/drawtriangles/main.go
@@ -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)
+ }
+}
diff --git a/various-tests/writepixel/go.mod b/various-tests/writepixel/go.mod
new file mode 100644
index 0000000..cde79f5
--- /dev/null
+++ b/various-tests/writepixel/go.mod
@@ -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
+)
diff --git a/various-tests/writepixel/go.sum b/various-tests/writepixel/go.sum
new file mode 100644
index 0000000..d3c38a0
--- /dev/null
+++ b/various-tests/writepixel/go.sum
@@ -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=
diff --git a/various-tests/writepixel/main.go b/various-tests/writepixel/main.go
new file mode 100644
index 0000000..edc7497
--- /dev/null
+++ b/various-tests/writepixel/main.go
@@ -0,0 +1,294 @@
+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)
+}
+
+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)
+ }
+}