clone

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

bug/architecture-test-trim-path
puzzletea / games / fillomino / model.go
model.go 3.6K
1package fillomino23import (4 "fmt"5 "strconv"67 "charm.land/bubbles/v2/key"8 tea "charm.land/bubbletea/v2"9 "github.com/FelineStateMachine/puzzletea/game"10)1112var _ game.Gamer = Model{}1314type Model struct {15 width int16 height int17 grid grid18 initialGrid grid19 provided [][]bool20 cursor game.Cursor21 conflicts [][]bool22 solved bool23 keys KeyMap24 modeTitle string25 showFullHelp bool26 maxCellValue int27 termWidth int28 termHeight int29 originX int30 originY int31 originValid bool32}3334func New(mode Mode, puzzle Puzzle) (game.Gamer, error) {35 if puzzle.Width != mode.Size || puzzle.Height != mode.Size {36 return nil, fmt.Errorf("fillomino puzzle size %dx%d does not match mode %dx%d", puzzle.Width, puzzle.Height, mode.Size, mode.Size)37 }3839 m := Model{40 width: puzzle.Width,41 height: puzzle.Height,42 grid: cloneGrid(puzzle.Givens),43 initialGrid: cloneGrid(puzzle.Givens),44 provided: newProvidedMask(puzzle.Givens),45 cursor: game.Cursor{},46 keys: DefaultKeyMap,47 modeTitle: mode.Title(),48 maxCellValue: mode.MaxRegion,49 }50 m.recompute()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.Clear):77 m.setCell(0)78 case isDigitKey(msg.String()):79 value, _ := strconv.Atoi(msg.String())80 if value > 0 && value <= m.maxCellValue {81 m.setCell(value)82 }83 default:84 m.cursor.Move(m.keys.CursorKeyMap, msg, m.width-1, m.height-1)85 }86 }8788 m.updateKeyBindings()89 return m, nil90}9192func isDigitKey(s string) bool {93 return len(s) == 1 && s[0] >= '1' && s[0] <= '9'94}9596func (m *Model) setCell(value int) {97 if m.solved || m.provided[m.cursor.Y][m.cursor.X] {98 return99 }100 m.grid[m.cursor.Y][m.cursor.X] = value101 m.recompute()102}103104func (m *Model) recompute() {105 result := validateGridState(m.grid)106 m.conflicts = result.conflicts107 m.solved = result.solved108}109110func (m Model) View() string {111 title := game.TitleBarView("Fillomino", m.modeTitle, m.solved)112 body := gridView(m)113 if m.solved {114 return game.ComposeGameView(title, body)115 }116 info := cursorRegionInfoView(m)117 status := statusBarView(m.showFullHelp)118 return game.ComposeGameViewRows(119 title,120 body,121 game.StableRow(info, cursorRegionInfoVariants(m)...),122 game.StableRow(status, statusBarView(false), statusBarView(true)),123 )124}125126func (m Model) SetTitle(t string) game.Gamer {127 m.modeTitle = t128 return m129}130131func (m Model) IsSolved() bool {132 return m.solved133}134135func (m Model) Reset() game.Gamer {136 m.grid = cloneGrid(m.initialGrid)137 m.cursor = game.Cursor{}138 m.recompute()139 return m140}141142func (m Model) GetDebugInfo() string {143 status := "In Progress"144 if m.solved {145 status = "Solved"146 }147148 filled := 0149 conflicts := 0150 for y := range m.height {151 for x := range m.width {152 if m.grid[y][x] != 0 {153 filled++154 }155 if m.conflicts[y][x] {156 conflicts++157 }158 }159 }160161 return game.DebugHeader("Fillomino", [][2]string{162 {"Status", status},163 {"Cursor", fmt.Sprintf("(%d, %d)", m.cursor.X, m.cursor.Y)},164 {"Cells Filled", fmt.Sprintf("%d / %d", filled, m.width*m.height)},165 {"Conflict Count", fmt.Sprintf("%d", conflicts)},166 {"Max Region", fmt.Sprintf("%d", m.maxCellValue)},167 })168}