Full-screen Tetris game using Bubbletea with title screen, ghost piece, lock delay, NES-style scoring, configurable difficulty (easy/normal/hard), and honeypot event logging. Bumps version to 0.17.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
package tetris
|
||
|
||
import "github.com/charmbracelet/lipgloss"
|
||
|
||
// pieceType identifies a tetromino (0–6).
|
||
type pieceType int
|
||
|
||
const (
|
||
pieceI pieceType = iota
|
||
pieceO
|
||
pieceT
|
||
pieceS
|
||
pieceZ
|
||
pieceJ
|
||
pieceL
|
||
)
|
||
|
||
const numPieceTypes = 7
|
||
|
||
// Standard Tetris colors.
|
||
var pieceColors = [numPieceTypes]lipgloss.Color{
|
||
lipgloss.Color("#00FFFF"), // I — cyan
|
||
lipgloss.Color("#FFFF00"), // O — yellow
|
||
lipgloss.Color("#AA00FF"), // T — purple
|
||
lipgloss.Color("#00FF00"), // S — green
|
||
lipgloss.Color("#FF0000"), // Z — red
|
||
lipgloss.Color("#0000FF"), // J — blue
|
||
lipgloss.Color("#FF8800"), // L — orange
|
||
}
|
||
|
||
// Each piece has 4 rotations, each rotation is a list of (row, col) offsets
|
||
// relative to the piece origin.
|
||
type rotation [4][2]int
|
||
|
||
var pieces = [numPieceTypes][4]rotation{
|
||
// I
|
||
{
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{0, 2}, [2]int{0, 3}},
|
||
{[2]int{0, 0}, [2]int{1, 0}, [2]int{2, 0}, [2]int{3, 0}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{0, 2}, [2]int{0, 3}},
|
||
{[2]int{0, 0}, [2]int{1, 0}, [2]int{2, 0}, [2]int{3, 0}},
|
||
},
|
||
// O
|
||
{
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{1, 0}, [2]int{1, 1}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{1, 0}, [2]int{1, 1}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{1, 0}, [2]int{1, 1}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{1, 0}, [2]int{1, 1}},
|
||
},
|
||
// T
|
||
{
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{0, 2}, [2]int{1, 1}},
|
||
{[2]int{0, 0}, [2]int{1, 0}, [2]int{2, 0}, [2]int{1, 1}},
|
||
{[2]int{0, 1}, [2]int{1, 0}, [2]int{1, 1}, [2]int{1, 2}},
|
||
{[2]int{0, 1}, [2]int{1, 0}, [2]int{1, 1}, [2]int{2, 1}},
|
||
},
|
||
// S
|
||
{
|
||
{[2]int{0, 1}, [2]int{0, 2}, [2]int{1, 0}, [2]int{1, 1}},
|
||
{[2]int{0, 0}, [2]int{1, 0}, [2]int{1, 1}, [2]int{2, 1}},
|
||
{[2]int{0, 1}, [2]int{0, 2}, [2]int{1, 0}, [2]int{1, 1}},
|
||
{[2]int{0, 0}, [2]int{1, 0}, [2]int{1, 1}, [2]int{2, 1}},
|
||
},
|
||
// Z
|
||
{
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{1, 1}, [2]int{1, 2}},
|
||
{[2]int{0, 1}, [2]int{1, 0}, [2]int{1, 1}, [2]int{2, 0}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{1, 1}, [2]int{1, 2}},
|
||
{[2]int{0, 1}, [2]int{1, 0}, [2]int{1, 1}, [2]int{2, 0}},
|
||
},
|
||
// J
|
||
{
|
||
{[2]int{0, 0}, [2]int{1, 0}, [2]int{1, 1}, [2]int{1, 2}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{1, 0}, [2]int{2, 0}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{0, 2}, [2]int{1, 2}},
|
||
{[2]int{0, 0}, [2]int{1, 0}, [2]int{2, 0}, [2]int{2, 1}},
|
||
},
|
||
// L
|
||
{
|
||
{[2]int{0, 2}, [2]int{1, 0}, [2]int{1, 1}, [2]int{1, 2}},
|
||
{[2]int{0, 0}, [2]int{1, 0}, [2]int{2, 0}, [2]int{2, 1}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{0, 2}, [2]int{1, 0}},
|
||
{[2]int{0, 0}, [2]int{0, 1}, [2]int{1, 1}, [2]int{2, 1}},
|
||
},
|
||
}
|
||
|
||
// spawnCol returns the starting column for a piece, centering it on the board.
|
||
func spawnCol(pt pieceType, rot int) int {
|
||
shape := pieces[pt][rot]
|
||
minC, maxC := shape[0][1], shape[0][1]
|
||
for _, off := range shape {
|
||
if off[1] < minC {
|
||
minC = off[1]
|
||
}
|
||
if off[1] > maxC {
|
||
maxC = off[1]
|
||
}
|
||
}
|
||
width := maxC - minC + 1
|
||
return (boardCols - width) / 2
|
||
}
|