20 Commits

Author SHA1 Message Date
46d410822a moved to codeberg 2025-11-13 21:32:56 +01:00
a115db60f6 Merge branch 'main' of github.com:TLINDEN/gameoflife 2024-10-20 13:10:15 +02:00
1250f1113e fixed standard evolution colors 2024-10-20 13:07:12 +02:00
78aba6f005 fixed standard evolution colors 2024-10-20 12:39:37 +02:00
a680cca30c always calculate neighbor count 2024-07-16 19:33:06 +02:00
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
69 changed files with 10 additions and 5760 deletions

View File

@@ -1,9 +0,0 @@
.PHONY: all
all:
make -C src
mv src/golsky .
.PHONY: clean
clean:
make -C src clean
rm -f dump* rect*

View File

@@ -1,9 +1,13 @@
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/golsky/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/golsky)](https://goreportcard.com/report/github.com/tlinden/golsky)
# golsky - Conway's game of life written in GO # golsky - Conway's game of life written in GO
> [!IMPORTANT]
> This software is now being maintained on [Codeberg](https://codeberg.org/scip/golsky/).
![Golsky Logo](https://github.com/TLINDEN/golsky/blob/main/.github/assets/golskylogo.png) ![Golsky Logo](https://github.com/TLINDEN/golsky/blob/main/.github/assets/golskylogo.png)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/golsky/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/golsky)](https://goreportcard.com/report/github.com/tlinden/golsky)
I wanted to play around a little bit with [**Conways Game of Life**](https://conwaylife.com/) I wanted to play around a little bit with [**Conways Game of Life**](https://conwaylife.com/)
in golang and here's the result. It's a simple game using in golang and here's the result. It's a simple game using
@@ -82,17 +86,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)

51
TODO.md
View File

@@ -1,51 +0,0 @@
- add all other options like size etc
- add gif export
- add toolbar (not working yet, see branch trackui)
- only draw visible part of the world
- print current mode to the bottom like pause, insert and mark
- add https://www.ibiblio.org/lifepatterns/october1970.html
- history: dont count age but do calc to get index to age tile based on cell age
- maybe pre calc neighbors as 8 slice of pointers to neighboring cells to faster do the count
see various-tests/perf-2dim-pointers/: it's NOT faster :(
- use an array of 8 pointers to neighbors. on edge just add either fake dead neighbors or the wrap around neighbors.
- try arche ecs variant with either a component of the cells neighbors or using relations.
- https://mattnakama.com/blog/go-branchless-coding/
- add performance measurements, see:
DrawTriangles: https://github.com/TLINDEN/testgol
WritePixels: https://github.com/TLINDEN/testgol/tree/wrpixels
https://www.tasnimzotder.com/blog/optimizing-game-of-life-algorithm
- pre-draw the grid separately to a cache grid image, then during
rendering, first draw the dead background, then the life cells, and
lastly the grid - if enabled. If disabled, there's be no gap between
the cells anymore.
- Speed
https://conwaylife.com/forums/viewtopic.php?f=7&t=3237
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:
A Catagolue textcensus of, say, period-2 oscillators from
non-symmetrical soups can be found at
https://catagolue.hatsya.com/textcensus/b3s23/C1/xp2
The URL is made by just adding the prefix "text" to the word "census",
in any URL linked to from a Catagolue census page such as this one:
https://catagolue.hatsya.com/census/b3s23/C1
Format:
https://conwaylife.com/wiki/Apgcode
Collections:
https://conwaylife.com/wiki/Pattern_of_the_Year
https://www.ibiblio.org/lifepatterns/
https://entropymine.com/jason/life/
https://github.com/Matthias-Merzenich/jslife-moving
https://conwaylife.com/ref/mniemiec/lifepage.htm
https://conwaylife.com/wiki/Spaceship ff.

25
go.mod
View File

@@ -1,25 +0,0 @@
module github.com/tlinden/golsky
go 1.22
require (
github.com/hajimehoshi/ebiten/v2 v2.7.4
github.com/spf13/pflag v1.0.5
golang.org/x/image v0.16.0
)
require (
github.com/alecthomas/repr v0.4.0 // indirect
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/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // 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
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)

36
go.sum
View File

@@ -1,36 +0,0 @@
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
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/ebitenui/ebitenui v0.5.6 h1:qyJRU5j+lQo1lamxB48IBwMxMfz1xNb5iWUayCtA0Wk=
github.com/ebitenui/ebitenui v0.5.6/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE=
github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21 h1:dElhYGyf+FYY+makAndUQNOSDwFSFYyFWziPwQrPObY=
github.com/ebitenui/ebitenui v0.5.8-0.20240608175527-424f62327b21/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/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/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=
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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc=
github.com/tinne26/etxt v0.0.8/go.mod h1:QM/hlNkstsKC39elTFNKAR34xsMb9QoVosf+g9wlYxM=
github.com/tinne26/etxt v0.0.9-alpha.6.0.20240409152929-91bfc562becc h1:+USGSXbkrRAy6bz3Qm4GUczhqeXe7XlRfkRexCSFxkw=
github.com/tinne26/etxt v0.0.9-alpha.6.0.20240409152929-91bfc562becc/go.mod h1:Icbd4bDjrXag1oYIhB51CrkMYqRb7YMv0AsrOSfNKfU=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
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=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

View File

@@ -1,69 +0,0 @@
#!/bin/bash
# Copyright © 2024 Thomas von Dein
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# get list with: go tool dist list
DIST="linux/amd64
windows/amd64 "
tool="$1"
version="$2"
if test -z "$version"; then
echo "Usage: $0 <tool name> <release version>"
exit 1
fi
rm -rf releases
mkdir -p releases
for D in $DIST; do
os=${D/\/*/}
arch=${D/*\//}
binfile="releases/${tool}-${os}-${arch}-${version}"
if test "$os" = "windows"; then
binfile="${binfile}.exe"
fi
tardir="${tool}-${os}-${arch}-${version}"
tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz"
set -x
GOOS=${os} GOARCH=${arch} go build -o ${binfile}
set +x
continue
mkdir -p ${tardir}
cp ${binfile} README.md LICENSE ${tardir}/
echo 'tool = gfn
PREFIX = /usr/local
UID = root
GID = 0
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile
tar cpzf ${tarfile} ${tardir}
sha256sum ${binfile} | cut -d' ' -f1 > ${binfile}.sha256
sha256sum ${tarfile} | cut -d' ' -f1 > ${tarfile}.sha256
rm -rf ${tardir}
set +x
done

View File

@@ -1,159 +0,0 @@
package rle
import (
"strconv"
)
type TokenType string
type Token struct {
Type TokenType
Literal string
}
const (
RUN_COUNT = "RUN_COUNT"
DEAD_CELL = "DEAD_CELL"
ALIVE_CELL = "ALIVE_CELL"
EOL = "EOL"
EOP = "EOP"
)
type Lexer struct {
input string
position int
readPosition int
char byte
}
func NewLexer(input string) *Lexer {
l := &Lexer{input: input}
l.readChar()
return l
}
func (l *Lexer) NextToken() Token {
var tok Token
l.skipWhitespace()
switch l.char {
case '$':
tok = newToken(EOL, l.char)
case '!':
tok = newToken(EOP, l.char)
case 'b':
tok = newToken(DEAD_CELL, l.char)
case 'o':
tok = newToken(ALIVE_CELL, l.char)
default:
if isDigit(l.char) {
tok.Type = RUN_COUNT
tok.Literal = l.readNumber()
return tok
}
}
l.readChar()
return tok
}
func newToken(tokenType TokenType, char byte) Token {
return Token{Type: tokenType, Literal: string(char)}
}
type PatternParser struct {
lexer *Lexer
currentToken Token
peekToken Token
}
func NewParser(lexer *Lexer) *PatternParser {
p := &PatternParser{
lexer: lexer,
}
p.nextToken()
p.nextToken()
return p
}
func (pp *PatternParser) ParsePattern(width, height int) [][]int {
result := make([][]int, height)
row := make([]int, width)
var rowIndex int
var colIndex int
for {
switch pp.currentToken.Type {
case RUN_COUNT:
count, _ := strconv.Atoi(pp.currentToken.Literal)
for i := 0; i < count; i++ {
switch pp.peekToken.Type {
case ALIVE_CELL:
row[rowIndex+i] = 1
case DEAD_CELL:
row[rowIndex+i] = 0
case EOL:
result[colIndex] = row
row = make([]int, width)
rowIndex = -1
colIndex++
}
}
if pp.peekToken.Type != EOL {
rowIndex += count - 1
}
pp.nextToken()
case ALIVE_CELL:
row[rowIndex] = 1
case DEAD_CELL:
row[rowIndex] = 0
case EOL:
result[colIndex] = row
row = make([]int, width)
rowIndex = -1
colIndex++
case EOP:
result[colIndex] = row
return result
}
rowIndex++
pp.nextToken()
}
}
func (pp *PatternParser) nextToken() {
pp.currentToken = pp.peekToken
pp.peekToken = pp.lexer.NextToken()
}
func isDigit(char byte) bool {
return '0' <= char && char <= '9'
}
func (l *Lexer) readChar() {
if l.readPosition >= len(l.input) {
l.char = 0
} else {
l.char = l.input[l.readPosition]
}
l.position = l.readPosition
l.readPosition++
}
func (l *Lexer) readNumber() string {
position := l.position
for isDigit(l.char) {
l.readChar()
}
return l.input[position:l.position]
}
func (l *Lexer) skipWhitespace() {
for l.char == ' ' || l.char == '\t' || l.char == '\n' || l.char == '\r' {
l.readChar()
}
}

View File

@@ -1,163 +0,0 @@
package rle
import (
"reflect"
"testing"
)
func TestNextToken(t *testing.T) {
input := "bo$2bo$3o!"
tests := []struct {
expectedType TokenType
expectedLiteral string
}{
{DEAD_CELL, "b"},
{ALIVE_CELL, "o"},
{EOL, "$"},
{RUN_COUNT, "2"},
{DEAD_CELL, "b"},
{ALIVE_CELL, "o"},
{EOL, "$"},
{RUN_COUNT, "3"},
{ALIVE_CELL, "o"},
{EOP, "!"},
}
l := NewLexer(input)
for _, test := range tests {
token := l.NextToken()
if token.Type != test.expectedType {
t.Errorf("Token typ not correct")
}
if token.Literal != test.expectedLiteral {
t.Errorf("Literal not correct")
}
}
}
func TestParsePattern(t *testing.T) {
tests := []struct {
input string
expected [][]int
width int
height int
}{
{
input: "bo$2bo$3o!",
expected: [][]int{
{0, 1, 0},
{0, 0, 1},
{1, 1, 1},
},
width: 3,
height: 3,
},
{
input: `24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4b
obo$10bo5bo7bo$11bo3bo$12b2o!`,
expected: [][]int{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
width: 36,
height: 9,
},
{
input: `20b2o$
20b2o4$
9b2o$
8bo2bo10b2o$
9b2o11bo$
22bo12bo$
23bo10bobo$
34bobo$
35bo7$
32bo2bo$
33b3o$
2o38b2o$
2o38b2o$
6b3o$
6bo2bo7$
6bo$
5bobo$
5bobo10bo$
6bo12bo$
19bo11b2o$
18b2o10bo2bo$
31b2o4$
20b2o$
20b2o!`,
expected: [][]int{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
width: 42,
height: 42,
},
}
for _, test := range tests {
l := NewLexer(test.input)
pp := NewParser(l)
result := pp.ParsePattern(test.width, test.height)
if !reflect.DeepEqual(result, test.expected) {
t.Fatalf(
"Patterns do not match.\nExpected: %v\nGot: %v",
test.expected,
result,
)
}
}
}

View File

@@ -1,195 +0,0 @@
// original source: https://github.com/nhoffmann/life by N.Hoffmann 2020.
package rle
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
)
type RLE struct {
Rule string // rule
Width int // x
Height int // y
Pattern [][]int // The actual pattern
inputLines []string
headerLineIndex int
patternLineIndex int
}
// wrapper to load a RLE file
func GetRLE(filename string) (*RLE, error) {
if filename == "" {
return nil, nil
}
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
parsedRle, err := Parse(string(content))
if err != nil {
return nil, fmt.Errorf("failed to load RLE pattern file: %s", err)
}
return &parsedRle, nil
}
func Parse(input string) (RLE, error) {
rle := RLE{
inputLines: strings.Split(input, "\n"),
}
rle.partitionFile()
err := rle.parseComments()
if err != nil {
return RLE{}, err
}
err = rle.parseHeader()
if err != nil {
return RLE{}, err
}
err = rle.parsePattern()
if err != nil {
return RLE{}, err
}
return rle, nil
}
func (rle *RLE) partitionFile() error {
for index, line := range rle.inputLines {
cleanLine := removeWhitespace(line)
if strings.HasPrefix(cleanLine, "x=") {
rle.headerLineIndex = index
rle.patternLineIndex = index + 1
return nil
}
}
return fmt.Errorf("invalid input: Header is missing")
}
func (rle *RLE) parseComments() error {
return nil
}
func (rle *RLE) parseHeader() (err error) {
headerLine := removeWhitespace(rle.inputLines[rle.headerLineIndex])
headerElements := strings.SplitN(headerLine, ",", 3)
rle.Width, err = strconv.Atoi(strings.TrimPrefix(headerElements[0], "x="))
if err != nil {
return err
}
rle.Height, err = strconv.Atoi(strings.TrimPrefix(headerElements[1], "y="))
if err != nil {
return err
}
rle.Pattern = make([][]int, rle.Width)
// check wehter a rule is present, since it's optional
if len(headerElements) == 3 {
rle.Rule = strings.TrimPrefix(headerElements[2], "rule=")
}
return nil
}
func (rle *RLE) parsePattern() error {
patternString := strings.Join(rle.inputLines[rle.patternLineIndex:], "")
l := NewLexer(patternString)
pp := NewParser(l)
rle.Pattern = pp.ParsePattern(rle.Width, rle.Height)
return nil
}
func removeWhitespace(input string) string {
re := regexp.MustCompile(` *\t*\r*\n*`)
return re.ReplaceAllString(input, "")
}
// Store a grid to an RLE file
func StoreGridToRLE(grid [][]bool, filename, rule string, width, height int) error {
fd, err := os.Create(filename)
if err != nil {
return err
}
var pattern string
for y := 0; y < height; y++ {
line := ""
for x := 0; x < width; x++ {
char := "b"
if grid[y][x] {
char = "o"
}
line += char
}
// if first row is: 001011110, then line is now:
// bboboooob
encoded := RunLengthEncode(line)
// and now its: 2bob4ob
pattern += encoded
if y != height-1 {
pattern += "$"
}
}
pattern += "!"
wrapped := ""
for idx, char := range pattern {
if idx%70 == 0 && idx != 0 {
wrapped += "\n"
}
wrapped += string(char)
}
_, err = fmt.Fprintf(fd, "#N %s\nx = %d, y = %d, rule = %s\n%s\n",
filename, width, height, rule, wrapped)
if err != nil {
return err
}
return nil
}
// by peterSO on
// https://codereview.stackexchange.com/questions/238893/run-length-encoding-in-golang
func RunLengthEncode(s string) string {
e := make([]byte, 0, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
j := i + 1
for ; j <= len(s); j++ {
if j < len(s) && s[j] == c {
continue
}
if j-i > 1 {
e = strconv.AppendInt(e, int64(j-i), 10)
}
e = append(e, c)
break
}
i = j - 1
}
return string(e)
}

View File

@@ -1,84 +0,0 @@
package rle
import (
"reflect"
"testing"
)
func TestRLE(t *testing.T) {
t.Run("Parse", func(t *testing.T) {
tests := []struct {
input string
expectedPattern [][]int
expectedComment string
expectedWidth int
expectedHeight int
expectedRule string
}{
{
input: `#C This is a glider.
x = 3, y = 3
bo$2bo$3o!`,
expectedPattern: [][]int{
{0, 1, 0},
{0, 0, 1},
{1, 1, 1},
},
expectedWidth: 3,
expectedHeight: 3,
expectedRule: "",
},
{
input: `#N Gosper glider gun
#C This was the first gun discovered.
#C As its name suggests, it was discovered by Bill Gosper.
x = 36, y = 9, rule = B3/S23
24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4b
obo$10bo5bo7bo$11bo3bo$12b2o!`,
expectedPattern: [][]int{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
expectedWidth: 36,
expectedHeight: 9,
expectedRule: "B3/S23",
},
}
for _, test := range tests {
rle, err := Parse(test.input)
if err != nil {
t.Error(err)
}
if rle.Width != test.expectedWidth {
t.Errorf("Width dos not match")
}
if rle.Height != test.expectedHeight {
t.Errorf("Height does not match")
}
if rle.Rule != test.expectedRule {
t.Errorf("Rule does not match")
}
if !reflect.DeepEqual(rle.Pattern, test.expectedPattern) {
t.Errorf(
"Patterns do not match.\nExpected: %v\nGot: %v",
test.expectedPattern,
rle.Pattern,
)
}
}
})
}

View File

@@ -1,3 +0,0 @@
x = 8, y = 31, rule = B3/S23
o$4o$2b2o$5bo$2b4o$6bo$2bo2b3o$4b3o$5bo$ob3o$2o2bo$b3o$bo$3bo$bobo$4bo$bobo$
3bo$bo$b3o$2o2bo$ob3o$5bo$4b3o$2bo2b3o$6bo$2b4o$5bo$2b2o$4o$o!

View File

@@ -1,2 +0,0 @@
x = 3, y = 3, rule = B3/S23
3o$2bo$bo!

View File

@@ -1,4 +0,0 @@
x = 33, y = 10, rule = B3/S23
5b3o17b3o$4bo3bo3bo7bo3bo3bo$3b2o4bob3o5b3obo4b2o$2bobob2obo3b2o3b2o3bob2obob
o$b2obo4bobo2b2ob2o2bobo4bob2o$o4bo3bo4bo3bo4bo3bo4bo$14b5o$2o9b2obo3bob2o9b
2o$11b2obo3bob2o$11b2obobobob2o!

View File

@@ -1,8 +0,0 @@
#N p39piheptominohasslerdimer.rle
#C https://conwaylife.com/wiki/P39_pi-heptomino_hassler
#C https://www.conwaylife.com/patterns/p39piheptominohasslerdimer.rle
x = 51, y = 30, rule = B3/S23
9b2o$8bobo$8bo$3bob2ob2o$3b2obo$6bo$6b2o27b2o$35b2o4$22b2o3b2o$22bobo
2b2o$10b3o10bo$2obo6bobo25bobo6b2obo$ob2o6bobo25bobo6bob2o$27bo10b3o$
22b2o2bobo$22b2o3b2o4$14b2o$14b2o27b2o$44bo$44bob2o$41b2ob2obo$42bo$
40bobo$40b2o!

View File

@@ -1,13 +0,0 @@
#N 244p7h3v0.rle
#C https://conwaylife.com/wiki/232P7H3V0
#C https://www.conwaylife.com/patterns/244p7h3v0.rle
x = 51, y = 52, rule = B3/S23
19b3o9b3o$18bo3bo7bo3bo$17bobo3bo5bo3bobo$17bo3b2o7b2o3bo$17b3o3bo5bo
3b3o$16bo3b2ob3ob3ob2o3bo$16b2o2bo3b2ob2o3bo2b2o$15b3o3b5ob5o3b3o$23bo
5bo$20bo11bo$15bo4bo11bo4bo$15bo4b4o5b4o4bo$19bo4bo3bo4bo$18b2ob3o5b3o
b2o$18b2obo3bobo3bob2o$14b3o7b2ob2o7b3o$13bo3b2o4bobobobo4b2o3bo$12bo
3bo19bo3bo$12bo9b3o3b3o9bo$16bo6b2o3b2o6bo$11bo12bo3bo12bo$11bo2b2o5bo
9bo5b2o2bo$12b2o8bo7bo8b2o$10bo12bo5bo12bo$9b3o29b3o$8b2o2bo27bo2b2o$
11b2o27b2o$11bo29bo2$8bo35bo$9b2o31b2o$7bo2bo31bo2bo$6bo39bo$5b2o39b2o
$4b4o37b4o$3bo45bo$3b3o41b3o$2bo47bo$4b2o41b2o$6bo39bo$4b2o41b2o$5bo
41bo$4bo43bo$4bo43bo$2b2o$2obo$o$2o$bo3bo$4bo$o2bo$o!

View File

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

View File

@@ -1,13 +0,0 @@
//kage:unit pixels
package main
var Alife int
func Fragment(_ vec4, pos vec2, _ vec4) vec4 {
if Alife == 1 {
return vec4(0.0)
}
return vec4(1.0)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Binary file not shown.

View File

@@ -1,81 +0,0 @@
// this comes from the camera example but I enhanced it a little bit
package main
import (
"fmt"
"math"
"github.com/hajimehoshi/ebiten/v2"
"golang.org/x/image/math/f64"
)
type Camera struct {
ViewPort f64.Vec2
Position f64.Vec2
ZoomFactor int
InitialZoomFactor int
InitialPosition f64.Vec2
ZoomOutFactor int
}
func (c *Camera) String() string {
return fmt.Sprintf(
"T: %.1f, S: %d",
c.Position, c.ZoomFactor,
)
}
func (c *Camera) viewportCenter() f64.Vec2 {
return f64.Vec2{
c.ViewPort[0] * 0.5,
c.ViewPort[1] * 0.5,
}
}
func (c *Camera) worldMatrix() ebiten.GeoM {
m := ebiten.GeoM{}
m.Translate(-c.Position[0], -c.Position[1])
viewportCenter := c.viewportCenter()
// We want to scale and rotate around center of image / screen
m.Translate(-viewportCenter[0], -viewportCenter[1])
m.Scale(
math.Pow(1.01, float64(c.ZoomFactor)),
math.Pow(1.01, float64(c.ZoomFactor)),
)
m.Translate(viewportCenter[0], viewportCenter[1])
return m
}
func (c *Camera) Render(world, screen *ebiten.Image) {
screen.DrawImage(world, &ebiten.DrawImageOptions{
GeoM: c.worldMatrix(),
})
}
func (c *Camera) ScreenToWorld(posX, posY int) (float64, float64) {
inverseMatrix := c.worldMatrix()
if inverseMatrix.IsInvertible() {
inverseMatrix.Invert()
return inverseMatrix.Apply(float64(posX), float64(posY))
} else {
// When scaling it can happened that matrix is not invertable
return math.NaN(), math.NaN()
}
}
func (c *Camera) Setup() {
c.Position[0] = c.InitialPosition[0]
c.Position[1] = c.InitialPosition[1]
c.ZoomFactor = c.InitialZoomFactor
}
func (c *Camera) Reset() {
c.Position[0] = c.InitialPosition[0]
c.Position[1] = c.InitialPosition[1]
c.ZoomFactor = c.ZoomOutFactor
}

View File

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

View File

@@ -1,82 +0,0 @@
package main
import (
"github.com/hajimehoshi/ebiten/v2"
)
type Game struct {
ScreenWidth, ScreenHeight, ReadlWidth, Cellsize int
Scenes map[SceneName]Scene
CurrentScene SceneName
Config *Config
Scale float32
}
func NewGame(config *Config, startscene SceneName) *Game {
game := &Game{
Config: config,
Scenes: map[SceneName]Scene{},
ScreenWidth: config.ScreenWidth,
ScreenHeight: config.ScreenHeight,
}
// setup scene[s]
game.CurrentScene = startscene
game.Scenes[Play] = NewPlayScene(game, config)
game.Scenes[Menu] = NewMenuScene(game, config)
game.Scenes[Options] = NewOptionsScene(game, config)
game.Scenes[Keybindings] = NewKeybindingsScene(game, config)
// setup environment
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
ebiten.SetWindowTitle("golsky - conway's game of life")
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
ebiten.SetScreenClearedEveryFrame(true)
return game
}
func (game *Game) GetCurrentScene() Scene {
return game.Scenes[game.CurrentScene]
}
func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
game.ReadlWidth = outsideWidth
game.Scale = float32(game.ScreenWidth) / float32(outsideWidth)
return game.ScreenWidth, game.ScreenHeight
}
func (game *Game) Update() error {
scene := game.GetCurrentScene()
if quit := scene.Update(); quit != nil {
return quit
}
next := scene.GetNext()
if next != game.CurrentScene {
game.Scenes[next].SetPrevious(game.CurrentScene)
scene.ResetNext()
game.CurrentScene = next
}
return nil
}
func (game *Game) Draw(screen *ebiten.Image) {
// first draw primary scene[s], although there are only 1
for current, scene := range game.Scenes {
if scene.IsPrimary() {
// primary scenes always draw
scene.Draw(screen)
if current == game.CurrentScene {
// avoid to redraw it in the next step
return
}
}
}
scene := game.GetCurrentScene()
scene.Draw(screen)
}

View File

@@ -1,19 +0,0 @@
package main
// find an item in a list, generic variant
func Contains[E comparable](s []E, v E) bool {
for _, vs := range s {
if v == vs {
return true
}
}
return false
}
func Exists[K comparable, V any](m map[K]V, v K) bool {
if _, ok := m[v]; ok {
return true
}
return false
}

View File

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

View File

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

View File

@@ -1,113 +0,0 @@
package main
import (
"log"
"github.com/golang/freetype/truetype"
"github.com/tinne26/etxt"
"golang.org/x/image/font"
)
var FontRenderer = LoadFonts("assets/fonts")
const (
GameFont string = "NotoSans-Regular"
GameFontETXT string = "Noto Sans"
FontSizeBig int = 48
FontSizeNormal int = 24
FontSizeSmall int = 12
)
type Texter struct {
Renderer *etxt.Renderer
FontNormal *font.Face
FontBig *font.Face
FontSmall *font.Face
}
func LoadFonts(dir string) Texter {
// load the font for use with ebitenui
fontbytes, err := assetfs.ReadFile(dir + "/" + GameFont + ".ttf")
if err != nil {
log.Fatal(err)
}
gamefont, err := truetype.Parse(fontbytes)
if err != nil {
log.Fatal(err)
}
gameface := truetype.NewFace(gamefont, &truetype.Options{
Size: float64(FontSizeNormal),
DPI: 72,
Hinting: font.HintingFull,
})
biggameface := truetype.NewFace(gamefont, &truetype.Options{
Size: float64(FontSizeBig),
DPI: 72,
Hinting: font.HintingFull,
})
smallgameface := truetype.NewFace(gamefont, &truetype.Options{
Size: float64(FontSizeSmall),
DPI: 72,
Hinting: font.HintingFull,
})
// load the font for use with etxt
fontlib := etxt.NewFontLibrary()
_, _, err = fontlib.ParseEmbedDirFonts(dir, assetfs)
if err != nil {
log.Fatalf("Error while loading fonts: %s", err.Error())
}
/*
err = fontlib.EachFont(
func(fontName string, font *etxt.Font) error {
fmt.Printf("font: %s\n", fontName)
return nil
})
if err != nil {
log.Fatal(err)
}
*/
if !fontlib.HasFont(GameFontETXT) {
log.Fatal("missing font: " + GameFontETXT)
}
err = fontlib.EachFont(checkMissingRunes)
if err != nil {
log.Fatal(err)
}
renderer := etxt.NewStdRenderer()
glyphsCache := etxt.NewDefaultCache(10 * 1024 * 1024) // 10MB
renderer.SetCacheHandler(glyphsCache.NewHandler())
renderer.SetFont(fontlib.GetFont(GameFontETXT))
return Texter{
Renderer: renderer,
FontNormal: &gameface,
FontBig: &biggameface,
FontSmall: &smallgameface,
}
}
// helper function used with FontLibrary.EachFont to make sure
// all loaded fonts contain the characters or alphabet we want
func checkMissingRunes(name string, font *etxt.Font) error {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const symbols = "0123456789 .,;:!?-()[]{}_&#@"
missing, err := etxt.GetMissingRunes(font, letters+symbols)
if err != nil {
return err
}
if len(missing) > 0 {
log.Fatalf("Font '%s' missing runes: %s", name, string(missing))
}
return nil
}

View File

@@ -1,49 +0,0 @@
package main
import (
"bytes"
"log"
"log/slog"
"path"
"strings"
"github.com/hajimehoshi/ebiten/v2"
)
type ShaderRegistry map[string]*ebiten.Shader
var Shaders = LoadShaders("assets/shaders")
func LoadShaders(dir string) ShaderRegistry {
shaders := ShaderRegistry{}
entries, err := assetfs.ReadDir(dir)
if err != nil {
log.Fatalf("failed to read shaders dir %s: %s", dir, err)
}
for _, file := range entries {
path := path.Join(dir, file.Name())
fd, err := assetfs.Open(path)
if err != nil {
log.Fatalf("failed to open shader file %s: %s", file.Name(), err)
}
defer fd.Close()
name := strings.TrimSuffix(file.Name(), ".kg")
buf := new(bytes.Buffer)
buf.ReadFrom(fd)
shader, err := ebiten.NewShader([]byte(buf.Bytes()))
if err != nil {
log.Fatal(err)
}
shaders[name] = shader
slog.Debug("loaded shader asset", "path", path)
}
return shaders
}

View File

@@ -1,69 +0,0 @@
package main
import (
"embed"
"image"
_ "image/png"
"io/fs"
"log"
"path"
"strings"
"github.com/hajimehoshi/ebiten/v2"
)
// Maps image name to image data
type AssetRegistry map[string]*ebiten.Image
// A helper to pass the registry easier around
type assetData struct {
Registry AssetRegistry
}
//go:embed assets/sprites/*.png assets/fonts/*.ttf assets/shaders/*.kg
var assetfs embed.FS
// Called at build time, creates the global asset and animation registries
var Assets = LoadImages("assets/sprites")
// load pngs and json files
func LoadImages(dir string) AssetRegistry {
Registry := AssetRegistry{}
// we use embed.FS to iterate over all files in ./assets/
entries, err := assetfs.ReadDir(dir)
if err != nil {
log.Fatalf("failed to read assets dir %s: %s", dir, err)
}
for _, imagefile := range entries {
path := path.Join(dir, imagefile.Name())
fd, err := assetfs.Open(path)
if err != nil {
log.Fatalf("failed to open file %s: %s", imagefile.Name(), err)
}
defer fd.Close()
switch {
case strings.HasSuffix(path, ".png"):
name, image := ReadImage(imagefile, fd)
Registry[name] = image
}
}
return Registry
}
func ReadImage(imagefile fs.DirEntry, fd fs.File) (string, *ebiten.Image) {
name := strings.TrimSuffix(imagefile.Name(), ".png")
img, _, err := image.Decode(fd)
if err != nil {
log.Fatalf("failed to decode image %s: %s", imagefile.Name(), err)
}
image := ebiten.NewImageFromImage(img)
return name, image
}

View File

@@ -1,55 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"runtime/pprof"
_ "net/http/pprof"
"github.com/hajimehoshi/ebiten/v2"
)
func main() {
var directstart bool
if len(os.Args) > 1 {
directstart = true
}
config, err := ParseCommandline()
if err != nil {
log.Fatal(err)
}
if config.ShowVersion {
fmt.Printf("This is golsky version %s\n", VERSION)
os.Exit(0)
}
start := Play
if !directstart {
start = Menu
config.DelayedStart = true
}
game := NewGame(config, SceneName(start))
if config.ProfileFile != "" {
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create(config.ProfileFile)
if err != nil {
log.Fatal(err)
}
defer fd.Close()
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
}
// main loop
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}

View File

@@ -1,145 +0,0 @@
package main
import (
"image/color"
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type SceneMenu struct {
Game *Game
Config *Config
Next SceneName
Prev SceneName
Whoami SceneName
Ui *ebitenui.UI
FontColor color.RGBA
First bool
Exit bool
}
func NewMenuScene(game *Game, config *Config) Scene {
scene := &SceneMenu{
Whoami: Menu,
Game: game,
Next: Menu,
Config: config,
FontColor: color.RGBA{255, 30, 30, 0xff},
}
scene.Init()
return scene
}
func (scene *SceneMenu) GetNext() SceneName {
return scene.Next
}
func (scene *SceneMenu) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *SceneMenu) ResetNext() {
scene.Next = scene.Whoami
}
func (scene *SceneMenu) SetNext(next SceneName) {
scene.Next = next
}
func (scene *SceneMenu) Update() error {
scene.Ui.Update()
if scene.Exit {
return ebiten.Termination
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.Config.DelayedStart = false
scene.Leave()
}
return nil
}
func (scene *SceneMenu) IsPrimary() bool {
return false
}
func (scene *SceneMenu) Draw(screen *ebiten.Image) {
scene.Ui.Draw(screen)
}
func (scene *SceneMenu) Leave() {
scene.SetNext(Play)
}
func (scene *SceneMenu) Init() {
rowContainer := NewRowContainer("Main Menu")
empty := NewMenuButton("Start with empty grid",
func(args *widget.ButtonClickedEventArgs) {
scene.Config.Empty = true
scene.Config.Restart = true
scene.Leave()
})
random := NewMenuButton("Start with random patterns",
func(args *widget.ButtonClickedEventArgs) {
scene.Config.Empty = false
scene.Config.Restart = true
scene.Leave()
})
copy := NewMenuButton("Save Copy as RLE",
func(args *widget.ButtonClickedEventArgs) {
scene.Config.Markmode = true
scene.Config.Paused = true
scene.Leave()
})
options := NewMenuButton("Options",
func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Options)
})
bindings := NewMenuButton("Show Key Bindings",
func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Keybindings)
})
separator1 := NewSeparator(3)
separator2 := NewSeparator(3)
separator3 := NewSeparator(10)
cancel := NewMenuButton("Back",
func(args *widget.ButtonClickedEventArgs) {
scene.Leave()
})
quit := NewMenuButton("Exit Golsky",
func(args *widget.ButtonClickedEventArgs) {
scene.Exit = true
})
rowContainer.AddChild(empty)
rowContainer.AddChild(random)
rowContainer.AddChild(separator1)
rowContainer.AddChild(options)
rowContainer.AddChild(copy)
rowContainer.AddChild(bindings)
rowContainer.AddChild(separator2)
rowContainer.AddChild(cancel)
rowContainer.AddChild(separator3)
rowContainer.AddChild(quit)
scene.Ui = &ebitenui.UI{
Container: rowContainer.Container(),
}
}

