Bash trick: Shell script that can move and rotate Tetris left and right

In Linux bash, the previous article introduced a shell script that uses the k, j, h, l keys to move a single block up, down, left, and right.

The following continues to describe how to rotate a single block.

Execution effect

The specific implementation effect is as follows:

  • Screenshot before rotation

  • Screenshot after moving and rotating

When actually executed, a horizontal zigzag square is displayed by default.

You can move the zigzag squares inside the frame, left and right, but not up. Tetris does not allow moving up.

You can press the k key to rotate the block back and forth between horizontal and vertical.

script code

Suppose there is a rotateblock_one.sh script, the specific code content is as follows.

In this code, almost every line of code is provided with detailed comments for easy reading.

Some key points will also be explained later in this article to help understanding.

#!/bin/bash
# Implement a block that can move left, right, down, and rotate,
# The movement and rotation of the block is limited to the specified bounding box.
# Only move and rotate zigzags of all shapes.

# The following constants specify the upper, lower, left, and right boundaries of the rectangular border
# Specifies the number of columns to the left of the border
FRAME_LEFT=3
# Specifies the number of columns to the right of the border
FRAME_RIGHT=26
# Specifies the number of lines above the border
FRAME_TOP=2
# Specifies the number of lines below the border
FRAME_BOTTOM=18

# The Z_BLOCKS array below defines all the shapes of zigzag blocks.
# The given initial value corresponds to the horizontal Z-shaped square, the specific shape is:
# [][]
#   [][]
# Here, the position of each small square is represented by the number of rows and columns of coordinate points.
# The starting row number and column number of the first small square are 0, which is the origin of the whole square.
# The second small square is on the same line as the first small square, and the number of lines is also 0. Each small square
#   Two characters are displayed, so the starting column number for the second small square is 2.
# The third small square is in the row below the first small square, and its row number is 1. Its column number is 2.
# The fourth small square is in the same row as the third small square, and its row number is 1. Its column number is 4.
# Using these numbers of rows and columns plus the starting row and column numbers of the squares, you can locate each small square.
# Display in which line and which column. You can then use ANSI escape codes to set the cursor position.
# The first 8 numbers correspond to the horizontal zigzag squares.
# The next 8 numbers correspond to the vertical zigzag squares.
Z_BLOCKS=(\
    0 0 0 2 1 2 1 4\
    0 2 1 0 1 2 2 0\
)
# The Z_BLOCKS array above holds all the shapes of the zigzag blocks.
# Use the displayBlockIndex variable to point to the currently displayed block shape.
# Each square shape is represented by 8 numbers. This value is incremented by 8.
displayBlockIndex=0

# This value plus the number of small block rows in the Z_BLOCKS array,
# will specify which line each small square should be displayed on.
# Its initial value is the row below the number of rows above the border.
blockLine=$((FRAME_TOP + 1))
# blockColumn specifies the starting column of the entire block display.
# This value plus the number of small block columns in the Z_BLOCKS array,
# will specify which column each small square should be displayed in.
# Its initial value is the column next to the number of columns to the left of the border.
blockColumn=$((FRAME_LEFT + 1))

# Display a rectangular border, as the bounding range of the block movement
function showFrame()
{
    # Set the display properties of border characters: highlight and highlight, green text, green background
    printf "\e[1;7;32;42m"

    local i
    # Use the "\e[line;columnH" ANSI escape code below to move
    # Move the cursor to the specified row and column, and then display the corresponding border boundary character.
    # The number of rows increases, the number of columns remains unchanged, and the left and right borders of the border are displayed vertically
    for ((i = FRAME_TOP; i <= FRAME_BOTTOM; ++i)); do
        printf "\e[${i};${FRAME_LEFT}H|"
        printf "\e[${i};${FRAME_RIGHT}H|"
    done

    # The number of columns increases, the number of rows remains the same, and the upper and lower borders of the border are displayed horizontally.
    for ((i = FRAME_LEFT + 1; i < FRAME_RIGHT; ++i)); do
        printf "\e[${FRAME_TOP};${i}H="
        printf "\e[${FRAME_BOTTOM};${i}H="
    done

    # After displaying the border, reset the character properties of the terminal to the original state
    printf "\e[0m"
}

