Tetris/Go

From Rosetta Code
Library: raylib-go


This is a translation of the sample Tetris Game written in C for the original raylib library of which raylib-go is a Go wrapper.

The arrow keys control the game as follows:

  • Left - moves the active piece to the left.
  • Right - moves the active piece to the right.
  • Up - rotates the active piece.
  • Down - hard drops the active piece.


Pressing the P key pauses the game and pressing it again resumes the game where it left off.

As currently written, the game only has one level (more than enough for me!).

The C game uses gray for all 7 pieces but I have changed this so that the active piece and the incoming (aka preview) piece display in the standard colors. However, when the active piece hits the bottom its color changes to gray as before.

package main

import (
    "fmt"
    rl "github.com/gen2brain/raylib-go/raylib"
    "math/rand"
    "time"
)

const (
    SQUARE_SIZE             = 20
    GRID_HORIZONTAL_SIZE    = 12
    GRID_VERTICAL_SIZE      = 20
    LATERAL_SPEED           = 10
    TURNING_SPEED           = 12
    FAST_FALL_AWAIT_COUNTER = 30
    FADING_TIME             = 33
)

type GridSquare int

const (
    EMPTY GridSquare = iota
    MOVING
    FULL
    BLOCK
    FADING
)

var (
    screenWidth  = int32(800)
    screenHeight = int32(450)

    gameOver = false
    pause    = false

    // These variables keep track of the active piece position
    piecePositionX = 0
    piecePositionY = 0

    // These variables record the active and incoming piece colors
    pieceColor         = rl.Gray
    incomingPieceColor = rl.Gray

    // Statistics
    level = 1
    lines = 0

    // Based on level
    gravitySpeed = 30
)

// Matrices
var (
    grid          [GRID_HORIZONTAL_SIZE][GRID_VERTICAL_SIZE]GridSquare
    piece         [4][4]GridSquare
    incomingPiece [4][4]GridSquare
)

// Game parameters
var (
    fadingColor  = rl.Gray
    beginPlay    = true
    pieceActive  = false
    detection    = false
    lineToDelete = false
)

// Counters
var (
    gravityMovementCounter  = 0
    lateralMovementCounter  = 0
    turnMovementCounter     = 0
    fastFallMovementCounter = 0
    fadeLineCounter         = 0
)

// Initialize game variables
func InitGame() {
    // Initialize game statistics
    level = 1 // Always stays at this level in the current program
    lines = 0

    fadingColor = rl.Gray

    piecePositionX = 0
    piecePositionY = 0
    pieceColor = rl.Gray
    incomingPieceColor = rl.Gray

    pause = false

    beginPlay = true
    pieceActive = false
    detection = false
    lineToDelete = false

    // Counters
    gravityMovementCounter = 0
    lateralMovementCounter = 0
    turnMovementCounter = 0
    fastFallMovementCounter = 0

    fadeLineCounter = 0
    gravitySpeed = 30

    // Initialize grid matrices
    for i := 0; i < GRID_HORIZONTAL_SIZE; i++ {
        for j := 0; j < GRID_VERTICAL_SIZE; j++ {
            if (j == GRID_VERTICAL_SIZE-1) || (i == 0) ||
                (i == GRID_HORIZONTAL_SIZE-1) {
                grid[i][j] = BLOCK
            } else {
                grid[i][j] = EMPTY
            }
        }
    }

    // Initialize incoming piece matrices
    for i := 0; i < 4; i++ {
        for j := 0; j < 4; j++ {
            incomingPiece[i][j] = EMPTY
        }
    }
}

