clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

main
puzzletea / games / sudoku / model.go
model.go 3.9K
1// Package sudoku implements the classic number-placement puzzle.2package sudoku34import (5 "fmt"6 "strconv"78 "charm.land/bubbles/v2/key"9 tea "charm.land/bubbletea/v2"10 "github.com/FelineStateMachine/puzzletea/game"11)1213const gridSize = 91415var _ game.Gamer = Model{}1617type Model struct {18 cursor game.Cursor19 grid grid20 provided []cell21 providedGrid [gridSize][gridSize]bool22 conflicts [gridSize][gridSize]bool23 keys KeyMap24 modeTitle string25 showFullHelp bool26 termWidth int27 termHeight int28 originX int29 originY int30 originValid bool31}3233func buildProvidedGrid(provided []cell) [gridSize][gridSize]bool {34 var pg [gridSize][gridSize]bool35 for _, c := range provided {36 pg[c.y][c.x] = true37 }38 return pg39}4041func New(mode SudokuMode, provided []cell) (game.Gamer, error) {42 g := newGrid(provided)43 m := Model{44 grid: g,45 provided: provided,46 providedGrid: buildProvidedGrid(provided),47 conflicts: computeConflicts(g),48 keys: DefaultKeyMap,49 modeTitle: mode.Title(),50 }51 return m, nil52}5354func (m Model) Init() tea.Cmd {55 return nil56}5758func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {59 switch msg := msg.(type) {60 case game.HelpToggleMsg:61 m.showFullHelp = msg.Show62 m.originValid = false63 case tea.WindowSizeMsg:64 m.termWidth = msg.Width65 m.termHeight = msg.Height66 m.originValid = false67 case tea.MouseClickMsg:68 if msg.Button == tea.MouseLeft {69 if col, row, ok := m.screenToGrid(msg.X, msg.Y); ok {70 m.cursor.X = col71 m.cursor.Y = row72 }73 }74 case tea.KeyPressMsg:75 switch {76 case key.Matches(msg, m.keys.FillValue):77 val, _ := strconv.Atoi(msg.String())78 m.updateCell(val)79 case key.Matches(msg, m.keys.ClearCell):80 m.updateCell(0)81 default:82 m.cursor.Move(m.keys.CursorKeyMap, msg, gridSize-1, gridSize-1)83 }84 }85 m.updateKeyBindings()86 return m, nil87}8889func (m Model) SetTitle(t string) game.Gamer {90 m.modeTitle = t91 return m92}9394func (m Model) IsSolved() bool {95 return m.isSolved()96}9798func (m Model) Reset() game.Gamer {99 m.grid = newGrid(m.provided)100 m.conflicts = computeConflicts(m.grid)101 m.originValid = false102 return m103}104105func (m *Model) updateCell(v int) {106 if m.providedGrid[m.cursor.Y][m.cursor.X] {107 return108 }109110 wasSolved := m.isSolved()111 m.grid[m.cursor.Y][m.cursor.X].v = v112 m.conflicts = computeConflicts(m.grid)113 if m.isSolved() != wasSolved {114 m.originValid = false115 }116}117118func (m Model) View() string {119 solved := isSolvedWith(m.grid, m.conflicts)120 title := game.TitleBarView("Sudoku", m.modeTitle, solved)121 grid := renderGrid(m, solved)122 if solved {123 return game.ComposeGameView(title, grid)124 }125126 status := statusBarView(m.showFullHelp)127128 return game.ComposeGameViewRows(title, grid, game.StableRow(status, statusBarView(false), statusBarView(true)))129}130131func (m Model) GetDebugInfo() string {132 cursorCell := m.grid[m.cursor.Y][m.cursor.X]133 isProvided := m.providedGrid[m.cursor.Y][m.cursor.X]134 conflict := m.conflicts[m.cursor.Y][m.cursor.X]135 solved := m.isSolved()136137 filledCount := 0138 conflictCount := 0139 for y := range gridSize {140 for x := range gridSize {141 if m.grid[y][x].v != 0 {142 filledCount++143 }144 if m.conflicts[y][x] {145 conflictCount++146 }147 }148 }149150 status := "In Progress"151 if solved {152 status = "Solved"153 }154155 s := game.DebugHeader("Sudoku", [][2]string{156 {"Status", status},157 {"Cursor", fmt.Sprintf("(%d, %d)", m.cursor.X, m.cursor.Y)},158 {"Cell Value", cellContent(cursorCell, conflict)},159 {"Is Provided", fmt.Sprintf("%v", isProvided)},160 {"Has Conflict", fmt.Sprintf("%v", conflict)},161 {"Cells Filled", fmt.Sprintf("%d / 81", filledCount)},162 {"Conflict Count", fmt.Sprintf("%d", conflictCount)},163 {"Provided Count", fmt.Sprintf("%d", len(m.provided))},164 })165166 if len(m.provided) > 0 {167 var rows [][]string168 for _, p := range m.provided {169 rows = append(rows, []string{170 fmt.Sprintf("%d", p.y),171 fmt.Sprintf("%d", p.x),172 fmt.Sprintf("%d", p.v),173 })174 }175 s += game.DebugTable("Provided Cells", []string{"Row", "Col", "Value"}, rows)176 }177178 return s179}