1// Package takuzu implements the binary (Binairo) puzzle game.2package takuzu34import (5 "fmt"67 "charm.land/bubbles/v2/key"8 tea "charm.land/bubbletea/v2"9 "github.com/FelineStateMachine/puzzletea/game"10)1112// Model implements game.Gamer for Takuzu.13type Model struct {14 size int15 grid grid16 initialGrid grid17 provided [][]bool18 cursor game.Cursor19 solved bool20 keys KeyMap21 modeTitle string22 showFullHelp bool23 termWidth int24 termHeight int25 originX int26 originY int27 originValid bool28}2930var _ game.Gamer = Model{}3132// New creates a new Takuzu game model.33func New(mode TakuzuMode, puzzle grid, provided [][]bool) (game.Gamer, error) {34 m := Model{35 size: mode.Size,36 grid: puzzle,37 initialGrid: puzzle.clone(),38 provided: provided,39 cursor: game.Cursor{X: 0, Y: 0},40 keys: DefaultKeyMap,41 modeTitle: mode.Title(),42 }43 m.solved = m.checkSolved()44 return m, nil45}4647func (m Model) Init() tea.Cmd {48 return nil49}5051func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {52 switch msg := msg.(type) {53 case game.HelpToggleMsg:54 m.showFullHelp = msg.Show55 m.originValid = false56 case tea.WindowSizeMsg:57 m.termWidth = msg.Width58 m.termHeight = msg.Height59 m.originValid = false60 case tea.MouseClickMsg:61 if msg.Button == tea.MouseLeft {62 m = m.handleMouseClick(msg)63 }64 case tea.KeyPressMsg:65 switch {66 case key.Matches(msg, m.keys.PlaceZero):67 m.setCurrentCell(zeroCell)68 case key.Matches(msg, m.keys.PlaceOne):69 m.setCurrentCell(oneCell)70 case key.Matches(msg, m.keys.Clear):71 m.setCurrentCell(emptyCell)72 default:73 m.cursor.Move(m.keys.CursorKeyMap, msg, m.size-1, m.size-1)74 }75 }76 m.updateKeyBindings()77 return m, nil78}7980func (m Model) View() string {81 title := game.TitleBarView("Takuzu", m.modeTitle, m.solved)82 grid := gridView(m)83 if m.solved {84 return game.ComposeGameView(title, grid)85 }86 info := countContextView(m)87 status := statusBarView(m.showFullHelp)88 return game.ComposeGameViewRows(89 title,90 grid,91 game.StaticRow(info),92 game.StableRow(status, statusBarView(false), statusBarView(true)),93 )94}9596func (m Model) SetTitle(t string) game.Gamer {97 m.modeTitle = t98 return m99}100101func (m Model) IsSolved() bool {102 return m.solved103}104105func (m Model) Reset() game.Gamer {106 m.grid = m.initialGrid.clone()107 m.solved = m.checkSolved()108 m.cursor = game.Cursor{}109 m.originValid = false110 return m111}112113func (m Model) checkSolved() bool {114 // All cells must be filled.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 checkConstraints(m.grid, m.size) && hasUniqueLines(m.grid, m.size)123}124125func (m Model) GetDebugInfo() string {126 status := "In Progress"127 if m.solved {128 status = "Solved"129 }130131 filled := 0132 for y := range m.size {133 for x := range m.size {134 if m.grid[y][x] != emptyCell {135 filled++136 }137 }138 }139140 return game.DebugHeader("Takuzu", [][2]string{141 {"Status", status},142 {"Cursor", fmt.Sprintf("(%d, %d)", m.cursor.X, m.cursor.Y)},143 {"Grid Size", fmt.Sprintf("%d×%d", m.size, m.size)},144 {"Cells Filled", fmt.Sprintf("%d / %d", filled, m.size*m.size)},145 {"Row Counts", countPairString(countLine(m.grid[m.cursor.Y]))},146 {"Col Counts", countPairString(countColumn(m.grid, m.cursor.X, m.size))},147 })148}149150func (m *Model) setCurrentCell(val rune) {151 if m.solved || m.provided[m.cursor.Y][m.cursor.X] {152 return153 }154155 wasSolved := m.solved156 m.grid[m.cursor.Y][m.cursor.X] = val157 m.solved = m.checkSolved()158 if m.solved != wasSolved {159 m.originValid = false160 }161}162163func (m *Model) cycleCurrentCell() {164 if m.solved || m.provided[m.cursor.Y][m.cursor.X] {165 return166 }167168 switch m.grid[m.cursor.Y][m.cursor.X] {169 case zeroCell:170 m.setCurrentCell(oneCell)171 case oneCell:172 m.setCurrentCell(emptyCell)173 default:174 m.setCurrentCell(zeroCell)175 }176}177178func (m Model) handleMouseClick(msg tea.MouseClickMsg) Model {179 col, row, ok := m.screenToGrid(msg.X, msg.Y)180 if !ok {181 return m182 }183184 sameCell := col == m.cursor.X && row == m.cursor.Y185 m.cursor.X = col186 m.cursor.Y = row187 if sameCell {188 m.cycleCurrentCell()189 }190 return m191}