Recently I was introduced to a game called Mastermind. It immediately clicked with my developer brain: you’re basically doing guided search through a big space of possibilities, and every guess should incorporate the feedback from the previous guess.
The rules are simple:
For example, the answer is red, green, blue, yellow, and the guess is red, green, blue, purple. The feedback will be 3, 0, because the first three slots match exactly, and purple doesn’t appear in the answer at all.
As a developer, I wanted a terminal version of the game, and I’m still a big fan of Go. After a bit of research, I landed on this toolkit:
mastermindtui should implement the interface of tea.Modelpackage main
import (
"fmt"
"gocli/internal/ui/tui/mastermindtui"
"os"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
p := tea.NewProgram(mastermindtui.InitialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
}Init(), Update(), and View().func (m MastermindModel) Init() tea.Cmd {
return nil
}
func (m MastermindModel) View() string {
//...
}
func (m MastermindModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q", "esc":
return m, tea.Quit
case "left", "h":
m.Left(true)
case "right", "l":
m.Right(true)
// ...
case "r":
m.game.Reset()
case "enter", " ":
// ....
}
}
return m, nil
}View() returns the current UI as a string. I use lipgloss for styling and layout.As a Lipgloss newbie, these two helpers were the ones I leaned on most:
lipgloss.JoinHorizontal()lipgloss.JoinVertical()My mental model is: the UI is one big <div>...</div>. JoinHorizontal is like <div className="flex flex-row">...</div>, and JoinVertical is like <div className="flex flex-col">...</div>.
Update() handles inputs and updates state. Each keypress runs Update(), then Bubble Tea calls View() again to re-render—very similar to a React-style loop (event → state change → render).Links: