added snake and added projects section, still need a bit of work
This commit is contained in:
parent
a7f1bbdd19
commit
e7af8511d1
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
snake.go
|
connections.log
|
||||||
|
|||||||
166
main.go
166
main.go
@ -2,17 +2,40 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"log"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/charmbracelet/ssh"
|
"github.com/charmbracelet/ssh"
|
||||||
"github.com/charmbracelet/wish"
|
"github.com/charmbracelet/wish"
|
||||||
wishtea "github.com/charmbracelet/wish/bubbletea"
|
wishtea "github.com/charmbracelet/wish/bubbletea"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type project struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
tech string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
var projects = []project{
|
||||||
|
{
|
||||||
|
name: "SSH-portfolio",
|
||||||
|
description: "This very terminal — a Bubbletea + Wish SSH app.",
|
||||||
|
tech: "Go · Bubbletea · Lipgloss · Wish",
|
||||||
|
url: "https://git.ksan.dev/ksan/ssh-portfolio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "E-Voting",
|
||||||
|
description: "A secure e-voting application for creating and participating in elections. It uses certificates, encryption, and digital signatures to protect identity, privacy, and data integrity.",
|
||||||
|
tech: "Java · BoucyCastle · JavaFx · Gradle",
|
||||||
|
url: "https://git.ksan.dev/ksan/e-voting",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const giteaLink = "https://git.ksan.dev/ksan"
|
||||||
|
|
||||||
var asciiArt = []string{}
|
var asciiArt = []string{}
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
@ -26,12 +49,14 @@ type model struct {
|
|||||||
mouseY int
|
mouseY int
|
||||||
mouseActive bool
|
mouseActive bool
|
||||||
isClicked bool
|
isClicked bool
|
||||||
|
snake SnakeModel
|
||||||
|
projectCursor int
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialModel() model {
|
func initialModel() model {
|
||||||
return model{
|
return model{
|
||||||
title: "Đorđe Kšan",
|
title: "Đorđe Kšan",
|
||||||
sections: []string{"About", "Projects", "Contact"},
|
sections: []string{"About", "Projects", "Contact", "Surprise"},
|
||||||
currentScreen: "menu",
|
currentScreen: "menu",
|
||||||
mouseActive: true,
|
mouseActive: true,
|
||||||
}
|
}
|
||||||
@ -42,6 +67,24 @@ func (m model) Init() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
|
if m.currentScreen == "Surprise" {
|
||||||
|
|
||||||
|
if key, ok := msg.(tea.KeyMsg); ok {
|
||||||
|
switch key.String() {
|
||||||
|
case "q", "ctrl+c":
|
||||||
|
return m, tea.Quit
|
||||||
|
case "b", "backspace":
|
||||||
|
m.currentScreen = "menu"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.snake, cmd = m.snake.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
@ -57,18 +100,35 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
|
if m.currentScreen == "Projects" {
|
||||||
|
if m.projectCursor > 0 {
|
||||||
|
m.projectCursor--
|
||||||
|
}
|
||||||
|
} else if m.currentScreen == "menu" {
|
||||||
if m.selected > 0 {
|
if m.selected > 0 {
|
||||||
m.selected--
|
m.selected--
|
||||||
fmt.Println("up")
|
}
|
||||||
}
|
}
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
|
if m.currentScreen == "Projects" {
|
||||||
|
if m.projectCursor < len(projects)-1 {
|
||||||
|
m.projectCursor++
|
||||||
|
}
|
||||||
|
} else if m.currentScreen == "menu" {
|
||||||
if m.selected < len(m.sections)-1 {
|
if m.selected < len(m.sections)-1 {
|
||||||
m.selected++
|
m.selected++
|
||||||
fmt.Println("down")
|
}
|
||||||
}
|
}
|
||||||
case "enter":
|
case "enter":
|
||||||
m.currentScreen = m.sections[m.selected]
|
|
||||||
|
|
||||||
|
if m.currentScreen == "menu" {
|
||||||
|
chosen := m.sections[m.selected]
|
||||||
|
m.currentScreen = chosen
|
||||||
|
if chosen == "Surprise" {
|
||||||
|
m.snake = newSnakeModel()
|
||||||
|
return m, m.snake.Init()
|
||||||
|
}
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case "left", "h":
|
case "left", "h":
|
||||||
@ -96,7 +156,22 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m model) center(content string) string {
|
||||||
|
if m.width == 0 || m.height == 0 {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
return lipgloss.Place(m.width, m.height,
|
||||||
|
lipgloss.Center, lipgloss.Center,
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (m model) View() string {
|
func (m model) View() string {
|
||||||
|
|
||||||
|
if m.currentScreen == "Surprise" {
|
||||||
|
return m.center(m.snake.View())
|
||||||
|
}
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#E2E8F0")).
|
Foreground(lipgloss.Color("#E2E8F0")).
|
||||||
@ -129,14 +204,15 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output += "\n" + footerStyle.Render("Press q to quit.") + "\n"
|
output += "\n" + footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n"
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
switch m.currentScreen {
|
switch m.currentScreen {
|
||||||
case "About":
|
case "About":
|
||||||
output = m.renderAbout()
|
output = m.renderAbout()
|
||||||
|
|
||||||
case "Projects":
|
case "Projects":
|
||||||
output = m.renderProjects()
|
output = m.center(m.renderProjects())
|
||||||
case "Contact":
|
case "Contact":
|
||||||
output = m.renderContact()
|
output = m.renderContact()
|
||||||
|
|
||||||
@ -158,7 +234,7 @@ var aboutText = `
|
|||||||
`
|
`
|
||||||
|
|
||||||
var contactText = `
|
var contactText = `
|
||||||
Get in touch with me at djordje@ksan.dev.
|
Get in touch with me at djordje@ksan.dev
|
||||||
`
|
`
|
||||||
|
|
||||||
var projectsText = `
|
var projectsText = `
|
||||||
@ -193,23 +269,56 @@ func (m model) renderAbout() string {
|
|||||||
str.WriteString("\n")
|
str.WriteString("\n")
|
||||||
str.WriteString(contentStyle.Render(aboutText))
|
str.WriteString(contentStyle.Render(aboutText))
|
||||||
str.WriteString("\n")
|
str.WriteString("\n")
|
||||||
str.WriteString(footerStyle.Render("esc: back to menu"))
|
|
||||||
|
str.WriteString(footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n")
|
||||||
|
|
||||||
return str.String()
|
return str.String()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func link(url, text string) string {
|
||||||
|
return "\033]8;;" + url + "\033\\" + text + "\033]8;;\033\\"
|
||||||
|
}
|
||||||
|
|
||||||
func (m model) renderProjects() string {
|
func (m model) renderProjects() string {
|
||||||
|
|
||||||
var str strings.Builder
|
output := titleStyle.Render("━━━ Projects ━━━") + "\n\n"
|
||||||
|
output += sectionStyle.Render("Projects I worked on:") + "\n\n"
|
||||||
|
|
||||||
str.WriteString(titleStyle.Render("━━━ Projects ━━━"))
|
cursorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#8A1D7D")).Bold(true)
|
||||||
str.WriteString("\n")
|
expandedNameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#E2E8F0")).Bold(true)
|
||||||
str.WriteString(contentStyle.Render(projectsText))
|
collapsedNameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#94A3B8"))
|
||||||
str.WriteString("\n")
|
descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#CBD5E1")).PaddingLeft(4).Width(55).Align(lipgloss.Center)
|
||||||
str.WriteString(footerStyle.Render("esc: back to menu"))
|
techStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#8A1D7D")).PaddingLeft(4)
|
||||||
|
dividerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#1E293B"))
|
||||||
|
urlStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#38BDF8")).Underline(true).PaddingLeft(4).Align(lipgloss.Center)
|
||||||
|
urlStyle2 := lipgloss.NewStyle().Foreground(lipgloss.Color("#38BDF8")).Underline(true).Align(lipgloss.Center).MaxWidth(55)
|
||||||
|
|
||||||
return str.String()
|
for i, p := range projects {
|
||||||
|
if i == m.projectCursor {
|
||||||
|
output += cursorStyle.Render("▶ ") + expandedNameStyle.Render(link(p.url, p.name)) + "\n"
|
||||||
|
output += descStyle.Render(p.description) + "\n"
|
||||||
|
output += techStyle.Render("[ "+p.tech+" ]") + "\n"
|
||||||
|
|
||||||
|
output += link(p.url, urlStyle.Render("→ Check it out here ←")) + "\n"
|
||||||
|
output += urlStyle2.Render(p.url) + "\n"
|
||||||
|
} else {
|
||||||
|
output += collapsedNameStyle.Render(" ▸ "+p.name) + "\n"
|
||||||
|
}
|
||||||
|
if i < len(projects)-1 {
|
||||||
|
output += dividerStyle.Render(" "+"─────────────────────────────") + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gitStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#38BDF8")).
|
||||||
|
Underline(true).
|
||||||
|
MarginTop(1)
|
||||||
|
output += "\n" + sectionStyle.Render("More on: ") + gitStyle.Render(giteaLink) + "\n"
|
||||||
|
|
||||||
|
output += "\n" + footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit")
|
||||||
|
|
||||||
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) renderContact() string {
|
func (m model) renderContact() string {
|
||||||
@ -219,12 +328,13 @@ func (m model) renderContact() string {
|
|||||||
str.WriteString("\n")
|
str.WriteString("\n")
|
||||||
str.WriteString(contentStyle.Render(contactText))
|
str.WriteString(contentStyle.Render(contactText))
|
||||||
str.WriteString("\n")
|
str.WriteString("\n")
|
||||||
str.WriteString(footerStyle.Render("esc: back to menu"))
|
str.WriteString(footerStyle.Render(" ↑↓←→ / hjkl to move · b to go back · q to quit") + "\n")
|
||||||
|
|
||||||
return str.String()
|
return str.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
initLogger()
|
||||||
|
|
||||||
server, _ := wish.NewServer(
|
server, _ := wish.NewServer(
|
||||||
wish.WithAddress("0.0.0.0:2222"),
|
wish.WithAddress("0.0.0.0:2222"),
|
||||||
@ -235,13 +345,25 @@ func main() {
|
|||||||
_ = server.ListenAndServe()
|
_ = server.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initLogger() {
|
||||||
|
f, err := os.OpenFile("connections.log",
|
||||||
|
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.SetOutput(f)
|
||||||
|
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||||
|
}
|
||||||
|
|
||||||
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||||
|
|
||||||
ip := s.RemoteAddr().String()
|
ip := s.RemoteAddr().String()
|
||||||
user := s.User()
|
user := s.User()
|
||||||
|
|
||||||
println("New connection:")
|
log.Printf("New connection | user=%s ip=%s", user, ip)
|
||||||
log.Println("User:", user, "IP:", ip)
|
|
||||||
|
|
||||||
return initialModel(), []tea.ProgramOption{
|
return initialModel(), []tea.ProgramOption{
|
||||||
tea.WithAltScreen(),
|
tea.WithAltScreen(),
|
||||||
|
|||||||
222
snake.go
Normal file
222
snake.go
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user