// Update game (one frame)
func UpdateGame() {
    if !gameOver {
        if rl.IsKeyPressed('P') {
            pause = !pause
        }
        if !pause {
            if !lineToDelete {
                if !pieceActive {
                    // Get another piece
                    pieceActive = CreatePiece()

                    // We leave a little time before starting the fast falling down
                    fastFallMovementCounter = 0
                } else { // Piece falling
                    // Counters update
                    fastFallMovementCounter++
                    gravityMovementCounter++
                    lateralMovementCounter++
                    turnMovementCounter++

                    // We make sure to move if we've pressed the key this frame
                    if rl.IsKeyPressed(rl.KeyLeft) || rl.IsKeyPressed(rl.KeyRight) {
                        lateralMovementCounter = LATERAL_SPEED
                    }
                    if rl.IsKeyPressed(rl.KeyUp) {
                        turnMovementCounter = TURNING_SPEED
                    }

                    // Fall down
                    if rl.IsKeyDown(rl.KeyDown) &&
                        fastFallMovementCounter >= FAST_FALL_AWAIT_COUNTER {
                        // We make sure the piece is going to fall this frame
                        gravityMovementCounter += gravitySpeed
                    }

                    if gravityMovementCounter >= gravitySpeed {
                        // Basic falling movement
                        CheckDetection(&detection)

                        // Check if the piece has collided with another piece or with the boundaries
                        ResolveFallingMovement(&detection, &pieceActive)

                        // Check if we completed a line and, if so, erase the line
                        // and pull down the the lines above
                        CheckCompletion(&lineToDelete)

                        gravityMovementCounter = 0
                    }

                    // Move laterally at player's will
                    if lateralMovementCounter >= LATERAL_SPEED {
                        // Update the lateral movement and, if successful, reset the lateral counter
                        if !ResolveLateralMovement() {
                            lateralMovementCounter = 0
                        }
                    }

                    // Turn the piece at player's will
                    if turnMovementCounter >= TURNING_SPEED {
                        // Update the turning movement and reset the turning counter
                        if ResolveTurnMovement() {
                            turnMovementCounter = 0
                        }
                    }
                }

                // Game over logic
                for j := 0; j < 2; j++ {
                    for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
                        if grid[i][j] == FULL {
                            gameOver = true
                        }
                    }
                }
            } else {
                // Animation when deleting lines
                fadeLineCounter++

                if fadeLineCounter%8 < 4 {
                    fadingColor = rl.Maroon
                } else {
                    fadingColor = rl.Gray
                }
                if fadeLineCounter >= FADING_TIME {
                    DeleteCompleteLines()
                    fadeLineCounter = 0
                    lineToDelete = false
                    lines++
                }
            }
        }
    } else if rl.IsKeyPressed(rl.KeyEnter) {
        InitGame()
        gameOver = false
    }
}

