Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# cli-play

Terminal game collection built with Go + [Bubbletea](https://github.com/charmbracelet/bubbletea). Quick games to play in VS Code's integrated terminal while waiting for builds, CI, etc.
Terminal game collection built with Go +
[Bubbletea](https://github.com/charmbracelet/bubbletea).
Quick games to play in VS Code's integrated terminal while
waiting for builds, CI, etc.

## Build and Run

Expand Down Expand Up @@ -31,7 +34,9 @@ cli-play/

## Architecture

Each game is a self-contained Bubbletea `tea.Model` in its own package under `internal/`. The menu package provides the launcher UI. Games return to the menu when finished or when the user quits.
Each game is a self-contained Bubbletea `tea.Model` in its own
package under `internal/`. The menu package provides the launcher
UI. Games return to the menu when finished or when the user quits.

Pattern for each game package:

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ splash screen and a game selection menu.
## Games

| Game | Status |
|------|--------|
| ------ | -------- |
| Yahtzee | Playable |
| Blackjack | Playable |
| Wordle | Coming soon |
Expand Down
2 changes: 2 additions & 0 deletions internal/blackjack/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updatePlayerTurn(key)
case PhaseResult:
return m.updateResult(key)
case PhaseDealerTurn:
// Dealer turn is automated; no player input accepted.
}
}