View File

@@ -1,157 +0,0 @@
package main
import (
"image/color"
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type SceneOptions struct {
Game *Game
Config *Config
Next SceneName
Prev SceneName
Whoami SceneName
Ui *ebitenui.UI
FontColor color.RGBA
}
func NewOptionsScene(game *Game, config *Config) Scene {
scene := &SceneOptions{
Whoami: Options,
Game: game,
Next: Options,
Config: config,
FontColor: color.RGBA{255, 30, 30, 0xff},
}
scene.Init()
return scene
}
func (scene *SceneOptions) GetNext() SceneName {
return scene.Next
}
func (scene *SceneOptions) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *SceneOptions) ResetNext() {
scene.Next = scene.Whoami
}
func (scene *SceneOptions) SetNext(next SceneName) {
scene.Next = next
}
func (scene *SceneOptions) IsPrimary() bool {
return false
}
func (scene *SceneOptions) Update() error {
scene.Ui.Update()
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.SetNext(Play)
}
return nil
}
func (scene *SceneOptions) Draw(screen *ebiten.Image) {
scene.Ui.Draw(screen)
}
func (scene *SceneOptions) SetInitialValue(w *widget.LabeledCheckbox, value bool) {
if value {
w.SetState(
widget.WidgetChecked,
)
}
}
func (scene *SceneOptions) Init() {
rowContainer := NewRowContainer("Options")
pause := NewCheckbox("Pause",
scene.Config.Paused,
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.TogglePaused()
})
debugging := NewCheckbox("Debugging",
scene.Config.Debug,
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleDebugging()
})
gridlines := NewCheckbox("Show grid lines",
scene.Config.ShowGrid,
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleGridlines()
})
evolution := NewCheckbox("Show evolution traces",
scene.Config.ShowEvolution,
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleEvolution()
})
wrap := NewCheckbox("Wrap around edges",
scene.Config.Wrap,
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleWrap()
})
themenames := make([]string, len(THEMES))
i := 0
for name := range THEMES {
themenames[i] = name
i++
}
themes := NewCombobox(
themenames,
scene.Config.Theme,
func(args *widget.ListComboButtonEntrySelectedEventArgs) {
scene.Config.SwitchTheme(args.Entry.(ListEntry).Name)
})
themelabel := NewLabel("Themes")
combocontainer := NewColumnContainer()
combocontainer.AddChild(themes)
combocontainer.AddChild(themelabel)
separator := NewSeparator(3)
separator2 := NewSeparator(3)
cancel := NewMenuButton("Close",
func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(scene.Prev)
})
rowContainer.AddChild(pause)
rowContainer.AddChild(debugging)
rowContainer.AddChild(gridlines)
rowContainer.AddChild(evolution)
rowContainer.AddChild(wrap)
rowContainer.AddChild(separator)
rowContainer.AddChild(combocontainer)
rowContainer.AddChild(separator2)
rowContainer.AddChild(cancel)
scene.Ui = &ebitenui.UI{
Container: rowContainer.Container(),
}
}