// Draw game (one frame)
func DrawGame() {
    rl.BeginDrawing()

    rl.ClearBackground(rl.RayWhite)

    if !gameOver {
        // Draw gameplay area
        var offset rl.Vector2
        offset.X = float32(screenWidth/2 - GRID_HORIZONTAL_SIZE*SQUARE_SIZE/2 - 50)
        offset.Y = float32(screenHeight/2 - (GRID_VERTICAL_SIZE-1)*SQUARE_SIZE/2 + SQUARE_SIZE*2)
        offset.Y -= 50 // NOTE: Hard-coded position!

        controller := offset.X

        for j := 0; j < GRID_VERTICAL_SIZE; j++ {
            for i := 0; i < GRID_HORIZONTAL_SIZE; i++ {
                // Draw each square of the grid
                ox, oy := int32(offset.X), int32(offset.Y)
                if grid[i][j] == EMPTY {
                    rl.DrawLine(ox, oy, ox+SQUARE_SIZE, oy, rl.LightGray)
                    rl.DrawLine(ox, oy, ox, oy+SQUARE_SIZE, rl.LightGray)
                    rl.DrawLine(ox+SQUARE_SIZE, oy, ox+SQUARE_SIZE,
                        oy+SQUARE_SIZE, rl.LightGray)
                    rl.DrawLine(ox, oy+SQUARE_SIZE, ox+SQUARE_SIZE,
                        oy+SQUARE_SIZE, rl.LightGray)
                    offset.X += SQUARE_SIZE
                } else if grid[i][j] == FULL {
                    rl.DrawRectangle(ox, oy, SQUARE_SIZE, SQUARE_SIZE, rl.Gray)
                    offset.X += SQUARE_SIZE
                } else if grid[i][j] == MOVING {
                    rl.DrawRectangle(ox, oy, SQUARE_SIZE, SQUARE_SIZE, pieceColor)
                    offset.X += SQUARE_SIZE
                } else if grid[i][j] == BLOCK {
                    rl.DrawRectangle(ox, oy, SQUARE_SIZE, SQUARE_SIZE, rl.LightGray)
                    offset.X += SQUARE_SIZE
                } else if grid[i][j] == FADING {
                    rl.DrawRectangle(ox, oy, SQUARE_SIZE, SQUARE_SIZE, fadingColor)
                    offset.X += SQUARE_SIZE
                }
            }

            offset.X = controller
            offset.Y += SQUARE_SIZE
        }

        // Draw incoming piece (hard-coded)
        offset.X = 500
        offset.Y = 45

        controler := offset.X

        for j := 0; j < 4; j++ {
            for i := 0; i < 4; i++ {
                ox, oy := int32(offset.X), int32(offset.Y)
                if incomingPiece[i][j] == EMPTY {
                    rl.DrawLine(ox, oy, ox+SQUARE_SIZE, oy, rl.LightGray)
                    rl.DrawLine(ox, oy, ox, oy+SQUARE_SIZE, rl.LightGray)
                    rl.DrawLine(ox+SQUARE_SIZE, oy, ox+SQUARE_SIZE,
                        oy+SQUARE_SIZE, rl.LightGray)
                    rl.DrawLine(ox, oy+SQUARE_SIZE, ox+SQUARE_SIZE,
                        oy+SQUARE_SIZE, rl.LightGray)
                    offset.X += SQUARE_SIZE
                } else if incomingPiece[i][j] == MOVING {
                    rl.DrawRectangle(ox, oy, SQUARE_SIZE, SQUARE_SIZE, incomingPieceColor)
                    offset.X += SQUARE_SIZE
                }
            }

            offset.X = controler
            offset.Y += SQUARE_SIZE
        }

        ox, oy := int32(offset.X), int32(offset.Y)
        rl.DrawText("INCOMING:", ox, oy-100, 10, rl.Gray)
        rl.DrawText(fmt.Sprintf("LINES:      %04d", lines), ox, oy+20, 10, rl.Gray)

        if pause {
            rl.DrawText("GAME PAUSED", screenWidth/2-rl.MeasureText("GAME PAUSED", 40)/2,
                screenHeight/2-40, 40, rl.Gray)
        }
    } else {
        rl.DrawText("PRESS [ENTER] TO PLAY AGAIN", int32(rl.GetScreenWidth()/2)-
            rl.MeasureText("PRESS [ENTER] TO PLAY AGAIN", 20)/2,
            int32(rl.GetScreenHeight()/2)-50, 20, rl.Gray)
    }

    rl.EndDrawing()
}

// Unload game variables
func UnloadGame() {
    // Nothing to do here
}

// Update and Draw (one frame)
func UpdateDrawFrame() {
    UpdateGame()
    DrawGame()
}

func CreatePiece() bool {
    piecePositionX = (GRID_HORIZONTAL_SIZE - 4) / 2
    piecePositionY = 0

    // If the game is starting and we are going to create the first piece,
    // we create an extra one
    if beginPlay {
        GetRandomPiece()
        beginPlay = false
    }

    // We assign the incoming piece to the actual piece
    for i := 0; i < 4; i++ {
        for j := 0; j < 4; j++ {
            piece[i][j] = incomingPiece[i][j]
        }
    }
    pieceColor = incomingPieceColor

    // We assign a random piece to the incoming one
    GetRandomPiece()

    // Assign the piece to the grid
    for i := piecePositionX; i < piecePositionX+4; i++ {
        for j := 0; j < 4; j++ {
            if piece[i-piecePositionX][j] == MOVING {
                grid[i][j] = MOVING
            }
        }
    }
    return true
}

