src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

life.go (4555B)


      1 package life
      2 
      3 import (
      4 	"bytes"
      5 	"context"
      6 	"fmt"
      7 	"math/rand"
      8 	"os"
      9 	"time"
     10 
     11 	"code.dwrz.net/src/pkg/color"
     12 	"code.dwrz.net/src/pkg/log"
     13 	"code.dwrz.net/src/pkg/terminal"
     14 	"code.dwrz.net/src/pkg/terminal/input"
     15 )
     16 
     17 const (
     18 	alive = color.BackgroundBlack + "  " + color.Reset
     19 	born  = color.BackgroundGreen + "  " + color.Reset
     20 	died  = color.BackgroundRed + "  " + color.Reset
     21 	dead  = "  "
     22 )
     23 
     24 var random = rand.New(rand.NewSource(time.Now().UnixNano()))
     25 
     26 type Life struct {
     27 	errs     chan error
     28 	events   chan *input.Event
     29 	height   int
     30 	in       *os.File
     31 	input    *input.Reader
     32 	last     [][]bool
     33 	log      *log.Logger
     34 	out      *os.File
     35 	terminal *terminal.Terminal
     36 	tick     time.Duration
     37 	ticker   *time.Ticker
     38 	turn     uint
     39 	width    int
     40 	world    [][]bool
     41 }
     42 
     43 type Parameters struct {
     44 	Height   int
     45 	In       *os.File
     46 	Log      *log.Logger
     47 	Out      *os.File
     48 	Terminal *terminal.Terminal
     49 	Tick     time.Duration
     50 	Width    int
     51 }
     52 
     53 func New(p Parameters) *Life {
     54 	// Account for two spaces to represent each cell.
     55 	p.Width /= 2
     56 	// Account one line to render the turn count.
     57 	p.Height -= 1
     58 
     59 	var life = &Life{
     60 		errs:     make(chan error),
     61 		events:   make(chan *input.Event),
     62 		height:   p.Height,
     63 		in:       p.In,
     64 		last:     make([][]bool, p.Height),
     65 		log:      p.Log,
     66 		out:      p.Out,
     67 		terminal: p.Terminal,
     68 		tick:     p.Tick,
     69 		ticker:   time.NewTicker(p.Tick),
     70 		width:    p.Width,
     71 		world:    make([][]bool, p.Height),
     72 	}
     73 	for i := range life.world {
     74 		life.last[i] = make([]bool, p.Width)
     75 		life.world[i] = make([]bool, p.Width)
     76 	}
     77 	for i := 0; i < (p.Height * p.Width / 4); i++ {
     78 		x := random.Intn(p.Width)
     79 		y := random.Intn(p.Height)
     80 
     81 		life.world[y][x] = true
     82 	}
     83 
     84 	life.input = input.New(input.Parameters{
     85 		Chan: life.events,
     86 		In:   os.Stdin,
     87 		Log:  life.log,
     88 	})
     89 
     90 	return life
     91 }
     92 
     93 func (l *Life) Run(ctx context.Context) error {
     94 	// Setup the terminal.
     95 	if err := l.terminal.SetRaw(); err != nil {
     96 		return fmt.Errorf(
     97 			"failed to set terminal attributes: %v", err,
     98 		)
     99 	}
    100 
    101 	// Clean up before exit.
    102 	defer l.quit()
    103 
    104 	// TODO: refactor to use a terminal output package.
    105 	l.out.WriteString(terminal.ClearScreen)
    106 	l.out.WriteString(terminal.CursorHide)
    107 	l.render()
    108 
    109 	// Start reading user input.
    110 	go func() {
    111 		if err := l.input.Run(ctx); err != nil {
    112 			l.errs <- err
    113 		}
    114 	}()
    115 
    116 	// Main loop.
    117 	for {
    118 		select {
    119 		case <-ctx.Done():
    120 			return nil
    121 
    122 		case <-l.ticker.C:
    123 			l.step()
    124 			l.render()
    125 
    126 		case event := <-l.events:
    127 			switch event.Rune {
    128 			case 'q': // Quit
    129 				return nil
    130 			}
    131 		}
    132 	}
    133 }
    134 
    135 func (l *Life) alive(x, y int) bool {
    136 	// Wrap coordinates toroidally.
    137 	x += l.width
    138 	x %= l.width
    139 	y += l.height
    140 	y %= l.height
    141 
    142 	return l.world[y][x]
    143 }
    144 
    145 func (l *Life) born(x, y int) bool {
    146 	// Wrap coordinates toroidally.
    147 	x += l.width
    148 	x %= l.width
    149 	y += l.height
    150 	y %= l.height
    151 
    152 	return !l.last[y][x] && l.world[y][x]
    153 }
    154 
    155 func (l *Life) died(x, y int) bool {
    156 	// Wrap coordinates toroidally.
    157 	x += l.width
    158 	x %= l.width
    159 	y += l.height
    160 	y %= l.height
    161 
    162 	return l.last[y][x] && !l.world[y][x]
    163 }
    164 
    165 func (l *Life) next(x, y int) bool {
    166 	// Count the numbers of living neighbors.
    167 	var alive = 0
    168 	for v := -1; v <= 1; v++ {
    169 		for h := -1; h <= 1; h++ {
    170 			// Ignore self.
    171 			if h == 0 && v == 0 {
    172 				continue
    173 			}
    174 			if l.alive(x+v, y+h) {
    175 				alive++
    176 			}
    177 		}
    178 	}
    179 
    180 	// Determine the next state.
    181 	switch alive {
    182 	case 3: // Turn on.
    183 		return true
    184 	case 2: // Maintain state.
    185 		return l.alive(x, y)
    186 	default: // Turn off.
    187 		return false
    188 	}
    189 }
    190 
    191 func (l *Life) quit() {
    192 	l.out.Write([]byte(terminal.CursorShow))
    193 
    194 	if err := l.terminal.Reset(); err != nil {
    195 		l.log.Error.Printf(
    196 			"failed to reset terminal attributes: %v", err,
    197 		)
    198 	}
    199 }
    200 
    201 func (l *Life) render() {
    202 	var buf bytes.Buffer
    203 
    204 	buf.WriteString(terminal.CursorTopLeft)
    205 
    206 	for y := 0; y < l.height; y++ {
    207 		for x := 0; x < l.width; x++ {
    208 			switch {
    209 			case l.born(x, y):
    210 				buf.WriteString(born)
    211 			case l.died(x, y):
    212 				buf.WriteString(died)
    213 			case l.alive(x, y):
    214 				buf.WriteString(alive)
    215 			default:
    216 				buf.WriteString(dead)
    217 
    218 			}
    219 		}
    220 		buf.WriteString("\r\n")
    221 	}
    222 	fmt.Fprintf(
    223 		&buf, "%sTurn:%s %d\r\n",
    224 		color.Bold, color.Reset, l.turn,
    225 	)
    226 
    227 	l.out.Write(buf.Bytes())
    228 }
    229 
    230 func (l *Life) step() {
    231 	// Create the next world.
    232 	var next = make([][]bool, l.height)
    233 	for i := range next {
    234 		next[i] = make([]bool, l.width)
    235 	}
    236 
    237 	// Set the new cells based on the existing cells.
    238 	for y := 0; y < l.height; y++ {
    239 		for x := 0; x < l.width; x++ {
    240 			next[y][x] = l.next(x, y)
    241 		}
    242 	}
    243 
    244 	// Increment the turn and set the next world.
    245 	l.turn++
    246 	l.last = l.world
    247 	l.world = next
    248 }