sdl2-life

Conway's Game of Life with go-sdl2
git clone https://www.brianlane.com/git/sdl2-life
Log | Files | Refs | README

main.go (30028B)


      1 // sdl2-life
      2 // by Brian C. Lane <bcl@brianlane.com>
      3 package main
      4 
      5 import (
      6 	"bufio"
      7 	"flag"
      8 	"fmt"
      9 	"log"
     10 	"math"
     11 	"math/rand"
     12 	"net/http"
     13 	"os"
     14 	"regexp"
     15 	"strconv"
     16 	"strings"
     17 	"time"
     18 
     19 	"github.com/veandco/go-sdl2/sdl"
     20 	"github.com/veandco/go-sdl2/ttf"
     21 )
     22 
     23 const (
     24 	threshold = 0.15
     25 	// LinearGradient cmdline selection
     26 	LinearGradient = 0
     27 	// PolylinearGradient cmdline selection
     28 	PolylinearGradient = 1
     29 	// BezierGradient cmdline selection
     30 	BezierGradient = 2
     31 )
     32 
     33 // RLE header with variable spacing and optional rules
     34 // Matches it with or without rule at the end, and with 0 or more spaces between elements.
     35 var rleHeaderRegex = regexp.MustCompile(`x\s*=\s*(\d+)\s*,\s*y\s*=\s*(\d+)(?:\s*,\s*rule\s*=\s*(.*))*`)
     36 
     37 /* commandline flags */
     38 type cmdlineArgs struct {
     39 	Width       int    // Width of window in pixels
     40 	Height      int    // Height of window in pixels
     41 	Rows        int    // Number of cell rows
     42 	Columns     int    // Number of cell columns
     43 	Seed        int64  // Seed for PRNG
     44 	Border      bool   // Border around cells
     45 	Font        string // Path to TTF to use for status bar
     46 	FontSize    int    // Size of font in points
     47 	Rule        string // Rulestring to use
     48 	Fps         int    // Frames per Second
     49 	PatternFile string // File with initial pattern
     50 	Pause       bool   // Start the game paused
     51 	Empty       bool   // Start with empty world
     52 	Color       bool   // Color the cells based on age
     53 	Colors      string // Comma separated color hex triplets
     54 	Gradient    int    // Gradient algorithm to use
     55 	MaxAge      int    // Maximum age for gradient colors
     56 	Port        int    // Port to listen to
     57 	Host        string // Host IP to bind to
     58 	Server      bool   // Launch an API server when true
     59 }
     60 
     61 /* commandline defaults */
     62 var cfg = cmdlineArgs{
     63 	Width:       500,
     64 	Height:      500,
     65 	Rows:        100,
     66 	Columns:     100,
     67 	Seed:        0,
     68 	Border:      false,
     69 	Font:        "",
     70 	FontSize:    14,
     71 	Rule:        "B3/S23",
     72 	Fps:         10,
     73 	PatternFile: "",
     74 	Pause:       false,
     75 	Empty:       false,
     76 	Color:       false,
     77 	Colors:      "#4682b4,#ffffff",
     78 	Gradient:    0,
     79 	MaxAge:      255,
     80 	Port:        3051,
     81 	Host:        "127.0.0.1",
     82 	Server:      false,
     83 }
     84 
     85 /* parseArgs handles parsing the cmdline args and setting values in the global cfg struct */
     86 func parseArgs() {
     87 	flag.IntVar(&cfg.Width, "width", cfg.Width, "Width of window in pixels")
     88 	flag.IntVar(&cfg.Height, "height", cfg.Height, "Height of window in pixels")
     89 	flag.IntVar(&cfg.Rows, "rows", cfg.Rows, "Number of cell rows")
     90 	flag.IntVar(&cfg.Columns, "columns", cfg.Columns, "Number of cell columns")
     91 	flag.Int64Var(&cfg.Seed, "seed", cfg.Seed, "PRNG seed")
     92 	flag.BoolVar(&cfg.Border, "border", cfg.Border, "Border around cells")
     93 	flag.StringVar(&cfg.Font, "font", cfg.Font, "Path to TTF to use for status bar")
     94 	flag.IntVar(&cfg.FontSize, "font-size", cfg.FontSize, "Size of font in points")
     95 	flag.StringVar(&cfg.Rule, "rule", cfg.Rule, "Rulestring Bn.../Sn... (B3/S23)")
     96 	flag.IntVar(&cfg.Fps, "fps", cfg.Fps, "Frames per Second update rate (10fps)")
     97 	flag.StringVar(&cfg.PatternFile, "pattern", cfg.PatternFile, "File with initial pattern to load")
     98 	flag.BoolVar(&cfg.Pause, "pause", cfg.Pause, "Start the game paused")
     99 	flag.BoolVar(&cfg.Empty, "empty", cfg.Empty, "Start with empty world")
    100 	flag.BoolVar(&cfg.Color, "color", cfg.Color, "Color cells based on age")
    101 	flag.StringVar(&cfg.Colors, "colors", cfg.Colors, "Comma separated color hex triplets")
    102 	flag.IntVar(&cfg.Gradient, "gradient", cfg.Gradient, "Gradient type. 0=Linear 1=Polylinear 2=Bezier")
    103 	flag.IntVar(&cfg.MaxAge, "age", cfg.MaxAge, "Maximum age for gradient colors")
    104 	flag.IntVar(&cfg.Port, "port", cfg.Port, "Port to listen to")
    105 	flag.StringVar(&cfg.Host, "host", cfg.Host, "Host IP to bind to")
    106 	flag.BoolVar(&cfg.Server, "server", cfg.Server, "Launch an API server")
    107 
    108 	flag.Parse()
    109 }
    110 
    111 // Possible default fonts to search for
    112 var defaultFonts = []string{"/usr/share/fonts/liberation/LiberationMono-Regular.ttf",
    113 	"/usr/local/share/fonts/TerminusTTF/TerminusTTF-4.49.2.ttf",
    114 	"/usr/X11/share/fonts/TTF/LiberationMono-Regular.ttf"}
    115 
    116 // RGBAColor holds a color
    117 type RGBAColor struct {
    118 	r, g, b, a uint8
    119 }
    120 
    121 // Gradient holds the colors to use for displaying cell age
    122 type Gradient struct {
    123 	controls []RGBAColor
    124 	points   []RGBAColor
    125 }
    126 
    127 // Print prints the gradient values to the console
    128 func (g *Gradient) Print() {
    129 	fmt.Printf("controls:\n%#v\n\n", g.controls)
    130 	for i := range g.points {
    131 		fmt.Printf("%d = %#v\n", i, g.points[i])
    132 	}
    133 }
    134 
    135 // Append adds the points from a gradient to this one at an insertion point
    136 func (g *Gradient) Append(from Gradient, start int) {
    137 	for i, p := range from.points {
    138 		g.points[start+i] = p
    139 	}
    140 }
    141 
    142 // NewLinearGradient returns a Linear Gradient with pre-computed colors for every age
    143 // from https://bsouthga.dev/posts/color-gradients-with-python
    144 //
    145 // Only uses the first and last color passed in
    146 func NewLinearGradient(colors []RGBAColor, maxAge int) (Gradient, error) {
    147 	if len(colors) < 1 {
    148 		return Gradient{}, fmt.Errorf("Linear Gradient requires at least 1 color")
    149 	}
    150 
    151 	// Use the first and last color in controls as start and end
    152 	gradient := Gradient{controls: []RGBAColor{colors[0], colors[len(colors)-1]}, points: make([]RGBAColor, maxAge)}
    153 
    154 	start := gradient.controls[0]
    155 	end := gradient.controls[1]
    156 
    157 	for t := 0; t < maxAge; t++ {
    158 		r := uint8(float64(start.r) + (float64(t)/float64(maxAge-1))*(float64(end.r)-float64(start.r)))
    159 		g := uint8(float64(start.g) + (float64(t)/float64(maxAge-1))*(float64(end.g)-float64(start.g)))
    160 		b := uint8(float64(start.b) + (float64(t)/float64(maxAge-1))*(float64(end.b)-float64(start.b)))
    161 		gradient.points[t] = RGBAColor{r, g, b, 255}
    162 	}
    163 	return gradient, nil
    164 }
    165 
    166 // NewPolylinearGradient returns a gradient that is linear between all control colors
    167 func NewPolylinearGradient(colors []RGBAColor, maxAge int) (Gradient, error) {
    168 	if len(colors) < 2 {
    169 		return Gradient{}, fmt.Errorf("Polylinear Gradient requires at least 2 colors")
    170 	}
    171 
    172 	gradient := Gradient{controls: colors, points: make([]RGBAColor, maxAge)}
    173 
    174 	n := int(float64(maxAge) / float64(len(colors)-1))
    175 	g, _ := NewLinearGradient(colors, n)
    176 	gradient.Append(g, 0)
    177 
    178 	if len(colors) == 2 {
    179 		return gradient, nil
    180 	}
    181 
    182 	for i := 1; i < len(colors)-1; i++ {
    183 		if i == len(colors)-2 {
    184 			// The last group may need to be extended if it doesn't fill all the way to the end
    185 			remainder := maxAge - ((i + 1) * n)
    186 			g, _ := NewLinearGradient(colors[i:i+1], n+remainder)
    187 			gradient.Append(g, (i * n))
    188 		} else {
    189 			g, _ := NewLinearGradient(colors[i:i+1], n)
    190 			gradient.Append(g, (i * n))
    191 		}
    192 	}
    193 
    194 	return gradient, nil
    195 }
    196 
    197 // FactorialCache saves the results for factorial calculations for faster access
    198 type FactorialCache struct {
    199 	cache map[int]float64
    200 }
    201 
    202 // NewFactorialCache returns a new empty cache
    203 func NewFactorialCache() *FactorialCache {
    204 	return &FactorialCache{cache: make(map[int]float64)}
    205 }
    206 
    207 // Fact calculates the n! and caches the results
    208 func (fc *FactorialCache) Fact(n int) float64 {
    209 	f, ok := fc.cache[n]
    210 	if ok {
    211 		return f
    212 	}
    213 	var result float64
    214 	if n == 1 || n == 0 {
    215 		result = 1
    216 	} else {
    217 		result = float64(n) * fc.Fact(n-1)
    218 	}
    219 
    220 	fc.cache[n] = result
    221 	return result
    222 }
    223 
    224 // Bernstein calculates the bernstein coefficient
    225 //
    226 // t runs from 0 -> 1 and is the 'position' on the curve (age / maxAge-1)
    227 // n is the number of control colors -1
    228 // i is the current control color from 0 -> n
    229 func (fc *FactorialCache) Bernstein(t float64, n, i int) float64 {
    230 	b := fc.Fact(n) / (fc.Fact(i) * fc.Fact(n-i))
    231 	b = b * math.Pow(1-t, float64(n-i)) * math.Pow(t, float64(i))
    232 	return b
    233 }
    234 
    235 // NewBezierGradient returns pre-computed colors using control colors and bezier curve
    236 // from https://bsouthga.dev/posts/color-gradients-with-python
    237 func NewBezierGradient(controls []RGBAColor, maxAge int) Gradient {
    238 	gradient := Gradient{controls: controls, points: make([]RGBAColor, maxAge)}
    239 	fc := NewFactorialCache()
    240 
    241 	for t := 0; t < maxAge; t++ {
    242 		color := RGBAColor{}
    243 		for i, c := range controls {
    244 			color.r += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.r))
    245 			color.g += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.g))
    246 			color.b += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.b))
    247 		}
    248 		color.a = 255
    249 		gradient.points[t] = color
    250 	}
    251 
    252 	return gradient
    253 }
    254 
    255 // Cell describes the location and state of a cell
    256 type Cell struct {
    257 	alive     bool
    258 	aliveNext bool
    259 
    260 	x int
    261 	y int
    262 
    263 	age int
    264 }
    265 
    266 // Pattern is used to pass patterns from the API to the game
    267 type Pattern []string
    268 
    269 // LifeGame holds all the global state of the game and the methods to operate on it
    270 type LifeGame struct {
    271 	mp        bool
    272 	erase     bool
    273 	cells     [][]*Cell // NOTE: This is an array of [row][columns] not x,y coordinates
    274 	liveCells int
    275 	age       int64
    276 	birth     map[int]bool
    277 	stayAlive map[int]bool
    278 
    279 	// Graphics
    280 	window     *sdl.Window
    281 	renderer   *sdl.Renderer
    282 	font       *ttf.Font
    283 	bg         RGBAColor
    284 	fg         RGBAColor
    285 	cellWidth  int32
    286 	cellHeight int32
    287 	gradient   Gradient
    288 	pChan      <-chan Pattern
    289 }
    290 
    291 // cleanup will handle cleanup of allocated resources
    292 func (g *LifeGame) cleanup() {
    293 	// Clean up all the allocated memory
    294 
    295 	g.renderer.Destroy()
    296 	g.window.Destroy()
    297 	g.font.Close()
    298 	ttf.Quit()
    299 	sdl.Quit()
    300 }
    301 
    302 // InitializeCells resets the world, either randomly or from a pattern file
    303 func (g *LifeGame) InitializeCells() {
    304 	g.age = 0
    305 
    306 	// Fill it with dead cells first
    307 	g.cells = make([][]*Cell, cfg.Rows, cfg.Columns)
    308 	for y := 0; y < cfg.Rows; y++ {
    309 		for x := 0; x < cfg.Columns; x++ {
    310 			c := &Cell{x: x, y: y}
    311 			g.cells[y] = append(g.cells[y], c)
    312 		}
    313 	}
    314 
    315 	if len(cfg.PatternFile) > 0 {
    316 		// Read all of the pattern file for parsing
    317 		f, err := os.Open(cfg.PatternFile)
    318 		if err != nil {
    319 			log.Fatalf("Error reading pattern file: %s", err)
    320 		}
    321 		defer f.Close()
    322 
    323 		scanner := bufio.NewScanner(f)
    324 		var lines []string
    325 		for scanner.Scan() {
    326 			lines = append(lines, scanner.Text())
    327 		}
    328 		if len(lines) == 0 {
    329 			log.Fatalf("%s is empty.", cfg.PatternFile)
    330 		}
    331 
    332 		if strings.HasPrefix(lines[0], "#Life 1.05") {
    333 			err = g.ParseLife105(lines)
    334 		} else if strings.HasPrefix(lines[0], "#Life 1.06") {
    335 			log.Fatal("Life 1.06 file format is not supported")
    336 		} else if isRLE(lines) {
    337 			err = g.ParseRLE(lines, 0, 0)
    338 		} else {
    339 			err = g.ParsePlaintext(lines)
    340 		}
    341 
    342 		if err != nil {
    343 			log.Fatalf("Error reading pattern file: %s", err)
    344 		}
    345 	} else if !cfg.Empty {
    346 		g.InitializeRandomCells()
    347 	}
    348 
    349 	var err error
    350 	if g.birth, g.stayAlive, err = ParseRulestring(cfg.Rule); err != nil {
    351 		log.Fatalf("Failed to parse the rule string (%s): %s\n", cfg.Rule, err)
    352 	}
    353 
    354 	// Draw initial world
    355 	g.Draw("")
    356 }
    357 
    358 // TranslateXY move the x, y coordinates so that 0, 0 is the center of the world
    359 // and handle wrapping at the edges
    360 func (g *LifeGame) TranslateXY(x, y int) (int, int) {
    361 	// Move x, y to center of field and wrap at the edges
    362 	// NOTE: % in go preserves sign of a, unlike Python :)
    363 	x = cfg.Columns/2 + x
    364 	x = (x%cfg.Columns + cfg.Columns) % cfg.Columns
    365 	y = cfg.Rows/2 + y
    366 	y = (y%cfg.Rows + cfg.Rows) % cfg.Rows
    367 
    368 	return x, y
    369 }
    370 
    371 // SetCellState sets the cell alive state
    372 // it also wraps the x and y at the edges and returns the new value
    373 func (g *LifeGame) SetCellState(x, y int, alive bool) (int, int) {
    374 	x = x % cfg.Columns
    375 	y = y % cfg.Rows
    376 	g.cells[y][x].alive = alive
    377 	g.cells[y][x].aliveNext = alive
    378 
    379 	if !alive {
    380 		g.cells[y][x].age = 0
    381 	}
    382 
    383 	return x, y
    384 }
    385 
    386 // PrintCellDetails prints the details for a cell, located by the window coordinates x, y
    387 func (g *LifeGame) PrintCellDetails(x, y int32) {
    388 	cellX := int(x / g.cellWidth)
    389 	cellY := int(y / g.cellHeight)
    390 
    391 	log.Printf("%d, %d = %#v\n", cellX, cellY, g.cells[cellY][cellX])
    392 }
    393 
    394 // FillDead makes sure the rest of a line, width long, is filled with dead cells
    395 // xEdge is the left side of the box of width length
    396 // x is the starting point for the first line, any further lines start at xEdge
    397 func (g *LifeGame) FillDead(xEdge, x, y, width, height int) {
    398 	for i := 0; i < height; i++ {
    399 		jlen := width - x
    400 		for j := 0; j < jlen; j++ {
    401 			x, y = g.SetCellState(x, y, false)
    402 			x++
    403 		}
    404 		y++
    405 		x = xEdge
    406 	}
    407 }
    408 
    409 // InitializeRandomCells resets the world to a random state
    410 func (g *LifeGame) InitializeRandomCells() {
    411 
    412 	if cfg.Seed == 0 {
    413 		seed := time.Now().UnixNano()
    414 		log.Printf("seed = %d\n", seed)
    415 		rand.Seed(seed)
    416 	} else {
    417 		log.Printf("seed = %d\n", cfg.Seed)
    418 		rand.Seed(cfg.Seed)
    419 	}
    420 
    421 	for y := 0; y < cfg.Rows; y++ {
    422 		for x := 0; x < cfg.Columns; x++ {
    423 			g.SetCellState(x, y, rand.Float64() < threshold)
    424 		}
    425 	}
    426 }
    427 
    428 // ParseLife105 pattern file
    429 // #D Descriptions lines (0+)
    430 // #R Rule line (0/1)
    431 // #P -1 4 (Upper left corner, required, center is 0,0)
    432 // The pattern is . for dead and * for live
    433 func (g *LifeGame) ParseLife105(lines []string) error {
    434 	var x, y int
    435 	var err error
    436 	for _, line := range lines {
    437 		if strings.HasPrefix(line, "#D") || strings.HasPrefix(line, "#Life") {
    438 			continue
    439 		} else if strings.HasPrefix(line, "#N") {
    440 			// Use default rules (from the cmdline in this case)
    441 			continue
    442 		} else if strings.HasPrefix(line, "#R ") {
    443 			// TODO Parse rule and return it or setup cfg.Rule
    444 			// Format is: sss/bbb where s is stay alive and b are birth values
    445 			// Need to flip it to Bbbb/Ssss format
    446 
    447 			// Make sure the rule has a / in it
    448 			if !strings.Contains(line, "/") {
    449 				return fmt.Errorf("ERROR: Rule must contain /")
    450 			}
    451 
    452 			fields := strings.Split(line[3:], "/")
    453 			if len(fields) != 2 {
    454 				return fmt.Errorf("ERROR: Problem splitting rule on /")
    455 			}
    456 
    457 			var stay, birth int
    458 			if stay, err = strconv.Atoi(fields[0]); err != nil {
    459 				return fmt.Errorf("Error parsing alive value: %s", err)
    460 			}
    461 
    462 			if birth, err = strconv.Atoi(fields[1]); err != nil {
    463 				return fmt.Errorf("Error parsing birth value: %s", err)
    464 			}
    465 
    466 			cfg.Rule = fmt.Sprintf("B%d/S%d", birth, stay)
    467 		} else if strings.HasPrefix(line, "#P") {
    468 			// Initial position
    469 			fields := strings.Split(line, " ")
    470 			if len(fields) != 3 {
    471 				return fmt.Errorf("Cannot parse position line: %s", line)
    472 			}
    473 			if y, err = strconv.Atoi(fields[1]); err != nil {
    474 				return fmt.Errorf("Error parsing position: %s", err)
    475 			}
    476 			if x, err = strconv.Atoi(fields[2]); err != nil {
    477 				return fmt.Errorf("Error parsing position: %s", err)
    478 			}
    479 
    480 			// Move to 0, 0 at the center of the world
    481 			x, y = g.TranslateXY(x, y)
    482 		} else {
    483 			// Parse the line, error if it isn't . or *
    484 			xLine := x
    485 			for _, c := range line {
    486 				if c != '.' && c != '*' {
    487 					return fmt.Errorf("Illegal characters in pattern: %s", line)
    488 				}
    489 				xLine, y = g.SetCellState(xLine, y, c == '*')
    490 				xLine++
    491 			}
    492 			y++
    493 		}
    494 	}
    495 	return nil
    496 }
    497 
    498 // ParsePlaintext pattern file
    499 // The header has already been read from the buffer when this is called
    500 // This is a bit more generic than the spec, skip lines starting with !
    501 // and assume the pattern is . for dead cells any anything else for live.
    502 func (g *LifeGame) ParsePlaintext(lines []string) error {
    503 	var x, y int
    504 
    505 	// Move x, y to center of field
    506 	x = cfg.Columns / 2
    507 	y = cfg.Rows / 2
    508 
    509 	for _, line := range lines {
    510 		if strings.HasPrefix(line, "!") {
    511 			continue
    512 		} else {
    513 			// Parse the line, . is dead, anything else is alive.
    514 			xLine := x
    515 			for _, c := range line {
    516 				g.SetCellState(xLine, y, c != '.')
    517 				xLine++
    518 			}
    519 			y++
    520 		}
    521 	}
    522 
    523 	return nil
    524 }
    525 
    526 // isRLEPattern checks the lines to determine if it is a RLE pattern
    527 func isRLE(lines []string) bool {
    528 	for _, line := range lines {
    529 		if rleHeaderRegex.MatchString(line) {
    530 			return true
    531 		}
    532 	}
    533 	return false
    534 }
    535 
    536 // ParseRLE pattern file
    537 // Parses files matching the RLE specification - https://conwaylife.com/wiki/Run_Length_Encoded
    538 // Optional x, y starting position for later use
    539 func (g *LifeGame) ParseRLE(lines []string, x, y int) error {
    540 	// Move to 0, 0 at the center of the world
    541 	x, y = g.TranslateXY(x, y)
    542 
    543 	var header []string
    544 	var first int
    545 	for i, line := range lines {
    546 		header = rleHeaderRegex.FindStringSubmatch(line)
    547 		if len(header) > 0 {
    548 			first = i + 1
    549 			break
    550 		}
    551 		// All lines before the header must be a # line
    552 		if line[0] != '#' {
    553 			return fmt.Errorf("Incorrect or missing RLE header")
    554 		}
    555 	}
    556 	if len(header) < 3 {
    557 		return fmt.Errorf("Incorrect or missing RLE header")
    558 	}
    559 	if first > len(lines)-1 {
    560 		return fmt.Errorf("Missing lines after RLE header")
    561 	}
    562 	width, err := strconv.Atoi(header[1])
    563 	if err != nil {
    564 		return fmt.Errorf("Error parsing width: %s", err)
    565 	}
    566 	height, err := strconv.Atoi(header[2])
    567 	if err != nil {
    568 		return fmt.Errorf("Error parsing height: %s", err)
    569 	}
    570 
    571 	// Were there rules? Use them. TODO override if cmdline rules passed in
    572 	if len(header) == 4 {
    573 		cfg.Rule = header[3]
    574 	}
    575 
    576 	count := 0
    577 	xLine := x
    578 	yStart := y
    579 	for _, line := range lines[first:] {
    580 		for _, c := range line {
    581 			if c == '$' {
    582 				// End of this line (which can have a count)
    583 				if count == 0 {
    584 					count = 1
    585 				}
    586 				// Blank cells to the edge of the pattern, and full empty lines
    587 				g.FillDead(x, xLine, y, width, count)
    588 
    589 				xLine = x
    590 				y = y + count
    591 				count = 0
    592 				continue
    593 			}
    594 			if c == '!' {
    595 				// Finished
    596 				// Fill in any remaining space with dead cells
    597 				g.FillDead(x, xLine, y, width, height-(y-yStart))
    598 				return nil
    599 			}
    600 
    601 			// Is it a digit?
    602 			digit, err := strconv.Atoi(string(c))
    603 			if err == nil {
    604 				count = (count * 10) + digit
    605 				continue
    606 			}
    607 
    608 			if count == 0 {
    609 				count = 1
    610 			}
    611 
    612 			for i := 0; i < count; i++ {
    613 				xLine, y = g.SetCellState(xLine, y, c != 'b')
    614 				xLine++
    615 			}
    616 			count = 0
    617 		}
    618 	}
    619 	return nil
    620 }
    621 
    622 // checkState determines the state of the cell for the next tick of the game.
    623 func (g *LifeGame) checkState(c *Cell) {
    624 	liveCount, avgAge := g.liveNeighbors(c)
    625 	if c.alive {
    626 		// Stay alive if the number of neighbors is in stayAlive
    627 		_, c.aliveNext = g.stayAlive[liveCount]
    628 	} else {
    629 		// Birth a new cell if number of neighbors is in birth
    630 		_, c.aliveNext = g.birth[liveCount]
    631 
    632 		// New cells inherit their age from parents
    633 		// TODO make this optional
    634 		if c.aliveNext {
    635 			c.age = avgAge
    636 		}
    637 	}
    638 
    639 	if c.aliveNext {
    640 		c.age++
    641 	} else {
    642 		c.age = 0
    643 	}
    644 }
    645 
    646 // liveNeighbors returns the number of live neighbors for a cell and their average age
    647 func (g *LifeGame) liveNeighbors(c *Cell) (int, int) {
    648 	var liveCount int
    649 	var ageSum int
    650 	add := func(x, y int) {
    651 		// If we're at an edge, check the other side of the board.
    652 		if y == len(g.cells) {
    653 			y = 0
    654 		} else if y == -1 {
    655 			y = len(g.cells) - 1
    656 		}
    657 		if x == len(g.cells[y]) {
    658 			x = 0
    659 		} else if x == -1 {
    660 			x = len(g.cells[y]) - 1
    661 		}
    662 
    663 		if g.cells[y][x].alive {
    664 			liveCount++
    665 			ageSum += g.cells[y][x].age
    666 		}
    667 	}
    668 
    669 	add(c.x-1, c.y)   // To the left
    670 	add(c.x+1, c.y)   // To the right
    671 	add(c.x, c.y+1)   // up
    672 	add(c.x, c.y-1)   // down
    673 	add(c.x-1, c.y+1) // top-left
    674 	add(c.x+1, c.y+1) // top-right
    675 	add(c.x-1, c.y-1) // bottom-left
    676 	add(c.x+1, c.y-1) // bottom-right
    677 
    678 	if liveCount > 0 {
    679 		return liveCount, int(ageSum / liveCount)
    680 	}
    681 	return liveCount, 0
    682 }
    683 
    684 // SetColorFromAge uses the cell's age to color it
    685 func (g *LifeGame) SetColorFromAge(age int) {
    686 	if age >= len(g.gradient.points) {
    687 		age = len(g.gradient.points) - 1
    688 	}
    689 	color := g.gradient.points[age]
    690 	g.renderer.SetDrawColor(color.r, color.g, color.b, color.a)
    691 }
    692 
    693 // Draw draws the current state of the world
    694 func (g *LifeGame) Draw(status string) {
    695 	// Clear the world to the background color
    696 	g.renderer.SetDrawColor(g.bg.r, g.bg.g, g.bg.b, g.bg.a)
    697 	g.renderer.Clear()
    698 	g.renderer.SetDrawColor(g.fg.r, g.fg.g, g.fg.b, g.fg.a)
    699 	for y := range g.cells {
    700 		for _, c := range g.cells[y] {
    701 			c.alive = c.aliveNext
    702 			if !c.alive {
    703 				continue
    704 			}
    705 			if cfg.Color {
    706 				g.SetColorFromAge(c.age)
    707 			}
    708 			g.DrawCell(*c)
    709 		}
    710 	}
    711 	// Default to background color
    712 	g.renderer.SetDrawColor(g.bg.r, g.bg.g, g.bg.b, g.bg.a)
    713 
    714 	g.UpdateStatus(status)
    715 
    716 	g.renderer.Present()
    717 }
    718 
    719 // DrawCell draws a new cell on an empty background
    720 func (g *LifeGame) DrawCell(c Cell) {
    721 	x := int32(c.x) * g.cellWidth
    722 	y := int32(c.y) * g.cellHeight
    723 	if cfg.Border {
    724 		g.renderer.FillRect(&sdl.Rect{x + 1, y + 1, g.cellWidth - 2, g.cellHeight - 2})
    725 	} else {
    726 		g.renderer.FillRect(&sdl.Rect{x, y, g.cellWidth, g.cellHeight})
    727 	}
    728 }
    729 
    730 // UpdateCell redraws an existing cell, optionally erasing it
    731 func (g *LifeGame) UpdateCell(x, y int, erase bool) {
    732 	g.cells[y][x].alive = !erase
    733 
    734 	// Update the image right now
    735 	if erase {
    736 		g.renderer.SetDrawColor(g.bg.r, g.bg.g, g.bg.b, g.bg.a)
    737 	} else {
    738 		g.renderer.SetDrawColor(g.fg.r, g.fg.g, g.fg.b, g.fg.a)
    739 	}
    740 	g.DrawCell(*g.cells[y][x])
    741 
    742 	// Default to background color
    743 	g.renderer.SetDrawColor(g.bg.r, g.bg.g, g.bg.b, g.bg.a)
    744 	g.renderer.Present()
    745 }
    746 
    747 // UpdateStatus draws the status bar
    748 func (g *LifeGame) UpdateStatus(status string) {
    749 	if len(status) == 0 {
    750 		return
    751 	}
    752 	text, err := g.font.RenderUTF8Solid(status, sdl.Color{255, 255, 255, 255})
    753 	if err != nil {
    754 		log.Printf("Failed to render text: %s\n", err)
    755 		return
    756 	}
    757 	defer text.Free()
    758 
    759 	texture, err := g.renderer.CreateTextureFromSurface(text)
    760 	if err != nil {
    761 		log.Printf("Failed to render text: %s\n", err)
    762 		return
    763 	}
    764 	defer texture.Destroy()
    765 
    766 	w, h, err := g.font.SizeUTF8(status)
    767 	if err != nil {
    768 		log.Printf("Failed to get size: %s\n", err)
    769 		return
    770 	}
    771 
    772 	x := int32((cfg.Width - w) / 2)
    773 	rect := &sdl.Rect{x, int32(cfg.Height + 2), int32(w), int32(h)}
    774 	if err = g.renderer.Copy(texture, nil, rect); err != nil {
    775 		log.Printf("Failed to copy texture: %s\n", err)
    776 		return
    777 	}
    778 }
    779 
    780 // NextFrame executes the next screen of the game
    781 func (g *LifeGame) NextFrame() {
    782 	last := g.liveCells
    783 	g.liveCells = 0
    784 	for y := range g.cells {
    785 		for _, c := range g.cells[y] {
    786 			g.checkState(c)
    787 			if c.aliveNext {
    788 				g.liveCells++
    789 			}
    790 		}
    791 	}
    792 	if g.liveCells-last != 0 {
    793 		g.age++
    794 	}
    795 
    796 	// Draw a new screen
    797 	status := fmt.Sprintf("age: %5d alive: %5d change: %5d", g.age, g.liveCells, g.liveCells-last)
    798 	g.Draw(status)
    799 }
    800 
    801 // ShowKeysHelp prints the keys that are reconized to control behavior
    802 func ShowKeysHelp() {
    803 	fmt.Println("h           - Print help")
    804 	fmt.Println("<space>     - Toggle pause/play")
    805 	fmt.Println("c           - Toggle color")
    806 	fmt.Println("q           - Quit")
    807 	fmt.Println("s           - Single step")
    808 	fmt.Println("r           - Reset the game")
    809 }
    810 
    811 // Run executes the main loop of the game
    812 // it handles user input and updating the display at the selected update rate
    813 func (g *LifeGame) Run() {
    814 	fpsTime := sdl.GetTicks()
    815 
    816 	running := true
    817 	oneStep := false
    818 	pause := cfg.Pause
    819 	for running {
    820 		for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
    821 			switch t := event.(type) {
    822 			case *sdl.QuitEvent:
    823 				running = false
    824 				break
    825 			case *sdl.KeyboardEvent:
    826 				if t.GetType() == sdl.KEYDOWN {
    827 					switch t.Keysym.Sym {
    828 					case sdl.K_h:
    829 						ShowKeysHelp()
    830 					case sdl.K_q:
    831 						running = false
    832 						break
    833 					case sdl.K_SPACE:
    834 						pause = !pause
    835 					case sdl.K_s:
    836 						pause = true
    837 						oneStep = true
    838 					case sdl.K_r:
    839 						g.InitializeCells()
    840 					case sdl.K_c:
    841 						cfg.Color = !cfg.Color
    842 					}
    843 
    844 				}
    845 			case *sdl.MouseButtonEvent:
    846 				if t.GetType() == sdl.MOUSEBUTTONDOWN {
    847 					// log.Printf("x=%d y=%d\n", t.X, t.Y)
    848 					g.PrintCellDetails(t.X, t.Y)
    849 				}
    850 			}
    851 		}
    852 		// Delay a small amount
    853 		time.Sleep(1 * time.Millisecond)
    854 		if sdl.GetTicks() > fpsTime+(1000/uint32(cfg.Fps)) {
    855 			if !pause || oneStep {
    856 				g.NextFrame()
    857 				fpsTime = sdl.GetTicks()
    858 				oneStep = false
    859 			}
    860 		}
    861 
    862 		if g.pChan != nil {
    863 			select {
    864 			case pattern := <-g.pChan:
    865 				var err error
    866 				if strings.HasPrefix(pattern[0], "#Life 1.05") {
    867 					err = g.ParseLife105(pattern)
    868 				} else if isRLE(pattern) {
    869 					err = g.ParseRLE(pattern, 0, 0)
    870 				} else {
    871 					err = g.ParsePlaintext(pattern)
    872 				}
    873 				if err != nil {
    874 					log.Printf("Pattern error: %s\n", err)
    875 				}
    876 			default:
    877 			}
    878 		}
    879 	}
    880 }
    881 
    882 // InitializeGame sets up the game struct and the SDL library
    883 // It also creates the main window
    884 func InitializeGame() *LifeGame {
    885 	game := &LifeGame{}
    886 
    887 	var err error
    888 	if err = sdl.Init(sdl.INIT_EVERYTHING); err != nil {
    889 		log.Fatalf("Problem initializing SDL: %s", err)
    890 	}
    891 
    892 	if err = ttf.Init(); err != nil {
    893 		log.Fatalf("Failed to initialize TTF: %s\n", err)
    894 	}
    895 
    896 	if game.font, err = ttf.OpenFont(cfg.Font, cfg.FontSize); err != nil {
    897 		log.Fatalf("Failed to open font: %s\n", err)
    898 	}
    899 	log.Printf("Font height is %d", game.font.Height())
    900 
    901 	game.font.SetHinting(ttf.HINTING_NORMAL)
    902 	game.font.SetKerning(true)
    903 
    904 	game.window, err = sdl.CreateWindow(
    905 		"Conway's Game of Life",
    906 		sdl.WINDOWPOS_UNDEFINED,
    907 		sdl.WINDOWPOS_UNDEFINED,
    908 		int32(cfg.Width),
    909 		int32(cfg.Height+4+game.font.Height()),
    910 		sdl.WINDOW_SHOWN)
    911 	if err != nil {
    912 		log.Fatalf("Problem initializing SDL window: %s", err)
    913 	}
    914 
    915 	game.renderer, err = sdl.CreateRenderer(game.window, -1, sdl.RENDERER_ACCELERATED|sdl.RENDERER_PRESENTVSYNC)
    916 	if err != nil {
    917 		log.Fatalf("Problem initializing SDL renderer: %s", err)
    918 	}
    919 
    920 	// White on Black background
    921 	game.bg = RGBAColor{0, 0, 0, 255}
    922 	game.fg = RGBAColor{255, 255, 255, 255}
    923 
    924 	// Calculate square cell size, take into account --border selection
    925 	w := cfg.Width / cfg.Columns
    926 	h := cfg.Height / cfg.Rows
    927 	if w < h {
    928 		h = w
    929 	} else {
    930 		w = h
    931 	}
    932 	game.cellWidth = int32(w)
    933 	game.cellHeight = int32(h)
    934 
    935 	// Parse the hex triplets
    936 	colors, err := ParseColorTriplets(cfg.Colors)
    937 	if err != nil {
    938 		log.Fatalf("Problem parsing colors: %s", err)
    939 	}
    940 
    941 	// Build the color gradient
    942 	switch cfg.Gradient {
    943 	case LinearGradient:
    944 		game.gradient, err = NewLinearGradient(colors, cfg.MaxAge)
    945 		if err != nil {
    946 			log.Fatalf("ERROR: %s", err)
    947 		}
    948 	case PolylinearGradient:
    949 		game.gradient, err = NewPolylinearGradient(colors, cfg.MaxAge)
    950 		if err != nil {
    951 			log.Fatalf("ERROR: %s", err)
    952 		}
    953 	case BezierGradient:
    954 		game.gradient = NewBezierGradient(colors, cfg.MaxAge)
    955 	}
    956 
    957 	return game
    958 }
    959 
    960 // ParseColorTriplets parses color hex values into an array
    961 // like ffffff,000000 or #ffffff,#000000 or #ffffff#000000
    962 func ParseColorTriplets(s string) ([]RGBAColor, error) {
    963 
    964 	// Remove leading # if present
    965 	s = strings.TrimPrefix(s, "#")
    966 	// Replace ,# combinations with just ,
    967 	s = strings.ReplaceAll(s, ",#", ",")
    968 	// Replace # alone with ,
    969 	s = strings.ReplaceAll(s, "#", ",")
    970 
    971 	var colors []RGBAColor
    972 	// Convert the tuples into RGBAColor
    973 	for _, c := range strings.Split(s, ",") {
    974 		r, err := strconv.ParseUint(c[:2], 16, 8)
    975 		if err != nil {
    976 			return colors, err
    977 		}
    978 		g, err := strconv.ParseUint(c[2:4], 16, 8)
    979 		if err != nil {
    980 			return colors, err
    981 		}
    982 		b, err := strconv.ParseUint(c[4:6], 16, 8)
    983 		if err != nil {
    984 			return colors, err
    985 		}
    986 
    987 		colors = append(colors, RGBAColor{uint8(r), uint8(g), uint8(b), 255})
    988 	}
    989 
    990 	return colors, nil
    991 }
    992 
    993 // Parse digits into a map of ints from 0-9
    994 //
    995 // Returns an error if they aren't digits, or if there are more than 10 of them
    996 func parseDigits(digits string) (map[int]bool, error) {
    997 	ruleMap := make(map[int]bool, 10)
    998 
    999 	var errors bool
   1000 	var err error
   1001 	var value int
   1002 	if value, err = strconv.Atoi(digits); err != nil {
   1003 		log.Printf("%s must be digits from 0-9\n", digits)
   1004 		errors = true
   1005 	}
   1006 	if value > 9999999999 {
   1007 		log.Printf("%d has more than 10 digits", value)
   1008 		errors = true
   1009 	}
   1010 	if errors {
   1011 		return nil, fmt.Errorf("ERROR: Problem parsing digits")
   1012 	}
   1013 
   1014 	// Add the digits to the map (order doesn't matter)
   1015 	for value > 0 {
   1016 		ruleMap[value%10] = true
   1017 		value = value / 10
   1018 	}
   1019 
   1020 	return ruleMap, nil
   1021 }
   1022 
   1023 // ParseRulestring parses the rules that control the game
   1024 //
   1025 // Rulestrings are of the form Bn.../Sn... which list the number of neighbors to birth a new one,
   1026 // and the number of neighbors to stay alive.
   1027 //
   1028 func ParseRulestring(rule string) (birth map[int]bool, stayAlive map[int]bool, e error) {
   1029 	var errors bool
   1030 
   1031 	// Make sure the rule starts with a B
   1032 	if !strings.HasPrefix(rule, "B") {
   1033 		log.Println("ERROR: Rule must start with a 'B'")
   1034 		errors = true
   1035 	}
   1036 
   1037 	// Make sure the rule has a /S in it
   1038 	if !strings.Contains(rule, "/S") {
   1039 		log.Println("ERROR: Rule must contain /S")
   1040 		errors = true
   1041 	}
   1042 	if errors {
   1043 		return nil, nil, fmt.Errorf("The Rule string should look similar to: B2/S23")
   1044 	}
   1045 
   1046 	// Split on the / returning 2 results like Bnn and Snn
   1047 	fields := strings.Split(rule, "/")
   1048 	if len(fields) != 2 {
   1049 		return nil, nil, fmt.Errorf("ERROR: Problem splitting rule on /")
   1050 	}
   1051 
   1052 	var err error
   1053 	// Convert the values to maps
   1054 	birth, err = parseDigits(strings.TrimPrefix(fields[0], "B"))
   1055 	if err != nil {
   1056 		errors = true
   1057 	}
   1058 	stayAlive, err = parseDigits(strings.TrimPrefix(fields[1], "S"))
   1059 	if err != nil {
   1060 		errors = true
   1061 	}
   1062 	if errors {
   1063 		return nil, nil, fmt.Errorf("ERROR: Problem with Birth or Stay alive values")
   1064 	}
   1065 
   1066 	return birth, stayAlive, nil
   1067 }
   1068 
   1069 // Server starts an API server to receive patterns
   1070 func Server(host string, port int, pChan chan<- Pattern) {
   1071 
   1072 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   1073 		if r.Method != "POST" {
   1074 			http.Error(w, "", http.StatusMethodNotAllowed)
   1075 			return
   1076 		}
   1077 
   1078 		scanner := bufio.NewScanner(r.Body)
   1079 		var pattern Pattern
   1080 		for scanner.Scan() {
   1081 			pattern = append(pattern, scanner.Text())
   1082 		}
   1083 		if len(pattern) == 0 {
   1084 			http.Error(w, "Empty pattern", http.StatusServiceUnavailable)
   1085 			return
   1086 		}
   1087 
   1088 		// Splat this pattern onto the world
   1089 		pChan <- pattern
   1090 	})
   1091 
   1092 	log.Printf("Starting server on %s:%d", host, port)
   1093 	log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), nil))
   1094 }
   1095 
   1096 func main() {
   1097 	parseArgs()
   1098 
   1099 	// If the user didn't specify a font, try to find a default one
   1100 	if len(cfg.Font) == 0 {
   1101 		for _, f := range defaultFonts {
   1102 			if _, err := os.Stat(f); !os.IsNotExist(err) {
   1103 				cfg.Font = f
   1104 				break
   1105 			}
   1106 		}
   1107 	}
   1108 
   1109 	if len(cfg.Font) == 0 {
   1110 		log.Fatal("Failed to find a font for the statusbar. Use -font to Pass the path to a monospaced font")
   1111 	}
   1112 
   1113 	// Initialize the main window
   1114 	game := InitializeGame()
   1115 	defer game.cleanup()
   1116 
   1117 	// TODO
   1118 	// * resize events?
   1119 	// * add a status bar (either add to height, or subtract from it)
   1120 
   1121 	// Setup the initial state of the world
   1122 	game.InitializeCells()
   1123 
   1124 	ShowKeysHelp()
   1125 
   1126 	if cfg.Server {
   1127 		ch := make(chan Pattern, 2)
   1128 		game.pChan = ch
   1129 		go Server(cfg.Host, cfg.Port, ch)
   1130 	}
   1131 
   1132 	game.Run()
   1133 }