# Show or clear the block. The block shape is specified by displayBlockIndex.
# The first parameter passed in is 1, which will display the block.
# The first parameter passed in is 0, which will clear the block.
function drawBlock()
{
    local i squareIndex
    # The square variable holds the content of the small square to be displayed.
    # If the content is "[]", the specific block will be displayed.
    # If the content is " ", which is two spaces, the box will be cleared
    local square
    # The line variable specifies on which line a small square is displayed
    local line
    # The column variable specifies which column a small square is displayed in
    local column

    # The value of the first parameter given is 1, indicating that a specific block is to be displayed
    # The value of the first parameter given is 0, which means to clear the current block
    # The position where the block is displayed is specified by blockLine and blockColumn
    if [ $1 -eq 1 ]; then
        square="[]"
        # When displaying squares, set the background color of the squares to red
        printf "\e[41m"
    else
        # Replace the contents of the originally displayed blocks with spaces and display them as empty
        square="  "
        # When clearing the block, the background color should be displayed as the original color
        printf "\e[0m"
    fi

    for ((i = 0; i < 8; i += 2)); do
        # Get the index of the small block to be displayed based on displayBlockIndex
        squareIndex=$((i + displayBlockIndex))
        # The number of small block lines specified using the blockLine and Z_BLOCKS arrays
        # to get which line each small square should be displayed on.
        line=$((blockLine + ${Z_BLOCKS[squareIndex]}))
        # The number of small block columns specified using the blockLine and Z_BLOCKS arrays
        # to get which column each small square should be displayed in.
        column=$((blockColumn + ${Z_BLOCKS[squareIndex + 1]}))
        # Use the "\e[line;columnH" escape code to move the cursor to the specified
        # row and column, and then start to display the corresponding small squares.
        printf "\e[${line};${column}H${square}"
    done
}

# This function determines whether a block of a specific shape can be placed in the specified row and column.
# Returns 0 if it can be placed. Returns 1 if it cannot be placed.
function canPlaceBlock()
{
    # The first argument given specifies the number of starting lines to move to
    local nextBaseLine="$1"
    # The second argument given specifies the number of starting columns to move to
    local nextBaseColumn="$2"
    # The third parameter given specifies the block shape index to place
    local blockIndex="$3"
    local i squareIndex nextLine nextColumn

    # The blockIndex variable points to the currently displayed block shape index.
    # The following traverses each small square of the current square and gets them
    # The number of rows and columns that will be displayed, check if it exceeds the bounds of the border.
    # If it exceeds, it will return 1, indicating that the block cannot be moved to the specified row or column.
    for ((i = 0; i < 8; i += 2)); do
        squareIndex=$((i + blockIndex))
        nextLine=$((nextBaseLine + ${Z_BLOCKS[squareIndex]}))
        nextColumn=$((nextBaseColumn + ${Z_BLOCKS[squareIndex+1]}))

        # The following two if conditions check whether the number of rows and columns to be displayed next exceeds the number of
        # The bounds of the border. If it exceeds, it returns 1, and the square cannot be placed on a new row or column.
        if ((nextLine<=FRAME_TOP || nextLine>=FRAME_BOTTOM)); then
            return 1
        fi

        if ((nextColumn<=FRAME_LEFT || nextColumn>=FRAME_RIGHT)); then
            return 1
        fi
    done
    # Traverse all the small squares of the square and find that they can be moved, then return 0
    return 0
}

# Move block left
function leftMoveBlock()
{
    # Each time you move to the left, you need to move a distance of a small square. Each small square occupies two columns,
    # So after moving to the left, the new starting column number is the second column in front, and 2 is subtracted below.
    local newBaseColumn=$((blockColumn - 2))
    # bash's if statement can judge the return value of any command, not
    # Only the return value of commands such as [ [[ (( can be judged. The following direct judgement
    # The return value of the canPlaceBlock function. If it returns 0, it is true.
    if canPlaceBlock "$blockLine" "$newBaseColumn" "$displayBlockIndex"; then
        # Blocks can be moved. Clear the original block first
        drawBlock 0
        # Update the value of blockColumn so that blocks are subsequently displayed on the new column
        ((blockColumn -= 2))
        drawBlock 1
    fi
}

# Move block right
function rightMoveBlock()
{
    # Each time you move to the right, you need to move a distance of a small square. Each small square occupies two columns,
    # So after shifting to the right, the new starting column number is the second column behind, and 2 is added below.
    local newBaseColumn=$((blockColumn + 2))
    # bash's if statement can judge the return value of any command, not
    # Only the return value of commands such as [ [[ (( can be judged. The following direct judgement
    # The return value of the canPlaceBlock function. If it returns 0, it is true.
    if canPlaceBlock "$blockLine" "$newBaseColumn" "$displayBlockIndex"; then
        # Blocks can be moved. Clear the original block first
        drawBlock 0
        # Update the value of blockColumn so that blocks are subsequently displayed on the new column
        ((blockColumn += 2))
        drawBlock 1
    fi
}

