ssh-portfolio/snake.go

223 lines
4.6 KiB
Go

package main
import (
"fmt"
"math/rand"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
gridWidth = 40
gridHeight = 20
)
type point struct {
x, y int
}
type snakeDirection int
const (
dirUp snakeDirection = iota
dirDown
dirLeft
dirRight
)
type snakeTickMsg time.Time
type SnakeModel struct {
snake []point
dir snakeDirection
nextDir snakeDirection
food point
score int
gameOver bool
rng *rand.Rand
}
func newSnakeModel() SnakeModel {
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
snake := []point{
{gridWidth/2 + 1, gridHeight / 2},
{gridWidth / 2, gridHeight / 2},
{gridWidth/2 - 1, gridHeight / 2},
}
m := SnakeModel{
snake: snake,
dir: dirRight,
nextDir: dirRight,
rng: rng,
}
m.food = m.spawnFood()
return m
}
func (m SnakeModel) spawnFood() point {
occupied := make(map[point]bool, len(m.snake))
for _, s := range m.snake {
occupied[s] = true
}
for {
f := point{m.rng.Intn(gridWidth), m.rng.Intn(gridHeight)}
if !occupied[f] {
return f
}
}
}
func snakeTickCmd() tea.Cmd {
return tea.Tick(130*time.Millisecond, func(t time.Time) tea.Msg {
return snakeTickMsg(t)
})
}
func (m SnakeModel) Init() tea.Cmd {
return snakeTickCmd()
}
func (m SnakeModel) Update(msg tea.Msg) (SnakeModel, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
if m.dir != dirDown {
m.nextDir = dirUp
}
case "down", "j":
if m.dir != dirUp {
m.nextDir = dirDown
}
case "left", "h":
if m.dir != dirRight {
m.nextDir = dirLeft
}
case "right", "l":
if m.dir != dirLeft {
m.nextDir = dirRight
}
case "r":
if m.gameOver {
fresh := newSnakeModel()
return fresh, fresh.Init()
}
}
case snakeTickMsg:
if m.gameOver {
return m, nil
}
m.dir = m.nextDir
head := m.snake[0]
var next point
switch m.dir {
case dirUp:
next = point{head.x, head.y - 1}
case dirDown:
next = point{head.x, head.y + 1}
case dirLeft:
next = point{head.x - 1, head.y}
case dirRight:
next = point{head.x + 1, head.y}
}
if next.x < 0 || next.x >= gridWidth || next.y < 0 || next.y >= gridHeight {
m.gameOver = true
return m, nil
}
for _, s := range m.snake {
if s == next {
m.gameOver = true
return m, nil
}
}
if next == m.food {
m.snake = append([]point{next}, m.snake...)
m.score++
m.food = m.spawnFood()
} else {
m.snake = append([]point{next}, m.snake[:len(m.snake)-1]...)
}
return m, snakeTickCmd()
}
return m, nil
}
//TODO test if the characters are rendered on different terminals
func (m SnakeModel) View() string {
snakeIndex := make(map[point]int, len(m.snake))
for i, s := range m.snake {
snakeIndex[s] = i
}
//TODO move to top
borderStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#8A1D7D"))
headStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#4ADE80")).Bold(true)
bodyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#16A34A"))
foodStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FB923C"))
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#E2E8F0"))
scoreStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#94A3B8"))
footerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#64748B"))
gameOverStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#EF4444"))
var sb strings.Builder
sb.WriteString(
titleStyle.Render(" SNAKE") +
" " +
scoreStyle.Render(fmt.Sprintf("Score: %d", m.score)) +
"\n",
)
sb.WriteString(borderStyle.Render("┌"+strings.Repeat("─", gridWidth)+"┐") + "\n")
for y := 0; y < gridHeight; y++ {
sb.WriteString(borderStyle.Render("│"))
for x := 0; x < gridWidth; x++ {
p := point{x, y}
if idx, isSnake := snakeIndex[p]; isSnake {
if idx == 0 {
sb.WriteString(headStyle.Render("■"))
} else {
sb.WriteString(bodyStyle.Render("□"))
}
} else if p == m.food {
sb.WriteString(foodStyle.Render("●"))
} else {
sb.WriteString(" ")
}
}
sb.WriteString(borderStyle.Render("│") + "\n")
}
sb.WriteString(borderStyle.Render("└"+strings.Repeat("─", gridWidth)+"┘") + "\n")
if m.gameOver {
sb.WriteString(
gameOverStyle.Render(" GAME OVER") +
" " +
scoreStyle.Render(fmt.Sprintf("Final score: %d", m.score)) +
"\n",
)
sb.WriteString(footerStyle.Render(" r to restart · b to go back · q to quit") + "\n")
} else {
sb.WriteString(footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n")
}
return sb.String()
}