View File

@@ -1,688 +0,0 @@
package main
import (
"fmt"
"image"
"log"
"unsafe"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/vector"
"github.com/tlinden/golsky/rle"
"golang.org/x/image/math/f64"
)
type Images struct {
Black, White, Age1, Age2, Age3, Age4, Old *ebiten.Image
}
const (
DEBUG_FORMAT = "FPS: %0.2f, TPG: %d, M: %0.2fMB, Generations: %d\nScale: %.02f, Zoom: %d, Cam: %.02f,%.02f Cursor: %d,%d %s"
)
type ScenePlay struct {
Game *Game
Config *Config
Next SceneName
Prev SceneName
Whoami SceneName
Clear bool
Grids []*Grid // 2 grids: one current, one next
History [][]int64 // holds state of past dead cells for evolution traces
Index int // points to current grid
Generations int64 // Stats
TicksElapsed int // tick counter for game speed
Camera Camera // for zoom+move
World, Cache *ebiten.Image // actual image we render to
WheelTurned bool // when user turns wheel multiple times, zoom faster
Dragging bool // middle mouse is pressed, move canvas
LastCursorPos []float64 // used to check if the user is dragging
MarkTaken bool // true when mouse1 pressed
MarkDone bool // true when mouse1 released, copy cells between Mark+Point
Mark, Point image.Point // area to marks+save
RunOneStep bool // mutable flags from config
TPG int // current game speed (ticks per game)
Theme Theme
}
func NewPlayScene(game *Game, config *Config) Scene {
scene := &ScenePlay{
Whoami: Play,
Game: game,
Next: Play,
Config: config,
TPG: config.TPG,
RunOneStep: config.RunOneStep,
}
scene.Init()
return scene
}
func (scene *ScenePlay) IsPrimary() bool {
return true
}
func (scene *ScenePlay) GetNext() SceneName {
return scene.Next
}
func (scene *ScenePlay) SetPrevious(prev SceneName) {
scene.Prev = prev
}
func (scene *ScenePlay) ResetNext() {
scene.Next = scene.Whoami
}
func (scene *ScenePlay) SetNext(next SceneName) {
scene.Next = next
}
func (scene *ScenePlay) CheckRule(state bool, neighbors int) bool {
var nextstate bool
// 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) {
nextstate = Alive
} else if state && Contains(scene.Config.Rule.Death, neighbors) {
nextstate = Alive
} else {
nextstate = Dead
}
return nextstate
}
// Update all cells according to the current rule
func (scene *ScenePlay) UpdateCells() {
// count ticks so we know when to actually run
scene.TicksElapsed++
if scene.TPG > scene.TicksElapsed {
// need to sleep a little more
return
}
// next grid index, we just xor 0|1 to 1|0
next := scene.Index ^ 1
// compute life status of cells
for y := 0; y < scene.Config.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
nextstate := scene.CheckRule(state, neighbors)
// change state of current cell in next grid
scene.Grids[next].Data[y][x] = nextstate
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[y][x] = scene.Generations
}
// 10FPS:
//scene.History.Data[y][x] = (state ^ (1 ^ nextstate)) * (scene.Generations - scene.History.Data[y][x])
}
}
}
// switch grid for rendering
scene.Index ^= 1
// global stats counter
scene.Generations++
if scene.Config.RunOneStep {
// setp-wise mode, halt the game
scene.Config.RunOneStep = false
}
// reset speed counter
scene.TicksElapsed = 0
}
func (scene *ScenePlay) Reset() {
scene.Config.Paused = true
scene.InitGrid()
scene.Config.Paused = false
}
// check user input
func (scene *ScenePlay) CheckExit() error {
if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
return ebiten.Termination
}
return nil
}
func (scene *ScenePlay) CheckInput() {
// primary functions, always available
switch {
case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
scene.SetNext(Menu)
case inpututil.IsKeyJustPressed(ebiten.KeyO):
scene.SetNext(Options)
case inpututil.IsKeyJustPressed(ebiten.KeyC):
scene.Config.Markmode = true
scene.Config.Drawmode = false
scene.Config.Paused = true
case inpututil.IsKeyJustPressed(ebiten.KeyI):
scene.Config.Drawmode = true
scene.Config.Paused = true
}
if scene.Config.Markmode {
// no need to check any more input in mark mode
return
}
switch {
case inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter):
scene.Config.TogglePaused()
case inpututil.IsKeyJustPressed(ebiten.KeyPageDown):
if scene.TPG < 120 {
scene.TPG++
}
case inpututil.IsKeyJustPressed(ebiten.KeyPageUp):
if scene.TPG >= 1 {
scene.TPG--
}
case inpututil.IsKeyJustPressed(ebiten.KeyS):
scene.SaveState()
case inpututil.IsKeyJustPressed(ebiten.KeyD):
scene.Config.Debug = !scene.Config.Debug
}
if scene.Config.Paused {
if inpututil.IsKeyJustPressed(ebiten.KeyN) {
scene.Config.RunOneStep = true
}
}
}
func (scene *ScenePlay) CheckDrawingInput() {
if scene.Config.Drawmode {
switch {
case ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft):
scene.ToggleCellOnCursorPos(Alive)
case ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight):
scene.ToggleCellOnCursorPos(Dead)
case inpututil.IsKeyJustPressed(ebiten.KeyEscape):
scene.Config.Drawmode = false
}
}
}
// Check dragging input. move the canvas with the mouse while pressing
// the middle mouse button, zoom in and out using the wheel.
func (scene *ScenePlay) CheckDraggingInput() {
if scene.Config.Markmode {
return
}
dragbutton := ebiten.MouseButtonLeft
if scene.Config.Drawmode {
dragbutton = ebiten.MouseButtonMiddle
}
// move canvas
if scene.Dragging && !ebiten.IsMouseButtonPressed(dragbutton) {
// release
scene.Dragging = false
}
if !scene.Dragging && ebiten.IsMouseButtonPressed(dragbutton) {
// start dragging
scene.Dragging = true
scene.LastCursorPos[0], scene.LastCursorPos[1] = scene.Camera.ScreenToWorld(ebiten.CursorPosition())
}
if scene.Dragging {
x, y := scene.Camera.ScreenToWorld(ebiten.CursorPosition())
if x != scene.LastCursorPos[0] || y != scene.LastCursorPos[1] {
// actually drag by mouse cursor pos diff to last cursor pos
scene.Camera.Position[0] -= float64(x - scene.LastCursorPos[0])
scene.Camera.Position[1] -= float64(y - scene.LastCursorPos[1])
}
scene.LastCursorPos[0], scene.LastCursorPos[1] = scene.Camera.ScreenToWorld(ebiten.CursorPosition())
}
// also support the arrow keys to move the canvas
switch {
case ebiten.IsKeyPressed(ebiten.KeyArrowLeft):
scene.Camera.Position[0] -= 1
case ebiten.IsKeyPressed(ebiten.KeyArrowRight):
scene.Camera.Position[0] += 1
case ebiten.IsKeyPressed(ebiten.KeyArrowUp):
scene.Camera.Position[1] -= 1
case ebiten.IsKeyPressed(ebiten.KeyArrowDown):
scene.Camera.Position[1] += 1
}
// Zoom
_, dy := ebiten.Wheel()
if dy != 0 {
scene.Camera.ZoomFactor += (int(dy) * 5)
}
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
scene.Camera.Reset()
}
}
func (scene *ScenePlay) GetWorldCursorPos() image.Point {
worldX, worldY := scene.Camera.ScreenToWorld(ebiten.CursorPosition())
return image.Point{
X: int(worldX) / scene.Config.Cellsize,
Y: int(worldY) / scene.Config.Cellsize,
}
}
func (scene *ScenePlay) CheckMarkInput() {
if !scene.Config.Markmode {
return
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
scene.Config.Markmode = false
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButton0) {
if !scene.MarkTaken {
scene.Mark = scene.GetWorldCursorPos()
scene.MarkTaken = true
scene.MarkDone = false
}
scene.Point = scene.GetWorldCursorPos()
//fmt.Printf("Mark: %v, Point: %v\n", scene.Mark, scene.Point)
} else if inpututil.IsMouseButtonJustReleased(ebiten.MouseButton0) {
scene.Config.Markmode = false
scene.MarkTaken = false
scene.MarkDone = true
scene.SaveRectRLE()
}
}
func (scene *ScenePlay) SaveState() {
filename := GetFilename(scene.Generations)
err := scene.Grids[scene.Index].SaveState(filename, scene.Config.Rule.Definition)
if err != nil {
log.Printf("failed to save game state to %s: %s", filename, err)
}
log.Printf("saved game state to %s at generation %d\n", filename, scene.Generations)
}
func (scene *ScenePlay) SaveRectRLE() {
filename := GetFilenameRLE(scene.Generations)
if scene.Mark.X == scene.Point.X || scene.Mark.Y == scene.Point.Y {
log.Printf("can't save non-rectangle\n")
return
}
var width int
var height int
var startx int
var starty int
if scene.Mark.X < scene.Point.X {
// mark left point
startx = scene.Mark.X
width = scene.Point.X - scene.Mark.X
} else {
// mark right point
startx = scene.Point.X
width = scene.Mark.X - scene.Point.X
}
if scene.Mark.Y < scene.Point.Y {
// mark above point
starty = scene.Mark.Y
height = scene.Point.Y - scene.Mark.Y
} else {
// mark below point
starty = scene.Point.Y
height = scene.Mark.Y - scene.Point.Y
}
grid := make([][]bool, height)
for y := 0; y < height; y++ {
grid[y] = make([]bool, width)
for x := 0; x < width; x++ {
grid[y][x] = scene.Grids[scene.Index].Data[y+starty][x+startx]
}
}
err := rle.StoreGridToRLE(grid, filename, scene.Config.Rule.Definition, width, height)
if err != nil {
log.Printf("failed to save rect to %s: %s\n", filename, err)
} else {
log.Printf("saved selected rect to %s at generation %d\n", filename, scene.Generations)
}
}
func (scene *ScenePlay) Update() error {
if scene.Config.Restart {
scene.Config.Restart = false
scene.Generations = 0
scene.InitGrid()
scene.InitCache()
return nil
}
if scene.Config.RestartCache {
scene.Config.RestartCache = false
scene.Theme = scene.Config.ThemeManager.GetCurrentTheme()
scene.InitCache()
return nil
}
if quit := scene.CheckExit(); quit != nil {
return quit
}
scene.CheckInput()
scene.CheckDrawingInput()
scene.CheckDraggingInput()
scene.CheckMarkInput()
if !scene.Config.Paused || scene.RunOneStep {
scene.UpdateCells()
}
return nil
}
// set a cell to alive or dead
func (scene *ScenePlay) ToggleCellOnCursorPos(alive bool) {
// use cursor pos relative to the world
worldX, worldY := scene.Camera.ScreenToWorld(ebiten.CursorPosition())
x := int(worldX) / scene.Config.Cellsize
y := int(worldY) / scene.Config.Cellsize
if x > -1 && y > -1 && x < scene.Config.Width && y < scene.Config.Height {
scene.Grids[scene.Index].Data[y][x] = alive
scene.History[y][x] = 1
}
}
// draw the new grid state
func (scene *ScenePlay) Draw(screen *ebiten.Image) {
// we fill the whole screen with a background color, the cells
// themselfes will be 1px smaller as their nominal size, producing
// a nice grey grid with grid lines
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(0, 0)
scene.World.DrawImage(scene.Cache, op)
for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ {
op.GeoM.Reset()
op.GeoM.Translate(
float64(x*scene.Config.Cellsize),
float64(y*scene.Config.Cellsize),
)
if scene.Config.ShowEvolution {
scene.DrawEvolution(screen, x, y, op)
} else {
if scene.Grids[scene.Index].Data[y][x] {
scene.World.DrawImage(scene.Theme.Tile(ColLife), op)
}
}
}
}
scene.DrawMark(scene.World)
scene.Camera.Render(scene.World, screen)
scene.DrawDebug(screen)
}
func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten.DrawImageOptions) {
age := scene.Generations - scene.History[y][x]
switch scene.Grids[scene.Index].Data[y][x] {
case Alive:
if age > 50 && scene.Config.ShowEvolution {
scene.World.DrawImage(scene.Theme.Tile(ColOld), op)
} else {
scene.World.DrawImage(scene.Theme.Tile(ColLife), op)
}
case Dead:
// only draw dead cells in case evolution trace is enabled
if scene.History[y][x] > 1 && scene.Config.ShowEvolution {
switch {
case age < 10:
scene.World.DrawImage(scene.Theme.Tile(ColAge1), op)
case age < 20:
scene.World.DrawImage(scene.Theme.Tile(ColAge2), op)
case age < 30:
scene.World.DrawImage(scene.Theme.Tile(ColAge3), op)
default:
scene.World.DrawImage(scene.Theme.Tile(ColAge4), op)
}
}
}
}
func (scene *ScenePlay) DrawMark(screen *ebiten.Image) {
if scene.Config.Markmode && scene.MarkTaken {
x := float32(scene.Mark.X * scene.Config.Cellsize)
y := float32(scene.Mark.Y * scene.Config.Cellsize)
w := float32((scene.Point.X - scene.Mark.X) * scene.Config.Cellsize)
h := float32((scene.Point.Y - scene.Mark.Y) * scene.Config.Cellsize)
vector.StrokeRect(
scene.World,
x+1, y+1,
w, h,
1.0, scene.Theme.Color(ColOld), false,
)
}
}
func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) {
if scene.Config.Debug {
paused := ""
if scene.Config.Paused {
paused = "-- paused --"
}
if scene.Config.Markmode {
paused = "-- mark --"
}
if scene.Config.Drawmode {
paused = "-- insert --"
}
x, y := ebiten.CursorPosition()
debug := fmt.Sprintf(
DEBUG_FORMAT,
ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations,
scene.Game.Scale, scene.Camera.ZoomFactor,
scene.Camera.Position[0], scene.Camera.Position[1],
x, y,
paused)
FontRenderer.Renderer.SetSizePx(10 + int(scene.Game.Scale*10))
FontRenderer.Renderer.SetTarget(screen)
FontRenderer.Renderer.SetColor(scene.Theme.Color(ColLife))
FontRenderer.Renderer.Draw(debug, 31, 31)
FontRenderer.Renderer.SetColor(scene.Theme.Color(ColOld))
FontRenderer.Renderer.Draw(debug, 30, 30)
fmt.Println(debug)
}
}
// load a pre-computed pattern from RLE file
func (scene *ScenePlay) InitPattern() {
scene.Grids[0].LoadRLE(scene.Config.RLE)
}
// pre-render offscreen cache image
func (scene *ScenePlay) InitCache() {
op := &ebiten.DrawImageOptions{}
if scene.Config.ShowGrid {
scene.Cache.Fill(scene.Theme.Color(ColGrid))
} else {
scene.Cache.Fill(scene.Theme.Color(ColDead))
}
for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ {
op.GeoM.Reset()
op.GeoM.Translate(
float64(x*scene.Config.Cellsize),
float64(y*scene.Config.Cellsize),
)
scene.Cache.DrawImage(scene.Theme.Tile(ColDead), op)
}
}
}
// initialize grid[s], either using pre-computed from state or rle file, or random
func (scene *ScenePlay) InitGrid() {
grida := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
// startup is delayed until user has selected options
grida.FillRandom()
scene.Grids = []*Grid{
grida,
gridb,
}
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() {
// setup the scene
scene.Camera = Camera{
ViewPort: f64.Vec2{
float64(scene.Config.ScreenWidth),
float64(scene.Config.ScreenHeight),
},
InitialZoomFactor: scene.Config.Zoomfactor,
InitialPosition: f64.Vec2{
scene.Config.InitialCamPos[0],
scene.Config.InitialCamPos[1],
},
ZoomOutFactor: scene.Config.ZoomOutFactor,
}
scene.World = ebiten.NewImage(
scene.Config.Width*scene.Config.Cellsize,
scene.Config.Height*scene.Config.Cellsize,
)
scene.Cache = ebiten.NewImage(
scene.Config.Width*scene.Config.Cellsize,
scene.Config.Height*scene.Config.Cellsize,
)
scene.Theme = scene.Config.ThemeManager.GetCurrentTheme()
scene.InitCache()
if scene.Config.DelayedStart && !scene.Config.Empty {
scene.Config.Empty = true
scene.InitGrid()
scene.Config.Empty = false
} else {
scene.InitGrid()
}
scene.InitPattern()
scene.Index = 0
scene.TicksElapsed = 0
scene.LastCursorPos = make([]float64, 2)
if scene.Config.Zoomfactor < 0 || scene.Config.Zoomfactor > 0 {
scene.Camera.ZoomFactor = scene.Config.Zoomfactor
}
scene.Camera.Setup()
}
func bool2int(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
// count the living neighbors of a cell
func (scene *ScenePlay) CountNeighbors(x, y int) int {
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 {
// 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

@@ -1,52 +0,0 @@
package main
import (
"log"
"strconv"
"strings"
)
// a GOL rule
type Rule struct {
Definition string
Birth []int
Death []int
}
// parse one part of a GOL rule into rule slice
func NumbersToList(numbers string) []int {
list := []int{}
items := strings.Split(numbers, "")
for _, item := range items {
num, err := strconv.Atoi(item)
if err != nil {
log.Fatalf("failed to parse game rule part <%s>: %s", numbers, err)
}
list = append(list, num)
}
return list
}
// parse GOL rule, used in CheckRule()
func ParseGameRule(rule string) *Rule {
parts := strings.Split(rule, "/")
if len(parts) < 2 {
log.Fatalf("Invalid game rule <%s>", rule)
}
golrule := &Rule{Definition: rule}
for _, part := range parts {
if part[0] == 'B' {
golrule.Birth = NumbersToList(part[1:])
} else {
golrule.Death = NumbersToList(part[1:])
}
}
return golrule
}

View File

@@ -1,28 +0,0 @@
package main
import "github.com/hajimehoshi/ebiten/v2"
// Wrapper for different screens to be shown, as Welcome, Options,
// About, Menu Level and of course the actual game
// Scenes are responsible for screen clearing! That way a scene is able
// to render its content onto the running level, e.g. the options scene
// etc.
type SceneName int
type Scene interface {
SetNext(SceneName)
GetNext() SceneName
SetPrevious(SceneName)
ResetNext()
Update() error
Draw(screen *ebiten.Image)
IsPrimary() bool // if true, this scene will be always drawn
}
const (
Menu = iota // main top level menu
Play // actual playing happens here
Options
Keybindings
)

View File

@@ -1,11 +0,0 @@
package main
import "runtime"
// returns current memory usage in MB
func GetMem() float64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return float64(m.Alloc) / 1024 / 1024
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,3 +0,0 @@
module perf
go 1.22

View File

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

View File

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

View File

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

View File

@@ -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,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

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