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 (33747B)


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