diff --git a/go.mod b/go.mod index b9a2c56..3e83f82 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gameoflife +module github.com/tlinden/gameoflife go 1.22 diff --git a/main.go b/main.go index 1fa7440..bd496d1 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,8 @@ import ( "strconv" "strings" - "github.com/alecthomas/repr" + "github.com/tlinden/gameoflife/rle" + "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" @@ -40,12 +41,13 @@ type Game struct { ScreenWidth, ScreenHeight int Generations int // Stats Black, White, Grey, Beige color.RGBA - TPG int // ticks per generation/game speed, 1==max - TicksElapsed int // tick counter for game speed - Debug, Paused, Empty, Invert bool // game modi - ShowEvolution, NoGrid, RunOneStep bool // flags - Rule *Rule // which rule to use, default: B3/S23 - Tiles Images // pre-computed tiles for dead and alife cells + TPG int // ticks per generation/game speed, 1==max + TicksElapsed int // tick counter for game speed + Debug, Paused, Empty, Invert bool // game modi + ShowEvolution, NoGrid, RunOneStep bool // flags + Rule *Rule // which rule to use, default: B3/S23 + Tiles Images // pre-computed tiles for dead and alife cells + RLE *rle.RLE // loaded GOL pattern from RLE file } func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) { @@ -294,6 +296,7 @@ func (game *Game) Draw(screen *ebiten.Image) { } } +// returns current memory usage in MB func GetMem() float64 { var m runtime.MemStats runtime.ReadMemStats(&m) @@ -301,6 +304,28 @@ func GetMem() float64 { return float64(m.Alloc) / 1024 / 1024 } +// load a pre-computed pattern from RLE file +func (game *Game) InitPattern() { + if game.RLE != nil { + startX := (game.Width / 2) - (game.RLE.Width / 2) + startY := (game.Height / 2) - (game.RLE.Height / 2) + var y, x int + + for rowIndex, patternRow := range game.RLE.Pattern { + for colIndex := range patternRow { + if game.RLE.Pattern[rowIndex][colIndex] > 0 { + x = colIndex + startX + y = rowIndex + startY + + game.History.Data[y][x] = 1 + game.Grids[0].Data[y][x] = 1 + } + } + } + } +} + +// initialize playing field/grid func (game *Game) InitGrid() { grid := &Grid{Data: make([][]int, game.Height)} gridb := &Grid{Data: make([][]int, game.Height)} @@ -310,6 +335,7 @@ func (game *Game) InitGrid() { grid.Data[y] = make([]int, game.Width) gridb.Data[y] = make([]int, game.Width) history.Data[y] = make([]int, game.Width) + if !game.Empty { for x := 0; x < game.Width; x++ { if rand.Intn(game.Density) == 1 { @@ -370,6 +396,7 @@ func (game *Game) Init() { game.ScreenHeight = game.Cellsize * game.Height game.InitGrid() + game.InitPattern() game.InitTiles() game.Index = 0 @@ -401,6 +428,7 @@ func main() { game := &Game{} showversion := false var rule string + var rlefile string pflag.IntVarP(&game.Width, "width", "W", 40, "grid width in cells") pflag.IntVarP(&game.Height, "height", "H", 40, "grid height in cells") @@ -409,6 +437,7 @@ func main() { pflag.IntVarP(&game.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, "rlefile", "f", "", "RLE pattern file") pflag.BoolVarP(&showversion, "version", "v", false, "show version") pflag.BoolVarP(&game.Paused, "paused", "p", false, "do not start simulation (use space to start)") @@ -427,7 +456,27 @@ func main() { game.Rule = ParseGameRule(rule) - repr.Print(game.TPG) + if rlefile != "" { + content, err := os.ReadFile(rlefile) + if err != nil { + log.Fatal(err) + } + + parsedRle, err := rle.Parse(string(content)) + if err != nil { + log.Fatalf("failed to load RLE pattern file: %s", err) + } + + if parsedRle.Width > game.Width || parsedRle.Height > game.Height { + log.Fatal("loaded RLE pattern is too large for game grid, adjust width+height") + } + + game.RLE = &parsedRle + + // RLE needs an empty grid + game.Empty = true + } + game.Init() ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight) diff --git a/rle/pattern_parser.go b/rle/pattern_parser.go new file mode 100644 index 0000000..fb80090 --- /dev/null +++ b/rle/pattern_parser.go @@ -0,0 +1,159 @@ +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() + } +} diff --git a/rle/pattern_parser_test.go b/rle/pattern_parser_test.go new file mode 100644 index 0000000..893e7f2 --- /dev/null +++ b/rle/pattern_parser_test.go @@ -0,0 +1,163 @@ +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, + ) + } + } +} diff --git a/rle/rle.go b/rle/rle.go new file mode 100644 index 0000000..a44e241 --- /dev/null +++ b/rle/rle.go @@ -0,0 +1,100 @@ +// source: https://github.com/nhoffmann/life by N.Hoffmann 2020. +package rle + +import ( + "fmt" + "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 +} + +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("Invlaid 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, "") +} diff --git a/rle/rle_test.go b/rle/rle_test.go new file mode 100644 index 0000000..265d993 --- /dev/null +++ b/rle/rle_test.go @@ -0,0 +1,84 @@ +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, + ) + } + } + }) + +} diff --git a/sample-rles/64P2H1V0.rle b/sample-rles/64P2H1V0.rle new file mode 100644 index 0000000..f2098c1 --- /dev/null +++ b/sample-rles/64P2H1V0.rle @@ -0,0 +1,3 @@ +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! diff --git a/sample-rles/p39piheptominohasslerdimer.rle b/sample-rles/p39piheptominohasslerdimer.rle new file mode 100644 index 0000000..d4280a2 --- /dev/null +++ b/sample-rles/p39piheptominohasslerdimer.rle @@ -0,0 +1,8 @@ +#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! \ No newline at end of file diff --git a/sample-rles/weekender.rle b/sample-rles/weekender.rle new file mode 100644 index 0000000..819ee64 --- /dev/null +++ b/sample-rles/weekender.rle @@ -0,0 +1,13 @@ +#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!