thomashamburg eb1f14d19c tetris v1
2025-09-29 17:46:48 +02:00

502 lines
11 KiB
Go

package main
import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"log"
"net/http"
"time"
)
// Teris rules https://tetris.fandom.com/wiki/Tetris_Guideline
// Tetrimino types
const (
EMPTY = iota
I_PIECE
O_PIECE
T_PIECE
S_PIECE
Z_PIECE
J_PIECE
L_PIECE
)
// Tetrimino shapes (4 rotations each)
var tetriminoShapes = map[int][4][4][4]int{
I_PIECE: {
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
},
O_PIECE: {
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
},
T_PIECE: {
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
},
S_PIECE: {
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
},
Z_PIECE: {
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
},
J_PIECE: {
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
},
L_PIECE: {
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
},
}
type Tetrimino struct {
Type int `json:"type"`
X int `json:"x"`
Y int `json:"y"`
Rotation int `json:"rotation"`
}
type GameState struct {
Board [40][10]int `json:"board"`
CurrentPiece *Tetrimino `json:"current_piece"`
NextPieces []int `json:"next_pieces"`
HoldPiece *int `json:"hold_piece"`
Score int `json:"score"`
Level int `json:"level"`
LinesCleared int `json:"lines_cleared"`
GameRunning bool `json:"game_running"`
GameOver bool `json:"game_over"`
CanHold bool `json:"can_hold"`
LinesForLevel int `json:"lines_for_level"`
}
type Game struct {
state *GameState
bag []int
bagIndex int
clients map[chan string]bool
}
func NewGame() *Game {
g := &Game{
state: &GameState{
NextPieces: make([]int, 6),
Level: 1,
GameRunning: false,
GameOver: false,
CanHold: true,
LinesForLevel: 5,
},
clients: make(map[chan string]bool),
}
g.fillBag()
g.fillNextPieces()
return g
}
func (g *Game) fillBag() {
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
// Simple shuffle using current time
for i := len(g.bag) - 1; i > 0; i-- {
j := int(time.Now().UnixNano()) % (i + 1)
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
}
g.bagIndex = 0
}
func (g *Game) getNextPiece() int {
if g.bagIndex >= len(g.bag) {
g.fillBag()
}
piece := g.bag[g.bagIndex]
g.bagIndex++
return piece
}
func (g *Game) fillNextPieces() {
for i := 0; i < 6; i++ {
g.state.NextPieces[i] = g.getNextPiece()
}
}
func (g *Game) spawnPiece() {
pieceType := g.state.NextPieces[0]
// Shift next pieces
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
g.state.NextPieces[5] = g.getNextPiece()
// Spawn position based on guidelines
x := 3 // left-middle for most pieces
if pieceType == I_PIECE || pieceType == O_PIECE {
x = 4 // middle for I and O
}
g.state.CurrentPiece = &Tetrimino{
Type: pieceType,
X: x,
Y: 21,
Rotation: 0,
}
// Drop one space if possible
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
g.state.CurrentPiece.Y--
}
// Check for game over
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
g.state.GameOver = true
g.state.GameRunning = false
}
g.state.CanHold = true
}
func (g *Game) canMove(x, y, rotation int) bool {
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
for py := 0; py < 4; py++ {
for px := 0; px < 4; px++ {
if shape[py][px] == 0 {
continue
}
boardX := x + px
boardY := y + py
if boardX < 0 || boardX >= 10 || boardY < 0 {
return false
}
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
return false
}
}
}
return true
}
func (g *Game) lockPiece() {
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
for py := 0; py < 4; py++ {
for px := 0; px < 4; px++ {
if shape[py][px] == 0 {
continue
}
boardX := g.state.CurrentPiece.X + px
boardY := g.state.CurrentPiece.Y + py
if boardY < 40 {
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
}
}
}
g.clearLines()
g.spawnPiece()
}
func (g *Game) clearLines() {
linesCleared := 0
for y := 0; y < 40; y++ {
full := true
for x := 0; x < 10; x++ {
if g.state.Board[y][x] == EMPTY {
full = false
break
}
}
if full {
// Remove line
for moveY := y; moveY < 39; moveY++ {
for x := 0; x < 10; x++ {
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
}
}
// Clear top line
for x := 0; x < 10; x++ {
g.state.Board[39][x] = EMPTY
}
linesCleared++
y-- // Check same line again
}
}
if linesCleared > 0 {
g.state.LinesCleared += linesCleared
g.updateScore(linesCleared)
g.updateLevel()
}
}
func (g *Game) updateScore(lines int) {
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
if score, ok := baseScore[lines]; ok {
g.state.Score += score * g.state.Level
}
}
func (g *Game) updateLevel() {
requiredLines := g.state.Level * g.state.LinesForLevel
if g.state.LinesCleared >= requiredLines {
g.state.Level++
}
}
func (g *Game) move(direction string) {
if !g.state.GameRunning || g.state.CurrentPiece == nil {
return
}
newX := g.state.CurrentPiece.X
newY := g.state.CurrentPiece.Y
switch direction {
case "left":
newX--
case "right":
newX++
case "down":
newY--
}
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
g.state.CurrentPiece.X = newX
g.state.CurrentPiece.Y = newY
}
}
func (g *Game) rotate(direction string) {
if !g.state.GameRunning || g.state.CurrentPiece == nil {
return
}
newRotation := g.state.CurrentPiece.Rotation
if direction == "cw" {
newRotation = (newRotation + 1) % 4
} else {
newRotation = (newRotation + 3) % 4
}
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
g.state.CurrentPiece.Rotation = newRotation
}
}
func (g *Game) hardDrop() {
if !g.state.GameRunning || g.state.CurrentPiece == nil {
return
}
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
g.state.CurrentPiece.Y--
}
g.lockPiece()
}
func (g *Game) hold() {
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
return
}
if g.state.HoldPiece == nil {
g.state.HoldPiece = &g.state.CurrentPiece.Type
g.spawnPiece()
} else {
temp := *g.state.HoldPiece
g.state.HoldPiece = &g.state.CurrentPiece.Type
x := 3
if temp == I_PIECE || temp == O_PIECE {
x = 4
}
g.state.CurrentPiece = &Tetrimino{
Type: temp,
X: x,
Y: 21,
Rotation: 0,
}
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
g.state.CurrentPiece.Y--
}
}
g.state.CanHold = false
}
func (g *Game) startGame() {
g.state = &GameState{
NextPieces: make([]int, 6),
Level: 1,
GameRunning: true,
GameOver: false,
CanHold: true,
LinesForLevel: 5,
}
g.fillBag()
g.fillNextPieces()
g.spawnPiece()
}
func (g *Game) tick() {
if !g.state.GameRunning || g.state.CurrentPiece == nil {
return
}
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
g.state.CurrentPiece.Y--
} else {
g.lockPiece()
}
}
func (g *Game) broadcast() {
data, err := json.Marshal(g.state)
if err != nil {
log.Printf("Error marshaling game state: %v", err)
return
}
message := fmt.Sprintf("data: %s\n\n", string(data))
// Non-blocking broadcast to all clients
for client := range g.clients {
select {
case client <- message:
// Message sent successfully
default:
// Channel full or client blocked, skip this client
log.Printf("Dropping message for slow client")
}
}
}
var game = NewGame()
//go:embed frontend/*
var frontendFS embed.FS
func main() {
// Serve static files (index.html)
// http.Handle("/", http.FileServer(http.Dir("./frontend")))
// Create a sub-filesystem to serve files from the 'frontend' directory.
fs, err := fs.Sub(frontendFS, "frontend")
if err != nil {
log.Fatal(err)
}
// Serve static files from the embedded filesystem
http.Handle("/", http.FileServer(http.FS(fs)))
// API endpoints
http.HandleFunc("/events", handleSSE)
http.HandleFunc("/action", handleAction)
// Game loop
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if game.state.GameRunning {
game.tick()
game.broadcast()
}
}
}
}()
fmt.Println("Server starting on :4444")
fmt.Println("Open http://localhost:4444 in your browser")
log.Fatal(http.ListenAndServe(":4444", nil))
}
func handleSSE(w http.ResponseWriter, r *http.Request) {
// Critical SSE headers for nginx compatibility
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering
w.Header().Set("Access-Control-Allow-Origin", "*")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
client := make(chan string, 10) // Buffered channel
game.clients[client] = true
// Send initial state
data, _ := json.Marshal(game.state)
fmt.Fprintf(w, "data: %s\n\n", string(data))
flusher.Flush()
// Heartbeat to keep connection alive
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for {
select {
case message := <-client:
fmt.Fprint(w, message)
flusher.Flush()
case <-ticker.C:
// Send comment as heartbeat
fmt.Fprintf(w, ": heartbeat\n\n")
flusher.Flush()
case <-r.Context().Done():
delete(game.clients, client)
close(client)
return
}
}
}
func handleAction(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
return
}
action := r.URL.Query().Get("action")
direction := r.URL.Query().Get("direction")
switch action {
case "move":
game.move(direction)
case "rotate":
game.rotate(direction)
case "hard_drop":
game.hardDrop()
case "hold":
game.hold()
case "start":
game.startGame()
}
game.broadcast()
w.WriteHeader(http.StatusOK)
}