func GetRandomPiece() {
    random := rand.Intn(7) // 0 to 6 inclusive
    for i := 0; i < 4; i++ {
        for j := 0; j < 4; j++ {
            incomingPiece[i][j] = EMPTY
        }
    }
    switch random {
    case 0: // O (Square)
        incomingPiece[1][1] = MOVING
        incomingPiece[2][1] = MOVING
        incomingPiece[1][2] = MOVING
        incomingPiece[2][2] = MOVING
        incomingPieceColor = rl.Yellow
    case 1: // L
        incomingPiece[1][0] = MOVING
        incomingPiece[1][1] = MOVING
        incomingPiece[1][2] = MOVING
        incomingPiece[2][2] = MOVING
        incomingPieceColor = rl.Blue
    case 2: // J (Inverted L)
        incomingPiece[1][2] = MOVING
        incomingPiece[2][0] = MOVING
        incomingPiece[2][1] = MOVING
        incomingPiece[2][2] = MOVING
        incomingPieceColor = rl.Brown
    case 3: // I (Straight)
        incomingPiece[0][1] = MOVING
        incomingPiece[1][1] = MOVING
        incomingPiece[2][1] = MOVING
        incomingPiece[3][1] = MOVING
        incomingPieceColor = rl.SkyBlue
    case 4: // T (Cross cut)
        incomingPiece[1][0] = MOVING
        incomingPiece[1][1] = MOVING
        incomingPiece[1][2] = MOVING
        incomingPiece[2][1] = MOVING
        incomingPieceColor = rl.Purple
    case 5: // S
        incomingPiece[1][1] = MOVING
        incomingPiece[2][1] = MOVING
        incomingPiece[2][2] = MOVING
        incomingPiece[3][2] = MOVING
        incomingPieceColor = rl.Green
    case 6: // Z (Inverted S)
        incomingPiece[1][2] = MOVING
        incomingPiece[2][2] = MOVING
        incomingPiece[2][1] = MOVING
        incomingPiece[3][1] = MOVING
        incomingPieceColor = rl.Red
    }
}

func ResolveFallingMovement(detection, pieceActive *bool) {
    // If we've finished moving this piece, we stop it
    if *detection {
        for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
            for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
                if grid[i][j] == MOVING {
                    grid[i][j] = FULL
                    *detection = false
                    *pieceActive = false
                }
            }
        }
    } else { // We move down the piece
        for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
            for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
                if grid[i][j] == MOVING {
                    grid[i][j+1] = MOVING
                    grid[i][j] = EMPTY
                }
            }
        }

        piecePositionY++
    }
}

func ResolveLateralMovement() bool {
    collision := false

    // Piece movement
    if rl.IsKeyDown(rl.KeyLeft) { // Move left
        // Check if it's possible to move to left
        for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
            for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
                if grid[i][j] == MOVING {
                    // Check if we are touching the left wall or
                    // we have a full square at the left
                    if i-1 == 0 || grid[i-1][j] == FULL {
                        collision = true
                    }
                }
            }
        }

        // If able, move left
        if !collision {
            for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
                // We check the matrix from left to right
                for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
                    // Move everything to the left
                    if grid[i][j] == MOVING {
                        grid[i-1][j] = MOVING
                        grid[i][j] = EMPTY
                    }
                }
            }

            piecePositionX--
        }
    } else if rl.IsKeyDown(rl.KeyRight) { // Move right
        // Check if it's possible to move to right
        for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
            for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
                if grid[i][j] == MOVING {
                    // Check if we are touching the right wall or
                    // we have a full square at the right
                    if i+1 == GRID_HORIZONTAL_SIZE-1 || grid[i+1][j] == FULL {
                        collision = true
                    }
                }
            }
        }

        // If able, move right
        if !collision {
            for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
                // We check the matrix from right to left
                for i := GRID_HORIZONTAL_SIZE - 1; i >= 1; i-- {
                    // Move everything to the right
                    if grid[i][j] == MOVING {
                        grid[i+1][j] = MOVING
                        grid[i][j] = EMPTY
                    }
                }
            }

            piecePositionX++
        }
    }
    return collision
}

