std/c/README.md: Add section skeletons and don't indent lists
[sunny256-utils.git] / shellshock
blob474c862cf107331bada5a5cdb8bc8dbe6f458db7
1 #!/usr/bin/env bash
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License version 2, June 1991.
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU General Public License for more details.
11 # You should have received a copy of the GNU General Public License
12 # along with this program. If not, see <https://www.gnu.org/licenses/gpl-2.0.html>,
13 # or in pdf format at <http://www.dhampir.no/stuff/private/gpl-2.0.pdf>
15 # Copyright 2012 - Øyvind 'bolt' Hvidsten <bolt@dhampir.no>
17 # Applied patches:
19 # v1.07, 2012.03.26 - Steve McMurphy
20 # v1.11, 2014.09.30 - Øyvind A. Holm
23 # Description:
25 # ShellShock is a top-down space shooter written for Bash 3 / Bash 4
26 # Tested on Linux (Debian, RedHat and CentOS)
28 # Please note: A game in Bash is very demanding on resources.
29 # This script requires a modern computer to run at a decent speed.
30 # This script uses a tab width of 4, which is automatically applied
31 # if you're using vim with modelines enabled (see bottom line).
33 # For updates, please see <http://www.dhampir.no/stuff/bash/shellshock.bash>.
35 # Comments welcome, improvements/patches twice as welcome.
38 # Releases / Changelog:
40 # v1.00, 2012.03.18 - Initial v1.0 release
41 # * All intended functionality implemented
43 # v1.01, 2012.03.19 - Bash 3
44 # * Tweaks to run on Bash 3
46 # v1.05, 2012.03.25 - Several changes based on feedback
47 # * Added score display
48 # * Added increasing difficulty
50 # v1.06, 2012.03.26 - More feedback
51 # * Ship aft collision improved
52 # * Added pause option
53 # * Added simple high score storage
54 # * Runs on Bash 3 again
56 # v1.07, 2012.03.26 - OSX
57 # * Added rudimentary support for Mac OS X
59 # v1.08, 2012.03.26 - Cleanup
60 # * Improved some comments and made clearer error messages
62 # v1.09, 2012.03.27 - Ubuntu
63 # * Various fixes for Ubuntu
65 # v1.10, 2012.03.29 - Pause
66 # * Improved the pause function to avoid problems with timers
68 # v1.11, 2014.09.30 - Better cleanup
69 # * Using "stty sane" to clean up terminal settings on exit
72 # get uname
73 uname=$(uname -s)
75 # queue temp files for deletion on script exit
76 function rm_queue { _rm_queue[${#_rm_queue[*]}]="$1"; }
77 function rm_process { local file; for file in "${_rm_queue[@]}"; do rm "$file"; done; }
79 # check if tput actually outputs something useful for this terminal
80 if [[ -z "$(tput sgr0)" ]] || [[ -z "$(tput bold)" ]]; then
81 echo 'Error: tput is not working as expected with your current terminal settings!' >&2
82 echo 'Try setting your $TERM to something more standard?' >&2
83 exit 1
86 # read a single character
87 function readc { IFS= read -r -n1 -s "$@" c; }
89 # variable variables :)
90 gamepid=""
91 saved_term=false
93 # list of files to be removed on cleanup
94 _rm_queue=()
96 # cleanup
97 function cleanup
99 [[ -z "$gamepid" ]] || { kill -TERM "$gamepid"; wait "$gamepid"; } 2>/dev/null
100 rm_process # remove temp files
101 tput sgr0 # reset color
102 clear # clear the screen
103 tput cnorm # show the cursor
104 stty echo # show input
105 stty sane # reset terminal
106 ! $saved_term || tput rmcup # restore the terminal view
107 case "$uname" in
108 Darwin)
109 reset
111 esac
113 trap cleanup EXIT
115 # make comm file
116 # TODO: better solution for inter-thread communication. must work on bash3
117 case "$uname" in
118 Darwin)
119 comm=$(mktemp /tmp/shellshock.XXXXXX)
122 comm=$(mktemp)
124 esac
125 if [[ -n "$comm" ]]; then
126 rm_queue "$comm"
127 else
128 echo "Error: Communications file creation failed!" >&2
129 exit 1
132 # init
133 tput smcup && saved_term=true # save the current terminal view
134 tput civis # hide the cursor
136 # game subshell
138 reset 2>/dev/null
139 input=false
140 trap 'input=true' USR1
141 trap 'exit 0' TERM INT HUP
143 # test parent shell
144 function testparent
146 kill -0 $$ 2>/dev/null || exit 1
149 # how to print stuff
150 function xyecho
152 local x=$1 y=$2
153 shift 2
155 # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages
156 while { ! builtin echo -n "${posarray[$((y*cols+x))]}$@"; } 2>/dev/null; do testparent; done
158 function safeecho
160 # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages
161 while { ! builtin echo -n "${posarray[$((y*cols+x))]}$@"; } 2>/dev/null; do testparent; done
163 function xyprintf
165 local x=$1 y=$2
166 shift 2
168 # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages
169 while { ! builtin printf "${posarray[$((y*cols+x))]}$@"; } 2>/dev/null; do testparent; done
171 function safeprintf
173 # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages
174 while { ! builtin printf "${posarray[$((y*cols+x))]}$@"; } 2>/dev/null; do testparent; done
177 # called when the player fires his/her weapons
178 function fire # no parameters
180 (( ff_ammo_current > 0 )) || return
182 if ${ff_alive[ff_next]}; then
183 xyprintf $((origo_x+ff_x[ff_next])) $((origo_y+ff_y[ff_next])) " "
186 if ((++ff_total%2 == 0)); then
187 ff_x[ff_next]=$((dynel_cx[0]-3))
188 ff_y[ff_next]=$((dynel_cy[0]-8))
189 else
190 ff_x[ff_next]=$((dynel_cx[0]+3))
191 ff_y[ff_next]=$((dynel_cy[0]-8))
194 if ! outofbounds "$((origo_x+ff_x[ff_next]))" "$((origo_y+ff_y[ff_next]))"; then
195 ff_ydiv[ff_next]=1
196 ff_symbol[ff_next]="|"
197 ff_damage[ff_next]=1
198 ff_alive[ff_next]=true
199 (( ++ff_count ))
200 ff_new[ff_next]=true
201 (( --ff_ammo_current ))
202 ff_next=$(( ++ff_next < ff_count_max ? ff_next : 0 ))
205 if ((ff_total%3 == 0)); then
206 if ${ff_alive[ff_next]}; then
207 xyprintf $((origo_x+ff_x[ff_next])) $((origo_y+ff_y[ff_next])) " "
210 if ((ff_total%2 == 0)); then
211 ff_x[ff_next]=$((dynel_cx[0]+5))
212 ff_y[ff_next]=$((dynel_cy[0]-7))
213 else
214 ff_x[ff_next]=$((dynel_cx[0]-5))
215 ff_y[ff_next]=$((dynel_cy[0]-7))
218 if ! outofbounds "$((origo_x+ff_x[ff_next]))" "$((origo_y+ff_y[ff_next]))"; then
219 ff_ydiv[ff_next]=2
220 ff_symbol[ff_next]="¤"
221 ff_damage[ff_next]=4
222 ff_alive[ff_next]=true
223 (( ++ff_count ))
224 ff_new[ff_next]=true
225 ff_next=$(( ++ff_next < ff_count_max ? ff_next : 0 ))
230 #######################################################
231 ## BEGIN ASCII ART SECTION - NO TABS, NO INDENTATION ##
232 #######################################################
233 # title
234 ascii_title_w=78
235 ascii_title_h=6
236 ascii_title=$(
237 cat - <<"EOF"
238 _________.__ .__ .__ _________.__ __ ._.
239 / _____/| |__ ____ | | | | / _____/| |__ ____ ____ | | _| |
240 \_____ \ | | \_/ __ \| | | | \_____ \ | | \ / _ \_/ ___\| |/ / |
241 / \| Y \ ___/| |_| |__/ \| Y ( <_> ) \___| < \|
242 /_______ /|___| /\___ >____/____/_______ /|___| /\____/ \___ >__|_ \__
243 \/ \/ \/ \/ \/ \/ \/\/
246 # keybindings
247 ascii_keybindings_w=74 #21 - fake width to print off-center
248 ascii_keybindings_h=5
249 ascii_keybindings=$(
250 cat - <<"EOF"
251 Keybindings:
252 arrow keys - move
253 SPACE - fire
254 z - toggle autofire
255 p - pause
256 q - quit
259 # dead meat - written in quotes because all the parenthesis confuse vim's syntax highlighting
260 # - this also means the first line has to be offset, so the ascii looks ugly in code
261 ascii_dead_w=36
262 ascii_dead_h=8
263 ascii_dead=\
264 ' ______ _______ _______ ______
265 ( __ \ ( ____ \( ___ )( __ \
266 | ( \ )| ( \/| ( ) || ( \ )
267 | | ) || (__ | (___) || | ) |
268 | | | || __) | ___ || | | |
269 | | ) || ( | ( ) || | ) |
270 | (__/ )| (____/\| ) ( || (__/ )
271 (______/ (_______/|/ \|(______/'
272 # press q
273 ascii_press_q_w=42
274 ascii_press_q_h=1
275 ascii_press_q=$(
276 cat - <<"EOF"
277 -= Press q to quit to the title screen! =-
280 # press fire
281 ascii_press_fire_w=27
282 ascii_press_fire_h=1
283 ascii_press_fire=$(
284 cat - <<"EOF"
285 -= Press SPACE to start! =-
288 # pause
289 ascii_pause_w=11
290 ascii_pause_h=1
291 ascii_pause=$(
292 cat - <<"EOF"
293 -= PAUSE =-
296 # pause frame
297 ascii_pauseframe_w=25
298 ascii_pauseframe_h=5
299 ascii_pauseframe=$(
300 cat - <<"EOF"
301 *************************
305 *************************
309 # player's spaceship
310 ascii_playerdynel_w=21
311 ascii_playerdynel_h=9
312 ascii_playership=$(
313 cat - <<"EOF"
314 08]
315 06] /^\
316 03] |/.!.\|
317 01] _|/_/]=[\_\|_
318 00] _/ | | \_
319 00] |_____. | | ._____|
320 00] \_________/
321 04] |@| |@|
322 05]
325 # rock 1
326 ascii_rock0_w=9
327 ascii_rock0_h=7
328 ascii_rock0=$(
329 cat - <<"EOF"
330 02]
331 01] __
332 01] / \_
333 00] | \
334 01] \ _|
335 01] \_/
336 02]
339 ascii_rock1_w=14
340 ascii_rock1_h=8
341 ascii_rock1=$(
342 cat - <<"EOF"
343 06]
344 01] __
345 00] ____/ \_
346 00] / \
347 00] \ |
348 00] \ ___/
349 01] \___/
350 02]
353 ascii_rock2_w=8
354 ascii_rock2_h=8
355 ascii_rock2=$(
356 cat - <<"EOF"
357 02]
358 01] __
359 00] / \
360 00] | |
361 00] | \
362 00] | _/
363 00] \__/
364 00]
367 ascii_rock3_w=9
368 ascii_rock3_h=7
369 ascii_rock3=$(
370 cat - <<"EOF"
371 01]
372 00] _____
373 00] / \
374 00] | |
375 00] \ _/
376 00] \_/
377 01]
380 ascii_rock4_w=13
381 ascii_rock4_h=8
382 ascii_rock4=$(
383 cat - <<"EOF"
384 01]
385 00] ___
386 00] / \_____
387 00] | _|
388 00] \ /
389 00] \_ /
390 01] \__/
391 03]
394 #######################################################
395 ## END ASCII ART SECTION ##
396 #######################################################
398 # pretty colours
399 color_background=$(tput setab 0)
400 color_reset="$(tput sgr0)${color_background}"
401 color_black="${color_reset}$(tput setaf 0)"
402 color_red="${color_reset}$(tput setaf 1)"
403 color_green="${color_reset}$(tput setaf 2)"
404 color_orange="${color_reset}$(tput setaf 3)"
405 color_blue="${color_reset}$(tput setaf 4)"
406 color_magenta="${color_reset}$(tput setaf 5)"
407 color_cyan="${color_reset}$(tput setaf 6)"
408 color_light_gray="${color_reset}$(tput setaf 7)"
409 color_dark_gray="${color_reset}$(tput bold)$(tput setaf 0)"
410 color_light_red="${color_reset}$(tput bold)$(tput setaf 1)"
411 color_light_green="${color_reset}$(tput bold)$(tput setaf 2)"
412 color_yellow="${color_reset}$(tput bold)$(tput setaf 3)"
413 color_light_blue="${color_reset}$(tput bold)$(tput setaf 4)"
414 color_light_magenta="${color_reset}$(tput bold)$(tput setaf 5)"
415 color_light_cyan="${color_reset}$(tput bold)$(tput setaf 6)"
416 color_white="${color_reset}$(tput bold)$(tput setaf 7)"
418 # specific colors for stuff
419 color_debug=$color_orange # debug prints (FPS, seconds, rocks count, etc)
420 color_ship=$color_white # player's ship
421 color_fire=$color_light_magenta # player's missiles
422 color_engine_1=$color_light_red # player's engines (blinking)
423 color_engine_2=$color_red # player's engines (blinking)
424 color_border=$color_red # border
425 color_score_result=$color_yellow # score result (death screen)
426 color_origo=$color_green # origo (bottom center)
427 color_rock_healthy=$color_light_cyan # healthy rocks
428 color_rock_damaged=$color_cyan # damaged rocks
429 color_death_1=$color_light_red # death animation stage 1
430 color_death_2=$color_red # death animation stage 2
431 color_death_3=$color_dark_gray # death animation stage 3
432 color_death_4=$color_black # death animation stage 4 (erase)
433 color_title=$color_yellow # ShellShock!
434 color_pause_1=$color_light_red # pause text (blinking)
435 color_pause_2=$color_red # pause text (blinking)
436 color_pauseframe=$color_red # frame around pause text
437 color_youaredead=$color_light_red # YOU ARE DEAD
438 color_keybindings=$color_green # keybindings...
439 color_pressfire_1=$color_blue # press space to start (blinking)
440 color_pressfire_2=$color_light_blue # press space to start (blinking)
441 color_pressq_1=$color_dark_gray # press q for title screen (blinking)
442 color_pressq_2=$color_light_gray # press q for title screen (blinking)
444 # score display (top right) (drawn on white background)
445 color_score="${color_black}$(tput setab 7)"
446 # ammo display (drawn with spaces on background color in the top left)
447 color_ammo_1="${color_black}$(tput setab 5)"
448 color_ammo_2="${color_black}$(tput setab 7)"
450 # home
451 home=$(tput home)
453 # has the size changed?
454 function sizechanged
456 if (( cols != $(tput cols) )) || (( rows != $(tput lines) )); then
457 cols=$(tput cols)
458 rows=$(tput lines)
459 origo_x=$((cols/2))
460 origo_y=$((rows-1))
462 return 0
464 return 1
467 # clear
468 redraw=false
469 function wipe
471 # "tput clear" doesn't fill with background color in bash3, screen, etc.
472 # must write a shitload of spaces instead
473 safeprintf "${color_reset}${home}%$((cols*rows))s" ""
475 # everything needs redrawing after this
476 redraw=true
479 # get the current time in microseconds
480 case "$uname" in
481 Darwin)
482 # this is slow. very slow.
483 function microtime {
484 local time=$(python <<<"import time; print \"%.6f\" % time.time();")
485 echo -n ${time/./}
489 function microtime {
490 local time=$(date +%s%N)
491 echo -n ${time:0:((${#time}-3))}
494 esac
496 # build tput position array
497 # moving using this 10x faster than running tput
498 _pos_cols=0
499 _pos_rows=0
500 function buildposarray
502 if ((cols == _pos_cols)) && ((rows == _pos_rows)); then
503 return 1
506 wipe
507 _pos_cols=$cols
508 _pos_rows=$rows
509 posarray=()
511 local q=false
512 local e=$(echo -e "\e")
513 if [[ "$(tput cup 0 0)" = "${e}[1;1H" ]]; then
514 # standard terminal movement commands - quick generation
515 q=true
518 local string="Building position array for ${cols}x${rows}... "
519 local pos=$(tput cup 0 ${#string})
521 local x y
522 safeecho "${color_debug}${home}${string}"
523 for ((x=0; x < cols; x++)); do
524 if sizechanged; then
525 buildposarray
526 return $?
528 echo -n "${pos}$((x*100/cols))%"
529 for ((y=0; y < rows; y++)); do
530 if $q; then
531 posarray[$((y*cols+x))]="${e}[$((y+1));$((x+1))H"
532 else
533 posarray[$((y*cols+x))]=$(tput cup "$y" "$x")
535 done
536 done
538 return 0
541 # print something at a spot
542 function catc { cat "$@"; } # draw centered (x coordinate specifies width, not x pos)
543 function catd { cat "$@"; } # draw a dynel (supports black outline)
544 function cat
546 local x=$1 y=$2 i=0 cy dynel=false first=true offset=0
548 case "${FUNCNAME[1]}" in
549 catd) dynel=true ;;
550 catc)
551 x=$(( (cols - x) / 2)) # center
552 (( x > 0 )) || x=1
554 esac
556 while IFS= read -r line; do
557 cy=$(( y + i++ ))
558 if $dynel; then
559 offset="$(( 10#${line:0:2} ))"
560 line=${line:3+offset}
561 cx=$(( x + offset ))
562 else
563 cx=$x
565 (( cx < cols-1 )) || continue # don't write on or outside the right border
566 (( cy > 0 )) || continue # don't write on or above the top border
567 (( cy < rows-1 )) || break # don't write on or below the bottom border
568 (( cx >= 1 )) || { line=${line:1-cx}; cx=1; } # cut to fit inside left border
569 line=${line:0:cols-1-cx} # cut to fit inside right border
571 xyecho $cx $cy "$line"
572 done
575 # border drawing
576 function border
578 safeecho $color_border
580 # no printf -v on bash3 :(
581 local line
582 while { ! line=$(builtin printf "%${cols}s" ""); } 2>/dev/null; do :; done
583 line=${line// /#}
585 xyecho 0 0 "$line"
587 local y
588 for (( y=1; y<rows-1; y++ )); do
589 xyecho 0 $y "#"
590 xyecho $((cols-1)) $y "#"
591 done
593 xyecho 0 $((rows-1)) "$line"
596 # is something outside the screen?
597 function outofbounds # $1 - x coordinate, $2 - y coordinate
599 local x=$1 y=$2
601 (( x < 1 )) || (( x >= cols-1 )) ||
602 (( y < 1 )) || (( y >= rows-1 ))
603 then
604 return 0
606 return 1
609 # pushes a dynel until it's within the right and left borders
610 function restrict_xaxis # $1 - dynel_* index
612 local i=$1
613 dynel_x[i]=${dynel_cx[i]}
614 while ! canmoveright $i; do (( dynel_cx[i]-- )); done;
615 while ! canmoveleft $i; do (( dynel_cx[i]++ )); done;
616 (( dynel_cx[i] < dynel_x[i] )) && (( dynel_cx[i]++ ))
617 (( dynel_cx[i] > dynel_x[i] )) && (( dynel_cx[i]-- ))
618 dynel_x[i]=${dynel_cx[i]}
621 # pushes a dynel until it's within the top and bottom borders
622 function restrict_yaxis # $1 - dynel_* index
624 local i=$1
625 dynel_y[i]=${dynel_cy[i]}
626 while ! canmoveup $i; do (( dynel_cy[i]++ )); done;
627 while ! canmovedown $i; do (( dynel_cy[i]-- )); done;
628 (( dynel_cy[i] < dynel_y[i] )) && (( dynel_cy[i]++ ))
629 (( dynel_cy[i] > dynel_y[i] )) && (( dynel_cy[i]-- ))
630 dynel_y[i]=${dynel_cy[i]}
633 # collides dynels based on simple squares (width, height)
634 function squarecollide # $1 - dynel_* index
636 local i=$1
637 for j in "${!dynel_alive[@]}"; do
638 ${dynel_alive[j]} || continue # don't check dead dynels
639 (( j != i )) || continue # don't check yourself
640 (( j >= rock_pos )) || continue # don't check the playership
641 local distance_x=$((dynel_cx[i] > dynel_cx[j] ? dynel_cx[i]-dynel_cx[j] : dynel_cx[j]-dynel_cx[i]))
642 local distance_y=$((dynel_cy[j] - dynel_cy[i]))
644 (( distance_x < (dynel_w[i]+dynel_w[j])/2 )) &&
646 if (( distance_y < 0 )); then
647 # j (compare dynel) is above i
648 (( -distance_y < dynel_h[j] ))
649 else
650 # j (compare dynel) is below i
651 (( distance_y < dynel_h[j] ))
654 then
655 # collision!
656 return 0
658 done
660 # no collision
661 return 1
664 # collides the player's ship
665 # basically the same as square collision, but with some tweaks to make it feel better
666 function shipcollide # no parameters
668 local i=0
669 for j in "${!dynel_alive[@]}"; do
670 ${dynel_alive[j]} || continue # don't check dead dynels
671 (( j != i )) || continue # don't check yourself
672 local distance_x=$((dynel_cx[i] > dynel_cx[j] ? dynel_cx[i]-dynel_cx[j] : dynel_cx[j]-dynel_cx[i]))
673 local distance_y=$((dynel_cy[j] - dynel_cy[i]))
675 (( distance_x + 2 < (dynel_w[i]+dynel_w[j])/2 )) && # make the ship a little narrower
677 if (( distance_y < 0 )); then
678 # j (compare dynel) is above i
679 (( -distance_y < dynel_h[j] )) &&
680 (( (distance_y + dynel_h[i]) > distance_x - 4 )) # make a somewhat cone-shaped ship
681 else
682 # j (compare dynel) is below i
683 (( distance_y + 2 < dynel_h[j] )) && # make the ship a little shorter
685 ((dynel_h[j] - distance_y - 2 > 2)) || # 2 lines into ship from bottom
686 ((4 + (dynel_h[j] - distance_y - 2) > distance_x)) # engine hit
690 then
691 # collision!
692 return 0
694 done
696 # no collision
697 return 1
700 # runs hit tests on friendly fire and damages any rocks encountered
701 function ffhit # $1 - ff_* index
703 local x=${ff_x[$1]} y=${ff_y[$1]} i j
704 for i in "${!dynel_alive[@]}"; do
705 (( i >= rock_pos )) || continue # only check rocks
706 ${dynel_alive[i]} || continue # don't check dead dynels
708 ((y > dynel_y[i]-2)) || # haven't reached rock yet - miss
709 ((y < dynel_y[i]-dynel_h[i])) || # behind the rock - miss
710 (( (x > dynel_x[i] ? x-dynel_x[i] : dynel_x[i]-x) > dynel_w[i]/2 )) # simple square collision
711 then
712 continue
713 else
714 ((dynel_hp[i] -= ff_damage[$1]))
715 if ((dynel_hp[i] > 0)) && ((dynel_hp[i] < rock_hp/2)); then
716 dynel_color[i]=$color_rock_damaged
717 dynel_redraw[i]=true
719 (( score_current += score_rockshot * ff_damage[$1] ))
720 # it's a hit!
721 return 0
723 done
725 # missed
726 return 1
729 # changes the color of a dynel several times until it's finally drawn with black to disappear
730 function deathanimation # $1 - dynel_* index
732 local i=$1
733 # it helps to read this backwards :)
734 case "${dynel_color[i]}" in
735 "$color_death_4") return 1 ;;
736 "$color_death_3") dynel_color[i]=$color_death_4 ;;
737 "$color_death_2") dynel_color[i]=$color_death_3 ;;
738 "$color_death_1") dynel_color[i]=$color_death_2 ;;
739 *) dynel_color[i]=$color_death_1 ;;
740 esac
741 return 0
744 # limit the amount of rocks
745 function limitrocks
747 rock_count_max=$(((rows*cols) / 720))
750 # update the amount of score you get for stuff
751 function updatescore
753 score_rockshot=10 # score per damage point that hits a rock
754 score_deadrock=$((500000 / (rows*cols))) # score per dead rock (off screen or shot to pieces)
755 case "$state_current" in
756 ingame) ;;
757 title)
758 score_current=0 # current score
759 score_last=-1 # last drawn sore
760 score_second=0 # score per second passed
762 esac
765 # movable? # $1 - dynel_* index
766 function canmoveup { (( origo_y+dynel_cy[$1]-dynel_h[$1] > 0 )); }
767 function canmovedown { (( origo_y+dynel_cy[$1] < rows )); }
768 function canmoveright { (( origo_x+dynel_cx[$1]+(dynel_w[$1]/2)+1 < cols )); }
769 function canmoveleft { (( origo_x+dynel_cx[$1]-(dynel_w[$1]/2) > 0 )); }
771 # tput position array
772 posarray=() # position array for faster cursor movement
773 sizechanged # run console size check (will always have changed)
775 # pause and unpause
776 function registerpausetimer
778 pausetimers[${#pausetimers[*]}]="$1"
780 function pause
782 if ! $pause; then
783 local timer value
784 for timer in "${pausetimers[@]}"; do
785 value=${!timer}
786 if (( value != 0 )); then
787 (( value -= time_now ))
788 else
789 value="zero"
791 IFS= read -r $timer <<< "$value"
792 done
793 pause=true
796 function unpause
798 if $pause; then
799 local timer value
800 for timer in "${pausetimers[@]}"; do
801 value=${!timer}
802 if [[ value != "zero" ]]; then
803 (( value += time_now ))
804 else
805 value=0
807 IFS= read -r $timer <<< "$value"
808 done
809 pause=false
813 # ammo line - no printf -v on bash3 :(
814 ff_ammo_max=30 # maximum ammunition
815 while { ! ff_line=$(builtin printf "%${ff_ammo_max}s" ""); } 2>/dev/null; do :; done
817 # init
818 time_start=$(microtime) # time the game was started
819 time_last=$time_start # time of last game loop
820 time_now=$time_start # current time
821 timer_resize=0 # resize check timer
822 timerd_resize=1000000 # resize check timer delta
823 state_current="title" # current game state
824 state_last="" # game state last loop
825 movespeed_x=7 # how fast the player ship moves horizontally
826 movespeed_y=3 # how fast the player ship moves vertically
827 blink_pressfire="" # blink status for the "press fire" text on title screen
828 blink_pressq="" # blink status for the "press q" text on death screen
829 blink_engines="" # blink status for the ship's engines
830 blink_pause="" # blink status for the pause message
831 messageheight=4 # how far away from the top we print the title and such
832 cpusavesleep=0.2 # time to sleep if saving cpu (dead, paused, menu)
833 redraw=true # should we redraw everything? (size probably changed)
835 # reset the game
836 function resetgame
838 # dynels
839 dynel_img=( "ascii_playership" ) # drawing
840 dynel_x=( 0 ) # current screen position (last drawn)
841 dynel_y=( 1 ) # current screen position (last drawn)
842 dynel_cx=( ${dynel_x[0]} ) # actual position
843 dynel_cy=( ${dynel_y[0]} ) # actual position
844 dynel_ydiv=( 0 ) # automatic movement (for non-player dynels)
845 dynel_w=( $ascii_playerdynel_w ) # width
846 dynel_h=( $ascii_playerdynel_h ) # height
847 dynel_hp=( 1 ) # health
848 dynel_color=( $color_ship ) # color
849 dynel_redraw=( true ) # needs redrawing or not
850 dynel_alive=( true ) # dynel exists
852 # rocks
853 rock_pos=${#dynel_alive[*]} # rock position in dynel array
854 rock_count=0 # current number of live rocks
855 rock_hp=12 # rock health
856 rock_total=0 # total number of rocks spawned
857 rock_add=0 # additional rocks for difficulty
858 limitrocks # set the max rock count
860 # friendly fire
861 ff_x=() # screen position
862 ff_y=() # screen position
863 ff_ydiv=() # speed divisor
864 ff_new=() # when new, don't move, only draw
865 ff_symbol=() # symbol to draw
866 ff_damage=() # how much damage this shot does
867 ff_count=0 # current number of live shots
868 ff_count_max=64 # max shot count at any given time
869 ff_total=0 # total number of shots fired
870 ff_next=0 # next shot
871 ff_alive=() # shot exists
872 ff_ammo_current=$((ff_ammo_max/2)) # current ammo
873 ff_ammo_last=0 # last drawn ammo
874 counter_fire=0 # number of shots fired
875 for (( i=0; i<ff_count_max; i++ )); do ff_alive[i]=false; done
877 # timers and stuff
878 pausetimers=()
879 timer_ammo=$time_now # timer for ammo generation
880 timerd_ammo=450000 # timer delta for above
881 registerpausetimer "timer_ammo"
882 timer_autofire=$time_now # timer for the autofire function
883 timerd_autofire=200000 # timer delta for above
884 registerpausetimer "timer_autofire"
885 timer_manualfire=$time_now # timer for manual fire
886 timerd_manualfire=100000 # timer delta for above
887 registerpausetimer "timer_manualfire"
888 timer_fire=$time_now # timer for update/drawing of fire
889 timerd_fire=50000 # timer delta for above
890 registerpausetimer "timer_fire"
891 timer_rocks=$time_now # timer for update/drawing of rocks
892 timerd_rocks="" # timer delta for above
893 registerpausetimer "timer_rocks"
894 timer_playerdeath=0 # used for drawing the player ship death animation
895 timerd_playerdeath=500000 # timer delta for above
896 registerpausetimer "timer_playerdeath"
897 seconds_last=0 # runtime in seconds (last printed)
899 # blinking - allows blinking text and stuff to blink in sync
900 blink_fast=false
901 timer_blink_fast=$time_start
902 blink_medium=false
903 timer_blink_medium=$time_start
904 blink_slow=false
905 timer_blink_slow=$time_start
907 # misc
908 autofire=false # is autofire enabled?
909 runshipcollision=false # run ship collision this frame?
910 pause=false # is the game paused?
911 pause_last=false # was the game paused last frame?
913 resetgame
915 # game loop
916 while true; do
917 time_now=$(microtime)
918 seconds=$(((time_now-time_start)/1000000))
919 framecounter=${framecounter:-0}
920 fps=${fps:-$framecounter}
921 (( framecounter++ ))
923 # blinking
924 if (( timer_blink_fast + 200000 < time_now )); then
925 timer_blink_fast=$time_now
926 $blink_fast && blink_fast=false || blink_fast=true
928 if (( timer_blink_medium + 600000 < time_now )); then
929 timer_blink_medium=$time_now
930 $blink_medium && blink_medium=false || blink_medium=true
932 if (( timer_blink_slow + 999999 < time_now )); then
933 timer_blink_slow=$time_now
934 $blink_slow && blink_slow=false || blink_slow=true
937 # resize if needed
939 [[ "$state_current" != "$state_last" ]] ||
940 [[ "$pause" != "$pause_last" ]] ||
942 (( timer_resize + timerd_resize < time_now )) &&
944 timer_resize=$time_now
945 # this check is frakkin' expensive - do it only once per second
946 sizechanged
949 then
950 state_last=$state_current
951 pause_last=$pause
953 # clean up and resize
954 wipe # wipe the screen
955 buildposarray # build new position array to move about
956 limitrocks # update the number of rocks we should have
957 updatescore # update how much score you get
959 # push dynels inwards
960 for i in "${!dynel_alive[@]}"; do
961 ${dynel_alive[i]} || continue
962 restrict_xaxis $i
963 if ((i == 0)); then
964 restrict_yaxis $i
966 done
968 if $redraw; then
969 border
970 safeecho $color_origo
971 xyecho $origo_x $origo_y "#"
974 # fps & counter
975 safeecho $color_debug
976 if (( seconds > seconds_last )); then
977 fps=$framecounter
978 framecounter=0
979 case "$state_current" in
980 ingame|dead)
981 xyprintf $((cols-2-9)) 2 "Dynl: %3d" "${#dynel_alive[*]}"
982 xyprintf $((cols-2-9)) 3 "Rock: %3d" "$rock_count"
983 xyprintf $((cols-2-9)) 4 "Shot: %3d" "$ff_count"
984 xyprintf $((cols-2-6)) 5 "%6d" "$timerd_rocks"
985 # xyecho $((cols-2-${#seconds})) 6 "$seconds"
987 esac
989 xyprintf $((cols-10)) 1 "FPS: %3d" "$fps"
991 # read input
992 if $input; then
993 readc <"$comm"
995 case "$state_current" in
996 ingame)
997 case "$c" in
998 A|B|C|D)
999 if ! $pause && ((dynel_hp[0] > 0)); then
1000 case "$c" in
1001 A) # up
1002 for (( i=0; i<movespeed_y; i++ )); do
1003 canmoveup 0 && dynel_redraw[0]=true && runshipcollision=true || break && (( dynel_cy[0]-- ))
1004 done
1006 B) # down
1007 for (( i=0; i<movespeed_y; i++ )); do
1008 canmovedown 0 && dynel_redraw[0]=true && runshipcollision=true || break && (( dynel_cy[0]++ ))
1009 done
1011 C) # right
1012 for (( i=0; i<movespeed_x; i++ )); do
1013 canmoveright 0 && dynel_redraw[0]=true && runshipcollision=true || break && (( dynel_cx[0]++ ))
1014 done
1016 D) # left
1017 for (( i=0; i<movespeed_x; i++ )); do
1018 canmoveleft 0 && dynel_redraw[0]=true && runshipcollision=true || break && (( dynel_cx[0]-- ))
1019 done
1021 esac
1024 ' ')
1025 if ! $pause && ((dynel_hp[0] > 0)) && ! $autofire && (( timer_manualfire + timerd_manualfire < time_now )); then
1026 timer_manualfire=$time_now
1027 fire
1030 'p')
1031 if ! $pause; then
1032 pause
1033 else
1034 unpause
1037 'z')
1038 if ! $pause; then
1039 if ((dynel_hp[0] > 0)); then
1040 $autofire && autofire=false || autofire=true
1044 'q')
1045 if ! $pause; then
1046 state_current="title"
1047 else
1048 unpause
1051 esac
1053 title)
1054 case "$c" in
1055 ' ')
1056 resetgame
1057 updatescore
1058 timerd_rocks=100000
1059 autofire=false
1060 state_current="ingame"
1062 'q') kill -TERM $$; exit 0; ;;
1063 esac
1065 dead)
1066 case "$c" in
1067 'q') resetgame; state_current="title" ;;
1068 esac
1070 esac
1072 input=false
1075 # move and draw
1076 case "$state_current" in
1077 title)
1078 if $redraw; then
1079 safeecho $color_title
1080 catc $ascii_title_w $messageheight <<<"${ascii_title}"
1082 if $redraw; then
1083 safeecho $color_keybindings
1084 catc $ascii_keybindings_w $((messageheight + ascii_title_h + 3)) <<<"${ascii_keybindings}"
1086 if $redraw || [[ "$blink_pressfire" != "$blink_medium" ]]; then
1087 blink_pressfire=$blink_medium
1088 $blink_medium && safeecho $color_pressfire_1 || safeecho $color_pressfire_2
1089 catc $ascii_press_fire_w $((messageheight + ascii_title_h + 1)) <<<"$ascii_press_fire"
1092 # sleep to save cpu
1093 sleep $cpusavesleep
1095 dead)
1096 if $redraw; then
1097 safeecho $color_youaredead
1098 catc 7 $messageheight <<<"YOU ARE"
1099 catc $ascii_dead_w $((messageheight + 1)) <<<"${ascii_dead}"
1101 if $redraw; then
1102 safeecho $color_score_result
1103 catc $((19+${#score_current})) $((messageheight + ascii_dead_h + 2)) <<<"You scored $score_current points!"
1105 if $redraw; then
1106 # reading and writing this high score is sensitive to signal interruption and is somewhat error prone
1107 highscore=$(command cat "${HOME}/.shellshock" 2>/dev/null)
1108 if ((score_current > highscore)); then
1109 echo -n "$score_current" >"${HOME}/.shellshock"
1111 safeecho $color_score_result
1112 if ((score_current >= highscore)); then
1113 catc 15 $((messageheight + ascii_dead_h + 3)) <<<"NEW HIGH SCORE!"
1114 else
1115 catc $((12+${#highscore})) $((messageheight + ascii_dead_h + 3)) <<<"High Score: $highscore"
1118 if $redraw || [[ "$blink_pressq" != "$blink_slow" ]]; then
1119 blink_pressq=$blink_slow
1120 $blink_slow && safeecho $color_pressq_1 || safeecho $color_pressq_2
1121 catc $ascii_press_q_w $((messageheight + ascii_dead_h + 5)) <<<"$ascii_press_q"
1124 # sleep to save cpu
1125 sleep $cpusavesleep
1127 ingame)
1128 if ! $pause; then
1129 # need to run ship collision?
1130 runshipcollision=false
1132 # speed up and add score every second
1133 if (( seconds > seconds_last )); then
1134 timerd_rocks=$(( timerd_rocks > 250 ? timerd_rocks - 250 : 0))
1135 (( score_current += ++score_second ))
1136 rock_add=$((rock_count_max * score_current / 200000))
1140 # move and impact friendly fire
1142 $redraw ||
1144 ! $pause &&
1145 (( timer_fire + timerd_fire < time_now ))
1147 then
1148 if ! $pause; then
1149 timer_fire=$time_now
1150 (( counter_fire++ ))
1152 safeecho $color_fire
1153 for (( i=0; i<ff_count_max; i++ )); do
1154 if ${ff_alive[i]}; then
1156 $redraw &&
1157 outofbounds "$((origo_x+ff_x[i]))" "$((origo_y+ff_y[i]))"
1158 then
1159 ff_alive[i]=false
1160 (( --ff_count ))
1161 elif ${ff_new[i]}; then
1162 ff_new[i]=false
1163 if ffhit $i; then
1164 ff_alive[i]=false
1165 (( --ff_count ))
1166 else
1167 xyecho $((origo_x+ff_x[i])) $((origo_y+ff_y[i])) "${ff_symbol[i]}"
1169 else
1170 if $redraw; then
1171 xyecho $((origo_x+ff_x[i])) $((origo_y+ff_y[i])) "${ff_symbol[i]}"
1173 if ! $pause && (( counter_fire % ff_ydiv[i] == 0 )); then
1174 xyecho $((origo_x+ff_x[i])) $((origo_y+ff_y[i])) " "
1175 if (( origo_y + --ff_y[i] == 0 )); then
1176 ff_alive[i]=false
1177 (( --ff_count ))
1178 else
1179 if ffhit $i; then
1180 ff_alive[i]=false
1181 (( --ff_count ))
1182 else
1183 xyecho $((origo_x+ff_x[i])) $((origo_y+ff_y[i])) "${ff_symbol[i]}"
1189 done
1192 if ! $pause; then
1193 # autofire
1194 if $autofire && (( timer_autofire + timerd_autofire < time_now )); then
1195 timer_autofire=$time_now
1196 timer_manualfire=$time_now
1197 fire
1200 # move rocks?
1201 if ((dynel_hp[0] > 0)) && ((timer_rocks + timerd_rocks < time_now)); then
1202 timer_rocks=$time_now
1203 (( ++rock_total ))
1206 # deal with rocks
1207 if ((dynel_hp[0] > 0)); then
1208 first_dead=""
1209 for i in "${!dynel_alive[@]}"; do
1210 (( i >= rock_pos )) || continue
1211 ${dynel_alive[i]} || { first_dead=${first_dead:-$i}; continue; }
1213 # outside bottom of screen by entire height
1214 (( dynel_y[i] - dynel_h[i] > 0 ))
1215 then
1216 dynel_alive[i]=false
1217 (( --rock_count ))
1218 first_dead=${first_dead:-$i}
1219 (( score_current += score_deadrock ))
1220 elif
1221 # should be moved now
1222 (( timer_rocks == time_now )) &&
1223 (( rock_total % dynel_ydiv[i] == 0 ))
1224 then
1225 if ((dynel_hp[i] <= 0)); then
1226 if ! deathanimation $i; then
1227 dynel_alive[i]=false
1228 (( --rock_count ))
1229 first_dead=${first_dead:-$i}
1230 (( score_current += score_deadrock ))
1233 (( dynel_cy[i]++ ))
1234 dynel_redraw[i]=true
1235 runshipcollision=true
1237 done
1239 if ((rock_count < rock_count_max + rock_add)); then
1240 i=${first_dead:-${#dynel_alive[*]}}
1241 dynel_w[i]="ascii_rock$((i%5))_w"
1242 dynel_w[i]=${!dynel_w[i]}
1243 dynel_h[i]="ascii_rock$((i%5))_h"
1244 dynel_h[i]=${!dynel_h[i]}
1245 dynel_x[i]=$(( (RANDOM % (cols-2-dynel_w[i])) - ( (cols-2)/2 ) ))
1246 dynel_y[i]=$((-origo_y))
1247 dynel_cx[i]=${dynel_x[i]}
1248 dynel_cy[i]=${dynel_y[i]}
1249 restrict_xaxis $i
1250 if ! squarecollide $i; then
1251 dynel_ydiv[i]=$((RANDOM%3+1))
1252 for j in "${!dynel_alive[@]}"; do
1253 (( j >= rock_pos )) || continue
1255 ${dynel_alive[j]} &&
1256 ((dynel_ydiv[i] < dynel_ydiv[j] )) &&
1257 (( (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) ))
1258 then
1259 dynel_ydiv[i]=${dynel_ydiv[j]}
1261 done
1262 dynel_hp[i]=$rock_hp
1263 dynel_color[i]=$color_rock_healthy
1264 dynel_redraw[i]=false
1265 dynel_img[i]="ascii_rock$((i%5))"
1266 dynel_alive[i]=true
1267 (( ++rock_count ))
1272 # do ship collision
1273 if $runshipcollision && shipcollide; then
1274 (( dynel_hp[0]-- ))
1276 if ((dynel_hp[0] <= 0)) && ((timer_playerdeath + timerd_playerdeath < time_now)); then
1277 timer_playerdeath=$time_now
1278 dynel_redraw[0]=true
1279 if ! deathanimation 0; then
1280 state_current="dead"
1284 # regenerate ammo
1285 if (( timer_ammo + timerd_ammo + (timerd_rocks*3) < time_now )) && ((ff_ammo_current < ff_ammo_max)); then
1286 timer_ammo=$time_now
1287 (( ++ff_ammo_current ))
1289 fi # if ! $pause
1291 # draw ammo
1292 if $redraw; then
1293 xyecho 0 0 "${color_ammo_1}${ff_line:0:$ff_ammo_current}"
1294 xyecho ${ff_ammo_current} 0 "${color_ammo_2}${ff_line:$ff_ammo_current:$ff_ammo_max}"
1295 else
1296 if ((ff_ammo_current < ff_ammo_last)); then
1297 xyecho $ff_ammo_current 0 "${color_ammo_2}${ff_line:0:$((ff_ammo_last-ff_ammo_current))}"
1298 elif ((ff_ammo_current > ff_ammo_last)); then
1299 xyecho $ff_ammo_last 0 "${color_ammo_1}${ff_line:0:$((ff_ammo_current-ff_ammo_last))}"
1303 # score
1304 if $redraw || (( score_current != score_last )); then
1305 score_last=$score_current
1306 xyecho $((cols-${#score_current})) 0 "${color_score}${score_current}"
1309 # blink engines of player ship
1310 if ! $pause && ((dynel_hp[0] > 0)) && ! ${dynel_redraw[0]} && [[ "$blink_engines" != "$blink_fast" ]]; then
1311 blink_engines=$blink_fast
1312 $blink_fast && safeecho $color_engine_1 || safeecho $color_engine_2
1313 xyecho $((origo_x+dynel_cx[0]+3)) $((origo_y+dynel_cy[0]-2)) "@"
1314 xyecho $((origo_x+dynel_cx[0]-3)) $((origo_y+dynel_cy[0]-2)) "@"
1317 # draw/move dynels one step at a time to their current position
1318 lastalive=0
1319 for i in "${!dynel_alive[@]}"; do
1320 ${dynel_alive[i]} && lastalive=$i || continue
1321 $redraw || ${dynel_redraw[i]} || continue
1323 dynel_redraw[i]=false
1325 safeecho ${dynel_color[i]}
1326 while
1327 if (( dynel_x[i] < dynel_cx[i] )); then (( dynel_x[i]++ ))
1328 elif (( dynel_x[i] > dynel_cx[i] )); then (( dynel_x[i]-- )); fi
1329 if (( dynel_y[i] < dynel_cy[i] )); then (( dynel_y[i]++ ))
1330 elif (( dynel_y[i] > dynel_cy[i] )); then (( dynel_y[i]-- )); fi
1332 catd \
1333 $(( origo_x-(dynel_w[i]/2)+dynel_x[i] )) \
1334 $(( origo_y-dynel_h[i]+dynel_y[i] )) \
1335 <<<"${!dynel_img[i]}"
1337 # fake do-while condition
1338 (( dynel_x[i] != dynel_cx[i] )) ||
1339 (( dynel_y[i] != dynel_cy[i] ))
1340 do :; done
1341 done
1343 # draw pause anim
1344 if $pause_last; then # this uses pause_last to avoid drawing the pause blinker before the frame
1345 if $redraw; then
1346 safeecho ${color_pauseframe}
1347 catc $ascii_pauseframe_w $((messageheight + 4)) <<<"$ascii_pauseframe"
1349 if $redraw || [[ "$blink_pause" != "$blink_slow" ]]; then
1350 blink_pause=$blink_slow
1351 $blink_slow && safeecho $color_pause_1 || safeecho $color_pause_2
1352 catc $ascii_pause_w $((messageheight + 6)) <<<"$ascii_pause"
1355 # sleep to save cpu
1356 sleep $cpusavesleep
1359 # purge dead dynels
1360 size=${#dynel_alive[*]}
1361 (( ++lastalive ))
1362 for ((i=lastalive; i<size; i++)); do
1363 unset dynel_alive[i]
1364 done
1366 esac
1367 redraw=false
1368 ff_ammo_last=$ff_ammo_current
1369 seconds_last=$seconds
1370 done
1372 gamepid=$!
1374 # exit normally on several signals
1375 trap 'exit 0' TERM INT HUP
1377 # input loop
1378 stty -echo
1379 while true; do
1380 readc
1381 case "$c" in
1382 $'\e')
1383 readc
1384 [[ "$c" = '[' ]] || continue
1385 readc
1386 case "$c" in
1387 A|B|C|D)
1388 builtin echo "$c" >"$comm"
1389 kill -USR1 "$gamepid"
1391 esac
1393 ' '|p|q|z)
1394 builtin echo "$c" >"$comm"
1395 kill -USR1 "$gamepid"
1397 esac
1398 done
1400 # vim: tabstop=4:softtabstop=4:shiftwidth=4:noexpandtab