Skip to content

Commit

Permalink
Refactor draw functions to be less repetitive and more readable. Add …
Browse files Browse the repository at this point in the history
…draw_lib.go with a general-purpose draw function and utilities for anchors and colors.
  • Loading branch information
armsnyder committed Oct 20, 2020
1 parent 2640a33 commit b2f657c
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 152 deletions.
20 changes: 10 additions & 10 deletions pkg/client/scenes/confetti.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var confettiShapes = []rune{'▪', '▮', '▰', '▴', '▸', '▾', '◂', '

type paper struct {
x, y int
color termbox.Attribute
color color
}

type confetti []*paper
Expand All @@ -43,16 +43,20 @@ func (c *confetti) tick() {
oldPaper := *c
*c = nil
for _, p := range oldPaper {
if p.x < width && p.y < height {
if p.x >= -width/2 && p.x < width/2 && p.y >= -height/2 && p.y < height/2 {
*c = append(*c, p)
}
}

// Spawn new paper.
for i := 0; i < width/30; i++ {
x := rand.Intn(width) //nolint:gosec
color := confettiColors[rand.Intn(len(confettiColors))] //nolint:gosec
*c = append(*c, &paper{x: x, color: color})
x := rand.Intn(width) - width/2 //nolint:gosec
y := -height / 2
color := func() (fg, bg termbox.Attribute) {
return confettiColors[rand.Intn(len(confettiColors))], termbox.ColorDefault //nolint:gosec
}

*c = append(*c, &paper{x: x, y: y, color: color})
}
}

Expand All @@ -67,10 +71,6 @@ func (c *confetti) draw() {

for _, p := range *c {
shape := confettiShapes[rand.Intn(len(confettiShapes))] //nolint:gosec
termbox.SetCell(p.x, p.y, shape, p.color, termbox.ColorDefault)
draw(offset(center, p.x, p.y), p.color, shape)
}

// Clear the last color.
width, height := termbox.Size()
termbox.SetCell(width-1, height-1, ' ', termbox.ColorDefault, termbox.ColorDefault)
}
159 changes: 159 additions & 0 deletions pkg/client/scenes/draw_lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package scenes

import (
"fmt"
"math"
"strings"

"github.com/nsf/termbox-go"
)

var gameBoyWidth, gameBoyHeight = 96, 24

// draw is a general function for drawing to the terminal.
// The text can be a rune, a string, or a multiline string, which will be drawn relative to the
// specified anchor. This function should always be used instead of termbox.SetCell().
func draw(anchor anchor, color color, text interface{}) {
switch t := text.(type) {
case rune:
positionX, positionY, _, _ := anchor()
fg, bg := color()
termbox.SetCell(positionX, positionY, t, fg, bg)

case string:
rows := strings.Split(t, "\n")
textHeight := len(rows)
textWidth := maxLength(rows)
_, _, drawDirectionX, drawDirectionY := anchor()
offsetX := int(math.Round(float64(textWidth) * drawDirectionX))
offsetY := int(math.Round(float64(textHeight) * drawDirectionY))

for i, row := range rows {
for j, ch := range []rune(row) { // Converting to []rune first gets us tight alignment.
draw(offset(anchor, offsetX+j, offsetY+i), color, ch)
}
}

default:
panic(fmt.Errorf("unsupported draw text type %T", text))
}
}

// anchor defines a position offset and direction that can be used for drawing.
type anchor func() (positionX, positionY int, drawDirectionX, drawDirectionY float64)

// origin is an anchor on the top-left corner of the terminal window.
func origin() (positionX, positionY int, drawDirectionX, drawDirectionY float64) {
return 0, 0, 0, 0
}

// topLeft is an anchor on the top-left corner of the game window.
func topLeft() (positionX, positionY int, drawDirectionX, drawDirectionY float64) {
termWidth, termHeight := termbox.Size()
leftX := (termWidth - gameBoyWidth) / 2
topY := (termHeight - gameBoyHeight) / 2
// Add an inner margin while also ensuring the text is always on-screen.
return max(0, leftX+5), max(0, topY+2), 0, 0
}

// topRight is an anchor on the top-right corner of the game window.
func topRight() (positionX, positionY int, drawDirectionX, drawDirectionY float64) {
termWidth, termHeight := termbox.Size()
rightX := (termWidth + gameBoyWidth) / 2
topY := (termHeight - gameBoyHeight) / 2
// Add an inner margin while also ensuring the text is always on-screen.
return min(termWidth, rightX-3), max(0, topY+2), -1, 0
}

// center is an anchor the center-middle of the terminal that draws from the center outward.
func center() (positionX, positionY int, drawDirectionX, drawDirectionY float64) {
termWidth, termHeight := termbox.Size()
centerX := termWidth / 2
centerY := termHeight / 2
return centerX, centerY, -0.5, -0.5
}

// centerLeft is an anchor the center-middle of the terminal that draws toward the left side.
func centerLeft() (positionX, positionY int, drawDirectionX, drawDirectionY float64) {
positionX, positionY, _, _ = center()
return positionX, positionY, -1, -0.5
}

// centerRight is an anchor the center-middle of the terminal that draws toward the right side.
func centerRight() (positionX, positionY int, drawDirectionX, drawDirectionY float64) {
positionX, positionY, _, _ = center()
return positionX, positionY, 0, -0.5
}

// centerTop is an anchor the center-middle of the terminal that draws toward the top.
func centerTop() (positionX, positionY int, drawDirectionX, drawDirectionY float64) {
positionX, positionY, _, _ = center()
return positionX, positionY, -0.5, -1
}

// offset returns an anchor that is offset from the specified anchor by a specified position.
func offset(anchor anchor, x, y int) anchor {
return func() (positionX, positionY int, drawDirectionX, drawDirectionY float64) {
positionX, positionY, drawDirectionX, drawDirectionY = anchor()
positionX += x
positionY += y
return
}
}

// color defines foreground and background attributes for drawing.
type color func() (fg, bg termbox.Attribute)

// normal is the default color.
func normal() (fg, bg termbox.Attribute) {
return termbox.ColorDefault, termbox.ColorDefault
}

// inverted is a color that inverts the text and background color to appear as a highlight.
func inverted() (fg, bg termbox.Attribute) {
return termbox.ColorBlack, termbox.ColorWhite
}

// magenta is a magenta color.
func magenta() (fg, bg termbox.Attribute) {
return termbox.ColorMagenta, termbox.ColorDefault
}

// green is a green color.
func green() (fg, bg termbox.Attribute) {
return termbox.ColorGreen, termbox.ColorDefault
}

func drawGameBoyBorder() {
termWidth, termHeight := termbox.Size()

topY := (termHeight - gameBoyHeight) / 2
bottomY := (termHeight + gameBoyHeight) / 2
leftX := (termWidth - gameBoyWidth) / 2
rightX := (termWidth + gameBoyWidth) / 2

borderRunes := []rune{'🎃', '🧟', '🔮', '🧛', '🍬', '👻'}

for i := 0; i < gameBoyWidth/2; i++ {
ch := borderRunes[i%len(borderRunes)]
draw(offset(origin, leftX+i*2, topY), normal, ch)
draw(offset(origin, leftX+i*2, bottomY), normal, ch)
}

for i := 0; i <= gameBoyHeight; i++ {
ch := borderRunes[i%len(borderRunes)]
draw(offset(origin, leftX, topY+i), normal, ch)
draw(offset(origin, rightX, topY+i), normal, ch)
}
}

func drawSplash() {
draw(offset(centerTop, 0, 1), normal, `
_ _ _
___ | |_| |__ ___| | __ _ ___
/ _ \| __| '_ \ / _ \ |/ _`+"`"+` |/ _ \
| (_) | |_| | | | __/ | (_| | (_) |
\___/ \__|_| |_|\___|_|\__, |\___/
|___/
`)
}
83 changes: 25 additions & 58 deletions pkg/client/scenes/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,17 @@ func (g *Game) Draw() {
g.confetti.draw()
}

var playerColors = map[int]termbox.Attribute{
1: termbox.ColorMagenta,
2: termbox.ColorGreen,
}
var playerColors = map[int]color{1: magenta, 2: green}

func drawDisk(player, x, y int) {
color := playerColors[player]
termbox.SetCell(x, y, '⬤', color, termbox.ColorDefault)
termbox.SetCell(x+1, y, ' ', color, termbox.ColorDefault) // Prevent half-circle on some terminals.
func drawDisk(anchor anchor, player int) {
// The extra space prevents a half-circle on some terminals.
draw(anchor, playerColors[player], "⬤ ")
}

func (g *Game) drawYouAre() {
topY, _, leftX, _ := corners()

youAreText := "You are: "
drawStringDefault(youAreText, leftX+5, topY+2)
drawDisk(g.player, leftX+5+len(youAreText), topY+2)
draw(topLeft, normal, youAreText)
drawDisk(offset(topLeft, len(youAreText), 0), g.player)
}

var (
Expand All @@ -113,46 +107,39 @@ var (
)

func (g *Game) drawScore() {
topY, _, _, rightX := corners()
// Text.
scoreText := "Score: "
draw(offset(topRight, len(scoreText)-20, 0), normal, scoreText)

var (
p2ScoreXOffset = rightX - 5
p2DiskXOffset = p2ScoreXOffset - 3
p1ScoreXOffset = p2DiskXOffset - 4
p1DiskXOffset = p1ScoreXOffset - 3
scoreText = "Score: "
scoreXOffset = p1DiskXOffset - len(scoreText)
)
// P1 score.
drawDisk(offset(topRight, -8, 0), 1)
draw(offset(topRight, -7, 0), normal, fmt.Sprintf("%2d", g.p1Score))

drawStringDefault(scoreText, scoreXOffset, topY+2)
drawStringDefault(fmt.Sprintf("%2d", g.p1Score), p1ScoreXOffset, topY+2)
drawStringDefault(fmt.Sprintf("%2d", g.p2Score), p2ScoreXOffset, topY+2)
drawDisk(1, p1DiskXOffset, topY+2)
drawDisk(2, p2DiskXOffset, topY+2)
// P2 score.
drawDisk(offset(topRight, -1, 0), 2)
draw(topRight, normal, fmt.Sprintf("%2d", g.p2Score))

// Current turn indicator
if !common.GameOver(g.board) {
var xOffset int
if common.WhoseTurn(g.board) == 1 {
drawStringDefault("﹌", p1DiskXOffset, topY+3)
xOffset = -9
} else {
drawStringDefault("﹌", p2DiskXOffset, topY+3)
xOffset = -2
}
draw(offset(topRight, xOffset, 1), normal, "﹌")
}
}

func drawBoardOutline() {
termWidth, termHeight := termbox.Size()

var (
boardWidth = common.BoardSize * squareWidth
boardHeight = common.BoardSize * squareHeight
)

offsetX, offsetY := (termWidth-boardWidth)/2, (termHeight-boardHeight)/2

// Outline
for x := 0; x <= boardWidth; x++ {
for y := 0; y <= boardHeight; y++ {
for x := -boardWidth / 2; x <= boardWidth/2; x++ {
for y := -boardHeight / 2; y <= boardHeight/2; y++ {
var value rune

switch {
Expand All @@ -164,32 +151,23 @@ func drawBoardOutline() {
value = '|'
}

termbox.SetCell(offsetX+x, offsetY+y, value, termbox.ColorDefault, termbox.ColorDefault)
draw(offset(center, x, y), normal, value)
}
}
}

func (g *Game) drawDisks() {
termWidth, termHeight := termbox.Size()

var (
boardWidth = common.BoardSize * squareWidth
boardHeight = common.BoardSize * squareHeight
)

offsetX, offsetY := (termWidth-boardWidth)/2, (termHeight-boardHeight)/2

for i := 0; i < common.BoardSize; i++ {
for j := 0; j < common.BoardSize; j++ {
player := g.board[i][j]
if player == 0 {
continue
}

x := offsetX + squareWidth/2 + squareWidth*i
y := offsetY + squareHeight/2 + squareHeight*j
x := (i+1-common.BoardSize/2)*squareWidth - 1
y := (j + 1 - common.BoardSize/2) * squareHeight

drawDisk(player, x, y)
drawDisk(offset(center, x, y), player)
}
}
}
Expand All @@ -213,14 +191,3 @@ func (g *Game) drawCursor() {
)
}
}

func clamp(val, min, max int) int {
switch {
case val < min:
return min
case val >= max:
return max - 1
default:
return val
}
}
24 changes: 12 additions & 12 deletions pkg/client/scenes/menu.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package scenes

import (
"fmt"

"github.com/nsf/termbox-go"
)

Expand Down Expand Up @@ -34,24 +36,22 @@ func (m *Menu) OnTerminalEvent(event termbox.Event) error {

func (m *Menu) Draw() {
drawGameBoyBorder()
drawUpperRight("Did you know? Your name is " + m.nickname + "!")
drawFromCenter(splashText, 0, -6, termbox.ColorDefault, termbox.ColorDefault)
drawSplash()

draw(topRight, normal, fmt.Sprintf("Did you know? Your name is %s!", m.nickname))

var (
newGameText = "[ NEW GAME ]"
joinGameText = "[ JOIN GAME ]"
)

drawStringHighlight := func(s string, dx, dy int, highlight bool) {
var fg, bg termbox.Attribute
if highlight {
fg = termbox.ColorBlack
bg = termbox.ColorWhite
}

drawFromCenter(s, dx, dy, fg, bg)
var buttonColors [2]color
if m.isJoinGame {
buttonColors = [2]color{normal, inverted}
} else {
buttonColors = [2]color{inverted, normal}
}

drawStringHighlight(newGameText, -len(newGameText)/2-1, 3, !m.isJoinGame)
drawStringHighlight(joinGameText, len(joinGameText)/2+1, 3, m.isJoinGame)
draw(offset(centerLeft, -1, 3), buttonColors[0], newGameText)
draw(offset(centerRight, 1, 3), buttonColors[1], joinGameText)
}
Loading

0 comments on commit b2f657c

Please sign in to comment.