diff --git a/internal/game/game.go b/internal/game/game.go index bb1be62..dd23525 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -4,6 +4,7 @@ import ( "fmt" "image/color" "math/rand" + "os" "time" "tetris/internal/font" @@ -19,27 +20,33 @@ import ( // Game represents the main game state type Game struct { - board [][]types.BlockType - currentPiece *tetromino.Tetromino - nextPiece *tetromino.Tetromino - dropCounter int - dropInterval int - score int - level int - gameOver bool - lastMoveDown time.Time - lastRotate time.Time - fontRenderer *font.FontRenderer + board [][]types.BlockType + currentPiece *tetromino.Tetromino + nextPiece *tetromino.Tetromino + dropCounter int + dropInterval int + score int + level int + gameOver bool + lastMoveDown time.Time + lastRotate time.Time + lastMoveLeft time.Time + lastMoveRight time.Time + fontRenderer *font.FontRenderer + hardDropping bool + hardDropCounter int } // New creates a new game instance func New() *Game { g := &Game{ - board: make([][]types.BlockType, config.BoardHeight), - dropInterval: config.InitialDropInterval, - lastMoveDown: time.Now(), - lastRotate: time.Now(), - fontRenderer: font.NewFontRenderer(), + board: make([][]types.BlockType, config.BoardHeight), + dropInterval: config.InitialDropInterval, + lastMoveDown: time.Now(), + lastRotate: time.Now(), + lastMoveLeft: time.Now(), + lastMoveRight: time.Now(), + fontRenderer: font.NewFontRenderer(), } for i := range g.board { @@ -53,7 +60,7 @@ func New() *Game { // Update handles game logic updates func (g *Game) Update() error { if g.gameOver { - if inpututil.IsKeyJustPressed(ebiten.KeySpace) { + if inpututil.IsKeyJustPressed(ebiten.KeyR) { *g = *New() return nil } @@ -114,10 +121,14 @@ func (g *Game) Draw(screen *ebiten.Image) { } } - // Draw actual piece + // Draw actual piece with special effect during hard drop for _, pos := range g.currentPiece.GetAbsolutePositions() { if pos[1] >= 0 { - g.drawBlock(screen, pos[0], pos[1], g.currentPiece.BlockType) + if g.hardDropping { + g.drawHardDropBlock(screen, pos[0], pos[1], g.currentPiece.BlockType) + } else { + g.drawBlock(screen, pos[0], pos[1], g.currentPiece.BlockType) + } } } } @@ -151,8 +162,8 @@ func (g *Game) Draw(screen *ebiten.Image) { if g.gameOver { text := i18n.GetText() - g.fontRenderer.DrawText(screen, text.GameOver, float64(config.PreviewX-config.BlockSize), float64(config.ScreenHeight/2), color.RGBA{255, 255, 255, 255}) - g.fontRenderer.DrawText(screen, text.Restart, float64(config.PreviewX-config.BlockSize), float64(config.ScreenHeight/2+20), color.RGBA{255, 255, 255, 255}) + g.fontRenderer.DrawText(screen, text.GameOver, float64(config.ScreenWidth/2-20), float64(config.ScreenHeight/2-100), color.RGBA{255, 255, 255, 255}) + g.fontRenderer.DrawText(screen, text.Restart, float64(config.ScreenWidth/2-20), float64(config.ScreenHeight/2-80), color.RGBA{255, 0, 0, 255}) } } @@ -168,8 +179,8 @@ func (g *Game) handleInput() { i18n.SwitchLanguage() } - // Restart game when game over - if g.gameOver && inpututil.IsKeyJustPressed(ebiten.KeyR) { + // Restart game anytime + if inpututil.IsKeyJustPressed(ebiten.KeyR) { g.restart() return } @@ -179,12 +190,37 @@ func (g *Game) handleInput() { return } - // Move left/right - if inpututil.IsKeyJustPressed(ebiten.KeyLeft) { - g.tryMove(-1, 0) + // Hard drop + if inpututil.IsKeyJustPressed(ebiten.KeySpace) { + g.hardDrop() } - if inpututil.IsKeyJustPressed(ebiten.KeyRight) { - g.tryMove(1, 0) + + // Skip movement inputs during hard drop animation + if g.hardDropping { + return + } + + // Move left/right - support continuous movement when holding keys + if ebiten.IsKeyPressed(ebiten.KeyLeft) { + // First press moves immediately, then add delay for continuous movement + if inpututil.IsKeyJustPressed(ebiten.KeyLeft) { + g.tryMove(-1, 0) + g.lastMoveLeft = time.Now() + } else if time.Since(g.lastMoveLeft) > time.Millisecond*120 { + g.tryMove(-1, 0) + g.lastMoveLeft = time.Now() + } + } + + if ebiten.IsKeyPressed(ebiten.KeyRight) { + // First press moves immediately, then add delay for continuous movement + if inpututil.IsKeyJustPressed(ebiten.KeyRight) { + g.tryMove(1, 0) + g.lastMoveRight = time.Now() + } else if time.Since(g.lastMoveRight) > time.Millisecond*120 { + g.tryMove(1, 0) + g.lastMoveRight = time.Now() + } } // Rotate @@ -202,20 +238,32 @@ func (g *Game) handleInput() { g.lastMoveDown = time.Now() } } - - // Hard drop - if inpututil.IsKeyJustPressed(ebiten.KeySpace) { - g.hardDrop() + // Exit game + if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + os.Exit(0) } } -// updateDropCounter handles automatic piece dropping +// updateDropCounter handles automatic piece dropping and hard drop animation func (g *Game) updateDropCounter() { - g.dropCounter++ - if g.dropCounter >= g.dropInterval { - g.dropCounter = 0 - if !g.tryMove(0, 1) { - g.lockPiece() + if g.hardDropping { + // Hard drop animation - move faster + g.hardDropCounter++ + if g.hardDropCounter >= 2 { // Move every 2 frames for visible animation + g.hardDropCounter = 0 + if !g.tryMove(0, 1) { + g.hardDropping = false + g.lockPiece() + } + } + } else { + // Normal drop + g.dropCounter++ + if g.dropCounter >= g.dropInterval { + g.dropCounter = 0 + if !g.tryMove(0, 1) { + g.lockPiece() + } } } } @@ -275,24 +323,14 @@ func (g *Game) tryRotate() bool { return false } -// hardDrop drops the piece instantly to the bottom +// hardDrop starts the hard drop animation func (g *Game) hardDrop() { - if g.currentPiece == nil { + if g.currentPiece == nil || g.hardDropping { return } - dropDistance := 0 - for { - if !g.tryMove(0, 1) { - break - } - dropDistance++ - } - - // Award extra points for hard drop - g.score += dropDistance * 2 - - g.lockPiece() + g.hardDropping = true + g.hardDropCounter = 0 } // isColliding checks if the current piece is colliding with the board or boundaries @@ -454,6 +492,40 @@ func (g *Game) drawBlock(screen *ebiten.Image, x, y int, blockType types.BlockTy borderColor, false) } +// drawHardDropBlock draws a block with special hard drop effect +func (g *Game) drawHardDropBlock(screen *ebiten.Image, x, y int, blockType types.BlockType) { + // Get original color and make it brighter for hard drop effect + originalColor := types.BlockColors[blockType].(color.RGBA) + hardDropColor := color.RGBA{ + min(255, originalColor.R+50), + min(255, originalColor.G+50), + min(255, originalColor.B+50), + 255, + } + + vector.DrawFilledRect(screen, + float32(x*config.BlockSize), + float32(y*config.BlockSize), + float32(config.BlockSize-1), + float32(config.BlockSize-1), + hardDropColor, false) + + // Add bright white border for hard drop effect + borderColor := color.RGBA{255, 255, 255, 200} + vector.DrawFilledRect(screen, + float32(x*config.BlockSize), + float32(y*config.BlockSize), + float32(config.BlockSize-1), + 3, + borderColor, false) + vector.DrawFilledRect(screen, + float32(x*config.BlockSize), + float32(y*config.BlockSize), + 3, + float32(config.BlockSize-1), + borderColor, false) +} + // max returns the maximum of two integers func max(a, b int) int { if a > b { @@ -462,6 +534,14 @@ func max(a, b int) int { return b } +// min returns the minimum of two uint8 values +func min(a, b uint8) uint8 { + if a < b { + return a + } + return b +} + // drawControls renders the control instructions on the right side of the screen func (g *Game) drawControls(screen *ebiten.Image) { // Starting position for controls display