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() }