229 lines
5.3 KiB
Go
229 lines
5.3 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
var (
|
||
msHiddenStyle = lipgloss.NewStyle().Background(lipgloss.Color("240")).Foreground(lipgloss.Color("240"))
|
||
msFlagStyle = lipgloss.NewStyle().Background(lipgloss.Color("240")).Foreground(lipgloss.Color("196")).Bold(true)
|
||
msMineStyle = lipgloss.NewStyle().Background(lipgloss.Color("52")).Foreground(lipgloss.Color("255")).Bold(true)
|
||
msCursorStyle = lipgloss.NewStyle().Background(lipgloss.Color("220")).Foreground(lipgloss.Color("16")).Bold(true)
|
||
msNumberColors = map[int]lipgloss.Color{
|
||
1: "39", 2: "34", 3: "196", 4: "129", 5: "94", 6: "37", 7: "255", 8: "245",
|
||
}
|
||
)
|
||
|
||
// MinesweeperModel — TUI поверх MinesweeperGameState.
|
||
type MinesweeperModel struct {
|
||
game *MinesweeperGameState
|
||
cursor MinesweeperPos
|
||
width, height, mines int
|
||
|
||
message string
|
||
isError bool
|
||
|
||
quitting bool
|
||
backToMenu bool
|
||
}
|
||
|
||
// NewMinesweeperModel создаёт новую партию width x height с mines
|
||
// минами.
|
||
func NewMinesweeperModel(width, height, mines int) MinesweeperModel {
|
||
return MinesweeperModel{
|
||
game: NewMinesweeperGame(width, height, mines),
|
||
cursor: MinesweeperPos{Row: height / 2, Col: width / 2},
|
||
width: width,
|
||
height: height,
|
||
mines: mines,
|
||
}
|
||
}
|
||
|
||
func (m MinesweeperModel) Init() tea.Cmd { return nil }
|
||
|
||
func (m *MinesweeperModel) setInfo(key string) {
|
||
m.message = T(key)
|
||
m.isError = false
|
||
}
|
||
|
||
func (m *MinesweeperModel) setError(err error) {
|
||
m.message = T(err.Error())
|
||
m.isError = true
|
||
}
|
||
|
||
func (m MinesweeperModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
return m.handleKey(keyMsg)
|
||
}
|
||
|
||
func (m MinesweeperModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||
key := msg.String()
|
||
|
||
if key == "ctrl+c" {
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
}
|
||
if key == "q" {
|
||
m.backToMenu = true
|
||
return m, nil
|
||
}
|
||
|
||
if m.game.Result != nil {
|
||
if key == "n" {
|
||
m.game = NewMinesweeperGame(m.width, m.height, m.mines)
|
||
m.cursor = MinesweeperPos{Row: m.height / 2, Col: m.width / 2}
|
||
m.message = ""
|
||
m.isError = false
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
switch key {
|
||
case "up", "k":
|
||
if m.cursor.Row > 0 {
|
||
m.cursor.Row--
|
||
}
|
||
case "down", "j":
|
||
if m.cursor.Row < m.height-1 {
|
||
m.cursor.Row++
|
||
}
|
||
case "left", "h":
|
||
if m.cursor.Col > 0 {
|
||
m.cursor.Col--
|
||
}
|
||
case "right", "l":
|
||
if m.cursor.Col < m.width-1 {
|
||
m.cursor.Col++
|
||
}
|
||
case "enter", " ":
|
||
cell := m.game.Board[m.cursor.Row][m.cursor.Col]
|
||
if cell.Revealed {
|
||
if err := m.game.Chord(m.cursor); err != nil {
|
||
m.setError(err)
|
||
return m, nil
|
||
}
|
||
if m.game.Result != nil && !m.game.Result.Won {
|
||
m.setInfo("minesweeper.msg.boom")
|
||
} else {
|
||
m.setInfo("minesweeper.msg.chorded")
|
||
}
|
||
return m, nil
|
||
}
|
||
if err := m.game.Reveal(m.cursor); err != nil {
|
||
m.setError(err)
|
||
return m, nil
|
||
}
|
||
if m.game.Result != nil && !m.game.Result.Won {
|
||
m.setInfo("minesweeper.msg.boom")
|
||
} else {
|
||
m.setInfo("minesweeper.msg.revealed")
|
||
}
|
||
case "f":
|
||
if err := m.game.ToggleFlag(m.cursor); err != nil {
|
||
m.setError(err)
|
||
return m, nil
|
||
}
|
||
if m.game.Board[m.cursor.Row][m.cursor.Col].Flagged {
|
||
m.setInfo("minesweeper.msg.flagged")
|
||
} else {
|
||
m.setInfo("minesweeper.msg.unflagged")
|
||
}
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
// --- Отрисовка -----------------------------------------------------
|
||
|
||
func (m MinesweeperModel) renderCell(pos MinesweeperPos) string {
|
||
cell := m.game.Board[pos.Row][pos.Col]
|
||
isCursor := pos == m.cursor && m.game.Result == nil
|
||
|
||
if !cell.Revealed {
|
||
style := msHiddenStyle
|
||
glyph := "▒"
|
||
if cell.Flagged {
|
||
style = msFlagStyle
|
||
glyph = "⚑"
|
||
}
|
||
if isCursor {
|
||
style = msCursorStyle
|
||
}
|
||
return style.Render(" " + glyph + " ")
|
||
}
|
||
|
||
if cell.IsMine {
|
||
return msMineStyle.Render(" ✱ ")
|
||
}
|
||
if cell.AdjacentMines == 0 {
|
||
style := lipgloss.NewStyle()
|
||
if isCursor {
|
||
style = msCursorStyle
|
||
}
|
||
return style.Render(" ")
|
||
}
|
||
color := msNumberColors[cell.AdjacentMines]
|
||
style := lipgloss.NewStyle().Foreground(color).Bold(true)
|
||
if isCursor {
|
||
style = msCursorStyle
|
||
}
|
||
return style.Render(fmt.Sprintf(" %d ", cell.AdjacentMines))
|
||
}
|
||
|
||
func (m MinesweeperModel) renderBoard() string {
|
||
var b strings.Builder
|
||
for r := 0; r < m.height; r++ {
|
||
for c := 0; c < m.width; c++ {
|
||
fmt.Fprint(&b, m.renderCell(MinesweeperPos{Row: r, Col: c}))
|
||
}
|
||
fmt.Fprintln(&b)
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func (m MinesweeperModel) View() string {
|
||
if m.quitting {
|
||
return T("common.goodbye")
|
||
}
|
||
|
||
g := m.game
|
||
var b strings.Builder
|
||
|
||
fmt.Fprintln(&b, titleStyle.Render(T("minesweeper.title")))
|
||
fmt.Fprintln(&b)
|
||
remaining := g.MineCount - g.FlagCount
|
||
fmt.Fprintln(&b, Tf("minesweeper.header.mines", remaining))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, m.renderBoard())
|
||
fmt.Fprintln(&b)
|
||
|
||
if g.Result != nil {
|
||
if g.Result.Won {
|
||
fmt.Fprintln(&b, winStyle.Render(T("minesweeper.result.win")))
|
||
} else {
|
||
fmt.Fprintln(&b, errorStyle.Render(T("minesweeper.result.lose")))
|
||
}
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, dimStyle.Render(T("minesweeper.hint.gameover")))
|
||
return b.String()
|
||
}
|
||
|
||
fmt.Fprintln(&b, T("minesweeper.hint.play"))
|
||
|
||
fmt.Fprintln(&b)
|
||
if m.message != "" {
|
||
if m.isError {
|
||
fmt.Fprintln(&b, errorStyle.Render("! "+m.message))
|
||
} else {
|
||
fmt.Fprintln(&b, infoStyle.Render(m.message))
|
||
}
|
||
}
|
||
|
||
return b.String()
|
||
}
|