func ResolveTurnMovement() bool {
    // Input for turning the piece
    if rl.IsKeyDown(rl.KeyUp) {
        aux := GridSquare(0)

        // Check all turning possibilities
        if grid[piecePositionX+3][piecePositionY] == MOVING &&
            grid[piecePositionX][piecePositionY] != EMPTY &&
            grid[piecePositionX][piecePositionY] != MOVING {
            goto next
        }

        if grid[piecePositionX+3][piecePositionY+3] == MOVING &&
            grid[piecePositionX+3][piecePositionY] != EMPTY &&
            grid[piecePositionX+3][piecePositionY] != MOVING {
            goto next
        }

        if grid[piecePositionX][piecePositionY+3] == MOVING &&
            grid[piecePositionX+3][piecePositionY+3] != EMPTY &&
            grid[piecePositionX+3][piecePositionY+3] != MOVING {
            goto next
        }

        if grid[piecePositionX][piecePositionY] == MOVING &&
            grid[piecePositionX][piecePositionY+3] != EMPTY &&
            grid[piecePositionX][piecePositionY+3] != MOVING {
            goto next
        }

        if grid[piecePositionX+1][piecePositionY] == MOVING &&
            grid[piecePositionX][piecePositionY+2] != EMPTY &&
            grid[piecePositionX][piecePositionY+2] != MOVING {
            goto next
        }

        if grid[piecePositionX+3][piecePositionY+1] == MOVING &&
            grid[piecePositionX+1][piecePositionY] != EMPTY &&
            grid[piecePositionX+1][piecePositionY] != MOVING {
            goto next
        }

        if grid[piecePositionX+2][piecePositionY+3] == MOVING &&
            grid[piecePositionX+3][piecePositionY+1] != EMPTY &&
            grid[piecePositionX+3][piecePositionY+1] != MOVING {
            goto next
        }

        if grid[piecePositionX][piecePositionY+2] == MOVING &&
            grid[piecePositionX+2][piecePositionY+3] != EMPTY &&
            grid[piecePositionX+2][piecePositionY+3] != MOVING {
            goto next
        }

        if grid[piecePositionX+2][piecePositionY] == MOVING &&
            grid[piecePositionX][piecePositionY+1] != EMPTY &&
            grid[piecePositionX][piecePositionY+1] != MOVING {
            goto next
        }

        if grid[piecePositionX+3][piecePositionY+2] == MOVING &&
            grid[piecePositionX+2][piecePositionY] != EMPTY &&
            grid[piecePositionX+2][piecePositionY] != MOVING {
            goto next
        }

        if grid[piecePositionX+1][piecePositionY+3] == MOVING &&
            grid[piecePositionX+3][piecePositionY+2] != EMPTY &&
            grid[piecePositionX+3][piecePositionY+2] != MOVING {
            goto next
        }

        if grid[piecePositionX][piecePositionY+1] == MOVING &&
            grid[piecePositionX+1][piecePositionY+3] != EMPTY &&
            grid[piecePositionX+1][piecePositionY+3] != MOVING {
            goto next
        }

        if grid[piecePositionX+1][piecePositionY+1] == MOVING &&
            grid[piecePositionX+1][piecePositionY+2] != EMPTY &&
            grid[piecePositionX+1][piecePositionY+2] != MOVING {
            goto next
        }

        if grid[piecePositionX+2][piecePositionY+1] == MOVING &&
            grid[piecePositionX+1][piecePositionY+1] != EMPTY &&
            grid[piecePositionX+1][piecePositionY+1] != MOVING {
            goto next
        }

        if grid[piecePositionX+2][piecePositionY+2] == MOVING &&
            grid[piecePositionX+2][piecePositionY+1] != EMPTY &&
            grid[piecePositionX+2][piecePositionY+1] != MOVING {
            goto next
        }

        if grid[piecePositionX+1][piecePositionY+2] == MOVING &&
            grid[piecePositionX+2][piecePositionY+2] != EMPTY &&
            grid[piecePositionX+2][piecePositionY+2] != MOVING {
            goto next
        }

        aux = piece[0][0]
        piece[0][0] = piece[3][0]
        piece[3][0] = piece[3][3]
        piece[3][3] = piece[0][3]
        piece[0][3] = aux

        aux = piece[1][0]
        piece[1][0] = piece[3][1]
        piece[3][1] = piece[2][3]
        piece[2][3] = piece[0][2]
        piece[0][2] = aux

        aux = piece[2][0]
        piece[2][0] = piece[3][2]
        piece[3][2] = piece[1][3]
        piece[1][3] = piece[0][1]
        piece[0][1] = aux

        aux = piece[1][1]
        piece[1][1] = piece[2][1]
        piece[2][1] = piece[2][2]
        piece[2][2] = piece[1][2]
        piece[1][2] = aux
    next:
        for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
            for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
                if grid[i][j] == MOVING {
                    grid[i][j] = EMPTY
                }
            }
        }
        for i := piecePositionX; i < piecePositionX+4; i++ {
            for j := piecePositionY; j < piecePositionY+4; j++ {
                if piece[i-piecePositionX][j-piecePositionY] == MOVING {
                    grid[i][j] = MOVING
                }
            }
        }
        return true
    }
    return false
}

