// Pack two cell rows into each Unicode half-block row; two buffers keep frame // updates allocation-free. package bg import ( "math/rand" "strings" ) const ( fgAlive = "\x1b[38;2;255;127;0m" fgDead = "\x1b[38;2;30;30;30m" bgAlive = "\x1b[48;2;255;127;0m" bgDead = "\x1b[48;2;30;30;30m" reset = "\x1b[0m" block = "▀" ) type Automata struct { w, h int a, b []uint8 useA bool buf strings.Builder } func NewAutomata(w, h int) *Automata { a := &Automata{useA: true} a.Resize(w, h) return a } func (a *Automata) Resize(w, h int) { logH := h * 2 if a.w == w && a.h == logH { return } a.w = w a.h = logH sz := w * logH a.a = make([]uint8, sz) a.b = make([]uint8, sz) for i := 0; i < sz; i++ { if rand.Float32() > 0.85 { a.a[i] = 1 } } a.buf.Grow(w * h * 40) } func (a *Automata) Tick() { src, dst := a.a, a.b if !a.useA { src, dst = a.b, a.a } w, h := a.w, a.h for y := range h { for x := range w { i := y*w + x alive := src[i] n := a.count(src, x, y, w, h) if alive == 1 && (n == 2 || n == 3) { dst[i] = 1 } else if alive == 0 && n == 3 { dst[i] = 1 } else { dst[i] = 0 } } } a.useA = !a.useA } func (a *Automata) count(g []uint8, x, y, w, h int) uint8 { var c uint8 for dy := -1; dy <= 1; dy++ { for dx := -1; dx <= 1; dx++ { if dx == 0 && dy == 0 { continue } nx, ny := x+dx, y+dy if nx >= 0 && nx < w && ny >= 0 && ny < h { c += g[ny*w+nx] } } } return c } func (a *Automata) Frame() string { a.buf.Reset() src := a.a if !a.useA { src = a.b } w, h := a.w, a.h for y := 0; y < h; y += 2 { for x := range w { top := src[y*w+x] bot := uint8(0) if y+1 < h { bot = src[(y+1)*w+x] } if top == 1 { a.buf.WriteString(fgAlive) } else { a.buf.WriteString(fgDead) } if bot == 1 { a.buf.WriteString(bgAlive) } else { a.buf.WriteString(bgDead) } a.buf.WriteString(block) } a.buf.WriteString(reset) if y < h-2 { a.buf.WriteByte('\n') } } return a.buf.String() }