Expand Down
2 changes: 2 additions & 0 deletions internal/connectfour/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ func (g *Game) switchTurn() {
g.current = Yellow
case Yellow:
g.current = Red
case Empty:
// Empty is not a valid player; no action.
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/mastermind/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type Game struct {
func NewGame() *Game {
var secret [CodeLength]int
for i := range secret {
secret[i] = rand.IntN(NumColors) + 1
secret[i] = rand.IntN(NumColors) + 1 //nolint:gosec // G602: i bounded by fixed-size array range
}
return &Game{secret: secret}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/menu/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var categories = []category{
{Name: "Puzzles", Icon: "\U0001f9e9", Indices: []int{2, 3, 4, 5, 11, 8}}, // Wordle, Minesweeper, Sudoku, 2048, Fifteen Puzzle, Mastermind
{Name: "Action", Icon: "\U0001f3ae", Indices: []int{12, 13}}, // Snake, Tetris
{Name: "Strategy", Icon: "\U0001f0cf", Indices: []int{6, 7, 10, 9}}, // Hangman, Tic-Tac-Toe, Connect Four, Memory
{Name: "Skills", Icon: "\U0001f3af", Indices: []int{15}}, // Typing Test (dart/target -- consistent 2-cell emoji)
{Name: "Skills", Icon: "\U0001f3af", Indices: []int{15}}, // Typing Test (dart/target -- consistent 2-cell emoji)
}

// gamePreview holds the info panel text for each game.
Expand Down Expand Up @@ -132,10 +132,10 @@ var tips = []string{
// uses "0", games 11-16 use "a"-"f".
func shortcutLabel(displayIndex int) string {
if displayIndex < 9 {
return string(rune('1' + displayIndex))
return string(rune('1' + displayIndex)) //nolint:gosec // G115: displayIndex bounded 0-8, fits in rune
}
if displayIndex == 9 {
return "0"
}
return string(rune('a' + displayIndex - 10))
return string(rune('a' + displayIndex - 10)) //nolint:gosec // G115: displayIndex bounded 10-15, fits in rune
}
11 changes: 5 additions & 6 deletions internal/menu/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,6 @@ func (m Model) renderPreview() string {
return previewBorder.Render(pb.String())
}


// TransitionText returns the menu layout as plain text (no ANSI, no border)
// for the transition reveal animation. It mirrors the View() content using
// the same responsive logic (column count, show/hide flags) so the revealed
Expand Down Expand Up @@ -678,7 +677,7 @@ func (m Model) TransitionText() string {
b.WriteString("\n")
}
if showStats {
b.WriteString(fmt.Sprintf("Games played: %d | Session: 0m", m.gamesPlayed))
fmt.Fprintf(&b, "Games played: %d | Session: 0m", m.gamesPlayed)
b.WriteString("\n")
}

Expand All @@ -687,7 +686,7 @@ func (m Model) TransitionText() string {

for catI := range categories {
cat := categories[catI]
b.WriteString(fmt.Sprintf(" %s\n", cat.Name))
fmt.Fprintf(&b, " %s\n", cat.Name)

// Collect game rows for this category.
var catGames []int
Expand Down Expand Up @@ -746,14 +745,14 @@ func plainEntry(row menuRow, selected, compact bool) string {

// Shortcut.
if row.gameIndex < SettingsIndex && row.displayIndex >= 0 {
e.WriteString(fmt.Sprintf("[%s] ", shortcutLabel(row.displayIndex)))
fmt.Fprintf(&e, "[%s] ", shortcutLabel(row.displayIndex))
} else {
e.WriteString(" ")
}

// Icon.
if icon, ok := gameIcon[row.gameIndex]; ok {
e.WriteString(fmt.Sprintf("%-3s", icon))
fmt.Fprintf(&e, "%-3s", icon)
} else if row.gameIndex == SettingsIndex {
e.WriteString("\u2699 ")
}
Expand All @@ -765,7 +764,7 @@ func plainEntry(row menuRow, selected, compact bool) string {
} else if row.gameIndex >= 0 && row.gameIndex < len(Games) {
name = Games[row.gameIndex].Name
}
e.WriteString(fmt.Sprintf("%-16s", name))
fmt.Fprintf(&e, "%-16s", name)

// Description only in single-column.
if !compact {
Expand Down
2 changes: 2 additions & 0 deletions internal/minesweeper/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ func (g *Game) ToggleFlag(row, col int) {
case Flagged:
cell.State = Hidden
g.FlagsUsed--
case Revealed:
// Already revealed; cannot toggle flag.
}
}

Expand Down
4 changes: 3 additions & 1 deletion internal/minesweeper/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ func (m Model) View() string {
}

func (m Model) viewDifficulty() string {
var sections []string
sections := make([]string, 0, 9)

sections = append(sections,
titleStyle.Render("M I N E S W E E P E R"),
Expand Down Expand Up @@ -276,6 +276,8 @@ func (m Model) viewGame() string {
}
case Lost:
sections = append(sections, loseStyle.Render("GAME OVER - Mine hit!"))
case Playing:
// Still playing; no game-over message.
}
sections = append(sections, "")
}
Expand Down
2 changes: 1 addition & 1 deletion internal/solitaire/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (r Rank) String() string {
case King:
return "K"
default:
return string(rune('0' + int(r)))
return string(rune('0' + int(r))) //nolint:gosec // G115: r is Rank 2-9, fits in rune
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/sudoku/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ func removeClues(full [9][9]int, diff Difficulty) (puzzle [9][9]Cell, solution [
var check [9][9]int
for ri := 0; ri < 9; ri++ {
for ci := 0; ci < 9; ci++ {
if puzzle[ri][ci].Given && !(ri == r && ci == c) {
if puzzle[ri][ci].Given && (ri != r || ci != c) {
check[ri][ci] = puzzle[ri][ci].Value
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/tetris/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (p Piece) Cells() [4]Point {
rot := rots[p.Rotation%len(rots)]
var cells [4]Point
for i, off := range rot {
cells[i] = Point{p.Row + off.Row, p.Col + off.Col}
cells[i] = Point{p.Row + off.Row, p.Col + off.Col} //nolint:gosec // G602: rot is [4]Point, i bounded by range
}
return cells
}
Expand Down
2 changes: 1 addition & 1 deletion internal/twofortyeight/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func slideLine(line [boardSize]int) slideResult {
if compact[i] == 0 {
break
}
if i+1 < boardSize && compact[i] == compact[i+1] {
if i+1 < boardSize && compact[i] == compact[i+1] { //nolint:gosec // G602: i+1 bounds-checked on same line
merged := compact[i] * 2
result[ri] = merged
score += merged
Expand Down
2 changes: 1 addition & 1 deletion internal/twofortyeight/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func (m Model) View() string {
func (m Model) renderBoard() string {
rows := make([]string, 0, boardSize)
for r := range boardSize {
var cells []string
cells := make([]string, 0, boardSize)
for c := range boardSize {
cells = append(cells, renderTile(m.game.Board[r][c]))
}
Expand Down
2 changes: 1 addition & 1 deletion internal/typingtest/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (g *Game) TypeChar(ch rune) {
}

g.TotalTyped++
if byte(ch) == word[g.CharPos] {
if byte(ch) == word[g.CharPos] { //nolint:gosec // G115: ch is ASCII typing input, fits in byte
g.CorrectChars++
g.CurrentErrors[g.CharPos] = false
} else {
Expand Down
4 changes: 2 additions & 2 deletions internal/typingtest/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func (m Model) View() string {
}

func (m Model) viewReady() []string {
var sections []string
sections := make([]string, 0, 5)

sections = append(sections,
m.renderTextBlock(),
Expand All @@ -175,7 +175,7 @@ func (m Model) viewReady() []string {
}

func (m Model) viewPlaying() []string {
var sections []string
sections := make([]string, 0, 5)

sections = append(sections,
m.renderTextBlock(),
Expand Down
5 changes: 3 additions & 2 deletions internal/wordle/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,10 @@ func (m Model) renderKeyboard() string {
}
line := strings.Join(keys, "")
// Indent the second and third rows for staggered keyboard look.
if i == 1 {
switch i {
case 1:
line = " " + line
} else if i == 2 {
case 2:
line = " " + line
}
lines = append(lines, line)
Expand Down
3 changes: 3 additions & 0 deletions internal/yahtzee/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ func ScoreCategory(dice [5]int, cat Category) int {
return scoreYahtzee(dice)
case Chance:
return scoreChance(dice)
case NumCategories:
// Sentinel value; not a real category.
return 0
}
return 0
}
Expand Down