Compare commits
4
Commits
fd077d1ab1
...
trunk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8869ea437f
|
||
|
|
fe4347d4ea
|
||
|
|
58fb23a668
|
||
|
|
f857cc18fd
|
@@ -0,0 +1,7 @@
|
|||||||
|
[[file:life.png]]
|
||||||
|
|
||||||
|
[[https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life][Conway's Game of Life]], written December 24-25, 2022.
|
||||||
|
|
||||||
|
Colors indicate change. Green for cells born, red for those deceased. Stable cells are black.
|
||||||
|
|
||||||
|
Press q to quit.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.chimeric.al/dwrz/life/internal/life"
|
||||||
|
"code.chimeric.al/dwrz/life/pkg/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
height = flag.Int("h", 0, "height")
|
||||||
|
width = flag.Int("w", 0, "width")
|
||||||
|
tick = flag.Int("t", 100, "ms between turns")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
AddSource: true,
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Setup the main context.
|
||||||
|
// The context cancels on SIGINT or SIGTERM.
|
||||||
|
ctx, stop := signal.NotifyContext(
|
||||||
|
context.Background(),
|
||||||
|
syscall.SIGINT,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
if err := run(ctx, log); err != nil {
|
||||||
|
log.Error("failed", slog.Any("error", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, log *slog.Logger) error {
|
||||||
|
if *height < 0 {
|
||||||
|
return fmt.Errorf("invalid height: %d", *height)
|
||||||
|
}
|
||||||
|
if *width < 0 {
|
||||||
|
return fmt.Errorf("invalid width: %d", *width)
|
||||||
|
}
|
||||||
|
if *tick <= 0 {
|
||||||
|
return fmt.Errorf("invalid tick: %d", *tick)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup workspace and log file.
|
||||||
|
cdir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"failed to determine user cache directory: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
wdir := filepath.Join(cdir, "life")
|
||||||
|
|
||||||
|
if err := os.MkdirAll(wdir, os.ModeDir|0700); err != nil {
|
||||||
|
return fmt.Errorf("failed to create tmp dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(wdir + "/log")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create log file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var gameLog = slog.New(slog.NewTextHandler(f, &slog.HandlerOptions{
|
||||||
|
AddSource: true,
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Retrieve terminal info.
|
||||||
|
t, err := terminal.New(os.Stdin.Fd())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get terminal attributes: %w", err)
|
||||||
|
}
|
||||||
|
size, err := t.Size()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get terminal size: %w", err)
|
||||||
|
}
|
||||||
|
if *height == 0 {
|
||||||
|
rows := int(size.Rows)
|
||||||
|
height = &rows
|
||||||
|
}
|
||||||
|
if *width == 0 {
|
||||||
|
cols := int(size.Columns)
|
||||||
|
width = &cols
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the game.
|
||||||
|
var game = life.New(life.Parameters{
|
||||||
|
Height: *height,
|
||||||
|
In: os.Stdin,
|
||||||
|
Log: gameLog,
|
||||||
|
Out: os.Stdout,
|
||||||
|
Terminal: t,
|
||||||
|
Tick: time.Duration(*tick) * time.Millisecond,
|
||||||
|
Width: *width,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run the game.
|
||||||
|
return game.Run(ctx)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClearScreen = "\u001Bc"
|
||||||
|
CursorHide = "\u001B[?25l"
|
||||||
|
CursorShow = "\u001B[?25h"
|
||||||
|
CursorTopLeft = "\u001B[H"
|
||||||
|
EraseLine = "\u001B[K"
|
||||||
|
|
||||||
|
SetCursorFmt = "\u001B[%d;%dH"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Terminal struct {
|
||||||
|
fd uintptr
|
||||||
|
termios *syscall.Termios
|
||||||
|
}
|
||||||
|
|
||||||
|
type Size struct {
|
||||||
|
Rows uint16
|
||||||
|
Columns uint16
|
||||||
|
XPixels uint16
|
||||||
|
YPixels uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fd uintptr) (*Terminal, error) {
|
||||||
|
var t = &Terminal{
|
||||||
|
fd: fd,
|
||||||
|
termios: &syscall.Termios{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
fd,
|
||||||
|
syscall.TCGETS,
|
||||||
|
uintptr(unsafe.Pointer(t.termios)),
|
||||||
|
); err != 0 {
|
||||||
|
return nil, fmt.Errorf("ioctl syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the terminal into the original termios.
|
||||||
|
func (t *Terminal) Reset() error {
|
||||||
|
if _, _, err := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
t.fd,
|
||||||
|
uintptr(syscall.TCSETS),
|
||||||
|
uintptr(unsafe.Pointer(t.termios)),
|
||||||
|
); err != 0 {
|
||||||
|
return fmt.Errorf("ioctl syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRaw enables raw mode.
|
||||||
|
func (t *Terminal) SetRaw() error {
|
||||||
|
var termios = *t.termios
|
||||||
|
termios.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
|
||||||
|
termios.Oflag &^= syscall.OPOST
|
||||||
|
termios.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
|
||||||
|
termios.Cflag &^= syscall.CSIZE | syscall.PARENB
|
||||||
|
termios.Cflag |= syscall.CS8
|
||||||
|
termios.Cc[syscall.VMIN] = 1
|
||||||
|
termios.Cc[syscall.VTIME] = 0
|
||||||
|
|
||||||
|
if _, _, err := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
t.fd,
|
||||||
|
uintptr(syscall.TCSETS),
|
||||||
|
uintptr(unsafe.Pointer(&termios)),
|
||||||
|
); err != 0 {
|
||||||
|
return fmt.Errorf("ioctl syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Size() (*Size, error) {
|
||||||
|
var size = &Size{}
|
||||||
|
if _, _, err := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
t.fd,
|
||||||
|
syscall.TIOCGWINSZ,
|
||||||
|
uintptr(unsafe.Pointer(size)),
|
||||||
|
); err != 0 {
|
||||||
|
return nil, fmt.Errorf("ioctl syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user