Skip to content

Commit

Permalink
[life]: left, left drag to set cells; right click to erase and suppor…
Browse files Browse the repository at this point in the history
…t for that in ansipixels. Also WriteBoxed handle multi lines (#64)

* life: left/right clicks. WriteBoxed handle multi line

* More help and cycle on 1/2 pixel

* Adding leftclick drag paint to life
  • Loading branch information
ldemailly authored Oct 7, 2024
1 parent 0ce54ee commit ac5c72d
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 33 deletions.
19 changes: 14 additions & 5 deletions ansipixels/ansipixels.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,20 @@ func (ap *AnsiPixels) DrawBox(x, y, w, h int, topLeft, topRight, bottomLeft, bot

func (ap *AnsiPixels) WriteBoxed(y int, msg string, args ...interface{}) {
s := fmt.Sprintf(msg, args...)
w := ap.ScreenWidth(s)
x := (ap.W - w) / 2
ap.MoveCursor(x, y)
ap.WriteString(s)
ap.DrawRoundBox(x-1, y-1, w+2, 3)
lines := strings.Split(s, "\n")
maxw := 0
widths := make([]int, 0, len(lines))
for _, l := range lines {
w := ap.ScreenWidth(l)
widths = append(widths, w)
maxw = max(maxw, w)
}
for i, l := range lines {
x := (ap.W - widths[i]) / 2
ap.MoveCursor(x, y+i)
ap.WriteString(l)
}
ap.DrawRoundBox((ap.W-maxw)/2-1, y-1, maxw+2, len(lines)+2)
}

func (ap *AnsiPixels) WriteRightBoxed(y int, msg string, args ...interface{}) {
Expand Down
22 changes: 22 additions & 0 deletions ansipixels/mouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,25 @@ func (ap *AnsiPixels) MouseDecode() {
ap.MouseDecode()
ap.Mouse = true
}

const (
MouseLeft = 0
MouseRight = 0b10
MouseMove = 0b100000
)

func (ap *AnsiPixels) LeftClick() bool {
return ap.Mouse && (ap.Mbuttons == MouseLeft)
}

func (ap *AnsiPixels) RightClick() bool {
return ap.Mouse && (ap.Mbuttons == MouseRight)
}

func (ap *AnsiPixels) LeftDrag() bool {
return ap.Mouse && (ap.Mbuttons == MouseMove|MouseLeft)
}

func (ap *AnsiPixels) RightDrag() bool {
return ap.Mouse && (ap.Mbuttons == MouseMove|MouseRight)
}
171 changes: 143 additions & 28 deletions life/life.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,18 @@ func (c *Conway) Set(x, y int) {
c.Cells[1-c.Current][y*c.Width+x] = 1
}

func (c *Conway) SetCurrent(x, y int) {
c.Cells[c.Current][y*c.Width+x] = 1
}

func (c *Conway) Clear(x, y int) {
c.Cells[1-c.Current][y*c.Width+x] = 0
}

func (c *Conway) ClearCurrent(x, y int) {
c.Cells[c.Current][y*c.Width+x] = 0
}

func (c *Conway) Copy(x, y int) {
idx := y*c.Width + x
c.Cells[1-c.Current][idx] = c.Cells[c.Current][idx]
Expand Down Expand Up @@ -125,57 +133,164 @@ func Draw(ap *ansipixels.AnsiPixels, c *Conway) {
}
}

type GameState string

const (
Paused = "Paused"
Running = "Running"
)

type Game struct {
ap *ansipixels.AnsiPixels
c *Conway
state GameState
showInfo bool
showHelp bool
generation uint64
lastClickX, lastClickY int
delta int // which 1/2 pixel we're targeting with the mouse.
lastWasClick bool
}

func Main() int {
fpsFlag := flag.Float64("fps", 60, "Frames per second")
flagRandomFill := flag.Float64("fill", 0.1, "Random fill factor (0 to 1)")
flagGlider := flag.Bool("glider", false, "Start with a glider (default is random)")
cli.Main()
game := &Game{}
ap := ansipixels.NewAnsiPixels(*fpsFlag)
err := ap.Open()
if err != nil {
return log.FErrf("Error opening AnsiPixels: %v", err)
}
defer ap.Restore()
game.ap = ap
defer game.End()
ap.HideCursor()
var generation uint64
var c *Conway
ap.MouseTrackingOn() // needed for drag, other ap.MouseClickOn() is enough.
fillFactor := float32(*flagRandomFill)
ap.OnResize = func() error {
c = NewConway(ap.W, 2*ap.H) // half pixels vertically.
game.c = NewConway(ap.W, 2*ap.H) // half pixels vertically.
if *flagGlider {
c.Glider(ap.W/3, 2*ap.H/3) // first third of the screen
game.c.Glider(ap.W/3, 2*ap.H/3) // first third of the screen
} else {
// Random
c.Randomize(fillFactor)
game.c.Randomize(fillFactor)
}
c.Current = 1 - c.Current
generation = 0
game.c.Current = 1 - game.c.Current
game.generation = 1
game.showInfo = true
game.state = Paused
game.showHelp = true
game.delta = 0
game.DrawOne()
return nil
}
_ = ap.OnResize()
showInfo := true
for {
ap.StartSyncMode()
ap.ClearScreen()
if showInfo {
ap.WriteRight(ap.H-1, "FPS %.0f Generation: %d ", ap.FPS, generation)
switch game.state {
case Running:
_, err := ap.ReadOrResizeOrSignalOnce()
if err != nil {
return log.FErrf("Error reading: %v", err)
}
case Paused:
err := ap.ReadOrResizeOrSignal()
if err != nil {
return log.FErrf("Error reading: %v", err)
}
}
Draw(ap, c)
generation++
ap.EndSyncMode()
n, err := ap.ReadOrResizeOrSignalOnce()
if err != nil {
return log.FErrf("Error reading: %v", err)
if ap.Mouse {
game.HandleMouse()
continue
}
if n > 0 {
switch ap.Data[0] {
case 'q', 'Q', 3:
ap.MoveCursor(0, 0)
return 0
case 'i', 'I':
showInfo = !showInfo
}
if len(ap.Data) == 0 {
game.Next()
continue
}
switch ap.Data[0] {
case 'q', 'Q', 3:
return 0
case 'i', 'I':
game.showInfo = !game.showInfo
case '?', 'h', 'H':
game.showHelp = true
game.state = Paused
case ' ':
game.state = Paused
default:
game.state = Running
}
c.Next()
game.Next()
}
}

func (g *Game) DrawOne() {
g.ap.StartSyncMode()
g.ap.ClearScreen()
if g.showInfo {
g.ap.WriteRight(g.ap.H-1, "%s FPS %.0f Generation: %d ", g.state, g.ap.FPS, g.generation)
}
Draw(g.ap, g.c)
if g.showHelp {
g.ap.WriteBoxed(g.ap.H/2+2, "Space to pause, q to quit, i for info, other key to run\n"+
"Left click or hold to set, right click to clear\nClick in same spot for other half pixel")
g.showHelp = false
}
g.ap.EndSyncMode()
}

func (g *Game) Next() {
g.c.Next()
g.generation++
g.DrawOne()
}

func (g *Game) End() {
g.ap.MouseTrackingOff() // g.ap.MouseClickOff()
g.ap.ShowCursor()
g.ap.MoveCursor(0, g.ap.H-2)
g.ap.Restore()
}

func (g *Game) HandleMouse() {
// maybe we need a different delta for left and right clicks
// but for now it's pretty good to cycle a pixels' 2 halves. (2 left clicks, 2 right clicks)
delta := 0
sameSpot := g.ap.Mx == g.lastClickX && g.ap.My == g.lastClickY
prevWasClick := g.lastWasClick
leftDrag := g.ap.LeftDrag()
ld := prevWasClick && leftDrag && !sameSpot
if sameSpot {
delta = 1 - g.delta
} else if ld {
delta = g.delta
}
g.lastWasClick = false
switch {
case g.ap.LeftClick(), ld:
log.LogVf("Mouse left (%06b) click (drag %t) at %d, %d", g.ap.Mbuttons, ld, g.ap.Mx, g.ap.My)
g.c.SetCurrent(g.ap.Mx-1, (g.ap.My-1)*2+delta)
g.lastWasClick = true
if ld {
g.DrawOne()
return
}
case g.ap.RightClick():
log.LogVf("Mouse right (%06b) click (drag %t) at %d, %d", g.ap.Mbuttons, leftDrag, g.ap.Mx, g.ap.My)
g.c.ClearCurrent(g.ap.Mx-1, (g.ap.My-1)*2+delta)
g.lastWasClick = true
default:
log.LogVf("Mouse %06b at %d, %d last was click %t same spot %t left drag %t",
g.ap.Mbuttons, g.ap.Mx, g.ap.My,
prevWasClick, sameSpot, leftDrag)
return
}
if sameSpot {
g.delta = 1 - g.delta
} else {
g.delta = 0
}
g.lastClickX = g.ap.Mx
g.lastClickY = g.ap.My
g.DrawOne()
}

0 comments on commit ac5c72d

Please sign in to comment.