17 Commits

Author SHA1 Message Date
4074debdbf using only one dimensional grid by calculating y+STRIDE*x 2024-07-16 19:07:51 +02:00
e8ed283233 next try to enhance performance: now using uint9 again and no pointers 2024-07-15 14:21:21 +02:00
89d25db9e7 added profiling target, makes it easier 2024-07-15 14:21:03 +02:00
89903fdcec better comments, simplified check 2024-07-13 20:46:11 +02:00
05d56568e4 fix #7: use only left mouse button to draw, it toggles cell state 2024-07-13 19:31:25 +02:00
Thomas von Dein
50b630791a bumpversion 2024-07-12 23:00:28 +02:00
957db29a37 added C test 2024-07-12 23:00:28 +02:00
eb95c72538 fixed grid lines performance problem 2024-07-12 23:00:28 +02:00
604cbea127 use optimized rule check if B3/S23 is active 2024-07-12 23:00:28 +02:00
f3e7428775 draw the grid explicitly thus leading to full cells w/o the grid 2024-07-12 23:00:28 +02:00
6685207fde revert last revert and fixed history slow down bug 2024-07-12 23:00:28 +02:00
41da9b8536 revert history=>struct, but evolution doesn't work anymore anyway 2024-07-12 23:00:28 +02:00
6dec8c74ef bool => uint8 2024-07-12 23:00:28 +02:00
861ba86b0c using go-routines (one per row), makes it faster 2024-07-12 23:00:28 +02:00
7b0a74fb93 use cells instead of only bools, use pointer list to all neighbors 2024-07-12 23:00:28 +02:00
e516b218fd tuning fail 2024-06-14 19:58:03 +02:00
6544052bb7 tried more variants: writepixel+pointer+int, which is not much better 2024-06-14 19:56:00 +02:00
24 changed files with 1135 additions and 187 deletions

View File

@@ -1,5 +1,8 @@
.PHONY: all .PHONY all:
all: all: build
.PHONY: build
build:
make -C src make -C src
mv src/golsky . mv src/golsky .
@@ -7,3 +10,8 @@ all:
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

@@ -82,17 +82,16 @@ 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 set cells alife and right * i: enter "insert" (draw) mode: use left mouse to toggle cells alife state.
button to dead. Leave with "space". While in insert mode, use middle mouse Leave with insert mode "space". While in insert mode, use middle mouse
button to drag grid. button to drag the 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)

View File

@@ -20,7 +20,11 @@ https://www.tasnimzotder.com/blog/optimizing-game-of-life-algorithm
the cells anymore. 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:
A Catagolue textcensus of, say, period-2 oscillators from A Catagolue textcensus of, say, period-2 oscillators from

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

View File

@@ -43,16 +43,16 @@ type Config struct {
} }
const ( const (
VERSION = "v0.0.8" VERSION = "v0.0.9"
Alive = true Alive = 1
Dead = false Dead = 0
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" // "light" // inverse => "dark" DEFAULT_THEME = "standard"
) )
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 set cells alife and right - I: enter "insert" (draw) mode: use left mouse to toggle a cells alife state.
button to dead. Leave with "space". While in insert mode, use middle mouse Leave with insert mode with "space". While in insert mode, use middle mouse
button to drag grid. button to drag the 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)

View File