# Move the block down
function downMoveBlock()
{
    local newBaseLine=$((blockLine + 1))
    if canPlaceBlock "$newBaseLine" "$blockColumn" "$displayBlockIndex"; then
        # Blocks can be moved. Clear the original block first
        drawBlock 0
        # Update the value of blockLine to display the block on a new line subsequently
        ((blockLine += 1))
        drawBlock 1
    fi
}

# Based on the value of displayBlockIndex, get the next one to display
# Square index. Need to check whether the new index is out of bounds, and reset to 0 if it exceeds the bounds.
function getNextBlockIndex()
{
    local nextBlockIndex=$((displayBlockIndex + 8))
    # ${#Z_BLOCKS[@]} gets the number of elements in the Z_BLOCKS array.
    # When nextBlockIndex is greater than the number of array elements, it has exceeded the bounds,
    # To reset to 0. Rotate the block from the beginning.
    if [ $nextBlockIndex -ge ${#Z_BLOCKS[@]} ]; then
        nextBlockIndex=0
    fi
    # Here we use the echo command to output the new index so that it can be used outside
    # $(getNextBlockIndex) method to get this value.
    # If you use the return command to return, you need to use $? to get it outside, which is inconvenient.
    echo "$nextBlockIndex"
}

# Rotate the block. According to the rules of Tetris, the block cannot be moved up.
function rotateBlock()
{
    local newBlockIndex="$(getNextBlockIndex)"
    # After rotating the block, the shape changes, to check whether it can be rotated
    if canPlaceBlock "$blockLine" "$blockColumn" "$newBlockIndex"; then
        # Can be rotated into a new block shape. Clear the original block first
        drawBlock 0
        # Update the value of displayBlockIndex to point to the next block shape after rotation
        displayBlockIndex="$(getNextBlockIndex)"
        # Show new blocks
        drawBlock 1
    fi
}

# Reset the display state of the terminal to the original state
function resetDisplay()
{
    # move the cursor to the next line at the bottom of the border,
    # so that the terminal prompt is displayed behind the border to avoid confusion
    printf "\e[$((FRAME_BOTTOM + 1));0H"
    # show cursor
    printf "\e[?25h"
    # Reset the character properties of the terminal to their original state
    printf "\e[0m"
}

# Initialize the display state. For example, show the border, hide the cursor, etc.
function initDisplay()
{
    # Since the block will be displayed in the specified row and column,
    # To avoid interference with existing content, clear the screen first.
    clear
    # hide cursor
    printf "\e[?25l"
    # display prompt string
    echo "Usage: k key: spinning block. j/h/l key: Down/left/Move block right. q key: quit"
    # show border
    showFrame
    # Show a default block first
    drawBlock 1
}

initDisplay

# Read user keys in a loop and process them accordingly
while read -s -n 1 char; do
    case "$char" in
        # h key to move left one column
        "h") leftMoveBlock ;;
        # l key to move one column to the right
        "l") rightMoveBlock ;;
        # j key to move down one line
        "j") downMoveBlock ;;
        # k key to rotate the block. Tetris cannot move the block up
        "k") rotateBlock ;;
        # q key to exit
        "q") break ;;
    esac
done

resetDisplay
exit

Code key points description

Confirm the shape of the rotated block

In the rotateblock_one.sh script, a Z_BLOCKS array is defined that holds all the shapes of the zigzag blocks.

Each shape is represented by 8 numbers. Use a displayBlockIndex variable to specify the first number of block shapes to display.

When you need to rotate to the next shape, add 8 to the displayBlockIndex variable to point to the first number of the next block shape.

You can then use the drawBlock 1 statement to display the next block shape.

Check if the block position exceeds the border

In the previous article on block movement, the length and height of the block are hardcoded and then checked based on the recorded length and height values ​​to see if the block position exceeds the border.

But in the current script, after the block is rotated, the length and height of the block will change.

If it is still hard-coded to record the length and height of each block, it will be more troublesome to judge.

The current script defines a canPlaceBlock function to check if the block position exceeds the bounding box.

The specific idea is to obtain the number of rows and columns of each small square based on the moving or rotated position of the square, and then check whether the display position of the small square exceeds the border range.

If the display position of any small square exceeds the bounds of the border, it means that the square cannot be displayed in the new position.

Tags: Linux shell bash

Posted by hammadtariq on Mon, 16 May 2022 21:58:38 +0300