clone

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

refactor/root-tui-model
puzzletea / games / takuzuplus / model.go
model.go 4.2K
1// Package takuzuplus implements the Takuzu+ puzzle game.2package takuzuplus34import (5 "fmt"67 "charm.land/bubbles/v2/key"8 tea "charm.land/bubbletea/v2"9 "github.com/FelineStateMachine/puzzletea/game"10 "github.com/FelineStateMachine/puzzletea/games/takuzu"11)1213type Model struct {14 size int15 grid grid16 initialGrid grid17 provided [][]bool18 relations relations19 cursor game.Cursor20 solved bool21 keys KeyMap22 modeTitle string23 showFullHelp bool24 termWidth int25 termHeight int26 originX int27 originY int28 originValid bool29}3031var _ game.Gamer = Model{}3233func New(mode TakuzuPlusMode, puzzle grid, provided [][]bool, rels relations) (game.Gamer, error) {34 m := Model{35 size: mode.Size,36 grid: puzzle,37 initialGrid: puzzle.clone(),38 provided: provided,39 relations: rels,40 cursor: game.Cursor{X: 0, Y: 0},41 keys: DefaultKeyMap,42 modeTitle: mode.Title(),43 }44 m.solved = m.checkSolved()45 return m, nil46}4748func (m Model) Init() tea.Cmd {49 return nil50}5152func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {53 switch msg := msg.(type) {54 case game.HelpToggleMsg:55 m.showFullHelp = msg.Show56 m.originValid = false57 case tea.WindowSizeMsg:58 m.termWidth = msg.Width59 m.termHeight = msg.Height60 m.originValid = false61 case tea.MouseClickMsg:62 if msg.Button == tea.MouseLeft {63 m = m.handleMouseClick(msg)64 }65 case tea.KeyPressMsg:66 switch {67 case key.Matches(msg, m.keys.PlaceZero):68 m.setCurrentCell(zeroCell)69 case key.Matches(msg, m.keys.PlaceOne):70 m.setCurrentCell(oneCell)71 case key.Matches(msg, m.keys.Clear):72 m.setCurrentCell(emptyCell)73 default:74 m.cursor.Move(m.keys.CursorKeyMap, msg, m.size-1, m.size-1)75 }76 }77 m.updateKeyBindings()78 return m, nil79}8081func (m Model) View() string {82 title := game.TitleBarView("Takuzu+", m.modeTitle, m.solved)83 grid := gridView(m)84 if m.solved {85 return game.ComposeGameView(title, grid)86 }87 info := countContextView(m)88 status := statusBarView(m.showFullHelp)89 return game.ComposeGameViewRows(90 title,91 grid,92 game.StaticRow(info),93 game.StableRow(status, statusBarView(false), statusBarView(true)),94 )95}9697func (m Model) SetTitle(t string) game.Gamer {98 m.modeTitle = t99 return m100}101102func (m Model) IsSolved() bool {103 return m.solved104}105106func (m Model) Reset() game.Gamer {107 m.grid = m.initialGrid.clone()108 m.solved = m.checkSolved()109 m.cursor = game.Cursor{}110 m.originValid = false111 return m112}113114func (m Model) checkSolved() bool {115 for y := range m.size {116 for x := range m.size {117 if m.grid[y][x] == emptyCell {118 return false119 }120 }121 }122 return takuzu.CheckConstraintsGrid(m.grid, m.size) &&123 takuzu.HasUniqueLinesGrid(m.grid, m.size) &&124 checkRelations(m.grid, m.relations)125}126127func (m Model) GetDebugInfo() string {128 status := "In Progress"129 if m.solved {130 status = "Solved"131 }132133 filled := 0134 for y := range m.size {135 for x := range m.size {136 if m.grid[y][x] != emptyCell {137 filled++138 }139 }140 }141142 return game.DebugHeader("Takuzu+", [][2]string{143 {"Status", status},144 {"Cursor", fmt.Sprintf("(%d, %d)", m.cursor.X, m.cursor.Y)},145 {"Grid Size", fmt.Sprintf("%d×%d", m.size, m.size)},146 {"Cells Filled", fmt.Sprintf("%d / %d", filled, m.size*m.size)},147 {"Row Counts", countPairString(countLine(m.grid[m.cursor.Y]))},148 {"Col Counts", countPairString(countColumn(m.grid, m.cursor.X, m.size))},149 {"Relation Clues", fmt.Sprintf("%d", countRelations(m.relations))},150 })151}152153func (m *Model) setCurrentCell(val rune) {154 if m.solved || m.provided[m.cursor.Y][m.cursor.X] {155 return156 }157158 wasSolved := m.solved159 m.grid[m.cursor.Y][m.cursor.X] = val160 m.solved = m.checkSolved()161 if m.solved != wasSolved {162 m.originValid = false163 }164}165166func (m *Model) cycleCurrentCell() {167 if m.solved || m.provided[m.cursor.Y][m.cursor.X] {168 return169 }170171 switch m.grid[m.cursor.Y][m.cursor.X] {172 case zeroCell:173 m.setCurrentCell(oneCell)174 case oneCell:175 m.setCurrentCell(emptyCell)176 default:177 m.setCurrentCell(zeroCell)178 }179}180181func (m Model) handleMouseClick(msg tea.MouseClickMsg) Model {182 col, row, ok := m.screenToGrid(msg.X, msg.Y)183 if !ok {184 return m185 }186187 sameCell := col == m.cursor.X && row == m.cursor.Y188 m.cursor.X = col189 m.cursor.Y = row190 if sameCell {191 m.cycleCurrentCell()192 }193 return m194}