Replace the legacy Go implementation as the production path, quarantine the reference tree, add deterministic release packaging and version metadata, and run verification through GitLab CI.
130 lines
2.4 KiB
Go
130 lines
2.4 KiB
Go
// bg — cellular automata background for the TUI.
|
|
//
|
|
// Conway's Game of Life rendered at 30 Hz using Unicode half-block
|
|
// characters (▀) to double the effective vertical resolution.
|
|
// The grid is double-buffered for zero-allocation ticking, and
|
|
// render output is built in a pre-allocated strings.Builder so the
|
|
|
|
package bg
|
|
|
|
import (
|
|
"math/rand"
|
|
"strings"
|
|
)
|
|
|
|
// Bright orange (accent) for alive cells, dark grey for dead.
|
|
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
|
|
}
|
|
|
|
// Frame returns the current generation as a single string suitable for
|
|
// writing to the alternate screen.
|
|
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()
|
|
}
|