trap"sig=$sigRotate;"$sigRotate trap"sig=$sigLeft;"$sigLeft trap"sig=$sigRight;"$sigRight trap"sig=$sigDown;"$sigDown trap"sig=$sigAllDown;"$sigAllDown trap"ShowExit;"$sigExit while : do
for ((i = 0; i < 21 - iLevel; i++)) do sleep 0.02 sigThis=$sig sig=0
if ((sigThis == sigRotate)); then BoxRotate; elif ((sigThis == sigLeft)); then BoxLeft; elif ((sigThis == sigRight)); then BoxRight; elif ((sigThis == sigDown)); then BoxDown; elif ((sigThis == sigAllDown)); then BoxAllDown; fi done #kill -$sigDown $$ BoxDown done }
functionBoxMove() { local j i x y xTest yTest yTest=$1 xTest=$2 for ((j = 0; j < 8; j += 2)) do ((i = j + 1)) ((y = ${boxCur[$j]} + yTest)) ((x = ${boxCur[$i]} + xTest)) if (( y < 0 || y >= iTrayHeight || x < 0 || x >= iTrayWidth)) then
return 1 fi if ((${iMap[y * iTrayWidth + x]} != -1 )) then
line=0 for ((j = 0; j < iTrayWidth * iTrayHeight; j += iTrayWidth)) do for ((i = j + iTrayWidth - 1; i >= j; i--)) do if ((${iMap[$i]} == -1)); thenbreak; fi done if ((i >= j)); thencontinue; fi ((line++)) for ((i = j - 1; i >= 0; i--)) do ((x = i + iTrayWidth)) iMap[$x]=${iMap[$i]} done for ((i = 0; i < iTrayWidth; i++)) do iMap[$i]=-1 done done if ((line == 0)); thenreturn; fi
echo -ne "\033[1m\033[3${cScoreValue}m\033[${y};${x}H${iScore} " if ((iScore % iScoreEachLevel < line * 2 - 1)) then if ((iLevel < 20)) then ((iLevel++)) ((y = iTop + 14))
echo -ne "\033[3${cScoreValue}m\033[${y};${x}H${iLevel} " fi fi echo -ne "\033[0m"
for ((y = 0; y < iTrayHeight; y++)) do ((yp = y + iTrayTop + 1)) ((xp = iTrayLeft + 1)) ((i = y * iTrayWidth)) echo -ne "\033[${yp};${xp}H" for ((x = 0; x < iTrayWidth; x++)) do ((j = i + x)) if ((${iMap[$j]} == -1)) then echo -ne " " else echo -ne "\033[1m\033[7m\033[3${iMap[$j]}m\033[4${iMap[$j]}m[]\033[0m" fi done done }
functionBoxDown() { local y s ((y = boxCurY + 1)) if BoxMove $y$boxCurX then s="`DrawCurBox 0`" ((boxCurY = y)) s="$s`DrawCurBox 1`" echo -ne $s else Box2Map RandomBox fi }
functionBoxLeft() { local x s ((x = boxCurX - 1)) if BoxMove $boxCurY$x then s=`DrawCurBox 0` ((boxCurX = x)) s=$s`DrawCurBox 1` echo -ne $s fi }
functionBoxRight() { local x s ((x = boxCurX + 1)) if BoxMove $boxCurY$x then s=`DrawCurBox 0` ((boxCurX = x)) s=$s`DrawCurBox 1` echo -ne $s fi }
functionBoxAllDown() { local k j i x y iDown s iDown=$iTrayHeight
for ((j = 0; j < 8; j += 2)) do ((i = j + 1)) ((y = ${boxCur[$j]} + boxCurY)) ((x = ${boxCur[$i]} + boxCurX)) for ((k = y + 1; k < iTrayHeight; k++)) do ((i = k * iTrayWidth + x)) if (( ${iMap[$i]} != -1)); thenbreak; fi done ((k -= y + 1)) if (( $iDown > $k )); then iDown=$k; fi done s=`DrawCurBox 0` ((boxCurY += iDown)) s=$s`DrawCurBox 1` echo -ne $s Box2Map RandomBox }
functionBoxRotate() { local iCount iTestRotate boxTest j i s iCount=${countBox[$iBoxCurType]} ((iTestRotate = iBoxCurRotate + 1)) if ((iTestRotate >= iCount)) then ((iTestRotate = 0)) fi for ((j = 0, i = (${offsetBox[$iBoxCurType]} + $iTestRotate) * 8; j < 8; j++, i++)) do boxTest[$j]=${boxCur[$j]} boxCur[$j]=${box[$i]} done if BoxMove $boxCurY$boxCurX then for ((j = 0; j < 8; j++)) do boxCur[$j]=${boxTest[$j]} done s=`DrawCurBox 0` for ((j = 0, i = (${offsetBox[$iBoxCurType]} + $iTestRotate) * 8; j < 8; j++, i++)) do boxCur[$j]=${box[$i]} done s=$s`DrawCurBox 1` echo -ne $s iBoxCurRotate=$iTestRotate else for ((j = 0; j < 8; j++)) do boxCur[$j]=${boxTest[$j]} done fi }
functionDrawCurBox() { local i j t bDraw sBox s bDraw=$1 s="" if (( bDraw == 0 )) then sBox="\040\040" else sBox="[]" s=$s"\033[1m\033[7m\033[3${cBoxCur}m\033[4${cBoxCur}m" fi for ((j = 0; j < 8; j += 2)) do ((i = iTrayTop + 1 + ${boxCur[$j]} + boxCurY)) ((t = iTrayLeft + 1 + 2 * (boxCurX + ${boxCur[$j + 1]})))
iBoxCurType=${iBoxNewType} iBoxCurRotate=${iBoxNewRotate} cBoxCur=${cBoxNew} for ((j = 0; j < ${#boxNew[@]}; j++)) do boxCur[$j]=${boxNew[$j]} done
if (( ${#boxCur[@]} == 8 )) then
for ((j = 0, t = 4; j < 8; j += 2)) do if ((${boxCur[$j]} < t)); then t=${boxCur[$j]}; fi done ((boxCurY = -t)) for ((j = 1, i = -4, t = 20; j < 8; j += 2)) do if ((${boxCur[$j]} > i)); then i=${boxCur[$j]}; fi if ((${boxCur[$j]} < t)); then t=${boxCur[$j]}; fi done ((boxCurX = (iTrayWidth - 1 - i - t) / 2))
echo -ne `DrawCurBox 1`
if ! BoxMove $boxCurY$boxCurX then kill -$sigExit${PPID} ShowExit fi fi
functionShowExit() { local y ((y = iTrayHeight + iTrayTop + 3)) echo -e "\033[${y};0HGameOver!\033[0m" exit }
function Usage { cat << EOF Usage: $APP_NAME Start tetris game. (linuxscriptshub.com) -h, --help display this help and exit --version output version information and exit EOF }
if [[ "$1" == "-h" || "$1" == "--help" ]]; then Usage elif [[ "$1" == "--version" ]]; then echo"$APP_NAME$APP_VERSION" elif [[ "$1" == "--show" ]]; then
RunAsDisplayer else bash $0 --show& RunAsKeyReceiver $! fi
Version commentée
Le script tétris en bash avec des commentaires :
#!/bin/bash
# Tetris game written in pure bash # # I tried to mimic as close as possible original tetris game # which was implemented on old soviet DVK computers (PDP-11 clones) # # Videos of this tetris can be found here: # # http://www.youtube.com/watch?v=O0gAgQQHFcQ # http://www.youtube.com/watch?v=iIQc1F3UuV4 # # This script was created on ubuntu 13.04 x64 and bash 4.2.45(1)-release. # It was not tested on other unix like operating systems. # # Enjoy :-)! # # Author: Kirill Timofeev <kt97679@gmail.com>
set -u # non initialized variable is an error
# 2 signals are used: SIGUSR1 to decrease delay after level up and SIGUSR2 to quit # they are sent to all instances of this script # because of that we should process them in each instance # in this instance we are ignoring both signals trap'' SIGUSR1 SIGUSR2
# Those are commands sent to controller by key press processing code # In controller they are used as index to retrieve actual functuon from array QUIT=0 RIGHT=1 LEFT=2 ROTATE=3 DOWN=4 DROP=5 TOGGLE_HELP=6 TOGGLE_NEXT=7 TOGGLE_COLOR=8
DELAY=1 # initial delay between piece movements DELAY_FACTOR=0.8 # this value controld delay decrease for each level up
# color codes RED=1 GREEN=2 YELLOW=3 BLUE=4 FUCHSIA=5 CYAN=6 WHITE=7
# Location and size of playfield, color of border PLAYFIELD_W=10 PLAYFIELD_H=20 PLAYFIELD_X=30 PLAYFIELD_Y=1 BORDER_COLOR=$YELLOW
# Location and color of score information SCORE_X=1 SCORE_Y=2 SCORE_COLOR=$GREEN
# Location and color of help information HELP_X=58 HELP_Y=1 HELP_COLOR=$CYAN
# Next piece location NEXT_X=14 NEXT_Y=11
# Location of "game over" in the end of the game GAMEOVER_X=1 GAMEOVER_Y=$((PLAYFIELD_H + 3))
# Intervals after which game level (and game speed) is increased LEVEL_UP=20
no_color=true# do we use color or not showtime=true# controller runs while this flag is true empty_cell=" ."# how we draw empty cell filled_cell="[]"# how we draw filled cell
# screen_buffer is variable, that accumulates all screen changes # this variable is printed in controller once per game cycle puts() { screen_buffer+=${1} }
# move cursor to (x,y) and print string # (1,1) is upper left corner of the screen xyprint() { puts "\033[${2};${1}H${3}" }
# playfield is 1-dimensional array, data is stored as follows: # [ a11, a21, ... aX1, a12, a22, ... aX2, ... a1Y, a2Y, ... aXY] # |< 1st line >| |< 2nd line >| ... |< last line >| # X is PLAYFIELD_W, Y is PLAYFIELD_H # each array element contains cell color value or -1 if cell is empty redraw_playfield() { local j i x y xp yp
((xp = PLAYFIELD_X)) for ((y = 0; y < PLAYFIELD_H; y++)) { ((yp = y + PLAYFIELD_Y)) ((i = y * PLAYFIELD_W)) xyprint $xp$yp"" for ((x = 0; x < PLAYFIELD_W; x++)) { ((j = i + x)) if ((${play_field[$j]} == -1)) ; then puts "$empty_cell" else set_fg ${play_field[$j]} set_bg ${play_field[$j]} puts "$filled_cell" reset_colors fi } } }
update_score() { # Arguments: 1 - number of completed lines ((lines_completed += $1)) # Unfortunately I don't know scoring algorithm of original tetris # Here score is incremented with squared number of lines completed # this seems reasonable since it takes more efforts to complete several lines at once ((score += ($1 * $1))) if (( score > LEVEL_UP * level)) ; then# if level should be increased ((level++)) # increment level pkill -SIGUSR1 -f "/bin/bash $0"# and send SIGUSR1 signal to all instances of this script (please see ticker for more details) fi set_bold set_fg $SCORE_COLOR xyprint $SCORE_X$SCORE_Y"Lines completed: $lines_completed" xyprint $SCORE_X $((SCORE_Y + 1)) "Level: $level" xyprint $SCORE_X $((SCORE_Y + 2)) "Score: $score" reset_colors }
set_bold set_fg $HELP_COLOR for ((i = 0; i < ${#help[@]}; i++ )) { # ternary assignment: if help_on is 1 use string as is, otherwise substitute all characters with spaces ((help_on == 1)) && s="${help[i]}" || s="${help[i]//?/ }" xyprint $HELP_X $((HELP_Y + i)) "$s" } ((help_on = -help_on)) reset_colors }
# this array holds all possible pieces that can be used in the game # each piece consists of 4 cells # each string is sequence of relative xy coordinates for different orientations # depending on piece symmetry there can be 1, 2 or 4 orientations piece=( "00011011"# square piece "0212223210111213"# line piece "0001111201101120"# S piece "0102101100101121"# Z piece "01021121101112220111202100101112"# L piece "01112122101112200001112102101112"# inverted L piece "01111221101112210110112101101112"# T piece )
draw_piece() { # Arguments: # 1 - x, 2 - y, 3 - type, 4 - rotation, 5 - cell content local i x y
# loop through piece cells: 4 cells, each has 2 coordinates for ((i = 0; i < 8; i += 2)) { # relative coordinates are retrieved based on orientation and added to absolute coordinates ((x = $1 + ${piece[$3]:$((i + $4 * 8 + 1)):1} * 2)) ((y = $2 + ${piece[$3]:$((i + $4 * 8)):1})) xyprint $x$y"$5" } }
draw_current() { # Arguments: 1 - string to draw single cell # factor 2 for x because each cell is 2 characters wide draw_piece $((current_piece_x * 2 + PLAYFIELD_X)) $((current_piece_y + PLAYFIELD_Y)) $current_piece$current_piece_rotation"$1" }
new_piece_location_ok() { # Arguments: 1 - new x coordinate of the piece, 2 - new y coordinate of the piece # test if piece can be moved to new location local j i x y x_test=$1 y_test=$2
for ((j = 0, i = 1; j < 8; j += 2, i = j + 1)) { ((y = ${piece[$current_piece]:$((j + current_piece_rotation * 8)):1} + y_test)) # new y coordinate of piece cell ((x = ${piece[$current_piece]:$((i + current_piece_rotation * 8)):1} + x_test)) # new x coordinate of piece cell ((y < 0 || y >= PLAYFIELD_H || x < 0 || x >= PLAYFIELD_W )) && return 1 # check if we are out of the play field ((${play_field[y * PLAYFIELD_W + x]} != -1 )) && return 1 # check if location is already ocupied } return 0 }
get_random_next() { # next piece becomes current current_piece=$next_piece current_piece_rotation=$next_piece_rotation current_piece_color=$next_piece_color # place current at the top of play field, approximately at the center ((current_piece_x = (PLAYFIELD_W - 4) / 2)) ((current_piece_y = 0)) # check if piece can be placed at this location, if not - game over new_piece_location_ok $current_piece_x$current_piece_y || cmd_quit show_current
clear_next # now let's get next piece ((next_piece = RANDOM % ${#piece[@]})) ((next_piece_rotation = RANDOM % (${#piece[$next_piece]} / 8))) ((next_piece_color = RANDOM % ${#colors[@]})) show_next }
draw_border() { local i x1 x2 y
set_bold set_fg $BORDER_COLOR ((x1 = PLAYFIELD_X - 2)) # 2 here is because border is 2 characters thick ((x2 = PLAYFIELD_X + PLAYFIELD_W * 2)) # 2 here is because each cell on play field is 2 characters wide for ((i = 0; i < PLAYFIELD_H + 1; i++)) { ((y = i + PLAYFIELD_Y)) xyprint $x1$y"<|" xyprint $x2$y"|>" }
((y = PLAYFIELD_Y + PLAYFIELD_H)) for ((i = 0; i < PLAYFIELD_W; i++)) { ((x1 = i * 2 + PLAYFIELD_X)) # 2 here is because each cell on play field is 2 characters wide xyprint $x1$y'==' xyprint $x1 $((y + 1)) "\/" } reset_colors }
# this function runs in separate process # it sends DOWN commands to controller with appropriate delay ticker() { # on SIGUSR2 this process should exit trapexit SIGUSR2 # on SIGUSR1 delay should be decreased, this happens during level ups trap'DELAY=$(awk "BEGIN {print $DELAY * $DELAY_FACTOR}")' SIGUSR1
whiletrue ; doecho -n $DOWN; sleep $DELAY; done }
# this function processes keyboard input reader() { trapexit SIGUSR2 # this process exits on SIGUSR2 trap'' SIGUSR1 # SIGUSR1 is ignored local -u key a='' b='' cmd esc_ch=$'\x1b' # commands is associative array, which maps pressed keys to commands, sent to controller declare -A commands=([A]=$ROTATE [C]=$RIGHT [D]=$LEFT [_S]=$ROTATE [_A]=$LEFT [_D]=$RIGHT [_]=$DROP [_Q]=$QUIT [_H]=$TOGGLE_HELP [_N]=$TOGGLE_NEXT [_C]=$TOGGLE_COLOR)
whileread -s -n 1 key ; do case"$a$b$key"in "${esc_ch}["[ACD]) cmd=${commands[$key]} ;; # cursor key *${esc_ch}${esc_ch}) cmd=$QUIT ;; # exit on 2 escapes *) cmd=${commands[_$key]:-} ;; # regular key. If space was pressed $key is empty esac a=$b# preserve previous keys b=$key [ -n "$cmd" ] && echo -n "$cmd" done }
# this function updates occupied cells in play_field array after piece is dropped flatten_playfield() { local i j k x y for ((i = 0, j = 1; i < 8; i += 2, j += 2)) { ((y = ${piece[$current_piece]:$((i + current_piece_rotation * 8)):1} + current_piece_y)) ((x = ${piece[$current_piece]:$((j + current_piece_rotation * 8)):1} + current_piece_x)) ((k = y * PLAYFIELD_W + x)) play_field[$k]=$current_piece_color } }
# this function goes through play_field array and eliminates lines without empty sells process_complete_lines() { local j i complete_lines ((complete_lines = 0)) for ((j = 0; j < PLAYFIELD_W * PLAYFIELD_H; j += PLAYFIELD_W)) { for ((i = j + PLAYFIELD_W - 1; i >= j; i--)) { ((${play_field[$i]} == -1)) && break# empty cell found } ((i >= j)) && continue# previous loop was interrupted because empty cell was found ((complete_lines++)) # move lines down for ((i = j - 1; i >= 0; i--)) { play_field[$((i + PLAYFIELD_W))]=${play_field[$i]} } # mark cells as free for ((i = 0; i < PLAYFIELD_W; i++)) { play_field[$i]=-1 } } return$complete_lines }
move_piece() { # arguments: 1 - new x coordinate, 2 - new y coordinate # moves the piece to the new location if possible if new_piece_location_ok $1$2 ; then# if new location is ok clear_current # let's wipe out piece current location current_piece_x=$1# update x ... current_piece_y=$2# ... and y of new location show_current # and draw piece in new location return 0 # nothing more to do here fi# if we could not move piece to new location (($2 == current_piece_y)) && return 0 # and this was not horizontal move process_fallen_piece # let's finalize this piece get_random_next # and start the new one return 1 }
cmd_rotate() { local available_rotations old_rotation new_rotation
available_rotations=$((${#piece[$current_piece]} / 8)) # number of orientations for this piece old_rotation=$current_piece_rotation# preserve current orientation new_rotation=$(((old_rotation + 1) % available_rotations)) # calculate new orientation current_piece_rotation=$new_rotation# set orientation to new if new_piece_location_ok $current_piece_x$current_piece_y ; then# check if new orientation is ok current_piece_rotation=$old_rotation# if yes - restore old orientation clear_current # clear piece image current_piece_rotation=$new_rotation# set new orientation show_current # draw piece with new orientation else# if new orientation is not ok current_piece_rotation=$old_rotation# restore old orientation fi }
cmd_drop() { # move piece all way down # this is example of do..while loop in bash # loop body is empty # loop condition is done at least once # loop runs until loop condition would return non zero exit code while move_piece $current_piece_x $((current_piece_y + 1)) ; do : ; done }
cmd_quit() { showtime=false# let's stop controller ... pkill -SIGUSR2 -f "/bin/bash $0"# ... send SIGUSR2 to all script instances to stop forked processes ... xyprint $GAMEOVER_X$GAMEOVER_Y"Game over!" echo -e "$screen_buffer"# ... and print final message }
controller() { # SIGUSR1 and SIGUSR2 are ignored trap'' SIGUSR1 SIGUSR2 local cmd commands
# initialization of commands array with appropriate functions commands[$QUIT]=cmd_quit commands[$RIGHT]=cmd_right commands[$LEFT]=cmd_left commands[$ROTATE]=cmd_rotate commands[$DOWN]=cmd_down commands[$DROP]=cmd_drop commands[$TOGGLE_HELP]=toggle_help commands[$TOGGLE_NEXT]=toggle_next commands[$TOGGLE_COLOR]=toggle_color
init
while$showtime; do# run while showtime variable is true, it is changed to false in cmd_quit function echo -ne "$screen_buffer"# output screen buffer ... screen_buffer=""# ... and reset it read -s -n 1 cmd # read next command from stdout ${commands[$cmd]}# run command done }
stty_g=`stty -g` # let's save terminal state
# output of ticker and reader is joined and piped into controller ( ticker & # ticker runs as separate process reader )|( controller )
show_cursor stty $stty_g# let's restore terminal state