func CheckDetection(detection *bool) {
    for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
        for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
            if grid[i][j] == MOVING && (grid[i][j+1] == FULL || grid[i][j+1] == BLOCK) {
                *detection = true
            }
        }
    }
}

func CheckCompletion(lineToDelete *bool) {
    for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
        calculator := 0
        for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
            // Count each square of the line
            if grid[i][j] == FULL {
                calculator++
            }

            // Check if we completed the whole line
            if calculator == GRID_HORIZONTAL_SIZE-2 {
                *lineToDelete = true
                calculator = 0

                // Mark the completed line
                for z := 1; z < GRID_HORIZONTAL_SIZE-1; z++ {
                    grid[z][j] = FADING
                }
            }
        }
    }
}

func DeleteCompleteLines() {
    for j := GRID_VERTICAL_SIZE - 2; j >= 0; j-- {
        for grid[1][j] == FADING {
            for i := 1; i < GRID_HORIZONTAL_SIZE-1; i++ {
                grid[i][j] = EMPTY
            }
            for j2 := j - 1; j2 >= 0; j2-- {
                for i2 := 1; i2 < GRID_HORIZONTAL_SIZE-1; i2++ {
                    if grid[i2][j2] == FULL {
                        grid[i2][j2+1] = FULL
                        grid[i2][j2] = EMPTY
                    } else if grid[i2][j2] == FADING {
                        grid[i2][j2+1] = FADING
                        grid[i2][j2] = EMPTY
                    }
                }
            }
        }
    }
}

func main() {
    rand.Seed(time.Now().UnixNano())
    rl.InitWindow(screenWidth, screenHeight, "Tetris")
    InitGame()
    rl.SetTargetFPS(60)
    for !rl.WindowShouldClose() { // Detect window close button or ESC key
        UpdateDrawFrame()
    }
    UnloadGame()
    rl.CloseWindow()
}