@@ -12,65 +12,146 @@ 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 [][]bool Data []uint8
Width, Height, Density int NeighborCount []int
Empty bool Neighbors [][]Neighbor
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(width, height, density int, empty bool) *Grid { func NewGrid(config *Config) *Grid {
grid := &Grid{ STRIDE = config.Height
Height: height, if config.Width > config.Height {
Width: width, STRIDE = config.Width
Density: density,
Data: make([][]bool, height),
Empty: empty,
} }
for y := 0; y < height; y++ { size := STRIDE * STRIDE
grid.Data[y] = make([]bool, width)
grid := &Grid{
Data: make([]uint8, size),
NeighborCount: make([]int, size),
Neighbors: make([][]Neighbor, size),
Empty: config.Empty,
Config: config,
}
// first setup the cells
for y := 0; y < config.Height; y++ {
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.Width = grid.Width newgrid.Config = grid.Config
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][x] = grid.Data[y][x] // other.Data[y+STRIDE*x] = grid.Data[y+STRIDE*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][x] = false // grid.Data[y+STRIDE*x] = 0
} // }
} // }
} // }
// 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 := range grid.Data { for y := 0; y < grid.Config.Height; y++ {
for x := range grid.Data[y] { for x := 0; x < grid.Config.Width; x++ {
if rand.Intn(grid.Density) == 1 { if rand.Intn(grid.Config.Density) == 1 {
grid.Data[y][x] = true grid.Data[y+STRIDE*x] = 1
} }
} }
} }
@@ -78,9 +159,9 @@ func (grid *Grid) FillRandom() {
} }
func (grid *Grid) Dump() { func (grid *Grid) Dump() {
for y := 0; y < grid.Height; y++ { for y := 0; y < grid.Config.Height; y++ {
for x := 0; x < grid.Width; x++ { for x := 0; x < grid.Config.Width; x++ {
if grid.Data[y][x] { if grid.Data[y+STRIDE*x] == 1 {
fmt.Print("XX") fmt.Print("XX")
} else { } else {
fmt.Print(" ") fmt.Print(" ")
@@ -93,8 +174,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.Width / 2) - (pattern.Width / 2) startX := (grid.Config.Width / 2) - (pattern.Width / 2)
startY := (grid.Height / 2) - (pattern.Height / 2) startY := (grid.Config.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 {
@@ -103,7 +184,7 @@ func (grid *Grid) LoadRLE(pattern *rle.RLE) {
x = colIndex + startX x = colIndex + startX
y = rowIndex + startY y = rowIndex + startY
grid.Data[y][x] = true grid.Data[y+STRIDE*x] = 1
} }
} }
} }
@@ -211,10 +292,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 := range grid.Data { for y := 0; y < grid.Config.Height; y++ {
for _, cell := range grid.Data[y] { for x := 0; x < grid.Config.Width; x++ {
row := "." row := "."
if cell { if grid.Data[y+STRIDE*x] == 1 {
row = "o" row = "o"
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"image" "image"
"log" "log"
"sync"
"unsafe" "unsafe"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
@@ -21,6 +22,21 @@ 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"
) )
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
@@ -31,7 +47,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 [][]int64 // holds state of past dead cells for evolution traces History History // holds state of past dead cells for evolution traces
Index int // points to current grid Index int // points to current grid
Generations int64 // Stats Generations int64 // Stats
TicksElapsed int // tick counter for game speed TicksElapsed int // tick counter for game speed
@@ -46,6 +62,7 @@ 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 {
@@ -83,18 +100,38 @@ func (scene *ScenePlay) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *ScenePlay) CheckRule(state bool, neighbors int) bool { /* The standard Scene of Life is symbolized in rule-string notation
var nextstate bool * 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.
* 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
}
// The standard Scene of Life is symbolized in rule-string notation return Dead
// 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 && Contains(scene.Config.Rule.Death, neighbors) { } else if state == 1 && Contains(scene.Config.Rule.Death, neighbors) {
nextstate = Alive nextstate = Alive
} else { } else {
nextstate = Dead nextstate = Dead
@@ -116,33 +153,43 @@ 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 < scene.Config.Height; y++ { for y := 0; y < height; y++ {
for x := 0; x < scene.Config.Width; x++ {
state := scene.Grids[scene.Index].Data[y][x] // 0|1 == dead or alive
neighbors := scene.CountNeighbors(x, y) // alive neighbor count
// actually apply the current rules go func() {
nextstate := scene.CheckRule(state, neighbors) defer wg.Done()
// change state of current cell in next grid for x := 0; x < width; x++ {
scene.Grids[next].Data[y][x] = nextstate state := scene.Grids[scene.Index].Data[y+STRIDE*x] // 0|1 == dead or alive
neighbors := scene.Grids[scene.Index].CountNeighbors(x, y)
if scene.Config.ShowEvolution { // actually apply the current rules
// set history to current generation so we can infer the nextstate := scene.RuleCheckFunc(state, neighbors)
// age of the cell's state during rendering and use it to
// deduce the color to use if evolution tracing is enabled // change state of current cell in next grid
// 60FPS: scene.Grids[next].Data[y+STRIDE*x] = nextstate
if state != nextstate {
scene.History[y][x] = scene.Generations if scene.Config.ShowEvolution {
// set history to current generation so we can infer the
// age of the cell's state during rendering and use it to
// deduce the color to use if evolution tracing is enabled
// 60FPS:
if state != nextstate {
scene.History.Age[y][x] = scene.Generations
}
} }
// 10FPS:
//scene.History.Data[y][x] = (state ^ (1 ^ nextstate)) * (scene.Generations - scene.History.Data[y][x])
} }
} }()
} }
wg.Wait()
// switch grid for rendering // switch grid for rendering
scene.Index ^= 1 scene.Index ^= 1
@@ -221,10 +268,8 @@ func (scene *ScenePlay) CheckInput() {
func (scene *ScenePlay) CheckDrawingInput() { func (scene *ScenePlay) CheckDrawingInput() {
if scene.Config.Drawmode { if scene.Config.Drawmode {
switch { switch {
case ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft): case inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft):
scene.ToggleCellOnCursorPos(Alive) scene.ToggleCellOnCursorPos()
case ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight):
scene.ToggleCellOnCursorPos(Dead)
case inpututil.IsKeyJustPressed(ebiten.KeyEscape): case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
scene.Config.Drawmode = false scene.Config.Drawmode = false
} }
@@ -370,13 +415,13 @@ func (scene *ScenePlay) SaveRectRLE() {
height = scene.Mark.Y - scene.Point.Y height = scene.Mark.Y - scene.Point.Y
} }
grid := make([][]bool, height) grid := make([][]uint8, height)
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
grid[y] = make([]bool, width) grid[y] = make([]uint8, width)
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
grid[y][x] = scene.Grids[scene.Index].Data[y+starty][x+startx] grid[y][x] = scene.Grids[scene.Index].Data[(y+starty)+STRIDE*(x+startx)]
} }
} }
@@ -422,15 +467,15 @@ func (scene *ScenePlay) Update() error {
} }
// set a cell to alive or dead // set a cell to alive or dead
func (scene *ScenePlay) ToggleCellOnCursorPos(alive bool) { func (scene *ScenePlay) ToggleCellOnCursorPos() {
// 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][x] = alive scene.Grids[scene.Index].Data[y+STRIDE*x] ^= 1
scene.History[y][x] = 1 scene.History.Age[y][x] = 1
} }
} }
@@ -455,7 +500,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][x] { if scene.Grids[scene.Index].Data[y+STRIDE*x] == 1 {
scene.World.DrawImage(scene.Theme.Tile(ColLife), op) scene.World.DrawImage(scene.Theme.Tile(ColLife), op)
} }
} }
@@ -470,9 +515,9 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
} }
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[y][x] age := scene.Generations - scene.History.Age[y][x]
switch scene.Grids[scene.Index].Data[y][x] { switch scene.Grids[scene.Index].Data[y+STRIDE*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)
@@ -481,7 +526,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[y][x] > 1 && scene.Config.ShowEvolution { if scene.History.Age[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)
@@ -553,17 +598,24 @@ 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++ {
@@ -580,8 +632,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.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) grida := NewGrid(scene.Config)
gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) gridb := NewGrid(scene.Config)
// startup is delayed until user has selected options // startup is delayed until user has selected options
grida.FillRandom() grida.FillRandom()
@@ -591,10 +643,8 @@ func (scene *ScenePlay) InitGrid() {
gridb, gridb,
} }
scene.History = make([][]int64, scene.Config.Height) scene.History = NewHistory(scene.Config.Height, scene.Config.Width)
for y := 0; y < scene.Config.Height; y++ {
scene.History[y] = make([]int64, scene.Config.Width)
}
} }
func (scene *ScenePlay) Init() { func (scene *ScenePlay) Init() {
@@ -626,6 +676,8 @@ 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
@@ -651,38 +703,10 @@ func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b))) return int(*(*byte)(unsafe.Pointer(&b)))
} }
// count the living neighbors of a cell func (scene *ScenePlay) InitRuleCheckFunc() {
func (scene *ScenePlay) CountNeighbors(x, y int) int { if scene.Config.Rule.Definition == "B3/S23" {
var sum int scene.RuleCheckFunc = scene.CheckRuleB3S23
} else {
grid := scene.Grids[scene.Index].Data scene.RuleCheckFunc = scene.CheckRuleGeneric
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 {
// 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 []int Birth []uint8
Death []int Death []uint8
} }
// parse one part of a GOL rule into rule slice // parse one part of a GOL rule into rule slice
func NumbersToList(numbers string) []int { func NumbersToList(numbers string) []uint8 {
list := []int{} list := []uint8{}
items := strings.Split(numbers, "") items := strings.Split(numbers, "")
for _, item := range items { for _, item := range items {
@@ -24,7 +24,7 @@ func NumbersToList(numbers string) []int {
log.Fatalf("failed to parse game rule part <%s>: %s", numbers, err) log.Fatalf("failed to parse game rule part <%s>: %s", numbers, err)
} }
list = append(list, num) list = append(list, uint8(num))
} }
return list return list

View File

@@ -25,9 +25,11 @@ const (
// the colors and the actual tile images here, so that they are // the colors and the actual tile images here, so that they are
// 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
Colors map[int]color.RGBA GridTiles map[int]*ebiten.Image
Name string Colors map[int]color.RGBA
Name string
ShowGrid bool
} }
type ThemeDef struct { type ThemeDef struct {
@@ -84,10 +86,14 @@ 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) FillCell(theme.Tiles[cid], cellsize, col, 0)
theme.GridTiles[cid] = ebiten.NewImage(cellsize, cellsize)
FillCell(theme.GridTiles[cid], cellsize, col, 1)
} }
return theme return theme
@@ -97,6 +103,10 @@ 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]
} }
@@ -104,6 +114,10 @@ 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
@@ -152,11 +166,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) { func FillCell(tile *ebiten.Image, cellsize int, col color.RGBA, x int) {
vector.DrawFilledRect( vector.DrawFilledRect(
tile, tile,
float32(1), float32(x),
float32(1), float32(x),
float32(cellsize), float32(cellsize),
float32(cellsize), float32(cellsize),
col, false, col, false,

View File

@@ -87,6 +87,17 @@ 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")

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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