#!/usr/bin/env bash # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2, June 1991. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see , # or in pdf format at # Copyright 2012 - Øyvind 'bolt' Hvidsten # Applied patches: # # v1.07, 2012.03.26 - Steve McMurphy # v1.11, 2014.09.30 - Øyvind A. Holm # # Description: # # ShellShock is a top-down space shooter written for Bash 3 / Bash 4 # Tested on Linux (Debian, RedHat and CentOS) # # Please note: A game in Bash is very demanding on resources. # This script requires a modern computer to run at a decent speed. # This script uses a tab width of 4, which is automatically applied # if you're using vim with modelines enabled (see bottom line). # # For updates, please see . # # Comments welcome, improvements/patches twice as welcome. # # Releases / Changelog: # # v1.00, 2012.03.18 - Initial v1.0 release # * All intended functionality implemented # # v1.01, 2012.03.19 - Bash 3 # * Tweaks to run on Bash 3 # # v1.05, 2012.03.25 - Several changes based on feedback # * Added score display # * Added increasing difficulty # # v1.06, 2012.03.26 - More feedback # * Ship aft collision improved # * Added pause option # * Added simple high score storage # * Runs on Bash 3 again # # v1.07, 2012.03.26 - OSX # * Added rudimentary support for Mac OS X # # v1.08, 2012.03.26 - Cleanup # * Improved some comments and made clearer error messages # # v1.09, 2012.03.27 - Ubuntu # * Various fixes for Ubuntu # # v1.10, 2012.03.29 - Pause # * Improved the pause function to avoid problems with timers # # v1.11, 2014.09.30 - Better cleanup # * Using "stty sane" to clean up terminal settings on exit # # v1.12, 2017.07.21 - Static code analysis # * Minor changes to appease the code analysis gods # # v1.13, 2017.09.05 - Fixes for strange terminals # * Fixed a bug with some strange terminal settings that prevented input # # v1.14, 2020.06.07 - Static code analysis # * Minor changes to appease the (updated) code analysis gods # # v1.15, 2021.12.29 - Static code analysis (again) # * Minor changes to appease the (updated, again) code analysis gods # # v1.16, 2024.09.13 - Static code analysis (yet again) # * Minor changes to appease the (updated, again) code analysis gods # # get uname uname=$(uname -s) # queue temp files for deletion on script exit function rm_queue { _rm_queue[${#_rm_queue[*]}]="$1"; } function rm_process { local file; for file in "${_rm_queue[@]}"; do rm "$file"; done; } # check if tput actually outputs something useful for this terminal if [[ -z "$(tput sgr0)" ]] || [[ -z "$(tput bold)" ]]; then echo 'Error: tput is not working as expected with your current terminal settings!' >&2 echo 'Try setting your $TERM to something more standard?' >&2 exit 1 fi # read a single character function readc { IFS= read -r -n1 -s c; } # variable variables :) gamepid="" saved_term=false # list of files to be removed on cleanup _rm_queue=() # cleanup function cleanup { [[ -z "$gamepid" ]] || { kill -TERM "$gamepid"; wait "$gamepid"; } 2>/dev/null rm_process # remove temp files tput sgr0 # reset color clear # clear the screen tput cnorm # show the cursor stty echo # show input stty sane # reset terminal reset 2>/dev/null # reset some more ! $saved_term || tput rmcup # restore the terminal view } trap cleanup EXIT # make comm file # TODO: better solution for inter-thread communication. must work on bash3 case "$uname" in Darwin) comm=$(mktemp /tmp/shellshock.XXXXXX) ;; *) comm=$(mktemp) ;; esac if [[ -n "$comm" ]]; then rm_queue "$comm" else echo "Error: Communications file creation failed!" >&2 exit 1 fi # init tput smcup && saved_term=true # save the current terminal view tput civis # hide the cursor # game subshell ( input=false trap 'input=true' USR1 trap 'exit 0' TERM INT HUP # test parent shell function testparent { kill -0 $$ 2>/dev/null || exit 1 } # how to print stuff function xyecho { local x=$1 y=$2 shift 2 # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages while { ! builtin echo -n "${posarray[$((y*cols+x))]}$*"; } 2>/dev/null; do testparent; done } function safeecho { # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages while { ! builtin echo -n "${posarray[$((y*cols+x))]}$*"; } 2>/dev/null; do testparent; done } function xyprintf { local x=$1 y=$2 fmt=$3 shift 3 # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages while { ! builtin printf "%s$fmt" "${posarray[$((y*cols+x))]}" "$@"; } 2>/dev/null; do testparent; done } function safeprintf { local fmt=$1 shift 1 # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages while { ! builtin printf "%s$fmt" "${posarray[$((y*cols+x))]}" "$@"; } 2>/dev/null; do testparent; done } # called when the player fires his/her weapons function fire # no parameters { (( ff_ammo_current > 0 )) || return if ${ff_alive[ff_next]}; then xyprintf $((origo_x+ff_x[ff_next])) $((origo_y+ff_y[ff_next])) " " fi if ((++ff_total%2 == 0)); then ff_x[ff_next]=$((dynel_cx[0]-3)) ff_y[ff_next]=$((dynel_cy[0]-8)) else ff_x[ff_next]=$((dynel_cx[0]+3)) ff_y[ff_next]=$((dynel_cy[0]-8)) fi if ! outofbounds "$((origo_x+ff_x[ff_next]))" "$((origo_y+ff_y[ff_next]))"; then ff_ydiv[ff_next]=1 ff_symbol[ff_next]="|" ff_damage[ff_next]=1 ff_alive[ff_next]=true (( ++ff_count )) ff_new[ff_next]=true (( --ff_ammo_current )) ff_next=$(( ++ff_next < ff_count_max ? ff_next : 0 )) fi if ((ff_total%3 == 0)); then if ${ff_alive[ff_next]}; then xyprintf $((origo_x+ff_x[ff_next])) $((origo_y+ff_y[ff_next])) " " fi if ((ff_total%2 == 0)); then ff_x[ff_next]=$((dynel_cx[0]+5)) ff_y[ff_next]=$((dynel_cy[0]-7)) else ff_x[ff_next]=$((dynel_cx[0]-5)) ff_y[ff_next]=$((dynel_cy[0]-7)) fi if ! outofbounds "$((origo_x+ff_x[ff_next]))" "$((origo_y+ff_y[ff_next]))"; then ff_ydiv[ff_next]=2 ff_symbol[ff_next]="¤" ff_damage[ff_next]=4 ff_alive[ff_next]=true (( ++ff_count )) ff_new[ff_next]=true ff_next=$(( ++ff_next < ff_count_max ? ff_next : 0 )) fi fi } ####################################################### ## BEGIN ASCII ART SECTION - NO TABS, NO INDENTATION ## ####################################################### # title ascii_title_w=78 ascii_title_h=6 ascii_title=$( cat - <<"EOF" _________.__ .__ .__ _________.__ __ ._. / _____/| |__ ____ | | | | / _____/| |__ ____ ____ | | _| | \_____ \ | | \_/ __ \| | | | \_____ \ | | \ / _ \_/ ___\| |/ / | / \| Y \ ___/| |_| |__/ \| Y ( <_> ) \___| < \| /_______ /|___| /\___ >____/____/_______ /|___| /\____/ \___ >__|_ \__ \/ \/ \/ \/ \/ \/ \/\/ EOF ) # keybindings ascii_keybindings_w=74 #21 - fake width to print off-center ascii_keybindings_h=5 ascii_keybindings=$( cat - <<"EOF" Keybindings: arrow keys - move SPACE - fire z - toggle autofire p - pause q - quit EOF ) # dead meat - written in quotes because all the parenthesis confuse vim's syntax highlighting # - this also means the first line has to be offset, so the ascii looks ugly in code ascii_dead_w=36 ascii_dead_h=8 ascii_dead=\ ' ______ _______ _______ ______ ( __ \ ( ____ \( ___ )( __ \ | ( \ )| ( \/| ( ) || ( \ ) | | ) || (__ | (___) || | ) | | | | || __) | ___ || | | | | | ) || ( | ( ) || | ) | | (__/ )| (____/\| ) ( || (__/ ) (______/ (_______/|/ \|(______/' # press q ascii_press_q_w=42 ascii_press_q_h=1 ascii_press_q=$( cat - <<"EOF" -= Press q to quit to the title screen! =- EOF ) # press fire ascii_press_fire_w=27 ascii_press_fire_h=1 ascii_press_fire=$( cat - <<"EOF" -= Press SPACE to start! =- EOF ) # pause ascii_pause_w=11 ascii_pause_h=1 ascii_pause=$( cat - <<"EOF" -= PAUSE =- EOF ) # pause frame ascii_pauseframe_w=25 ascii_pauseframe_h=5 ascii_pauseframe=$( cat - <<"EOF" ************************* * * * * * * ************************* EOF ) # player's spaceship ascii_playerdynel_w=21 ascii_playerdynel_h=9 ascii_playership=$( cat - <<"EOF" 08] 06] /^\ 03] |/.!.\| 01] _|/_/]=[\_\|_ 00] _/ | | \_ 00] |_____. | | ._____| 00] \_________/ 04] |@| |@| 05] EOF ) # rock 1 ascii_rock0_w=9 ascii_rock0_h=7 ascii_rock0=$( cat - <<"EOF" 02] 01] __ 01] / \_ 00] | \ 01] \ _| 01] \_/ 02] EOF ) ascii_rock1_w=14 ascii_rock1_h=8 ascii_rock1=$( cat - <<"EOF" 06] 01] __ 00] ____/ \_ 00] / \ 00] \ | 00] \ ___/ 01] \___/ 02] EOF ) ascii_rock2_w=8 ascii_rock2_h=8 ascii_rock2=$( cat - <<"EOF" 02] 01] __ 00] / \ 00] | | 00] | \ 00] | _/ 00] \__/ 00] EOF ) ascii_rock3_w=9 ascii_rock3_h=7 ascii_rock3=$( cat - <<"EOF" 01] 00] _____ 00] / \ 00] | | 00] \ _/ 00] \_/ 01] EOF ) ascii_rock4_w=13 ascii_rock4_h=8 ascii_rock4=$( cat - <<"EOF" 01] 00] ___ 00] / \_____ 00] | _| 00] \ / 00] \_ / 01] \__/ 03] EOF ) ####################################################### ## END ASCII ART SECTION ## ####################################################### # pretty colours color_background=$(tput setab 0) color_reset="$(tput sgr0)${color_background}" color_black="${color_reset}$(tput setaf 0)" color_red="${color_reset}$(tput setaf 1)" color_green="${color_reset}$(tput setaf 2)" color_orange="${color_reset}$(tput setaf 3)" color_blue="${color_reset}$(tput setaf 4)" color_magenta="${color_reset}$(tput setaf 5)" color_cyan="${color_reset}$(tput setaf 6)" color_light_gray="${color_reset}$(tput setaf 7)" color_dark_gray="${color_reset}$(tput bold)$(tput setaf 0)" color_light_red="${color_reset}$(tput bold)$(tput setaf 1)" color_light_green="${color_reset}$(tput bold)$(tput setaf 2)" color_yellow="${color_reset}$(tput bold)$(tput setaf 3)" color_light_blue="${color_reset}$(tput bold)$(tput setaf 4)" color_light_magenta="${color_reset}$(tput bold)$(tput setaf 5)" color_light_cyan="${color_reset}$(tput bold)$(tput setaf 6)" color_white="${color_reset}$(tput bold)$(tput setaf 7)" # specific colors for stuff color_debug=$color_orange # debug prints (FPS, seconds, rocks count, etc) color_ship=$color_white # player's ship color_fire=$color_light_magenta # player's missiles color_engine_1=$color_light_red # player's engines (blinking) color_engine_2=$color_red # player's engines (blinking) color_border=$color_red # border color_score_result=$color_yellow # score result (death screen) color_origo=$color_green # origo (bottom center) color_rock_healthy=$color_light_cyan # healthy rocks color_rock_damaged=$color_cyan # damaged rocks color_death_1=$color_light_red # death animation stage 1 color_death_2=$color_red # death animation stage 2 color_death_3=$color_dark_gray # death animation stage 3 color_death_4=$color_black # death animation stage 4 (erase) color_title=$color_yellow # ShellShock! color_pause_1=$color_light_red # pause text (blinking) color_pause_2=$color_red # pause text (blinking) color_pauseframe=$color_red # frame around pause text color_youaredead=$color_light_red # YOU ARE DEAD color_keybindings=$color_green # keybindings... color_pressfire_1=$color_blue # press space to start (blinking) color_pressfire_2=$color_light_blue # press space to start (blinking) color_pressq_1=$color_dark_gray # press q for title screen (blinking) color_pressq_2=$color_light_gray # press q for title screen (blinking) # score display (top right) (drawn on white background) color_score="${color_black}$(tput setab 7)" # ammo display (drawn with spaces on background color in the top left) color_ammo_1="${color_black}$(tput setab 5)" color_ammo_2="${color_black}$(tput setab 7)" # home home=$(tput home) # has the size changed? function sizechanged { if (( cols != $(tput cols) )) || (( rows != $(tput lines) )); then cols=$(tput cols) rows=$(tput lines) origo_x=$((cols/2)) origo_y=$((rows-1)) return 0 fi return 1 } # clear redraw=false function wipe { # "tput clear" doesn't fill with background color in bash3, screen, etc. # must write a shitload of spaces instead safeprintf "${color_reset}${home}%$((cols*rows))s" "" # everything needs redrawing after this redraw=true } # get the current time in microseconds case "$uname" in Darwin) # this is slow. very slow. function microtime { local time time=$(python <<<"import time; print \"%.6f\" % time.time();") echo -n "${time/./}" } ;; *) function microtime { local time time=$(date +%s%N) echo -n "${time:0:((${#time}-3))}" } ;; esac # build tput position array # moving using this 10x faster than running tput _pos_cols=0 _pos_rows=0 function buildposarray { if ((cols == _pos_cols)) && ((rows == _pos_rows)); then return 1 fi wipe _pos_cols=$cols _pos_rows=$rows posarray=() local q e pos string x y q=false e=$(echo -e '\e') if [[ "$(tput cup 0 0)" = "${e}[1;1H" ]]; then # standard terminal movement commands - quick generation q=true fi string="Building position array for ${cols}x${rows}... " pos=$(tput cup 0 ${#string}) safeecho "${color_debug}${home}${string}" for ((x=0; x < cols; x++)); do if sizechanged; then buildposarray return $? fi echo -n "${pos}$((x*100/cols))%" for ((y=0; y < rows; y++)); do if $q; then posarray["$((y*cols+x))"]="${e}[$((y+1));$((x+1))H" else posarray["$((y*cols+x))"]=$(tput cup "$y" "$x") fi done done return 0 } # print something at a spot function catc { cat "$@"; } # draw centered (x coordinate specifies width, not x pos) function catd { cat "$@"; } # draw a dynel (supports black outline) function cat { local x=$1 y=$2 i=0 cy dynel=false first=true offset=0 case "${FUNCNAME[1]}" in catd) dynel=true ;; catc) x=$(( (cols - x) / 2)) # center (( x > 0 )) || x=1 ;; esac while IFS= read -r line; do cy=$(( y + i++ )) if $dynel; then offset="$(( 10#${line:0:2} ))" line=${line:3+offset} cx=$(( x + offset )) else cx=$x fi (( cx < cols-1 )) || continue # don't write on or outside the right border (( cy > 0 )) || continue # don't write on or above the top border (( cy < rows-1 )) || break # don't write on or below the bottom border (( cx >= 1 )) || { line=${line:1-cx}; cx=1; } # cut to fit inside left border line=${line:0:cols-1-cx} # cut to fit inside right border xyecho "$cx" "$cy" "$line" done } # border drawing function border { safeecho "$color_border" # no printf -v on bash3 :( local line while { ! line=$(builtin printf "%${cols}s" ""); } 2>/dev/null; do :; done line=${line// /#} xyecho 0 0 "$line" local y for (( y=1; y= cols-1 )) || (( y < 1 )) || (( y >= rows-1 )) then return 0 fi return 1 } # pushes a dynel until it's within the right and left borders function restrict_xaxis # $1 - dynel_* index { local i=$1 dynel_x[i]=${dynel_cx[i]} while ! canmoveright "$i"; do (( dynel_cx[i]-- )); done; while ! canmoveleft "$i"; do (( dynel_cx[i]++ )); done; (( dynel_cx[i] < dynel_x[i] )) && (( dynel_cx[i]++ )) (( dynel_cx[i] > dynel_x[i] )) && (( dynel_cx[i]-- )) dynel_x[i]=${dynel_cx[i]} } # pushes a dynel until it's within the top and bottom borders function restrict_yaxis # $1 - dynel_* index { local i=$1 dynel_y[i]=${dynel_cy[i]} while ! canmoveup "$i"; do (( dynel_cy[i]++ )); done; while ! canmovedown "$i"; do (( dynel_cy[i]-- )); done; (( dynel_cy[i] < dynel_y[i] )) && (( dynel_cy[i]++ )) (( dynel_cy[i] > dynel_y[i] )) && (( dynel_cy[i]-- )) dynel_y[i]=${dynel_cy[i]} } # collides dynels based on simple squares (width, height) function squarecollide # $1 - dynel_* index { local i=$1 for j in "${!dynel_alive[@]}"; do ${dynel_alive[j]} || continue # don't check dead dynels (( j != i )) || continue # don't check yourself (( j >= rock_pos )) || continue # don't check the playership local distance_x=$((dynel_cx[i] > dynel_cx[j] ? dynel_cx[i]-dynel_cx[j] : dynel_cx[j]-dynel_cx[i])) local distance_y=$((dynel_cy[j] - dynel_cy[i])) if (( distance_x < (dynel_w[i]+dynel_w[j])/2 )) && { if (( distance_y < 0 )); then # j (compare dynel) is above i (( -distance_y < dynel_h[j] )) else # j (compare dynel) is below i (( distance_y < dynel_h[j] )) fi } then # collision! return 0 fi done # no collision return 1 } # collides the player's ship # basically the same as square collision, but with some tweaks to make it feel better function shipcollide # no parameters { local i=0 for j in "${!dynel_alive[@]}"; do ${dynel_alive[j]} || continue # don't check dead dynels (( j != i )) || continue # don't check yourself local distance_x=$((dynel_cx[i] > dynel_cx[j] ? dynel_cx[i]-dynel_cx[j] : dynel_cx[j]-dynel_cx[i])) local distance_y=$((dynel_cy[j] - dynel_cy[i])) if (( distance_x + 2 < (dynel_w[i]+dynel_w[j])/2 )) && # make the ship a little narrower { if (( distance_y < 0 )); then # j (compare dynel) is above i (( -distance_y < dynel_h[j] )) && (( (distance_y + dynel_h[i]) > distance_x - 4 )) # make a somewhat cone-shaped ship else # j (compare dynel) is below i (( distance_y + 2 < dynel_h[j] )) && # make the ship a little shorter { ((dynel_h[j] - distance_y - 2 > 2)) || # 2 lines into ship from bottom ((4 + (dynel_h[j] - distance_y - 2) > distance_x)) # engine hit } fi } then # collision! return 0 fi done # no collision return 1 } # runs hit tests on friendly fire and damages any rocks encountered function ffhit # $1 - ff_* index { local x=${ff_x[$1]} y=${ff_y[$1]} i j for i in "${!dynel_alive[@]}"; do (( i >= rock_pos )) || continue # only check rocks ${dynel_alive[i]} || continue # don't check dead dynels if ((y > dynel_y[i]-2)) || # haven't reached rock yet - miss ((y < dynel_y[i]-dynel_h[i])) || # behind the rock - miss (( (x > dynel_x[i] ? x-dynel_x[i] : dynel_x[i]-x) > dynel_w[i]/2 )) # simple square collision then continue else ((dynel_hp[i] -= ff_damage[$1])) if ((dynel_hp[i] > 0)) && ((dynel_hp[i] < rock_hp/2)); then dynel_color[i]=$color_rock_damaged dynel_redraw[i]=true fi (( score_current += score_rockshot * ff_damage[$1] )) # it's a hit! return 0 fi done # missed return 1 } # changes the color of a dynel several times until it's finally drawn with black to disappear function deathanimation # $1 - dynel_* index { local i=$1 # it helps to read this backwards :) case "${dynel_color[i]}" in "$color_death_4") return 1 ;; "$color_death_3") dynel_color[i]=$color_death_4 ;; "$color_death_2") dynel_color[i]=$color_death_3 ;; "$color_death_1") dynel_color[i]=$color_death_2 ;; *) dynel_color[i]=$color_death_1 ;; esac return 0 } # limit the amount of rocks function limitrocks { rock_count_max=$(((rows*cols) / 720)) } # update the amount of score you get for stuff function updatescore { score_rockshot=10 # score per damage point that hits a rock score_deadrock=$((500000 / (rows*cols))) # score per dead rock (off screen or shot to pieces) case "$state_current" in ingame) ;; title) score_current=0 # current score score_last=-1 # last drawn sore score_second=0 # score per second passed ;; esac } # movable? # $1 - dynel_* index function canmoveup { (( origo_y+dynel_cy[$1]-dynel_h[$1] > 0 )); } function canmovedown { (( origo_y+dynel_cy[$1] < rows )); } function canmoveright { (( origo_x+dynel_cx[$1]+(dynel_w[$1]/2)+1 < cols )); } function canmoveleft { (( origo_x+dynel_cx[$1]-(dynel_w[$1]/2) > 0 )); } # tput position array posarray=() # position array for faster cursor movement sizechanged # run console size check (will always have changed) # pause and unpause function registerpausetimer { pausetimers[${#pausetimers[*]}]="$1" } function pause { if ! $pause; then local timer value for timer in "${pausetimers[@]}"; do value=${!timer} if (( value != 0 )); then (( value -= time_now )) else value="zero" fi IFS= read -r "${timer?}" <<< "$value" done pause=true fi } function unpause { if $pause; then local timer value for timer in "${pausetimers[@]}"; do value=${!timer} if [[ "$value" != "zero" ]]; then (( value += time_now )) else value=0 fi IFS= read -r "${timer?}" <<< "$value" done pause=false fi } # ammo line - no printf -v on bash3 :( ff_ammo_max=30 # maximum ammunition while { ! ff_line=$(builtin printf "%${ff_ammo_max}s" ""); } 2>/dev/null; do :; done # init time_start=$(microtime) # time the game was started time_last=$time_start # time of last game loop time_now=$time_start # current time timer_resize=0 # resize check timer timerd_resize=1000000 # resize check timer delta state_current="title" # current game state state_last="" # game state last loop movespeed_x=7 # how fast the player ship moves horizontally movespeed_y=3 # how fast the player ship moves vertically blink_pressfire="" # blink status for the "press fire" text on title screen blink_pressq="" # blink status for the "press q" text on death screen blink_engines="" # blink status for the ship's engines blink_pause="" # blink status for the pause message messageheight=4 # how far away from the top we print the title and such cpusavesleep=0.2 # time to sleep if saving cpu (dead, paused, menu) redraw=true # should we redraw everything? (size probably changed) # reset the game function resetgame { # dynels dynel_img=( "ascii_playership" ) # drawing dynel_x=( 0 ) # current screen position (last drawn) dynel_y=( 1 ) # current screen position (last drawn) dynel_cx=( "${dynel_x[0]}" ) # actual position dynel_cy=( "${dynel_y[0]}" ) # actual position dynel_ydiv=( 0 ) # automatic movement (for non-player dynels) dynel_w=( "$ascii_playerdynel_w" ) # width dynel_h=( "$ascii_playerdynel_h" ) # height dynel_hp=( 1 ) # health dynel_color=( "$color_ship" ) # color dynel_redraw=( true ) # needs redrawing or not dynel_alive=( true ) # dynel exists # rocks rock_pos=${#dynel_alive[*]} # rock position in dynel array rock_count=0 # current number of live rocks rock_hp=12 # rock health rock_total=0 # total number of rocks spawned rock_add=0 # additional rocks for difficulty limitrocks # set the max rock count # friendly fire ff_x=() # screen position ff_y=() # screen position ff_ydiv=() # speed divisor ff_new=() # when new, don't move, only draw ff_symbol=() # symbol to draw ff_damage=() # how much damage this shot does ff_count=0 # current number of live shots ff_count_max=64 # max shot count at any given time ff_total=0 # total number of shots fired ff_next=0 # next shot ff_alive=() # shot exists ff_ammo_current=$((ff_ammo_max/2)) # current ammo ff_ammo_last=0 # last drawn ammo counter_fire=0 # number of shots fired for (( i=0; i seconds_last )); then fps=$framecounter framecounter=0 case "$state_current" in ingame|dead) xyprintf $((cols-2-9)) 2 "Dynl: %3d" "${#dynel_alive[*]}" xyprintf $((cols-2-9)) 3 "Rock: %3d" "$rock_count" xyprintf $((cols-2-9)) 4 "Shot: %3d" "$ff_count" xyprintf $((cols-2-6)) 5 "%6d" "$timerd_rocks" # xyecho $((cols-2-${#seconds})) 6 "$seconds" ;; esac fi xyprintf $((cols-10)) 1 "FPS: %3d" "$fps" # read input if $input; then readc <"$comm" case "$state_current" in ingame) case "$c" in A|B|C|D) if ! $pause && ((dynel_hp[0] > 0)); then case "$c" in A) # up for (( i=0; i 0)) && ! $autofire && (( timer_manualfire + timerd_manualfire < time_now )); then timer_manualfire=$time_now fire fi ;; 'p') if ! $pause; then pause else unpause fi ;; 'z') if ! $pause; then if ((dynel_hp[0] > 0)); then $autofire && autofire=false || autofire=true fi fi ;; 'q') if ! $pause; then state_current="title" else unpause fi ;; esac ;; title) case "$c" in ' ') resetgame updatescore timerd_rocks=100000 autofire=false state_current="ingame" ;; 'q') kill -TERM $$; exit 0; ;; esac ;; dead) case "$c" in 'q') resetgame; state_current="title" ;; esac ;; esac input=false fi # move and draw case "$state_current" in title) if $redraw; then safeecho "$color_title" catc $ascii_title_w $messageheight <<<"${ascii_title}" fi if $redraw; then safeecho "$color_keybindings" catc $ascii_keybindings_w $((messageheight + ascii_title_h + 3)) <<<"${ascii_keybindings}" fi if $redraw || [[ "$blink_pressfire" != "$blink_medium" ]]; then blink_pressfire=$blink_medium $blink_medium && safeecho "$color_pressfire_1" || safeecho "$color_pressfire_2" catc $ascii_press_fire_w $((messageheight + ascii_title_h + 1)) <<<"$ascii_press_fire" fi # sleep to save cpu sleep $cpusavesleep ;; dead) if $redraw; then safeecho "$color_youaredead" catc 7 $messageheight <<<"YOU ARE" catc $ascii_dead_w $((messageheight + 1)) <<<"${ascii_dead}" fi if $redraw; then safeecho "$color_score_result" catc $((19+${#score_current})) $((messageheight + ascii_dead_h + 2)) <<<"You scored $score_current points!" fi if $redraw; then # reading and writing this high score is sensitive to signal interruption and is somewhat error prone highscore=$(command cat "${HOME}/.shellshock" 2>/dev/null) if ((score_current > highscore)); then echo -n "$score_current" >"${HOME}/.shellshock" fi safeecho "$color_score_result" if ((score_current >= highscore)); then catc 15 $((messageheight + ascii_dead_h + 3)) <<<"NEW HIGH SCORE!" else catc $((12+${#highscore})) $((messageheight + ascii_dead_h + 3)) <<<"High Score: $highscore" fi fi if $redraw || [[ "$blink_pressq" != "$blink_slow" ]]; then blink_pressq=$blink_slow $blink_slow && safeecho "$color_pressq_1" || safeecho "$color_pressq_2" catc $ascii_press_q_w $((messageheight + ascii_dead_h + 5)) <<<"$ascii_press_q" fi # sleep to save cpu sleep $cpusavesleep ;; ingame) if ! $pause; then # need to run ship collision? runshipcollision=false # speed up and add score every second if (( seconds > seconds_last )); then timerd_rocks=$(( timerd_rocks > 250 ? timerd_rocks - 250 : 0)) (( score_current += ++score_second )) rock_add=$((rock_count_max * score_current / 200000)) fi fi # move and impact friendly fire if $redraw || { ! $pause && (( timer_fire + timerd_fire < time_now )) } then if ! $pause; then timer_fire=$time_now (( counter_fire++ )) fi safeecho "$color_fire" for (( i=0; i 0)) && ((timer_rocks + timerd_rocks < time_now)); then timer_rocks=$time_now (( ++rock_total )) fi # deal with rocks if ((dynel_hp[0] > 0)); then first_dead="" for i in "${!dynel_alive[@]}"; do (( i >= rock_pos )) || continue ${dynel_alive[i]} || { first_dead=${first_dead:-$i}; continue; } if # outside bottom of screen by entire height (( dynel_y[i] - dynel_h[i] > 0 )) then dynel_alive[i]=false (( --rock_count )) first_dead=${first_dead:-$i} (( score_current += score_deadrock )) elif # should be moved now (( timer_rocks == time_now )) && (( rock_total % dynel_ydiv[i] == 0 )) then if ((dynel_hp[i] <= 0)); then if ! deathanimation "$i"; then dynel_alive[i]=false (( --rock_count )) first_dead=${first_dead:-$i} (( score_current += score_deadrock )) fi fi (( dynel_cy[i]++ )) dynel_redraw[i]=true runshipcollision=true fi done if ((rock_count < rock_count_max + rock_add)); then i=${first_dead:-${#dynel_alive[*]}} dynel_w[i]="ascii_rock$((i%5))_w" dynel_w[i]=${!dynel_w[i]} dynel_h[i]="ascii_rock$((i%5))_h" dynel_h[i]=${!dynel_h[i]} dynel_x[i]=$(( (RANDOM % (cols-2-dynel_w[i])) - ( (cols-2)/2 ) )) dynel_y[i]=$((-origo_y)) dynel_cx[i]=${dynel_x[i]} dynel_cy[i]=${dynel_y[i]} restrict_xaxis "$i" if ! squarecollide "$i"; then dynel_ydiv[i]=$((RANDOM%3+1)) for j in "${!dynel_alive[@]}"; do (( j >= rock_pos )) || continue if ${dynel_alive[j]} && ((dynel_ydiv[i] < dynel_ydiv[j] )) && (( (dynel_x[i] > dynel_x[j] ? dynel_x[i]-dynel_x[j] : dynel_x[j]-dynel_x[i]) < ((dynel_w[i]+dynel_w[j])/2) )) then dynel_ydiv[i]=${dynel_ydiv[j]} fi done dynel_hp[i]=$rock_hp dynel_color[i]=$color_rock_healthy dynel_redraw[i]=false dynel_img[i]="ascii_rock$((i%5))" dynel_alive[i]=true (( ++rock_count )) fi fi fi # do ship collision if $runshipcollision && shipcollide; then (( dynel_hp[0]-- )) fi if ((dynel_hp[0] <= 0)) && ((timer_playerdeath + timerd_playerdeath < time_now)); then timer_playerdeath=$time_now dynel_redraw[0]=true if ! deathanimation 0; then state_current="dead" fi fi # regenerate ammo if (( timer_ammo + timerd_ammo + (timerd_rocks*3) < time_now )) && ((ff_ammo_current < ff_ammo_max)); then timer_ammo=$time_now (( ++ff_ammo_current )) fi fi # if ! $pause # draw ammo if $redraw; then xyecho 0 0 "${color_ammo_1}${ff_line:0:$ff_ammo_current}" xyecho "${ff_ammo_current}" 0 "${color_ammo_2}${ff_line:$ff_ammo_current:$ff_ammo_max}" else if ((ff_ammo_current < ff_ammo_last)); then xyecho "$ff_ammo_current" 0 "${color_ammo_2}${ff_line:0:$((ff_ammo_last-ff_ammo_current))}" elif ((ff_ammo_current > ff_ammo_last)); then xyecho "$ff_ammo_last" 0 "${color_ammo_1}${ff_line:0:$((ff_ammo_current-ff_ammo_last))}" fi fi # score if $redraw || (( score_current != score_last )); then score_last=$score_current xyecho $((cols-${#score_current})) 0 "${color_score}${score_current}" fi # blink engines of player ship if ! $pause && ((dynel_hp[0] > 0)) && ! ${dynel_redraw[0]} && [[ "$blink_engines" != "$blink_fast" ]]; then blink_engines=$blink_fast $blink_fast && safeecho "$color_engine_1" || safeecho "$color_engine_2" xyecho $((origo_x+dynel_cx[0]+3)) $((origo_y+dynel_cy[0]-2)) "@" xyecho $((origo_x+dynel_cx[0]-3)) $((origo_y+dynel_cy[0]-2)) "@" fi # draw/move dynels one step at a time to their current position lastalive=0 for i in "${!dynel_alive[@]}"; do ${dynel_alive[i]} && lastalive=$i || continue $redraw || ${dynel_redraw[i]} || continue dynel_redraw[i]=false safeecho "${dynel_color[i]}" while if (( dynel_x[i] < dynel_cx[i] )); then (( dynel_x[i]++ )) elif (( dynel_x[i] > dynel_cx[i] )); then (( dynel_x[i]-- )); fi if (( dynel_y[i] < dynel_cy[i] )); then (( dynel_y[i]++ )) elif (( dynel_y[i] > dynel_cy[i] )); then (( dynel_y[i]-- )); fi catd \ $(( origo_x-(dynel_w[i]/2)+dynel_x[i] )) \ $(( origo_y-dynel_h[i]+dynel_y[i] )) \ <<<"${!dynel_img[i]}" # fake do-while condition (( dynel_x[i] != dynel_cx[i] )) || (( dynel_y[i] != dynel_cy[i] )) do :; done done # draw pause anim if $pause_last; then # this uses pause_last to avoid drawing the pause blinker before the frame if $redraw; then safeecho "${color_pauseframe}" catc $ascii_pauseframe_w $((messageheight + 4)) <<<"$ascii_pauseframe" fi if $redraw || [[ "$blink_pause" != "$blink_slow" ]]; then blink_pause=$blink_slow $blink_slow && safeecho "$color_pause_1" || safeecho "$color_pause_2" catc $ascii_pause_w $((messageheight + 6)) <<<"$ascii_pause" fi # sleep to save cpu sleep $cpusavesleep fi # purge dead dynels size=${#dynel_alive[*]} (( ++lastalive )) for ((i=lastalive; i"$comm" kill -USR1 "$gamepid" ;; esac ;; ' '|p|q|z) builtin echo "$c" >"$comm" kill -USR1 "$gamepid" ;; esac done # vim: tabstop=4:softtabstop=4:shiftwidth=4:noexpandtab