Intro
The following is a Bash script found on Rosetta Code. This script implements a basic version of the game of Snake and seems to have been contributed by Mark J. Reed. Let’s see how it works. Here’s a screenshot after a particularly unsuccessful game:
@@@@ *
GAME OVER
Time: 27 seconds
Final length: 4
Bash functions
My Bash is a bit rusty, so here is some relevant documentation:
typeset
declares a variable of a certain type. The-i
flag means integer,-a
is for arrays.clear
, clears the screen.tput
output special operators to terminal, specifically (man terminfo
):tput cup
set cursor positiontput civis
set cursor to invisibletput cnorm
set cursor back to normaltput el
clear to end of line
stty
set how terminal interacts, specifically:stty -echo
disable input echostty echo
enable input echo
read [-t <t>] [-N <n>] [-s] <var>
reads<n>
number of characters from input in-s
silent mode, on<t>
seconds, put result in<var>
.$((RANDOM))
or$RANDOM
draws a random integer in the range 0 - 32767.- Anything in double parentheses can do arithmetic and allows for a C-like syntax.
Code
We start by defining a main
function and a
center
function.
file:snake.sh
function main {
<<main>>
}
<<function-center>>
main "$@"
The center
function seems to be used to print messages
that are centered on the console.
«function-center»
function center {
typeset -i width=$1 i
shift
typeset message=$(printf "$@")
tput cuf $(( (width-${#message}) / 2 ))
printf '%s' "$message"
}
Setup
We’ll see a lot of tput cup "$y" "$x" && printf
'0'
in here. This is just: put the character 0
on
location x, y
.
«main»
typeset -i game_over=0
typeset -i height=$(tput lines) width=$(tput cols)
# start out in the middle moving to the right
typeset -i dx dy hx=$(( width/2 )) hy=$(( height/2 ))
typeset -a sx=($hx) sy=($hy)
typeset -a timeout
clear
tput cup "$sy" "$sx" && printf '@'
tput cup $(( height/2+2 )) 0
center $width "Press h, j, k, l to move left, down, up, right"
The fx
and fy
coordinates tell us where the
food is.
«main»
# place first food
typeset -i fx=hx fy=hy
while (( fx == hx && fy == hy )); do
fx=$(( RANDOM % (width-2)+1 )) fy=$(( RANDOM % (height-2)+1 ))
done
tput cup "$fy" "$fx" && printf '*'
This little bit also makes the script work with Zshell.
«main»
# handle variations between shells
keypress=(-N 1) origin=0
if [[ -n $ZSH_VERSION ]]; then
keypress=(-k)
origin=1
fi
Set the terminal to non-echo mode and hide the cursor.
«main»
stty -echo
tput civis
Wait until a key is pressed.
«main»
typeset key
read "${keypress[@]}" -s key
«main»
typeset -i start_time=$(date +%s)
Main loop
We’ll look at several stages in the main loop:
- read a key
- handle potential key press
- update game state and display
The loop ends when game_over
is signaled.
«main»
tput cup "$(( height/2+2 ))" 0 && tput el
while (( ! game_over )); do
<<read-key-stroke>>
<<handle-input>>
<<update-state>>
done
We read a potential key stroke with a timeout of say 0.1 seconds, by
calling read -n 1 -t 0.1 -s key
. After that function is
done, and a key has been pressed, the variable $key
will
contain the corresponding character.
«read-key-stroke»
timeout=(-t $(printf '0.%04d' $(( 2000 / (${#sx[@]}+1) )) ) )
if [[ -z $key ]]; then
read "${timeout[@]}" "${keypress[@]}" -s key
fi
The game uses hjkl
to move around, like vim. If you
prefer asdw
, this is where to change that. Since I use
Dvorak layout, this becomes aoe,
.
«handle-input»
case "$key" in
a) if (( dx != 1 )); then dx=-1; dy=0; fi;;
o) if (( dy != -1 )); then dy=1; dx=0; fi;;
,) if (( dy != 1 )); then dy=-1; dx=0; fi;;
e) if (( dx != -1 )); then dx=1; dy=0; fi;;
q) game_over=1; tput cup 0 0 && print "Final food was at ($fx,$fy)";;
esac
key=
«update-state»
(( hx += dx, hy += dy ))
# if we try to go off screen, game over
if (( hx < 0 || hx >= width || hy < 0 || hy >= height )); then
game_over=1
else
# if we run into ourself, game over
for (( i=0; i<${#sx[@]}; ++i )); do
if (( hx==sx[i+origin] && hy==sy[i+origin] )); then
game_over=1
break
fi
done
fi
if (( game_over )); then
break
fi
# add new spot
sx+=($hx) sy+=($hy)
The next bit also takes care of updating the screen.
«update-state»
if (( hx == fx && hy == fy )); then
# if we just ate some food, place some new food out
ok=0
while (( ! ok )); do
# make sure we don't put it under ourselves
ok=1
fx=$(( RANDOM % (width-2)+1 )) fy=$(( RANDOM % (height-2)+1 ))
for (( i=0; i<${#sx[@]}; ++i )); do
if (( fx == sx[i+origin] && fy == sy[i+origin] )); then
ok=0
break
fi
done
done
tput cup "$fy" "$fx" && printf '*'
# and don't remove our tail because we've just grown by 1
else
# if we didn't just eat food, remove our tail from its previous spot
tput cup ${sy[origin]} ${sx[origin]} && printf ' '
sx=( ${sx[@]:1} )
sy=( ${sy[@]:1} )
fi
# draw our new head
tput cup "$hy" "$hx" && printf '@'
Post mortem
When it is game over, some messages are printed and we return the terminal to normal behaviour.
«main»
typeset -i end_time=$(date +%s)
tput cup $(( height / 2 -1 )) 0 && center $width 'GAME OVER'
tput cup $(( height / 2 )) 0 &&
center $width 'Time: %d seconds' $(( end_time - start_time ))
tput cup $(( height / 2 + 1 )) 0 &&
center $width 'Final length: %d' ${#sx[@]}
echo
stty echo
tput cnorm