Add life pkg
This commit is contained in:
@@ -0,0 +1,259 @@
|
|||||||
|
package life
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.chimeric.al/dwrz/life/pkg/color"
|
||||||
|
"code.chimeric.al/dwrz/life/pkg/terminal"
|
||||||
|
"code.chimeric.al/dwrz/life/pkg/terminal/input"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
alive = color.BackgroundBlack + " " + color.Reset
|
||||||
|
born = color.BackgroundGreen + " " + color.Reset
|
||||||
|
died = color.BackgroundRed + " " + color.Reset
|
||||||
|
dead = " "
|
||||||
|
)
|
||||||
|
|
||||||
|
var random = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
type Life struct {
|
||||||
|
errs chan error
|
||||||
|
events chan *input.Event
|
||||||
|
height int
|
||||||
|
in *os.File
|
||||||
|
input *input.Reader
|
||||||
|
last [][]bool
|
||||||
|
log *slog.Logger
|
||||||
|
out *os.File
|
||||||
|
terminal *terminal.Terminal
|
||||||
|
tick time.Duration
|
||||||
|
ticker *time.Ticker
|
||||||
|
turn uint
|
||||||
|
width int
|
||||||
|
world [][]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parameters struct {
|
||||||
|
Height int
|
||||||
|
In *os.File
|
||||||
|
Log *slog.Logger
|
||||||
|
Out *os.File
|
||||||
|
Terminal *terminal.Terminal
|
||||||
|
Tick time.Duration
|
||||||
|
Width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(p Parameters) *Life {
|
||||||
|
// Account for two spaces to represent each cell.
|
||||||
|
p.Width /= 2
|
||||||
|
// Account one line to render the turn count.
|
||||||
|
p.Height -= 1
|
||||||
|
|
||||||
|
var life = &Life{
|
||||||
|
errs: make(chan error, 1),
|
||||||
|
events: make(chan *input.Event),
|
||||||
|
height: p.Height,
|
||||||
|
in: p.In,
|
||||||
|
last: make([][]bool, p.Height),
|
||||||
|
log: p.Log,
|
||||||
|
out: p.Out,
|
||||||
|
terminal: p.Terminal,
|
||||||
|
tick: p.Tick,
|
||||||
|
ticker: time.NewTicker(p.Tick),
|
||||||
|
width: p.Width,
|
||||||
|
world: make([][]bool, p.Height),
|
||||||
|
}
|
||||||
|
for i := range life.world {
|
||||||
|
life.last[i] = make([]bool, p.Width)
|
||||||
|
life.world[i] = make([]bool, p.Width)
|
||||||
|
}
|
||||||
|
for i := 0; i < (p.Height * p.Width / 4); i++ {
|
||||||
|
x := random.Intn(p.Width)
|
||||||
|
y := random.Intn(p.Height)
|
||||||
|
|
||||||
|
life.world[y][x] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
life.input = input.New(input.Parameters{
|
||||||
|
Chan: life.events,
|
||||||
|
In: os.Stdin,
|
||||||
|
Log: life.log,
|
||||||
|
})
|
||||||
|
|
||||||
|
return life
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Life) Run(ctx context.Context) error {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Setup the terminal.
|
||||||
|
if err := l.terminal.SetRaw(); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"failed to set terminal attributes: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up before exit.
|
||||||
|
defer l.quit()
|
||||||
|
|
||||||
|
l.out.WriteString(terminal.ClearScreen)
|
||||||
|
l.out.WriteString(terminal.CursorHide)
|
||||||
|
l.render()
|
||||||
|
|
||||||
|
// Start reading user input.
|
||||||
|
// The reader may be blocked in a read when the game ends.
|
||||||
|
// The reader stops when the process stops.
|
||||||
|
go func() {
|
||||||
|
if err := l.input.Run(ctx); err != nil &&
|
||||||
|
!errors.Is(err, context.Canceled) {
|
||||||
|
l.errs <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Main loop.
|
||||||
|
// TODO: handle sigwinch; resize the world on terminal resize.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case err := <-l.errs:
|
||||||
|
return err
|
||||||
|
|
||||||
|
case <-l.ticker.C:
|
||||||
|
l.step()
|
||||||
|
l.render()
|
||||||
|
|
||||||
|
case event := <-l.events:
|
||||||
|
switch event.Rune {
|
||||||
|
case 'q': // Quit
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Life) alive(x, y int) bool {
|
||||||
|
// Wrap coordinates toroidally.
|
||||||
|
x += l.width
|
||||||
|
x %= l.width
|
||||||
|
y += l.height
|
||||||
|
y %= l.height
|
||||||
|
|
||||||
|
return l.world[y][x]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Life) born(x, y int) bool {
|
||||||
|
// Wrap coordinates toroidally.
|
||||||
|
x += l.width
|
||||||
|
x %= l.width
|
||||||
|
y += l.height
|
||||||
|
y %= l.height
|
||||||
|
|
||||||
|
return !l.last[y][x] && l.world[y][x]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Life) died(x, y int) bool {
|
||||||
|
// Wrap coordinates toroidally.
|
||||||
|
x += l.width
|
||||||
|
x %= l.width
|
||||||
|
y += l.height
|
||||||
|
y %= l.height
|
||||||
|
|
||||||
|
return l.last[y][x] && !l.world[y][x]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Life) next(x, y int) bool {
|
||||||
|
// Count the numbers of living neighbors.
|
||||||
|
var alive = 0
|
||||||
|
for v := -1; v <= 1; v++ {
|
||||||
|
for h := -1; h <= 1; h++ {
|
||||||
|
// Ignore self.
|
||||||
|
if h == 0 && v == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if l.alive(x+v, y+h) {
|
||||||
|
alive++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the next state.
|
||||||
|
switch alive {
|
||||||
|
case 3: // Turn on.
|
||||||
|
return true
|
||||||
|
case 2: // Maintain state.
|
||||||
|
return l.alive(x, y)
|
||||||
|
default: // Turn off.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Life) quit() {
|
||||||
|
l.out.Write([]byte(terminal.CursorShow))
|
||||||
|
|
||||||
|
if err := l.terminal.Reset(); err != nil {
|
||||||
|
l.log.Error(
|
||||||
|
"failed to reset terminal attributes",
|
||||||
|
slog.Any("error", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Life) render() {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
buf.WriteString(terminal.CursorTopLeft)
|
||||||
|
|
||||||
|
for y := 0; y < l.height; y++ {
|
||||||
|
for x := 0; x < l.width; x++ {
|
||||||
|
switch {
|
||||||
|
case l.born(x, y):
|
||||||
|
buf.WriteString(born)
|
||||||
|
case l.died(x, y):
|
||||||
|
buf.WriteString(died)
|
||||||
|
case l.alive(x, y):
|
||||||
|
buf.WriteString(alive)
|
||||||
|
default:
|
||||||
|
buf.WriteString(dead)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString("\r\n")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(
|
||||||
|
&buf, "%sTurn:%s %d\r\n",
|
||||||
|
color.Bold, color.Reset, l.turn,
|
||||||
|
)
|
||||||
|
|
||||||
|
l.out.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Life) step() {
|
||||||
|
// Create the next world.
|
||||||
|
var next = make([][]bool, l.height)
|
||||||
|
for i := range next {
|
||||||
|
next[i] = make([]bool, l.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new cells based on the existing cells.
|
||||||
|
for y := 0; y < l.height; y++ {
|
||||||
|
for x := 0; x < l.width; x++ {
|
||||||
|
next[y][x] = l.next(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the turn and set the next world.
|
||||||
|
l.turn++
|
||||||
|
l.last = l.world
|
||||||
|
l.world = next
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user