mirror of
https://codeberg.org/scip/golsky.git
synced 2025-12-16 20:20:57 +01:00
Added RLE parser by N.Hoffmann and incorporated it onto my gol.
This commit is contained in:
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module gameoflife
|
module github.com/tlinden/gameoflife
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
|
|||||||
65
main.go
65
main.go
@@ -10,7 +10,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/repr"
|
"github.com/tlinden/gameoflife/rle"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
@@ -40,12 +41,13 @@ type Game struct {
|
|||||||
ScreenWidth, ScreenHeight int
|
ScreenWidth, ScreenHeight int
|
||||||
Generations int // Stats
|
Generations int // Stats
|
||||||
Black, White, Grey, Beige color.RGBA
|
Black, White, Grey, Beige color.RGBA
|
||||||
TPG int // ticks per generation/game speed, 1==max
|
TPG int // ticks per generation/game speed, 1==max
|
||||||
TicksElapsed int // tick counter for game speed
|
TicksElapsed int // tick counter for game speed
|
||||||
Debug, Paused, Empty, Invert bool // game modi
|
Debug, Paused, Empty, Invert bool // game modi
|
||||||
ShowEvolution, NoGrid, RunOneStep bool // flags
|
ShowEvolution, NoGrid, RunOneStep bool // flags
|
||||||
Rule *Rule // which rule to use, default: B3/S23
|
Rule *Rule // which rule to use, default: B3/S23
|
||||||
Tiles Images // pre-computed tiles for dead and alife cells
|
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) {
|
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 {
|
func GetMem() float64 {
|
||||||
var m runtime.MemStats
|
var m runtime.MemStats
|
||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
@@ -301,6 +304,28 @@ func GetMem() float64 {
|
|||||||
return float64(m.Alloc) / 1024 / 1024
|
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() {
|
func (game *Game) InitGrid() {
|
||||||
grid := &Grid{Data: make([][]int, game.Height)}
|
grid := &Grid{Data: make([][]int, game.Height)}
|
||||||
gridb := &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)
|
grid.Data[y] = make([]int, game.Width)
|
||||||
gridb.Data[y] = make([]int, game.Width)
|
gridb.Data[y] = make([]int, game.Width)
|
||||||
history.Data[y] = make([]int, game.Width)
|
history.Data[y] = make([]int, game.Width)
|
||||||
|
|
||||||
if !game.Empty {
|
if !game.Empty {
|
||||||
for x := 0; x < game.Width; x++ {
|
for x := 0; x < game.Width; x++ {
|
||||||
if rand.Intn(game.Density) == 1 {
|
if rand.Intn(game.Density) == 1 {
|
||||||
@@ -370,6 +396,7 @@ func (game *Game) Init() {
|
|||||||
game.ScreenHeight = game.Cellsize * game.Height
|
game.ScreenHeight = game.Cellsize * game.Height
|
||||||
|
|
||||||
game.InitGrid()
|
game.InitGrid()
|
||||||
|
game.InitPattern()
|
||||||
game.InitTiles()
|
game.InitTiles()
|
||||||
|
|
||||||
game.Index = 0
|
game.Index = 0
|
||||||
@@ -401,6 +428,7 @@ func main() {
|
|||||||
game := &Game{}
|
game := &Game{}
|
||||||
showversion := false
|
showversion := false
|
||||||
var rule string
|
var rule string
|
||||||
|
var rlefile string
|
||||||
|
|
||||||
pflag.IntVarP(&game.Width, "width", "W", 40, "grid width in cells")
|
pflag.IntVarP(&game.Width, "width", "W", 40, "grid width in cells")
|
||||||
pflag.IntVarP(&game.Height, "height", "H", 40, "grid height 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.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(&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(&showversion, "version", "v", false, "show version")
|
||||||
pflag.BoolVarP(&game.Paused, "paused", "p", false, "do not start simulation (use space to start)")
|
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)
|
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()
|
game.Init()
|
||||||
|
|
||||||
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
|
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
|
||||||
|
|||||||
159
rle/pattern_parser.go
Normal file
159
rle/pattern_parser.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
163
rle/pattern_parser_test.go
Normal file
163
rle/pattern_parser_test.go
Normal file
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
rle/rle.go
Normal file
100
rle/rle.go
Normal file
@@ -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, "")
|
||||||
|
}
|
||||||
84
rle/rle_test.go
Normal file
84
rle/rle_test.go
Normal file
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
3
sample-rles/64P2H1V0.rle
Normal file
3
sample-rles/64P2H1V0.rle
Normal file
@@ -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!
|
||||||
8
sample-rles/p39piheptominohasslerdimer.rle
Normal file
8
sample-rles/p39piheptominohasslerdimer.rle
Normal file
@@ -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!
|
||||||
13
sample-rles/weekender.rle
Normal file
13
sample-rles/weekender.rle
Normal file
@@ -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!
|
||||||
Reference in New Issue
Block a user