clone

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

chore/app-model-decomp
puzzletea / games / netwalk / model.go
model.go 4.3K
1package netwalk23import (4 "fmt"56 "charm.land/bubbles/v2/key"7 tea "charm.land/bubbletea/v2"8 "github.com/FelineStateMachine/puzzletea/game"9)1011type Model struct {12 puzzle Puzzle13 cursor game.Cursor14 keys KeyMap15 modeTitle string16 showFullHelp bool17 termWidth int18 termHeight int19 originX int20 originY int21 originValid bool22 state boardState23}2425var _ game.Gamer = Model{}2627func New(mode NetwalkMode, puzzle Puzzle) (game.Gamer, error) {28 cursor := puzzle.firstActive()29 m := Model{30 puzzle: puzzle,31 cursor: game.Cursor{X: cursor.X, Y: cursor.Y},32 keys: DefaultKeyMap,33 modeTitle: mode.Title(),34 }35 m.recompute()36 return m, nil37}3839func (m Model) Init() tea.Cmd {40 return nil41}4243func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {44 switch msg := msg.(type) {45 case game.HelpToggleMsg:46 m.showFullHelp = msg.Show47 m.originValid = false48 case tea.WindowSizeMsg:49 m.termWidth = msg.Width50 m.termHeight = msg.Height51 m.originValid = false52 case tea.MouseClickMsg:53 m = m.handleMouseClick(msg)54 case tea.KeyPressMsg:55 switch {56 case key.Matches(msg, m.keys.Rotate):57 m.rotateCurrent(1)58 case key.Matches(msg, m.keys.RotateBack):59 m.rotateCurrent(3)60 case key.Matches(msg, m.keys.Lock):61 m.toggleCurrentLock()62 default:63 m.cursor.Move(m.keys.CursorKeyMap, msg, m.puzzle.Size-1, m.puzzle.Size-1)64 }65 }66 m.updateKeyBindings()67 return m, nil68}6970func (m Model) View() string {71 title := game.TitleBarView("Netwalk", m.modeTitle, m.state.solved)72 grid := gridView(m)73 if m.state.solved {74 return game.ComposeGameView(title, grid)75 }76 return game.ComposeGameViewRows(77 title,78 grid,79 game.StableRow(statusBarView(m, m.showFullHelp), statusBarView(m, false), statusBarView(m, true)),80 )81}8283func (m Model) SetTitle(t string) game.Gamer {84 m.modeTitle = t85 return m86}8788func (m Model) IsSolved() bool {89 return m.state.solved90}9192func (m Model) Reset() game.Gamer {93 for y := range m.puzzle.Size {94 for x := range m.puzzle.Size {95 m.puzzle.Tiles[y][x].Rotation = m.puzzle.Tiles[y][x].InitialRotation96 m.puzzle.Tiles[y][x].Locked = false97 }98 }99 cursor := m.puzzle.firstActive()100 m.cursor = game.Cursor{X: cursor.X, Y: cursor.Y}101 m.originValid = false102 m.recompute()103 return m104}105106func (m Model) GetDebugInfo() string {107 return game.DebugHeader("Netwalk", [][2]string{108 {"Status", stateLabel(m.state.solved)},109 {"Cursor", fmt.Sprintf("(%d, %d)", m.cursor.X, m.cursor.Y)},110 {"Grid Size", fmt.Sprintf("%d×%d", m.puzzle.Size, m.puzzle.Size)},111 {"Root", fmt.Sprintf("(%d, %d)", m.puzzle.Root.X, m.puzzle.Root.Y)},112 {"Connected", fmt.Sprintf("%d / %d", m.state.connected, m.state.nonEmpty)},113 {"Dangling", fmt.Sprintf("%d", m.state.dangling)},114 {"Locked", fmt.Sprintf("%d", m.state.locked)},115 })116}117118func stateLabel(solved bool) string {119 if solved {120 return "Solved"121 }122 return "In Progress"123}124125func (m *Model) rotateCurrent(delta uint8) {126 if m.state.solved {127 return128 }129 t := &m.puzzle.Tiles[m.cursor.Y][m.cursor.X]130 if !isActive(*t) || t.Locked {131 return132 }133 t.Rotation = (t.Rotation + delta) % 4134 m.recompute()135}136137func (m *Model) toggleCurrentLock() {138 if m.state.solved {139 return140 }141 t := &m.puzzle.Tiles[m.cursor.Y][m.cursor.X]142 if !isActive(*t) {143 return144 }145 t.Locked = !t.Locked146 m.recompute()147}148149func (m *Model) recompute() {150 m.state = analyzePuzzle(m.puzzle)151 m.originValid = false152}153154func (m Model) handleMouseClick(msg tea.MouseClickMsg) Model {155 col, row, ok := m.screenToGrid(msg.X, msg.Y)156 if !ok {157 return m158 }159 m.cursor.X = col160 m.cursor.Y = row161162 switch msg.Button {163 case tea.MouseLeft:164 m.rotateCurrent(1)165 case tea.MouseRight:166 m.toggleCurrentLock()167 }168 return m169}170171func (m *Model) screenToGrid(screenX, screenY int) (col, row int, ok bool) {172 ox, oy := m.cachedGridOrigin()173 lx := screenX - ox174 ly := screenY - oy175 if lx < 0 || ly < 0 {176 return 0, 0, false177 }178179 col = lx / cellWidth180 row = ly / cellHeight181 if col < 0 || col >= m.puzzle.Size || row < 0 || row >= m.puzzle.Size {182 return 0, 0, false183 }184 return col, row, true185}186187func (m *Model) cachedGridOrigin() (x, y int) {188 if m.originValid {189 return m.originX, m.originY190 }191 x, y = m.gridOrigin()192 m.originX, m.originY = x, y193 m.originValid = true194 return x, y195}196197func (m *Model) gridOrigin() (x, y int) {198 title := game.TitleBarView("Netwalk", m.modeTitle, m.state.solved)199 grid := gridView(*m)200 return game.DynamicGridOrigin(m.termWidth, m.termHeight, m.View(), title, grid)201}