clone

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

chore/app-model-decomp
puzzletea / games / lightsout / model.go
model.go 2.9K
1// Package lightsout implements the lights out toggle puzzle game.2package lightsout34import (5 "fmt"6 "math/rand/v2"78 "charm.land/bubbles/v2/key"9 tea "charm.land/bubbletea/v2"10 "github.com/FelineStateMachine/puzzletea/game"11)1213type Model struct {14 grid [][]bool15 initialGrid [][]bool16 width int17 height int18 cursor game.Cursor19 keys KeyMap20 modeTitle string21 showFullHelp bool2223 termWidth int24 termHeight int25}2627var _ game.Gamer = Model{}2829func New(w, h int) (Model, error) {30 grid := Generate(w, h)31 return newFromGrid(w, h, grid), nil32}3334func NewSeeded(w, h int, rng *rand.Rand) (Model, error) {35 grid := GenerateSeeded(w, h, rng)36 return newFromGrid(w, h, grid), nil37}3839func newFromGrid(w, h int, grid [][]bool) Model {40 initial := make([][]bool, h)41 for y := range h {42 initial[y] = make([]bool, w)43 copy(initial[y], grid[y])44 }45 return Model{46 grid: grid,47 initialGrid: initial,48 width: w,49 height: h,50 cursor: game.Cursor{X: w / 2, Y: h / 2},51 keys: DefaultKeyMap,52 }53}5455func (m Model) Init() tea.Cmd {56 return nil57}5859func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {60 switch msg := msg.(type) {61 case game.HelpToggleMsg:62 m.showFullHelp = msg.Show63 case tea.WindowSizeMsg:64 m.termWidth = msg.Width65 m.termHeight = msg.Height66 case tea.MouseClickMsg:67 if msg.Button == tea.MouseLeft && !m.IsSolved() {68 if col, row, ok := m.screenToGrid(msg.X, msg.Y); ok {69 m.cursor.X, m.cursor.Y = col, row70 Toggle(m.grid, col, row)71 }72 }73 case tea.KeyPressMsg:74 switch {75 case key.Matches(msg, m.keys.Toggle):76 if !m.IsSolved() {77 Toggle(m.grid, m.cursor.X, m.cursor.Y)78 }79 default:80 m.cursor.Move(m.keys.CursorKeyMap, msg, m.width-1, m.height-1)81 }82 }83 m.updateKeyBindings()84 return m, nil85}8687func (m Model) View() string {88 solved := m.IsSolved()8990 title := game.TitleBarView("Lights Out", m.modeTitle, solved)91 grid := gridView(m.grid, m.cursor, solved)92 if solved {93 return game.ComposeGameView(title, grid)94 }9596 status := statusBarView(m.showFullHelp)9798 return game.ComposeGameViewRows(title, grid, game.StableRow(status, statusBarView(false), statusBarView(true)))99}100101func (m Model) SetTitle(t string) game.Gamer {102 m.modeTitle = t103 return m104}105106func (m Model) IsSolved() bool {107 return IsSolved(m.grid)108}109110func (m Model) Reset() game.Gamer {111 for y := range m.height {112 copy(m.grid[y], m.initialGrid[y])113 }114 m.cursor = game.Cursor{X: m.width / 2, Y: m.height / 2}115 return m116}117118func (m Model) GetDebugInfo() string {119 status := "In Progress"120 if m.IsSolved() {121 status = "Solved"122 }123124 lightsOn := 0125 for _, row := range m.grid {126 for _, cell := range row {127 if cell {128 lightsOn++129 }130 }131 }132133 return game.DebugHeader("Lights Out", [][2]string{134 {"Status", status},135 {"Cursor", fmt.Sprintf("(%d, %d)", m.cursor.X, m.cursor.Y)},136 {"Grid Size", fmt.Sprintf("%d x %d", m.width, m.height)},137 {"Lights On", fmt.Sprintf("%d / %d", lightsOn, m.width*m.height)},138 })139}