6 Commits

Author SHA1 Message Date
42ee242f0c nope, it's a hopeless mess 2024-06-11 19:05:29 +02:00
b1b2a6901f not work 2024-06-11 14:55:13 +02:00
68b7eb60a8 add demo 2024-06-11 14:44:05 +02:00
31d7f5bc4f added toolbar icon widget 2024-06-10 19:30:07 +02:00
0fdd4bbd81 fix flickering 2024-06-10 14:17:36 +02:00
63d8d09fc0 add test code 2024-06-10 13:02:32 +02:00
59 changed files with 435 additions and 1638 deletions

1
.gitignore vendored
View File

@@ -6,4 +6,3 @@ rect*
*prof *prof
*lif *lif
*rle *rle
svgicons

View File

@@ -1,8 +1,5 @@
.PHONY all: .PHONY: all
all: build all:
.PHONY: build
build:
make -C src make -C src
mv src/golsky . mv src/golsky .
@@ -10,8 +7,3 @@ build:
clean: clean:
make -C src clean make -C src clean
rm -f dump* rect* rm -f dump* rect*
.PHONY: profile
profile: build
./golsky -W 1500 -H 1500 -d --profile-file cpu.profile
go tool pprof --http localhost:8888 golsky cpu.profile

View File

@@ -24,7 +24,6 @@ Based on: https://youtu.be/FWSR_7kZuYg?si=ix1dmo76D8AmF25F
[![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-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) [![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) [Youtube video game preview](https://www.youtube.com/watch?v=xEto6Oew16I)
# Features # Features
@@ -82,16 +81,17 @@ Usage of ./golsky:
While it runs, there are a couple of commands you can use: While it runs, there are a couple of commands you can use:
* left mouse click: set a cell to alife (also pauses the game)
* right mouse click: set a cell to dead
* space: pause or resume the game * space: pause or resume the game
* while game is paused: press n to forward one step * while game is paused: press n to forward one step
* 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 left mouse button pressed: move canvas * move mouse while left mouse button pressed: move canvas
* i: enter "insert" (draw) mode: use left mouse to toggle cells alife state. * i: enter "insert" (draw) mode: use left mouse to set cells alife and right
Leave with insert mode "space". While in insert mode, use middle mouse button to dead. Leave with "space". While in insert mode, use middle mouse
button to drag the grid. 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)

18
TODO.md
View File

@@ -1,29 +1,23 @@
- add all other options like size etc - add all other options like size etc
- add gif export - add gif export
- add toolbar (not working yet, see branch trackui) - add toolbar
- turn input ifs to switch
- only draw visible part of the world - only draw visible part of the world
- use themes instead of the current weird color lists
- print current mode to the bottom like pause, insert and mark - print current mode to the bottom like pause, insert and mark
- add https://www.ibiblio.org/lifepatterns/october1970.html - 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 - 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 - 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/ - https://mattnakama.com/blog/go-branchless-coding/
- add performance measurements, see: - add performance measurements, see:
DrawTriangles: https://github.com/TLINDEN/testgol DrawTriangles: https://github.com/TLINDEN/testgol
WritePixels: https://github.com/TLINDEN/testgol/tree/wrpixels WritePixels: https://github.com/TLINDEN/testgol/tree/wrpixels
https://www.tasnimzotder.com/blog/optimizing-game-of-life-algorithm 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 - show gridlines menu has no effect of grid was enabled with -g
lastly the grid - if enabled. If disabled, there's be no gap between
the cells anymore.
- Speed - Speed
https://conwaylife.com/forums/viewtopic.php?f=7&t=3237 https://conwaylife.com/forums/viewtopic.php?f=7&t=3237
Look at try-pointers-and-cells branch, we're using pre-calculated
neighbor list of pointers to cells, but it's only a liiiiitle bit
better :(
- Patterns: - Patterns:

3
go.mod
View File

@@ -13,10 +13,9 @@ 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.8-0.20240608175527-424f62327b21 // indirect github.com/ebitenui/ebitenui v0.5.8-0.20240608230235-27496c28f409 // 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

@@ -10,14 +10,14 @@ github.com/ebitenui/ebitenui v0.5.6 h1:qyJRU5j+lQo1lamxB48IBwMxMfz1xNb5iWUayCtA0
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 h1:dElhYGyf+FYY+makAndUQNOSDwFSFYyFWziPwQrPObY=
github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE= github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE=
github.com/ebitenui/ebitenui v0.5.8-0.20240608230235-27496c28f409 h1:wsPobs+O3ZmZvhtNtmnMkaB1FRM7tuZ60P0/jegRQGg=
github.com/ebitenui/ebitenui v0.5.8-0.20240608230235-27496c28f409/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=

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 [][]uint8, filename, rule string, width, height int) error { func StoreGridToRLE(grid [][]bool, 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
@@ -132,7 +132,7 @@ func StoreGridToRLE(grid [][]uint8, filename, rule string, width, height int) er
line := "" line := ""
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
char := "b" char := "b"
if grid[y][x] == 1 { if grid[y][x] {
char = "o" char = "o"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

BIN
src/assets/sprites/grid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

BIN
src/assets/sprites/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

BIN
src/assets/sprites/mark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

BIN
src/assets/sprites/menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

BIN
src/assets/sprites/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

View File

@@ -43,16 +43,16 @@ type Config struct {
} }
const ( const (
VERSION = "v0.0.9" VERSION = "v0.0.8"
Alive = 1 Alive = true
Dead = 0 Dead = false
DEFAULT_GRID_WIDTH = 600 DEFAULT_GRID_WIDTH = 600
DEFAULT_GRID_HEIGHT = 400 DEFAULT_GRID_HEIGHT = 400
DEFAULT_CELLSIZE = 4 DEFAULT_CELLSIZE = 4
DEFAULT_ZOOMFACTOR = 400 DEFAULT_ZOOMFACTOR = 400
DEFAULT_GEOM = "640x384" DEFAULT_GEOM = "640x384"
DEFAULT_THEME = "standard" DEFAULT_THEME = "standard" // "light" // inverse => "dark"
) )
const KEYBINDINGS string = ` const KEYBINDINGS string = `
@@ -62,9 +62,9 @@ const KEYBINDINGS string = `
- PAGE DOWN: slow down - PAGE DOWN: slow down
- MOUSE WHEEL: zoom in or out - MOUSE WHEEL: zoom in or out
- LEFT MOUSE BUTTON: use to drag canvas, keep clicked and move mouse - LEFT MOUSE BUTTON: use to drag canvas, keep clicked and move mouse
- I: enter "insert" (draw) mode: use left mouse to toggle a cells alife state. - I: enter "insert" (draw) mode: use left mouse to set cells alife and right
Leave with insert mode with "space". While in insert mode, use middle mouse button to dead. Leave with "space". While in insert mode, use middle mouse
button to drag the grid. button to drag grid.
- R: reset to 1:1 zoom - R: reset to 1:1 zoom
- ESCAPE: open menu, o: open options menu - ESCAPE: open menu, o: open options menu
- S: save game state to file (can be loaded with -l) - S: save game state to file (can be loaded with -l)
@@ -278,8 +278,10 @@ func (config *Config) SwitchTheme(theme string) {
} }
func (config *Config) ToggleGridlines() { func (config *Config) ToggleGridlines() {
fmt.Printf("toggle grid lines, current: %t\n", config.ShowGrid)
config.ShowGrid = !config.ShowGrid config.ShowGrid = !config.ShowGrid
config.RestartCache = true config.RestartCache = true
fmt.Printf("toggle grid lines, new: %t\n", config.ShowGrid)
} }
func (config *Config) ToggleEvolution() { func (config *Config) ToggleEvolution() {

View File

@@ -23,6 +23,7 @@ func NewGame(config *Config, startscene SceneName) *Game {
// setup scene[s] // setup scene[s]
game.CurrentScene = startscene game.CurrentScene = startscene
game.Scenes[Play] = NewPlayScene(game, config) game.Scenes[Play] = NewPlayScene(game, config)
game.Scenes[Toolbar] = NewToolbarScene(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) game.Scenes[Keybindings] = NewKeybindingsScene(game, config)
@@ -47,16 +48,22 @@ func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
} }
func (game *Game) Update() error { func (game *Game) Update() error {
scene := game.GetCurrentScene() currentscene := game.GetCurrentScene()
for _, scene := range game.Scenes {
if scene.IsPrimary() || scene == currentscene {
if quit := scene.Update(); quit != nil { if quit := scene.Update(); quit != nil {
return quit return quit
} }
next := scene.GetNext() }
}
next := currentscene.GetNext()
if next != game.CurrentScene { if next != game.CurrentScene {
game.Scenes[next].SetPrevious(game.CurrentScene) game.Scenes[next].SetPrevious(game.CurrentScene)
scene.ResetNext() currentscene.ResetNext()
game.CurrentScene = next game.CurrentScene = next
} }
@@ -65,6 +72,7 @@ func (game *Game) Update() error {
func (game *Game) Draw(screen *ebiten.Image) { func (game *Game) Draw(screen *ebiten.Image) {
// first draw primary scene[s], although there are only 1 // first draw primary scene[s], although there are only 1
skip := false
for current, scene := range game.Scenes { for current, scene := range game.Scenes {
if scene.IsPrimary() { if scene.IsPrimary() {
// primary scenes always draw // primary scenes always draw
@@ -72,10 +80,15 @@ func (game *Game) Draw(screen *ebiten.Image) {
if current == game.CurrentScene { if current == game.CurrentScene {
// avoid to redraw it in the next step // avoid to redraw it in the next step
skip = true
break
}
}
}
if skip {
return return
} }
}
}
scene := game.GetCurrentScene() scene := game.GetCurrentScene()
scene.Draw(screen) scene.Draw(screen)

View File

@@ -12,146 +12,65 @@ import (
"github.com/tlinden/golsky/rle" "github.com/tlinden/golsky/rle"
) )
// equals grid height, is being used to access grid elements and must be global
var STRIDE int
type Neighbor struct {
X, Y int
}
type Grid struct { type Grid struct {
Data []uint8 Data [][]bool
NeighborCount []int Width, Height, Density int
Neighbors [][]Neighbor
Empty bool Empty bool
Config *Config
} }
// Create new empty grid and allocate Data according to provided dimensions // Create new empty grid and allocate Data according to provided dimensions
func NewGrid(config *Config) *Grid { func NewGrid(width, height, density int, empty bool) *Grid {
STRIDE = config.Height
if config.Width > config.Height {
STRIDE = config.Width
}
size := STRIDE * STRIDE
grid := &Grid{ grid := &Grid{
Data: make([]uint8, size), Height: height,
NeighborCount: make([]int, size), Width: width,
Neighbors: make([][]Neighbor, size), Density: density,
Empty: config.Empty, Data: make([][]bool, height),
Config: config, Empty: empty,
} }
// first setup the cells for y := 0; y < height; y++ {
for y := 0; y < config.Height; y++ { grid.Data[y] = make([]bool, width)
for x := 0; x < config.Width; x++ {
grid.Data[y+STRIDE*x] = 0
}
}
// in a second pass, collect positions 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 return grid
} }
func (grid *Grid) SetupNeighbors(x, y int) {
idx := 0
var neighbors []Neighbor
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
}
neighbors = append(neighbors, Neighbor{X: col, Y: row})
grid.NeighborCount[y+STRIDE*x]++
idx++
}
}
grid.Neighbors[y+STRIDE*x] = neighbors
}
// count the living neighbors of a cell
func (grid *Grid) CountNeighbors(x, y int) uint8 {
var count uint8
pos := y + STRIDE*x
neighbors := grid.Neighbors[pos]
neighborCount := grid.NeighborCount[pos]
for idx := 0; idx < neighborCount; idx++ {
neighbor := neighbors[idx]
count += grid.Data[neighbor.Y+STRIDE*neighbor.X]
}
return count
}
// Create a new 1:1 instance // Create a new 1:1 instance
func (grid *Grid) Clone() *Grid { func (grid *Grid) Clone() *Grid {
newgrid := &Grid{} newgrid := &Grid{}
newgrid.Config = grid.Config newgrid.Width = grid.Width
newgrid.Height = grid.Height
newgrid.Data = grid.Data newgrid.Data = grid.Data
return newgrid return newgrid
} }
// copy data // copy data
// func (grid *Grid) Copy(other *Grid) { func (grid *Grid) Copy(other *Grid) {
// for y := range grid.Data { for y := range grid.Data {
// for x := range grid.Data[y] { for x := range grid.Data[y] {
// other.Data[y+STRIDE*x] = grid.Data[y+STRIDE*x] other.Data[y][x] = grid.Data[y][x]
// } }
// } }
// } }
// delete all contents // delete all contents
// func (grid *Grid) Clear() { func (grid *Grid) Clear() {
// for y := range grid.Data { for y := range grid.Data {
// for x := range grid.Data[y] { for x := range grid.Data[y] {
// grid.Data[y+STRIDE*x] = 0 grid.Data[y][x] = false
// } }
// } }
// } }
// initialize with random life cells using the given density // initialize with random life cells using the given density
func (grid *Grid) FillRandom() { func (grid *Grid) FillRandom() {
if !grid.Empty { if !grid.Empty {
for y := 0; y < grid.Config.Height; y++ { for y := range grid.Data {
for x := 0; x < grid.Config.Width; x++ { for x := range grid.Data[y] {
if rand.Intn(grid.Config.Density) == 1 { if rand.Intn(grid.Density) == 1 {
grid.Data[y+STRIDE*x] = 1 grid.Data[y][x] = true
} }
} }
} }
@@ -159,9 +78,9 @@ func (grid *Grid) FillRandom() {
} }
func (grid *Grid) Dump() { func (grid *Grid) Dump() {
for y := 0; y < grid.Config.Height; y++ { for y := 0; y < grid.Height; y++ {
for x := 0; x < grid.Config.Width; x++ { for x := 0; x < grid.Width; x++ {
if grid.Data[y+STRIDE*x] == 1 { if grid.Data[y][x] {
fmt.Print("XX") fmt.Print("XX")
} else { } else {
fmt.Print(" ") fmt.Print(" ")
@@ -174,8 +93,8 @@ func (grid *Grid) Dump() {
// initialize using a given RLE pattern // initialize using a given RLE pattern
func (grid *Grid) LoadRLE(pattern *rle.RLE) { func (grid *Grid) LoadRLE(pattern *rle.RLE) {
if pattern != nil { if pattern != nil {
startX := (grid.Config.Width / 2) - (pattern.Width / 2) startX := (grid.Width / 2) - (pattern.Width / 2)
startY := (grid.Config.Height / 2) - (pattern.Height / 2) startY := (grid.Height / 2) - (pattern.Height / 2)
var y, x int var y, x int
for rowIndex, patternRow := range pattern.Pattern { for rowIndex, patternRow := range pattern.Pattern {
@@ -184,7 +103,7 @@ func (grid *Grid) LoadRLE(pattern *rle.RLE) {
x = colIndex + startX x = colIndex + startX
y = rowIndex + startY y = rowIndex + startY
grid.Data[y+STRIDE*x] = 1 grid.Data[y][x] = true
} }
} }
} }
@@ -292,10 +211,10 @@ func (grid *Grid) SaveState(filename, rule string) error {
fmt.Fprintf(file, "#Life 1.05\n#R %s\n#D golsky state file\n#P -1 -1\n", rule) fmt.Fprintf(file, "#Life 1.05\n#R %s\n#D golsky state file\n#P -1 -1\n", rule)
for y := 0; y < grid.Config.Height; y++ { for y := range grid.Data {
for x := 0; x < grid.Config.Width; x++ { for _, cell := range grid.Data[y] {
row := "." row := "."
if grid.Data[y+STRIDE*x] == 1 { if cell {
row = "o" row = "o"
} }

View File

@@ -1,7 +1,9 @@
package main package main
import ( import (
"fmt"
"image/color" "image/color"
"os"
"github.com/ebitenui/ebitenui" "github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget" "github.com/ebitenui/ebitenui/widget"
@@ -18,7 +20,6 @@ type SceneMenu struct {
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 {
@@ -54,10 +55,6 @@ func (scene *SceneMenu) SetNext(next SceneName) {
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.Config.DelayedStart = false
scene.Leave() scene.Leave()
@@ -105,6 +102,7 @@ func (scene *SceneMenu) Init() {
options := NewMenuButton("Options", options := NewMenuButton("Options",
func(args *widget.ButtonClickedEventArgs) { func(args *widget.ButtonClickedEventArgs) {
fmt.Println("menu => options")
scene.SetNext(Options) scene.SetNext(Options)
}) })
@@ -124,7 +122,7 @@ func (scene *SceneMenu) Init() {
quit := NewMenuButton("Exit Golsky", quit := NewMenuButton("Exit Golsky",
func(args *widget.ButtonClickedEventArgs) { func(args *widget.ButtonClickedEventArgs) {
scene.Exit = true os.Exit(0)
}) })
rowContainer.AddChild(empty) rowContainer.AddChild(empty)

View File

@@ -1,8 +1,10 @@
package main package main
import ( import (
"fmt"
"image/color" "image/color"
"github.com/alecthomas/repr"
"github.com/ebitenui/ebitenui" "github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget" "github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
@@ -94,6 +96,8 @@ func (scene *SceneOptions) Init() {
gridlines := NewCheckbox("Show grid lines", gridlines := NewCheckbox("Show grid lines",
scene.Config.ShowGrid, scene.Config.ShowGrid,
func(args *widget.CheckboxChangedEventArgs) { func(args *widget.CheckboxChangedEventArgs) {
fmt.Println("CHECKBOX CALLED")
repr.Println(args.State)
scene.Config.ToggleGridlines() scene.Config.ToggleGridlines()
}) })

View File

@@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"image" "image"
"log" "log"
"sync"
"unsafe" "unsafe"
uiinput "github.com/ebitenui/ebitenui/input"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
@@ -19,24 +19,9 @@ type Images struct {
} }
const ( const (
DEBUG_FORMAT = "FPS: %0.2f, TPG: %d, M: %0.2fMB, Generations: %d\nScale: %.02f, Zoom: %d, Cam: %.02f,%.02f Cursor: %d,%d %s" DEBUG_FORMAT = "FPS: %0.2f, TPG: %d, M: %0.2fMB, Generations: %d\nScale: %.02f, Zoom: %d, Cam: %.02f,%.02f Cursor: %d,%d %s, UI Active: %t"
) )
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
@@ -47,7 +32,7 @@ type ScenePlay struct {
Clear bool Clear bool
Grids []*Grid // 2 grids: one current, one next Grids []*Grid // 2 grids: one current, one next
History History // holds state of past dead cells for evolution traces History [][]int64 // 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
TicksElapsed int // tick counter for game speed TicksElapsed int // tick counter for game speed
@@ -62,7 +47,6 @@ type ScenePlay struct {
RunOneStep bool // mutable flags from config RunOneStep bool // mutable flags from config
TPG int // current game speed (ticks per game) TPG int // current game speed (ticks per game)
Theme Theme Theme Theme
RuleCheckFunc func(uint8, uint8) uint8
} }
func NewPlayScene(game *Game, config *Config) Scene { func NewPlayScene(game *Game, config *Config) Scene {
@@ -100,38 +84,18 @@ func (scene *ScenePlay) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
/* The standard Scene of Life is symbolized in rule-string notation func (scene *ScenePlay) CheckRule(state bool, neighbors int) bool {
* as B3/S23 (23/3 here). A cell is born if it has exactly three var nextstate bool
* neighbors, survives if it has two or three living neighbors,
* and dies otherwise.
* we abbreviate the calculation: if state is 0 and 3 neighbors
* are a life, check will be just 3. If the cell is alive, 9 will
* be added to the life neighbors (to avoid a collision with the
* result 3), which will be 11|12 in case of 2|3 life neighbors.
*/
func (scene *ScenePlay) CheckRuleB3S23(state uint8, neighbors uint8) uint8 {
switch (9 * state) + neighbors {
case 11:
fallthrough
case 12:
fallthrough
case 3:
return Alive
}
return Dead // 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
// neighbors, survives if it has two or three living neighbors,
// and dies otherwise. The first number, or list of numbers, is
// what is required for a dead cell to be born.
/* if !state && Contains(scene.Config.Rule.Birth, neighbors) {
* The generic rule checker is able to calculate cell state for any
* GOL rul, including B3/S23.
*/
func (scene *ScenePlay) CheckRuleGeneric(state uint8, neighbors uint8) uint8 {
var nextstate uint8
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 && Contains(scene.Config.Rule.Death, neighbors) {
nextstate = Alive nextstate = Alive
} else { } else {
nextstate = Dead nextstate = Dead
@@ -153,27 +117,17 @@ 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)
width := scene.Config.Width
height := scene.Config.Height
// compute life status of cells // compute life status of cells
for y := 0; y < height; y++ { for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ {
go func() { state := scene.Grids[scene.Index].Data[y][x] // 0|1 == dead or alive
defer wg.Done() neighbors := scene.CountNeighbors(x, y) // alive neighbor count
for x := 0; x < width; x++ {
state := scene.Grids[scene.Index].Data[y+STRIDE*x] // 0|1 == dead or alive
neighbors := scene.Grids[scene.Index].CountNeighbors(x, y)
// actually apply the current rules // actually apply the current rules
nextstate := scene.RuleCheckFunc(state, neighbors) nextstate := scene.CheckRule(state, neighbors)
// change state of current cell in next grid // change state of current cell in next grid
scene.Grids[next].Data[y+STRIDE*x] = nextstate scene.Grids[next].Data[y][x] = nextstate
if scene.Config.ShowEvolution { if scene.Config.ShowEvolution {
// set history to current generation so we can infer the // set history to current generation so we can infer the
@@ -181,14 +135,14 @@ func (scene *ScenePlay) UpdateCells() {
// deduce the color to use if evolution tracing is enabled // deduce the color to use if evolution tracing is enabled
// 60FPS: // 60FPS:
if state != nextstate { if state != nextstate {
scene.History.Age[y][x] = scene.Generations scene.History[y][x] = scene.Generations
}
}
}
}()
} }
wg.Wait() // 10FPS:
//scene.History.Data[y][x] = (state ^ (1 ^ nextstate)) * (scene.Generations - scene.History.Data[y][x])
}
}
}
// switch grid for rendering // switch grid for rendering
scene.Index ^= 1 scene.Index ^= 1
@@ -221,40 +175,50 @@ func (scene *ScenePlay) CheckExit() error {
} }
func (scene *ScenePlay) CheckInput() { func (scene *ScenePlay) CheckInput() {
// primary functions, always available if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
switch {
case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
scene.SetNext(Menu) scene.SetNext(Menu)
case inpututil.IsKeyJustPressed(ebiten.KeyO): }
if inpututil.IsKeyJustPressed(ebiten.KeyO) {
scene.SetNext(Options) scene.SetNext(Options)
case inpututil.IsKeyJustPressed(ebiten.KeyC): }
if inpututil.IsKeyJustPressed(ebiten.KeyC) {
scene.Config.Markmode = true scene.Config.Markmode = true
scene.Config.Drawmode = false scene.Config.Drawmode = false
scene.Config.Paused = true scene.Config.Paused = true
case inpututil.IsKeyJustPressed(ebiten.KeyI): }
if inpututil.IsKeyJustPressed(ebiten.KeyI) {
scene.Config.Drawmode = true 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
} }
switch { if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
case inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter):
scene.Config.TogglePaused() scene.Config.TogglePaused()
case inpututil.IsKeyJustPressed(ebiten.KeyPageDown): }
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
} }
@@ -267,10 +231,15 @@ func (scene *ScenePlay) CheckInput() {
func (scene *ScenePlay) CheckDrawingInput() { func (scene *ScenePlay) CheckDrawingInput() {
if scene.Config.Drawmode { if scene.Config.Drawmode {
switch { if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
case inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft): scene.ToggleCellOnCursorPos(Alive)
scene.ToggleCellOnCursorPos() }
case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
scene.ToggleCellOnCursorPos(Dead)
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
scene.Config.Drawmode = false scene.Config.Drawmode = false
} }
} }
@@ -314,14 +283,16 @@ func (scene *ScenePlay) CheckDraggingInput() {
} }
// also support the arrow keys to move the canvas // also support the arrow keys to move the canvas
switch { if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
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
} }
@@ -415,13 +386,13 @@ func (scene *ScenePlay) SaveRectRLE() {
height = scene.Mark.Y - scene.Point.Y height = scene.Mark.Y - scene.Point.Y
} }
grid := make([][]uint8, height) grid := make([][]bool, height)
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
grid[y] = make([]uint8, width) grid[y] = make([]bool, width)
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
grid[y][x] = scene.Grids[scene.Index].Data[(y+starty)+STRIDE*(x+startx)] grid[y][x] = scene.Grids[scene.Index].Data[y+starty][x+startx]
} }
} }
@@ -454,11 +425,12 @@ func (scene *ScenePlay) Update() error {
return quit return quit
} }
if !uiinput.UIActive {
scene.CheckInput() scene.CheckInput()
scene.CheckDrawingInput() scene.CheckDrawingInput()
scene.CheckDraggingInput() scene.CheckDraggingInput()
scene.CheckMarkInput() scene.CheckMarkInput()
}
if !scene.Config.Paused || scene.RunOneStep { if !scene.Config.Paused || scene.RunOneStep {
scene.UpdateCells() scene.UpdateCells()
} }
@@ -467,15 +439,15 @@ func (scene *ScenePlay) Update() error {
} }
// set a cell to alive or dead // set a cell to alive or dead
func (scene *ScenePlay) ToggleCellOnCursorPos() { func (scene *ScenePlay) ToggleCellOnCursorPos(alive bool) {
// 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 && x < scene.Config.Width && y < scene.Config.Height { if x > -1 && y > -1 && x < scene.Config.Width && y < scene.Config.Height {
scene.Grids[scene.Index].Data[y+STRIDE*x] ^= 1 scene.Grids[scene.Index].Data[y][x] = alive
scene.History.Age[y][x] = 1 scene.History[y][x] = 1
} }
} }
@@ -500,7 +472,7 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
if scene.Config.ShowEvolution { if scene.Config.ShowEvolution {
scene.DrawEvolution(screen, x, y, op) scene.DrawEvolution(screen, x, y, op)
} else { } else {
if scene.Grids[scene.Index].Data[y+STRIDE*x] == 1 { if scene.Grids[scene.Index].Data[y][x] {
scene.World.DrawImage(scene.Theme.Tile(ColLife), op) scene.World.DrawImage(scene.Theme.Tile(ColLife), op)
} }
} }
@@ -512,12 +484,15 @@ 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)
// draw the toolbar directly from here, because otherwise it flickers
scene.Game.Scenes[Toolbar].Draw(screen)
} }
func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten.DrawImageOptions) { func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten.DrawImageOptions) {
age := scene.Generations - scene.History.Age[y][x] age := scene.Generations - scene.History[y][x]
switch scene.Grids[scene.Index].Data[y+STRIDE*x] { switch scene.Grids[scene.Index].Data[y][x] {
case Alive: case Alive:
if age > 50 && scene.Config.ShowEvolution { if age > 50 && scene.Config.ShowEvolution {
scene.World.DrawImage(scene.Theme.Tile(ColOld), op) scene.World.DrawImage(scene.Theme.Tile(ColOld), op)
@@ -526,7 +501,7 @@ func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten
} }
case Dead: case Dead:
// only draw dead cells in case evolution trace is enabled // only draw dead cells in case evolution trace is enabled
if scene.History.Age[y][x] > 1 && scene.Config.ShowEvolution { if scene.History[y][x] > 1 && scene.Config.ShowEvolution {
switch { switch {
case age < 10: case age < 10:
scene.World.DrawImage(scene.Theme.Tile(ColAge1), op) scene.World.DrawImage(scene.Theme.Tile(ColAge1), op)
@@ -579,7 +554,7 @@ func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) {
scene.Game.Scale, scene.Camera.ZoomFactor, scene.Game.Scale, scene.Camera.ZoomFactor,
scene.Camera.Position[0], scene.Camera.Position[1], scene.Camera.Position[0], scene.Camera.Position[1],
x, y, x, y,
paused) paused, uiinput.UIActive)
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)
@@ -598,24 +573,17 @@ func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) {
// 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)
// 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.Theme.Color(ColGrid))
} else {
scene.Cache.Fill(scene.Theme.Color(ColDead))
}
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++ {
@@ -632,8 +600,8 @@ func (scene *ScenePlay) InitCache() {
// 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() { func (scene *ScenePlay) InitGrid() {
grida := NewGrid(scene.Config) grida := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
gridb := NewGrid(scene.Config) gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
// startup is delayed until user has selected options // startup is delayed until user has selected options
grida.FillRandom() grida.FillRandom()
@@ -643,8 +611,10 @@ func (scene *ScenePlay) InitGrid() {
gridb, gridb,
} }
scene.History = NewHistory(scene.Config.Height, scene.Config.Width) scene.History = make([][]int64, scene.Config.Height)
for y := 0; y < scene.Config.Height; y++ {
scene.History[y] = make([]int64, scene.Config.Width)
}
} }
func (scene *ScenePlay) Init() { func (scene *ScenePlay) Init() {
@@ -676,8 +646,6 @@ func (scene *ScenePlay) Init() {
scene.InitCache() scene.InitCache()
if scene.Config.DelayedStart && !scene.Config.Empty { 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.Config.Empty = true
scene.InitGrid() scene.InitGrid()
scene.Config.Empty = false scene.Config.Empty = false
@@ -703,10 +671,38 @@ func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b))) return int(*(*byte)(unsafe.Pointer(&b)))
} }
func (scene *ScenePlay) InitRuleCheckFunc() { // count the living neighbors of a cell
if scene.Config.Rule.Definition == "B3/S23" { func (scene *ScenePlay) CountNeighbors(x, y int) int {
scene.RuleCheckFunc = scene.CheckRuleB3S23 var sum int
grid := scene.Grids[scene.Index].Data
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
} else { } else {
scene.RuleCheckFunc = scene.CheckRuleGeneric // In traditional grid mode the edges are deadly
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 += bool2int(grid[row][col])
}
}
// don't count ourselfes though
sum -= bool2int(grid[y][x])
return sum
} }

View File

@@ -9,13 +9,13 @@ import (
// a GOL rule // a GOL rule
type Rule struct { type Rule struct {
Definition string Definition string
Birth []uint8 Birth []int
Death []uint8 Death []int
} }
// parse one part of a GOL rule into rule slice // parse one part of a GOL rule into rule slice
func NumbersToList(numbers string) []uint8 { func NumbersToList(numbers string) []int {
list := []uint8{} list := []int{}
items := strings.Split(numbers, "") items := strings.Split(numbers, "")
for _, item := range items { for _, item := range items {
@@ -24,7 +24,7 @@ func NumbersToList(numbers string) []uint8 {
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, uint8(num)) list = append(list, num)
} }
return list return list

View File

@@ -25,4 +25,5 @@ const (
Play // actual playing happens here Play // actual playing happens here
Options Options
Keybindings Keybindings
Toolbar
) )

View File

@@ -26,10 +26,8 @@ const (
// readily available from play.go // readily available from play.go
type Theme struct { type Theme struct {
Tiles map[int]*ebiten.Image Tiles map[int]*ebiten.Image
GridTiles map[int]*ebiten.Image
Colors map[int]color.RGBA Colors map[int]color.RGBA
Name string Name string
ShowGrid bool
} }
type ThemeDef struct { type ThemeDef struct {
@@ -40,32 +38,32 @@ var THEMES = map[string]ThemeDef{
"standard": { "standard": {
life: "e15f0b", life: "e15f0b",
dead: "5a5a5a", dead: "5a5a5a",
old: "ff1e1e",
grid: "808080", grid: "808080",
old: "7b5e4b",
age1: "735f52", age1: "735f52",
age2: "6c6059", age2: "6c6059",
age3: "635d59", age3: "635d59",
age4: "7b5e4b", age4: "808080",
}, },
"dark": { "dark": {
life: "c8c8c8", life: "c8c8c8",
dead: "000000", dead: "000000",
old: "ff1e1e", old: "ff1e1e",
grid: "808080",
age1: "522600", age1: "522600",
age2: "422300", age2: "422300",
age3: "2b1b00", age3: "2b1b00",
age4: "191100", age4: "191100",
grid: "808080",
}, },
"light": { "light": {
life: "000000", life: "000000",
dead: "c8c8c8", dead: "c8c8c8",
old: "ff1e1e", old: "ff1e1e",
grid: "808080",
age1: "ffc361", age1: "ffc361",
age2: "ffd38c", age2: "ffd38c",
age3: "ffe3b5", age3: "ffe3b5",
age4: "fff0e0", age4: "fff0e0",
grid: "808080",
}, },
} }
@@ -86,14 +84,10 @@ func NewTheme(def ThemeDef, cellsize int, name string) Theme {
} }
theme.Tiles = make(map[int]*ebiten.Image, 6) theme.Tiles = make(map[int]*ebiten.Image, 6)
theme.GridTiles = make(map[int]*ebiten.Image, 6)
for cid, col := range theme.Colors { for cid, col := range theme.Colors {
theme.Tiles[cid] = ebiten.NewImage(cellsize, cellsize) theme.Tiles[cid] = ebiten.NewImage(cellsize, cellsize)
FillCell(theme.Tiles[cid], cellsize, col, 0) FillCell(theme.Tiles[cid], cellsize, col)
theme.GridTiles[cid] = ebiten.NewImage(cellsize, cellsize)
FillCell(theme.GridTiles[cid], cellsize, col, 1)
} }
return theme return theme
@@ -103,10 +97,6 @@ func NewTheme(def ThemeDef, cellsize int, name string) Theme {
// unknown type is being used, which is ok, since the code is the only // unknown type is being used, which is ok, since the code is the only
// user anyway // user anyway
func (theme *Theme) Tile(col int) *ebiten.Image { func (theme *Theme) Tile(col int) *ebiten.Image {
if theme.ShowGrid {
return theme.GridTiles[col]
}
return theme.Tiles[col] return theme.Tiles[col]
} }
@@ -114,10 +104,6 @@ func (theme *Theme) Color(col int) color.RGBA {
return theme.Colors[col] return theme.Colors[col]
} }
func (theme *Theme) SetGrid(showgrid bool) {
theme.ShowGrid = showgrid
}
type ThemeManager struct { type ThemeManager struct {
Theme string Theme string
Themes map[string]Theme Themes map[string]Theme
@@ -166,11 +152,11 @@ func (manager *ThemeManager) SetCurrentTheme(theme string) {
// //
// So we don't draw a grid, we just left a grid behind, which saves us // So we don't draw a grid, we just left a grid behind, which saves us
// from a lot of drawing operations. // from a lot of drawing operations.
func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA, x int) { func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA) {
vector.DrawFilledRect( vector.DrawFilledRect(
tile, tile,
float32(x), float32(1),
float32(x), float32(1),
float32(cellsize), float32(cellsize),
float32(cellsize), float32(cellsize),
col, false, col, false,

95
src/toolbar.go Normal file
View File

@@ -0,0 +1,95 @@
package main
import (
"fmt"
"image/color"
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type SceneToolbar struct {
Game *Game
Config *Config
Next SceneName
Prev SceneName
Whoami SceneName
Ui *ebitenui.UI
FontColor color.RGBA
}
func NewToolbarScene(game *Game, config *Config) Scene {
scene := &SceneToolbar{
Whoami: Toolbar,
Game: game,
Next: Toolbar,
Config: config,
FontColor: color.RGBA{255, 30, 30, 0xff},
}
scene.Init()
return scene
}
func (scene *SceneToolbar) GetNext() SceneName {
return scene.Next
}
func (scene *SceneToolbar) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *SceneToolbar) ResetNext() {
scene.Next = scene.Whoami
}
func (scene *SceneToolbar) SetNext(next SceneName) {
scene.Next = next
}
func (scene *SceneToolbar) IsPrimary() bool {
return true
}
func (scene *SceneToolbar) Update() error {
scene.Ui.Update()
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.SetNext(Play)
}
return nil
}
func (scene *SceneToolbar) Draw(screen *ebiten.Image) {
scene.Ui.Draw(screen)
}
func (scene *SceneToolbar) SetInitialValue(w *widget.LabeledCheckbox, value bool) {
if value {
w.SetState(
widget.WidgetChecked,
)
}
}
func (scene *SceneToolbar) Init() {
rowContainer := NewTopRowContainer("Toolbar")
options := NewToolbarButton(Assets["options"],
func(args *widget.ButtonClickedEventArgs) {
fmt.Println("options")
scene.SetNext(Options)
})
rowContainer.AddChild(options)
scene.Ui = &ebitenui.UI{
Container: rowContainer.Container(),
}
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/ebitenui/ebitenui/image" "github.com/ebitenui/ebitenui/image"
"github.com/ebitenui/ebitenui/widget" "github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
) )
func NewMenuButton( func NewMenuButton(
@@ -40,6 +41,32 @@ func NewMenuButton(
) )
} }
func NewToolbarButton(
icon *ebiten.Image,
action func(args *widget.ButtonClickedEventArgs)) *widget.Container {
buttonImage, _ := LoadButtonImage()
iconContainer := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewStackedLayout()),
widget.ContainerOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionCenter,
VerticalPosition: widget.AnchorLayoutPositionCenter,
})),
)
button := widget.NewButton(
widget.ButtonOpts.Image(buttonImage),
widget.ButtonOpts.ClickedHandler(action),
)
iconContainer.AddChild(button)
iconContainer.AddChild(widget.NewGraphic(widget.GraphicOpts.Image(icon)))
return iconContainer
}
func NewCheckbox( func NewCheckbox(
text string, text string,
initialvalue bool, initialvalue bool,
@@ -215,6 +242,37 @@ func (container *RowContainer) Container() *widget.Container {
return container.Root return container.Root
} }
// setup a top level toolbar container
func NewTopRowContainer(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()),
)
rowContainer := widget.NewContainer(
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionStart,
VerticalPosition: widget.AnchorLayoutPositionStart,
}),
),
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(8)),
widget.RowLayoutOpts.Spacing(0),
)),
widget.ContainerOpts.BackgroundImage(buttonImageHover),
)
uiContainer.AddChild(rowContainer)
return &RowContainer{
Root: uiContainer,
Row: rowContainer,
}
}
// set arg to false if no background needed // set arg to false if no background needed
func NewRowContainer(title string) *RowContainer { func NewRowContainer(title string) *RowContainer {
buttonImageHover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3}) buttonImageHover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3})

View File

@@ -3,10 +3,7 @@
Running with 1500x1500 grid 5k times Running with 1500x1500 grid 5k times
| Variation | Description | Duration | | Variation | Description | Duration |
|------------------------------|-----------------------------------------------------------------------------|-------------------| |--------------------|-----------------------------------------------------------------------------|----------|
| perf-2dim | uses 2d grid of bools, no tuning | 00:03:14 | | 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 | use 2d grid of `Cell{Neighbors,NeighborCount}`s using pointers to neighbors | 00:03:35 |
| 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-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

@@ -87,17 +87,6 @@ func Loop(grid []bool) {
} }
func main() { func main() {
grid := make([]int, 50*50)
for y := 0; y < 50; y++ {
for x := 0; x < 50; x++ {
grid[y+50*x] = 1
fmt.Printf("%d,%d => %d\n", x, y, x+50*y)
}
}
}
func xmain() {
// enable cpu profiling. Do NOT use q to stop the game but // enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile // close the window to get a profile
fd, err := os.Create("cpu.profile") fd, err := os.Create("cpu.profile")

View File

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

View File

@@ -1,141 +0,0 @@
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

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

View File

@@ -1,139 +0,0 @@
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

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

View File

@@ -1,2 +0,0 @@
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

@@ -1,145 +0,0 @@
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)
}

View File

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

View File

@@ -1,33 +0,0 @@
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

@@ -1,48 +0,0 @@
#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

@@ -1,25 +0,0 @@
#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

@@ -1,28 +0,0 @@
#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

@@ -1,18 +0,0 @@
#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

@@ -1,15 +0,0 @@
#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

@@ -1,13 +0,0 @@
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

@@ -1,14 +0,0 @@
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

@@ -1,306 +0,0 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"runtime/pprof"
"unsafe"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Images struct {
Black, White *ebiten.Image
}
type Cell struct {
State uint8
Neighbors [8]*Cell
NeighborCount int
}
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) uint8 {
var sum uint8
for idx := 0; idx < cell.NeighborCount; idx++ {
sum += cell.Neighbors[idx].State
}
return sum
}
func SetNeighbors(grid [][]*Cell, x, y, width, height int) {
idx := 0
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= width || y+nbgY < 0 || y+nbgY >= height {
continue
}
col = x + nbgX
row = y + nbgY
if col == x && row == y {
continue
}
grid[y][x].Neighbors[idx] = grid[row][col]
grid[y][x].NeighborCount++
idx++
}
}
}
type Grid struct {
Data [][]*Cell
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([][]*Cell, height),
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]*Cell, width)
for x := 0; x < width; x++ {
grid.Data[y][x] = &Cell{}
if rand.Intn(density) == 1 {
grid.Data[y][x].State = 1
}
}
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
SetNeighbors(grid.Data, x, y, width, height)
}
}
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].State == 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)
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) uint8 {
return game.Grids[game.Index].Data[y][x].Count(x, y)
}
// the heart of the game
func (game *Game) CheckRule(state uint8, neighbors uint8) uint8 {
var nextstate uint8
if state == 1 && 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].State
neighbors := game.CountNeighbors(x, y)
// actually apply the current rules
nextstate := game.CheckRule(state, neighbors)
// change state of current cell in next grid
game.Grids[next].Data[y][x].State = 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].State == 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() {
size := 1500
game := &Game{
Width: size,
Height: size,
Cellsize: 4,
Density: 8,
TPG: 10,
Debug: false,
Profile: true,
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,13 +0,0 @@
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

@@ -1,14 +0,0 @@
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

@@ -1,306 +0,0 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"runtime/pprof"
"unsafe"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Images struct {
Black, White *ebiten.Image
}
type Cell struct {
State bool
Neighbors [8]*Cell
NeighborCount int
}
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
func (cell *Cell) Count(x, y int) int {
sum := 0
for idx := 0; idx < cell.NeighborCount; idx++ {
sum += bool2int(cell.Neighbors[idx].State)
}
return sum
}
func SetNeighbors(grid [][]*Cell, x, y, width, height int) {
idx := 0
for nbgX := -1; nbgX < 2; nbgX++ {
for nbgY := -1; nbgY < 2; nbgY++ {
var col, row int
if x+nbgX < 0 || x+nbgX >= width || y+nbgY < 0 || y+nbgY >= height {
continue
}
col = x + nbgX
row = y + nbgY
if col == x && row == y {
continue
}
grid[y][x].Neighbors[idx] = grid[row][col]
grid[y][x].NeighborCount++
idx++
}
}
}
type Grid struct {
Data [][]*Cell
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([][]*Cell, height),
}
for y := 0; y < height; y++ {
grid.Data[y] = make([]*Cell, width)
for x := 0; x < width; x++ {
grid.Data[y][x] = &Cell{}
if rand.Intn(density) == 1 {
grid.Data[y][x].State = true
}
}
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
SetNeighbors(grid.Data, x, y, width, height)
}
}
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].State {
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)
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) int {
return game.Grids[game.Index].Data[y][x].Count(x, y)
}
// the heart of the game
func (game *Game) CheckRule(state bool, neighbors int) bool {
var nextstate bool
if state && neighbors == 3 {
nextstate = true
} else if state && (neighbors == 2 || neighbors == 3) {
nextstate = true
} else {
nextstate = false
}
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].State
neighbors := game.CountNeighbors(x, y)
// actually apply the current rules
nextstate := game.CheckRule(state, neighbors)
// change state of current cell in next grid
game.Grids[next].Data[y][x].State = 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].State {
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() {
size := 1500
game := &Game{
Width: size,
Height: size,
Cellsize: 4,
Density: 8,
TPG: 10,
Debug: false,
Profile: true,
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

@@ -238,7 +238,35 @@ func (game *Game) Draw(screen *ebiten.Image) {
} }
func main() { func main() {
size := 1500 //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{ game := &Game{
Width: size, Width: size,
@@ -247,7 +275,7 @@ func main() {
Density: 8, Density: 8,
TPG: 10, TPG: 10,
Debug: false, Debug: false,
Profile: true, Profile: false,
Gridlines: false, Gridlines: false,
} }