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