├── Dockerfile ├── README.md ├── shellshock └── shellshock.png /Dockerfile: -------------------------------------------------------------------------------- 1 | # define 2 | FROM alpine 3 | LABEL maintainer "bolt@dhampir.no" 4 | 5 | 6 | # required runtime 7 | RUN set -eux && \ 8 | apk add --no-cache bash ncurses coreutils 9 | 10 | 11 | # user 12 | RUN set -eux && \ 13 | adduser -D -u 57005 user 14 | 15 | 16 | # sync 17 | COPY --chown=0:0 shellshock /usr/local/bin/ 18 | 19 | 20 | # run! 21 | ENTRYPOINT ["/usr/local/bin/shellshock"] 22 | 23 | 24 | 25 | # vim: tabstop=4:softtabstop=4:shiftwidth=4:expandtab 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A spaceshooter in Bash 2 | Programming exercise that turned out to be rather decent, for what it is. 3 | -------------------------------------------------------------------------------- /shellshock: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 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. 5 | # 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. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see , 13 | # or in pdf format at 14 | 15 | # Copyright 2012 - Øyvind 'bolt' Hvidsten 16 | 17 | # Applied patches: 18 | # 19 | # v1.07, 2012.03.26 - Steve McMurphy 20 | # v1.11, 2014.09.30 - Øyvind A. Holm 21 | # 22 | 23 | # Description: 24 | # 25 | # ShellShock is a top-down space shooter written for Bash 3 / Bash 4 26 | # Tested on Linux (Debian, RedHat and CentOS) 27 | # 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). 32 | # 33 | # For updates, please see . 34 | # 35 | # Comments welcome, improvements/patches twice as welcome. 36 | # 37 | 38 | # Releases / Changelog: 39 | # 40 | # v1.00, 2012.03.18 - Initial v1.0 release 41 | # * All intended functionality implemented 42 | # 43 | # v1.01, 2012.03.19 - Bash 3 44 | # * Tweaks to run on Bash 3 45 | # 46 | # v1.05, 2012.03.25 - Several changes based on feedback 47 | # * Added score display 48 | # * Added increasing difficulty 49 | # 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 55 | # 56 | # v1.07, 2012.03.26 - OSX 57 | # * Added rudimentary support for Mac OS X 58 | # 59 | # v1.08, 2012.03.26 - Cleanup 60 | # * Improved some comments and made clearer error messages 61 | # 62 | # v1.09, 2012.03.27 - Ubuntu 63 | # * Various fixes for Ubuntu 64 | # 65 | # v1.10, 2012.03.29 - Pause 66 | # * Improved the pause function to avoid problems with timers 67 | # 68 | # v1.11, 2014.09.30 - Better cleanup 69 | # * Using "stty sane" to clean up terminal settings on exit 70 | # 71 | # v1.12, 2017.07.21 - Static code analysis 72 | # * Minor changes to appease the code analysis gods 73 | # 74 | # v1.13, 2017.09.05 - Fixes for strange terminals 75 | # * Fixed a bug with some strange terminal settings that prevented input 76 | # 77 | # v1.14, 2020.06.07 - Static code analysis 78 | # * Minor changes to appease the (updated) code analysis gods 79 | # 80 | 81 | # get uname 82 | uname=$(uname -s) 83 | 84 | # queue temp files for deletion on script exit 85 | function rm_queue { _rm_queue[${#_rm_queue[*]}]="$1"; } 86 | function rm_process { local file; for file in "${_rm_queue[@]}"; do rm "$file"; done; } 87 | 88 | # check if tput actually outputs something useful for this terminal 89 | if [[ -z "$(tput sgr0)" ]] || [[ -z "$(tput bold)" ]]; then 90 | echo 'Error: tput is not working as expected with your current terminal settings!' >&2 91 | echo 'Try setting your $TERM to something more standard?' >&2 92 | exit 1 93 | fi 94 | 95 | # read a single character 96 | function readc { IFS= read -r -n1 -s c; } 97 | 98 | # variable variables :) 99 | gamepid="" 100 | saved_term=false 101 | 102 | # list of files to be removed on cleanup 103 | _rm_queue=() 104 | 105 | # cleanup 106 | function cleanup 107 | { 108 | [[ -z "$gamepid" ]] || { kill -TERM "$gamepid"; wait "$gamepid"; } 2>/dev/null 109 | rm_process # remove temp files 110 | tput sgr0 # reset color 111 | clear # clear the screen 112 | tput cnorm # show the cursor 113 | stty echo # show input 114 | stty sane # reset terminal 115 | reset 2>/dev/null # reset some more 116 | ! $saved_term || tput rmcup # restore the terminal view 117 | } 118 | trap cleanup EXIT 119 | 120 | # make comm file 121 | # TODO: better solution for inter-thread communication. must work on bash3 122 | case "$uname" in 123 | Darwin) 124 | comm=$(mktemp /tmp/shellshock.XXXXXX) 125 | ;; 126 | *) 127 | comm=$(mktemp) 128 | ;; 129 | esac 130 | if [[ -n "$comm" ]]; then 131 | rm_queue "$comm" 132 | else 133 | echo "Error: Communications file creation failed!" >&2 134 | exit 1 135 | fi 136 | 137 | # init 138 | tput smcup && saved_term=true # save the current terminal view 139 | tput civis # hide the cursor 140 | 141 | # game subshell 142 | ( 143 | input=false 144 | trap 'input=true' USR1 145 | trap 'exit 0' TERM INT HUP 146 | 147 | # test parent shell 148 | function testparent 149 | { 150 | kill -0 $$ 2>/dev/null || exit 1 151 | } 152 | 153 | # how to print stuff 154 | function xyecho 155 | { 156 | local x=$1 y=$2 157 | shift 2 158 | 159 | # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages 160 | while { ! builtin echo -n "${posarray[$((y*cols+x))]}$*"; } 2>/dev/null; do testparent; done 161 | } 162 | function safeecho 163 | { 164 | # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages 165 | while { ! builtin echo -n "${posarray[$((y*cols+x))]}$*"; } 2>/dev/null; do testparent; done 166 | } 167 | function xyprintf 168 | { 169 | local x=$1 y=$2 fmt=$3 170 | shift 3 171 | 172 | # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages 173 | while { ! builtin printf "%s$fmt" "${posarray[$((y*cols+x))]}" "$@"; } 2>/dev/null; do testparent; done 174 | } 175 | function safeprintf 176 | { 177 | local fmt=$1 178 | shift 1 179 | 180 | # running this in a loop with 2>/dev/null to avoid "interrupted system call" messages 181 | while { ! builtin printf "%s$fmt" "${posarray[$((y*cols+x))]}" "$@"; } 2>/dev/null; do testparent; done 182 | } 183 | 184 | # called when the player fires his/her weapons 185 | function fire # no parameters 186 | { 187 | (( ff_ammo_current > 0 )) || return 188 | 189 | if ${ff_alive[ff_next]}; then 190 | xyprintf $((origo_x+ff_x[ff_next])) $((origo_y+ff_y[ff_next])) " " 191 | fi 192 | 193 | if ((++ff_total%2 == 0)); then 194 | ff_x[ff_next]=$((dynel_cx[0]-3)) 195 | ff_y[ff_next]=$((dynel_cy[0]-8)) 196 | else 197 | ff_x[ff_next]=$((dynel_cx[0]+3)) 198 | ff_y[ff_next]=$((dynel_cy[0]-8)) 199 | fi 200 | 201 | if ! outofbounds "$((origo_x+ff_x[ff_next]))" "$((origo_y+ff_y[ff_next]))"; then 202 | ff_ydiv[ff_next]=1 203 | ff_symbol[ff_next]="|" 204 | ff_damage[ff_next]=1 205 | ff_alive[ff_next]=true 206 | (( ++ff_count )) 207 | ff_new[ff_next]=true 208 | (( --ff_ammo_current )) 209 | ff_next=$(( ++ff_next < ff_count_max ? ff_next : 0 )) 210 | fi 211 | 212 | if ((ff_total%3 == 0)); then 213 | if ${ff_alive[ff_next]}; then 214 | xyprintf $((origo_x+ff_x[ff_next])) $((origo_y+ff_y[ff_next])) " " 215 | fi 216 | 217 | if ((ff_total%2 == 0)); then 218 | ff_x[ff_next]=$((dynel_cx[0]+5)) 219 | ff_y[ff_next]=$((dynel_cy[0]-7)) 220 | else 221 | ff_x[ff_next]=$((dynel_cx[0]-5)) 222 | ff_y[ff_next]=$((dynel_cy[0]-7)) 223 | fi 224 | 225 | if ! outofbounds "$((origo_x+ff_x[ff_next]))" "$((origo_y+ff_y[ff_next]))"; then 226 | ff_ydiv[ff_next]=2 227 | ff_symbol[ff_next]="¤" 228 | ff_damage[ff_next]=4 229 | ff_alive[ff_next]=true 230 | (( ++ff_count )) 231 | ff_new[ff_next]=true 232 | ff_next=$(( ++ff_next < ff_count_max ? ff_next : 0 )) 233 | fi 234 | fi 235 | } 236 | 237 | ####################################################### 238 | ## BEGIN ASCII ART SECTION - NO TABS, NO INDENTATION ## 239 | ####################################################### 240 | # title 241 | ascii_title_w=78 242 | ascii_title_h=6 243 | ascii_title=$( 244 | cat - <<"EOF" 245 | _________.__ .__ .__ _________.__ __ ._. 246 | / _____/| |__ ____ | | | | / _____/| |__ ____ ____ | | _| | 247 | \_____ \ | | \_/ __ \| | | | \_____ \ | | \ / _ \_/ ___\| |/ / | 248 | / \| Y \ ___/| |_| |__/ \| Y ( <_> ) \___| < \| 249 | /_______ /|___| /\___ >____/____/_______ /|___| /\____/ \___ >__|_ \__ 250 | \/ \/ \/ \/ \/ \/ \/\/ 251 | EOF 252 | ) 253 | # keybindings 254 | ascii_keybindings_w=74 #21 - fake width to print off-center 255 | ascii_keybindings_h=5 256 | ascii_keybindings=$( 257 | cat - <<"EOF" 258 | Keybindings: 259 | arrow keys - move 260 | SPACE - fire 261 | z - toggle autofire 262 | p - pause 263 | q - quit 264 | EOF 265 | ) 266 | # dead meat - written in quotes because all the parenthesis confuse vim's syntax highlighting 267 | # - this also means the first line has to be offset, so the ascii looks ugly in code 268 | ascii_dead_w=36 269 | ascii_dead_h=8 270 | ascii_dead=\ 271 | ' ______ _______ _______ ______ 272 | ( __ \ ( ____ \( ___ )( __ \ 273 | | ( \ )| ( \/| ( ) || ( \ ) 274 | | | ) || (__ | (___) || | ) | 275 | | | | || __) | ___ || | | | 276 | | | ) || ( | ( ) || | ) | 277 | | (__/ )| (____/\| ) ( || (__/ ) 278 | (______/ (_______/|/ \|(______/' 279 | # press q 280 | ascii_press_q_w=42 281 | ascii_press_q_h=1 282 | ascii_press_q=$( 283 | cat - <<"EOF" 284 | -= Press q to quit to the title screen! =- 285 | EOF 286 | ) 287 | # press fire 288 | ascii_press_fire_w=27 289 | ascii_press_fire_h=1 290 | ascii_press_fire=$( 291 | cat - <<"EOF" 292 | -= Press SPACE to start! =- 293 | EOF 294 | ) 295 | # pause 296 | ascii_pause_w=11 297 | ascii_pause_h=1 298 | ascii_pause=$( 299 | cat - <<"EOF" 300 | -= PAUSE =- 301 | EOF 302 | ) 303 | # pause frame 304 | ascii_pauseframe_w=25 305 | ascii_pauseframe_h=5 306 | ascii_pauseframe=$( 307 | cat - <<"EOF" 308 | ************************* 309 | * * 310 | * * 311 | * * 312 | ************************* 313 | EOF 314 | ) 315 | 316 | # player's spaceship 317 | ascii_playerdynel_w=21 318 | ascii_playerdynel_h=9 319 | ascii_playership=$( 320 | cat - <<"EOF" 321 | 08] 322 | 06] /^\ 323 | 03] |/.!.\| 324 | 01] _|/_/]=[\_\|_ 325 | 00] _/ | | \_ 326 | 00] |_____. | | ._____| 327 | 00] \_________/ 328 | 04] |@| |@| 329 | 05] 330 | EOF 331 | ) 332 | # rock 1 333 | ascii_rock0_w=9 334 | ascii_rock0_h=7 335 | ascii_rock0=$( 336 | cat - <<"EOF" 337 | 02] 338 | 01] __ 339 | 01] / \_ 340 | 00] | \ 341 | 01] \ _| 342 | 01] \_/ 343 | 02] 344 | EOF 345 | ) 346 | ascii_rock1_w=14 347 | ascii_rock1_h=8 348 | ascii_rock1=$( 349 | cat - <<"EOF" 350 | 06] 351 | 01] __ 352 | 00] ____/ \_ 353 | 00] / \ 354 | 00] \ | 355 | 00] \ ___/ 356 | 01] \___/ 357 | 02] 358 | EOF 359 | ) 360 | ascii_rock2_w=8 361 | ascii_rock2_h=8 362 | ascii_rock2=$( 363 | cat - <<"EOF" 364 | 02] 365 | 01] __ 366 | 00] / \ 367 | 00] | | 368 | 00] | \ 369 | 00] | _/ 370 | 00] \__/ 371 | 00] 372 | EOF 373 | ) 374 | ascii_rock3_w=9 375 | ascii_rock3_h=7 376 | ascii_rock3=$( 377 | cat - <<"EOF" 378 | 01] 379 | 00] _____ 380 | 00] / \ 381 | 00] | | 382 | 00] \ _/ 383 | 00] \_/ 384 | 01] 385 | EOF 386 | ) 387 | ascii_rock4_w=13 388 | ascii_rock4_h=8 389 | ascii_rock4=$( 390 | cat - <<"EOF" 391 | 01] 392 | 00] ___ 393 | 00] / \_____ 394 | 00] | _| 395 | 00] \ / 396 | 00] \_ / 397 | 01] \__/ 398 | 03] 399 | EOF 400 | ) 401 | ####################################################### 402 | ## END ASCII ART SECTION ## 403 | ####################################################### 404 | 405 | # pretty colours 406 | color_background=$(tput setab 0) 407 | color_reset="$(tput sgr0)${color_background}" 408 | color_black="${color_reset}$(tput setaf 0)" 409 | color_red="${color_reset}$(tput setaf 1)" 410 | color_green="${color_reset}$(tput setaf 2)" 411 | color_orange="${color_reset}$(tput setaf 3)" 412 | color_blue="${color_reset}$(tput setaf 4)" 413 | color_magenta="${color_reset}$(tput setaf 5)" 414 | color_cyan="${color_reset}$(tput setaf 6)" 415 | color_light_gray="${color_reset}$(tput setaf 7)" 416 | color_dark_gray="${color_reset}$(tput bold)$(tput setaf 0)" 417 | color_light_red="${color_reset}$(tput bold)$(tput setaf 1)" 418 | color_light_green="${color_reset}$(tput bold)$(tput setaf 2)" 419 | color_yellow="${color_reset}$(tput bold)$(tput setaf 3)" 420 | color_light_blue="${color_reset}$(tput bold)$(tput setaf 4)" 421 | color_light_magenta="${color_reset}$(tput bold)$(tput setaf 5)" 422 | color_light_cyan="${color_reset}$(tput bold)$(tput setaf 6)" 423 | color_white="${color_reset}$(tput bold)$(tput setaf 7)" 424 | 425 | # specific colors for stuff 426 | color_debug=$color_orange # debug prints (FPS, seconds, rocks count, etc) 427 | color_ship=$color_white # player's ship 428 | color_fire=$color_light_magenta # player's missiles 429 | color_engine_1=$color_light_red # player's engines (blinking) 430 | color_engine_2=$color_red # player's engines (blinking) 431 | color_border=$color_red # border 432 | color_score_result=$color_yellow # score result (death screen) 433 | color_origo=$color_green # origo (bottom center) 434 | color_rock_healthy=$color_light_cyan # healthy rocks 435 | color_rock_damaged=$color_cyan # damaged rocks 436 | color_death_1=$color_light_red # death animation stage 1 437 | color_death_2=$color_red # death animation stage 2 438 | color_death_3=$color_dark_gray # death animation stage 3 439 | color_death_4=$color_black # death animation stage 4 (erase) 440 | color_title=$color_yellow # ShellShock! 441 | color_pause_1=$color_light_red # pause text (blinking) 442 | color_pause_2=$color_red # pause text (blinking) 443 | color_pauseframe=$color_red # frame around pause text 444 | color_youaredead=$color_light_red # YOU ARE DEAD 445 | color_keybindings=$color_green # keybindings... 446 | color_pressfire_1=$color_blue # press space to start (blinking) 447 | color_pressfire_2=$color_light_blue # press space to start (blinking) 448 | color_pressq_1=$color_dark_gray # press q for title screen (blinking) 449 | color_pressq_2=$color_light_gray # press q for title screen (blinking) 450 | 451 | # score display (top right) (drawn on white background) 452 | color_score="${color_black}$(tput setab 7)" 453 | # ammo display (drawn with spaces on background color in the top left) 454 | color_ammo_1="${color_black}$(tput setab 5)" 455 | color_ammo_2="${color_black}$(tput setab 7)" 456 | 457 | # home 458 | home=$(tput home) 459 | 460 | # has the size changed? 461 | function sizechanged 462 | { 463 | if (( cols != $(tput cols) )) || (( rows != $(tput lines) )); then 464 | cols=$(tput cols) 465 | rows=$(tput lines) 466 | origo_x=$((cols/2)) 467 | origo_y=$((rows-1)) 468 | 469 | return 0 470 | fi 471 | return 1 472 | } 473 | 474 | # clear 475 | redraw=false 476 | function wipe 477 | { 478 | # "tput clear" doesn't fill with background color in bash3, screen, etc. 479 | # must write a shitload of spaces instead 480 | safeprintf "${color_reset}${home}%$((cols*rows))s" "" 481 | 482 | # everything needs redrawing after this 483 | redraw=true 484 | } 485 | 486 | # get the current time in microseconds 487 | case "$uname" in 488 | Darwin) 489 | # this is slow. very slow. 490 | function microtime { 491 | local time 492 | time=$(python <<<"import time; print \"%.6f\" % time.time();") 493 | echo -n "${time/./}" 494 | } 495 | ;; 496 | *) 497 | function microtime { 498 | local time 499 | time=$(date +%s%N) 500 | echo -n "${time:0:((${#time}-3))}" 501 | } 502 | ;; 503 | esac 504 | 505 | # build tput position array 506 | # moving using this 10x faster than running tput 507 | _pos_cols=0 508 | _pos_rows=0 509 | function buildposarray 510 | { 511 | if ((cols == _pos_cols)) && ((rows == _pos_rows)); then 512 | return 1 513 | fi 514 | 515 | wipe 516 | _pos_cols=$cols 517 | _pos_rows=$rows 518 | posarray=() 519 | 520 | local q e pos string x y 521 | 522 | q=false 523 | e=$(echo -e '\e') 524 | if [[ "$(tput cup 0 0)" = "${e}[1;1H" ]]; then 525 | # standard terminal movement commands - quick generation 526 | q=true 527 | fi 528 | 529 | string="Building position array for ${cols}x${rows}... " 530 | pos=$(tput cup 0 ${#string}) 531 | 532 | safeecho "${color_debug}${home}${string}" 533 | for ((x=0; x < cols; x++)); do 534 | if sizechanged; then 535 | buildposarray 536 | return $? 537 | fi 538 | echo -n "${pos}$((x*100/cols))%" 539 | for ((y=0; y < rows; y++)); do 540 | if $q; then 541 | posarray[$((y*cols+x))]="${e}[$((y+1));$((x+1))H" 542 | else 543 | posarray[$((y*cols+x))]=$(tput cup "$y" "$x") 544 | fi 545 | done 546 | done 547 | 548 | return 0 549 | } 550 | 551 | # print something at a spot 552 | function catc { cat "$@"; } # draw centered (x coordinate specifies width, not x pos) 553 | function catd { cat "$@"; } # draw a dynel (supports black outline) 554 | function cat 555 | { 556 | local x=$1 y=$2 i=0 cy dynel=false first=true offset=0 557 | 558 | case "${FUNCNAME[1]}" in 559 | catd) dynel=true ;; 560 | catc) 561 | x=$(( (cols - x) / 2)) # center 562 | (( x > 0 )) || x=1 563 | ;; 564 | esac 565 | 566 | while IFS= read -r line; do 567 | cy=$(( y + i++ )) 568 | if $dynel; then 569 | offset="$(( 10#${line:0:2} ))" 570 | line=${line:3+offset} 571 | cx=$(( x + offset )) 572 | else 573 | cx=$x 574 | fi 575 | (( cx < cols-1 )) || continue # don't write on or outside the right border 576 | (( cy > 0 )) || continue # don't write on or above the top border 577 | (( cy < rows-1 )) || break # don't write on or below the bottom border 578 | (( cx >= 1 )) || { line=${line:1-cx}; cx=1; } # cut to fit inside left border 579 | line=${line:0:cols-1-cx} # cut to fit inside right border 580 | 581 | xyecho $cx $cy "$line" 582 | done 583 | } 584 | 585 | # border drawing 586 | function border 587 | { 588 | safeecho "$color_border" 589 | 590 | # no printf -v on bash3 :( 591 | local line 592 | while { ! line=$(builtin printf "%${cols}s" ""); } 2>/dev/null; do :; done 593 | line=${line// /#} 594 | 595 | xyecho 0 0 "$line" 596 | 597 | local y 598 | for (( y=1; y= cols-1 )) || 612 | (( y < 1 )) || (( y >= rows-1 )) 613 | then 614 | return 0 615 | fi 616 | return 1 617 | } 618 | 619 | # pushes a dynel until it's within the right and left borders 620 | function restrict_xaxis # $1 - dynel_* index 621 | { 622 | local i=$1 623 | dynel_x[i]=${dynel_cx[i]} 624 | while ! canmoveright "$i"; do (( dynel_cx[i]-- )); done; 625 | while ! canmoveleft "$i"; do (( dynel_cx[i]++ )); done; 626 | (( dynel_cx[i] < dynel_x[i] )) && (( dynel_cx[i]++ )) 627 | (( dynel_cx[i] > dynel_x[i] )) && (( dynel_cx[i]-- )) 628 | dynel_x[i]=${dynel_cx[i]} 629 | } 630 | 631 | # pushes a dynel until it's within the top and bottom borders 632 | function restrict_yaxis # $1 - dynel_* index 633 | { 634 | local i=$1 635 | dynel_y[i]=${dynel_cy[i]} 636 | while ! canmoveup "$i"; do (( dynel_cy[i]++ )); done; 637 | while ! canmovedown "$i"; do (( dynel_cy[i]-- )); done; 638 | (( dynel_cy[i] < dynel_y[i] )) && (( dynel_cy[i]++ )) 639 | (( dynel_cy[i] > dynel_y[i] )) && (( dynel_cy[i]-- )) 640 | dynel_y[i]=${dynel_cy[i]} 641 | } 642 | 643 | # collides dynels based on simple squares (width, height) 644 | function squarecollide # $1 - dynel_* index 645 | { 646 | local i=$1 647 | for j in "${!dynel_alive[@]}"; do 648 | ${dynel_alive[j]} || continue # don't check dead dynels 649 | (( j != i )) || continue # don't check yourself 650 | (( j >= rock_pos )) || continue # don't check the playership 651 | local distance_x=$((dynel_cx[i] > dynel_cx[j] ? dynel_cx[i]-dynel_cx[j] : dynel_cx[j]-dynel_cx[i])) 652 | local distance_y=$((dynel_cy[j] - dynel_cy[i])) 653 | if 654 | (( distance_x < (dynel_w[i]+dynel_w[j])/2 )) && 655 | { 656 | if (( distance_y < 0 )); then 657 | # j (compare dynel) is above i 658 | (( -distance_y < dynel_h[j] )) 659 | else 660 | # j (compare dynel) is below i 661 | (( distance_y < dynel_h[j] )) 662 | fi 663 | } 664 | then 665 | # collision! 666 | return 0 667 | fi 668 | done 669 | 670 | # no collision 671 | return 1 672 | } 673 | 674 | # collides the player's ship 675 | # basically the same as square collision, but with some tweaks to make it feel better 676 | function shipcollide # no parameters 677 | { 678 | local i=0 679 | for j in "${!dynel_alive[@]}"; do 680 | ${dynel_alive[j]} || continue # don't check dead dynels 681 | (( j != i )) || continue # don't check yourself 682 | local distance_x=$((dynel_cx[i] > dynel_cx[j] ? dynel_cx[i]-dynel_cx[j] : dynel_cx[j]-dynel_cx[i])) 683 | local distance_y=$((dynel_cy[j] - dynel_cy[i])) 684 | if 685 | (( distance_x + 2 < (dynel_w[i]+dynel_w[j])/2 )) && # make the ship a little narrower 686 | { 687 | if (( distance_y < 0 )); then 688 | # j (compare dynel) is above i 689 | (( -distance_y < dynel_h[j] )) && 690 | (( (distance_y + dynel_h[i]) > distance_x - 4 )) # make a somewhat cone-shaped ship 691 | else 692 | # j (compare dynel) is below i 693 | (( distance_y + 2 < dynel_h[j] )) && # make the ship a little shorter 694 | { 695 | ((dynel_h[j] - distance_y - 2 > 2)) || # 2 lines into ship from bottom 696 | ((4 + (dynel_h[j] - distance_y - 2) > distance_x)) # engine hit 697 | } 698 | fi 699 | } 700 | then 701 | # collision! 702 | return 0 703 | fi 704 | done 705 | 706 | # no collision 707 | return 1 708 | } 709 | 710 | # runs hit tests on friendly fire and damages any rocks encountered 711 | function ffhit # $1 - ff_* index 712 | { 713 | local x=${ff_x[$1]} y=${ff_y[$1]} i j 714 | for i in "${!dynel_alive[@]}"; do 715 | (( i >= rock_pos )) || continue # only check rocks 716 | ${dynel_alive[i]} || continue # don't check dead dynels 717 | if 718 | ((y > dynel_y[i]-2)) || # haven't reached rock yet - miss 719 | ((y < dynel_y[i]-dynel_h[i])) || # behind the rock - miss 720 | (( (x > dynel_x[i] ? x-dynel_x[i] : dynel_x[i]-x) > dynel_w[i]/2 )) # simple square collision 721 | then 722 | continue 723 | else 724 | ((dynel_hp[i] -= ff_damage[$1])) 725 | if ((dynel_hp[i] > 0)) && ((dynel_hp[i] < rock_hp/2)); then 726 | dynel_color[i]=$color_rock_damaged 727 | dynel_redraw[i]=true 728 | fi 729 | (( score_current += score_rockshot * ff_damage[$1] )) 730 | # it's a hit! 731 | return 0 732 | fi 733 | done 734 | 735 | # missed 736 | return 1 737 | } 738 | 739 | # changes the color of a dynel several times until it's finally drawn with black to disappear 740 | function deathanimation # $1 - dynel_* index 741 | { 742 | local i=$1 743 | # it helps to read this backwards :) 744 | case "${dynel_color[i]}" in 745 | "$color_death_4") return 1 ;; 746 | "$color_death_3") dynel_color[i]=$color_death_4 ;; 747 | "$color_death_2") dynel_color[i]=$color_death_3 ;; 748 | "$color_death_1") dynel_color[i]=$color_death_2 ;; 749 | *) dynel_color[i]=$color_death_1 ;; 750 | esac 751 | return 0 752 | } 753 | 754 | # limit the amount of rocks 755 | function limitrocks 756 | { 757 | rock_count_max=$(((rows*cols) / 720)) 758 | } 759 | 760 | # update the amount of score you get for stuff 761 | function updatescore 762 | { 763 | score_rockshot=10 # score per damage point that hits a rock 764 | score_deadrock=$((500000 / (rows*cols))) # score per dead rock (off screen or shot to pieces) 765 | case "$state_current" in 766 | ingame) ;; 767 | title) 768 | score_current=0 # current score 769 | score_last=-1 # last drawn sore 770 | score_second=0 # score per second passed 771 | ;; 772 | esac 773 | } 774 | 775 | # movable? # $1 - dynel_* index 776 | function canmoveup { (( origo_y+dynel_cy[$1]-dynel_h[$1] > 0 )); } 777 | function canmovedown { (( origo_y+dynel_cy[$1] < rows )); } 778 | function canmoveright { (( origo_x+dynel_cx[$1]+(dynel_w[$1]/2)+1 < cols )); } 779 | function canmoveleft { (( origo_x+dynel_cx[$1]-(dynel_w[$1]/2) > 0 )); } 780 | 781 | # tput position array 782 | posarray=() # position array for faster cursor movement 783 | sizechanged # run console size check (will always have changed) 784 | 785 | # pause and unpause 786 | function registerpausetimer 787 | { 788 | pausetimers[${#pausetimers[*]}]="$1" 789 | } 790 | function pause 791 | { 792 | if ! $pause; then 793 | local timer value 794 | for timer in "${pausetimers[@]}"; do 795 | value=${!timer} 796 | if (( value != 0 )); then 797 | (( value -= time_now )) 798 | else 799 | value="zero" 800 | fi 801 | IFS= read -r "${timer?}" <<< "$value" 802 | done 803 | pause=true 804 | fi 805 | } 806 | function unpause 807 | { 808 | if $pause; then 809 | local timer value 810 | for timer in "${pausetimers[@]}"; do 811 | value=${!timer} 812 | if [[ "$value" != "zero" ]]; then 813 | (( value += time_now )) 814 | else 815 | value=0 816 | fi 817 | IFS= read -r "${timer?}" <<< "$value" 818 | done 819 | pause=false 820 | fi 821 | } 822 | 823 | # ammo line - no printf -v on bash3 :( 824 | ff_ammo_max=30 # maximum ammunition 825 | while { ! ff_line=$(builtin printf "%${ff_ammo_max}s" ""); } 2>/dev/null; do :; done 826 | 827 | # init 828 | time_start=$(microtime) # time the game was started 829 | time_last=$time_start # time of last game loop 830 | time_now=$time_start # current time 831 | timer_resize=0 # resize check timer 832 | timerd_resize=1000000 # resize check timer delta 833 | state_current="title" # current game state 834 | state_last="" # game state last loop 835 | movespeed_x=7 # how fast the player ship moves horizontally 836 | movespeed_y=3 # how fast the player ship moves vertically 837 | blink_pressfire="" # blink status for the "press fire" text on title screen 838 | blink_pressq="" # blink status for the "press q" text on death screen 839 | blink_engines="" # blink status for the ship's engines 840 | blink_pause="" # blink status for the pause message 841 | messageheight=4 # how far away from the top we print the title and such 842 | cpusavesleep=0.2 # time to sleep if saving cpu (dead, paused, menu) 843 | redraw=true # should we redraw everything? (size probably changed) 844 | 845 | # reset the game 846 | function resetgame 847 | { 848 | # dynels 849 | dynel_img=( "ascii_playership" ) # drawing 850 | dynel_x=( 0 ) # current screen position (last drawn) 851 | dynel_y=( 1 ) # current screen position (last drawn) 852 | dynel_cx=( "${dynel_x[0]}" ) # actual position 853 | dynel_cy=( "${dynel_y[0]}" ) # actual position 854 | dynel_ydiv=( 0 ) # automatic movement (for non-player dynels) 855 | dynel_w=( "$ascii_playerdynel_w" ) # width 856 | dynel_h=( "$ascii_playerdynel_h" ) # height 857 | dynel_hp=( 1 ) # health 858 | dynel_color=( "$color_ship" ) # color 859 | dynel_redraw=( true ) # needs redrawing or not 860 | dynel_alive=( true ) # dynel exists 861 | 862 | # rocks 863 | rock_pos=${#dynel_alive[*]} # rock position in dynel array 864 | rock_count=0 # current number of live rocks 865 | rock_hp=12 # rock health 866 | rock_total=0 # total number of rocks spawned 867 | rock_add=0 # additional rocks for difficulty 868 | limitrocks # set the max rock count 869 | 870 | # friendly fire 871 | ff_x=() # screen position 872 | ff_y=() # screen position 873 | ff_ydiv=() # speed divisor 874 | ff_new=() # when new, don't move, only draw 875 | ff_symbol=() # symbol to draw 876 | ff_damage=() # how much damage this shot does 877 | ff_count=0 # current number of live shots 878 | ff_count_max=64 # max shot count at any given time 879 | ff_total=0 # total number of shots fired 880 | ff_next=0 # next shot 881 | ff_alive=() # shot exists 882 | ff_ammo_current=$((ff_ammo_max/2)) # current ammo 883 | ff_ammo_last=0 # last drawn ammo 884 | counter_fire=0 # number of shots fired 885 | for (( i=0; i seconds_last )); then 987 | fps=$framecounter 988 | framecounter=0 989 | case "$state_current" in 990 | ingame|dead) 991 | xyprintf $((cols-2-9)) 2 "Dynl: %3d" "${#dynel_alive[*]}" 992 | xyprintf $((cols-2-9)) 3 "Rock: %3d" "$rock_count" 993 | xyprintf $((cols-2-9)) 4 "Shot: %3d" "$ff_count" 994 | xyprintf $((cols-2-6)) 5 "%6d" "$timerd_rocks" 995 | # xyecho $((cols-2-${#seconds})) 6 "$seconds" 996 | ;; 997 | esac 998 | fi 999 | xyprintf $((cols-10)) 1 "FPS: %3d" "$fps" 1000 | 1001 | # read input 1002 | if $input; then 1003 | readc <"$comm" 1004 | 1005 | case "$state_current" in 1006 | ingame) 1007 | case "$c" in 1008 | A|B|C|D) 1009 | if ! $pause && ((dynel_hp[0] > 0)); then 1010 | case "$c" in 1011 | A) # up 1012 | for (( i=0; i 0)) && ! $autofire && (( timer_manualfire + timerd_manualfire < time_now )); then 1036 | timer_manualfire=$time_now 1037 | fire 1038 | fi 1039 | ;; 1040 | 'p') 1041 | if ! $pause; then 1042 | pause 1043 | else 1044 | unpause 1045 | fi 1046 | ;; 1047 | 'z') 1048 | if ! $pause; then 1049 | if ((dynel_hp[0] > 0)); then 1050 | $autofire && autofire=false || autofire=true 1051 | fi 1052 | fi 1053 | ;; 1054 | 'q') 1055 | if ! $pause; then 1056 | state_current="title" 1057 | else 1058 | unpause 1059 | fi 1060 | ;; 1061 | esac 1062 | ;; 1063 | title) 1064 | case "$c" in 1065 | ' ') 1066 | resetgame 1067 | updatescore 1068 | timerd_rocks=100000 1069 | autofire=false 1070 | state_current="ingame" 1071 | ;; 1072 | 'q') kill -TERM $$; exit 0; ;; 1073 | esac 1074 | ;; 1075 | dead) 1076 | case "$c" in 1077 | 'q') resetgame; state_current="title" ;; 1078 | esac 1079 | ;; 1080 | esac 1081 | 1082 | input=false 1083 | fi 1084 | 1085 | # move and draw 1086 | case "$state_current" in 1087 | title) 1088 | if $redraw; then 1089 | safeecho "$color_title" 1090 | catc $ascii_title_w $messageheight <<<"${ascii_title}" 1091 | fi 1092 | if $redraw; then 1093 | safeecho "$color_keybindings" 1094 | catc $ascii_keybindings_w $((messageheight + ascii_title_h + 3)) <<<"${ascii_keybindings}" 1095 | fi 1096 | if $redraw || [[ "$blink_pressfire" != "$blink_medium" ]]; then 1097 | blink_pressfire=$blink_medium 1098 | $blink_medium && safeecho "$color_pressfire_1" || safeecho "$color_pressfire_2" 1099 | catc $ascii_press_fire_w $((messageheight + ascii_title_h + 1)) <<<"$ascii_press_fire" 1100 | fi 1101 | 1102 | # sleep to save cpu 1103 | sleep $cpusavesleep 1104 | ;; 1105 | dead) 1106 | if $redraw; then 1107 | safeecho "$color_youaredead" 1108 | catc 7 $messageheight <<<"YOU ARE" 1109 | catc $ascii_dead_w $((messageheight + 1)) <<<"${ascii_dead}" 1110 | fi 1111 | if $redraw; then 1112 | safeecho "$color_score_result" 1113 | catc $((19+${#score_current})) $((messageheight + ascii_dead_h + 2)) <<<"You scored $score_current points!" 1114 | fi 1115 | if $redraw; then 1116 | # reading and writing this high score is sensitive to signal interruption and is somewhat error prone 1117 | highscore=$(command cat "${HOME}/.shellshock" 2>/dev/null) 1118 | if ((score_current > highscore)); then 1119 | echo -n "$score_current" >"${HOME}/.shellshock" 1120 | fi 1121 | safeecho "$color_score_result" 1122 | if ((score_current >= highscore)); then 1123 | catc 15 $((messageheight + ascii_dead_h + 3)) <<<"NEW HIGH SCORE!" 1124 | else 1125 | catc $((12+${#highscore})) $((messageheight + ascii_dead_h + 3)) <<<"High Score: $highscore" 1126 | fi 1127 | fi 1128 | if $redraw || [[ "$blink_pressq" != "$blink_slow" ]]; then 1129 | blink_pressq=$blink_slow 1130 | $blink_slow && safeecho "$color_pressq_1" || safeecho "$color_pressq_2" 1131 | catc $ascii_press_q_w $((messageheight + ascii_dead_h + 5)) <<<"$ascii_press_q" 1132 | fi 1133 | 1134 | # sleep to save cpu 1135 | sleep $cpusavesleep 1136 | ;; 1137 | ingame) 1138 | if ! $pause; then 1139 | # need to run ship collision? 1140 | runshipcollision=false 1141 | 1142 | # speed up and add score every second 1143 | if (( seconds > seconds_last )); then 1144 | timerd_rocks=$(( timerd_rocks > 250 ? timerd_rocks - 250 : 0)) 1145 | (( score_current += ++score_second )) 1146 | rock_add=$((rock_count_max * score_current / 200000)) 1147 | fi 1148 | fi 1149 | 1150 | # move and impact friendly fire 1151 | if 1152 | $redraw || 1153 | { 1154 | ! $pause && 1155 | (( timer_fire + timerd_fire < time_now )) 1156 | } 1157 | then 1158 | if ! $pause; then 1159 | timer_fire=$time_now 1160 | (( counter_fire++ )) 1161 | fi 1162 | safeecho "$color_fire" 1163 | for (( i=0; i 0)) && ((timer_rocks + timerd_rocks < time_now)); then 1212 | timer_rocks=$time_now 1213 | (( ++rock_total )) 1214 | fi 1215 | 1216 | # deal with rocks 1217 | if ((dynel_hp[0] > 0)); then 1218 | first_dead="" 1219 | for i in "${!dynel_alive[@]}"; do 1220 | (( i >= rock_pos )) || continue 1221 | ${dynel_alive[i]} || { first_dead=${first_dead:-$i}; continue; } 1222 | if 1223 | # outside bottom of screen by entire height 1224 | (( dynel_y[i] - dynel_h[i] > 0 )) 1225 | then 1226 | dynel_alive[i]=false 1227 | (( --rock_count )) 1228 | first_dead=${first_dead:-$i} 1229 | (( score_current += score_deadrock )) 1230 | elif 1231 | # should be moved now 1232 | (( timer_rocks == time_now )) && 1233 | (( rock_total % dynel_ydiv[i] == 0 )) 1234 | then 1235 | if ((dynel_hp[i] <= 0)); then 1236 | if ! deathanimation "$i"; then 1237 | dynel_alive[i]=false 1238 | (( --rock_count )) 1239 | first_dead=${first_dead:-$i} 1240 | (( score_current += score_deadrock )) 1241 | fi 1242 | fi 1243 | (( dynel_cy[i]++ )) 1244 | dynel_redraw[i]=true 1245 | runshipcollision=true 1246 | fi 1247 | done 1248 | 1249 | if ((rock_count < rock_count_max + rock_add)); then 1250 | i=${first_dead:-${#dynel_alive[*]}} 1251 | dynel_w[i]="ascii_rock$((i%5))_w" 1252 | dynel_w[i]=${!dynel_w[i]} 1253 | dynel_h[i]="ascii_rock$((i%5))_h" 1254 | dynel_h[i]=${!dynel_h[i]} 1255 | dynel_x[i]=$(( (RANDOM % (cols-2-dynel_w[i])) - ( (cols-2)/2 ) )) 1256 | dynel_y[i]=$((-origo_y)) 1257 | dynel_cx[i]=${dynel_x[i]} 1258 | dynel_cy[i]=${dynel_y[i]} 1259 | restrict_xaxis $i 1260 | if ! squarecollide $i; then 1261 | dynel_ydiv[i]=$((RANDOM%3+1)) 1262 | for j in "${!dynel_alive[@]}"; do 1263 | (( j >= rock_pos )) || continue 1264 | if 1265 | ${dynel_alive[j]} && 1266 | ((dynel_ydiv[i] < dynel_ydiv[j] )) && 1267 | (( (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) )) 1268 | then 1269 | dynel_ydiv[i]=${dynel_ydiv[j]} 1270 | fi 1271 | done 1272 | dynel_hp[i]=$rock_hp 1273 | dynel_color[i]=$color_rock_healthy 1274 | dynel_redraw[i]=false 1275 | dynel_img[i]="ascii_rock$((i%5))" 1276 | dynel_alive[i]=true 1277 | (( ++rock_count )) 1278 | fi 1279 | fi 1280 | fi 1281 | 1282 | # do ship collision 1283 | if $runshipcollision && shipcollide; then 1284 | (( dynel_hp[0]-- )) 1285 | fi 1286 | if ((dynel_hp[0] <= 0)) && ((timer_playerdeath + timerd_playerdeath < time_now)); then 1287 | timer_playerdeath=$time_now 1288 | dynel_redraw[0]=true 1289 | if ! deathanimation 0; then 1290 | state_current="dead" 1291 | fi 1292 | fi 1293 | 1294 | # regenerate ammo 1295 | if (( timer_ammo + timerd_ammo + (timerd_rocks*3) < time_now )) && ((ff_ammo_current < ff_ammo_max)); then 1296 | timer_ammo=$time_now 1297 | (( ++ff_ammo_current )) 1298 | fi 1299 | fi # if ! $pause 1300 | 1301 | # draw ammo 1302 | if $redraw; then 1303 | xyecho 0 0 "${color_ammo_1}${ff_line:0:$ff_ammo_current}" 1304 | xyecho ${ff_ammo_current} 0 "${color_ammo_2}${ff_line:$ff_ammo_current:$ff_ammo_max}" 1305 | else 1306 | if ((ff_ammo_current < ff_ammo_last)); then 1307 | xyecho $ff_ammo_current 0 "${color_ammo_2}${ff_line:0:$((ff_ammo_last-ff_ammo_current))}" 1308 | elif ((ff_ammo_current > ff_ammo_last)); then 1309 | xyecho $ff_ammo_last 0 "${color_ammo_1}${ff_line:0:$((ff_ammo_current-ff_ammo_last))}" 1310 | fi 1311 | fi 1312 | 1313 | # score 1314 | if $redraw || (( score_current != score_last )); then 1315 | score_last=$score_current 1316 | xyecho $((cols-${#score_current})) 0 "${color_score}${score_current}" 1317 | fi 1318 | 1319 | # blink engines of player ship 1320 | if ! $pause && ((dynel_hp[0] > 0)) && ! ${dynel_redraw[0]} && [[ "$blink_engines" != "$blink_fast" ]]; then 1321 | blink_engines=$blink_fast 1322 | $blink_fast && safeecho "$color_engine_1" || safeecho "$color_engine_2" 1323 | xyecho $((origo_x+dynel_cx[0]+3)) $((origo_y+dynel_cy[0]-2)) "@" 1324 | xyecho $((origo_x+dynel_cx[0]-3)) $((origo_y+dynel_cy[0]-2)) "@" 1325 | fi 1326 | 1327 | # draw/move dynels one step at a time to their current position 1328 | lastalive=0 1329 | for i in "${!dynel_alive[@]}"; do 1330 | ${dynel_alive[i]} && lastalive=$i || continue 1331 | $redraw || ${dynel_redraw[i]} || continue 1332 | 1333 | dynel_redraw[i]=false 1334 | 1335 | safeecho "${dynel_color[i]}" 1336 | while 1337 | if (( dynel_x[i] < dynel_cx[i] )); then (( dynel_x[i]++ )) 1338 | elif (( dynel_x[i] > dynel_cx[i] )); then (( dynel_x[i]-- )); fi 1339 | if (( dynel_y[i] < dynel_cy[i] )); then (( dynel_y[i]++ )) 1340 | elif (( dynel_y[i] > dynel_cy[i] )); then (( dynel_y[i]-- )); fi 1341 | 1342 | catd \ 1343 | $(( origo_x-(dynel_w[i]/2)+dynel_x[i] )) \ 1344 | $(( origo_y-dynel_h[i]+dynel_y[i] )) \ 1345 | <<<"${!dynel_img[i]}" 1346 | 1347 | # fake do-while condition 1348 | (( dynel_x[i] != dynel_cx[i] )) || 1349 | (( dynel_y[i] != dynel_cy[i] )) 1350 | do :; done 1351 | done 1352 | 1353 | # draw pause anim 1354 | if $pause_last; then # this uses pause_last to avoid drawing the pause blinker before the frame 1355 | if $redraw; then 1356 | safeecho "${color_pauseframe}" 1357 | catc $ascii_pauseframe_w $((messageheight + 4)) <<<"$ascii_pauseframe" 1358 | fi 1359 | if $redraw || [[ "$blink_pause" != "$blink_slow" ]]; then 1360 | blink_pause=$blink_slow 1361 | $blink_slow && safeecho "$color_pause_1" || safeecho "$color_pause_2" 1362 | catc $ascii_pause_w $((messageheight + 6)) <<<"$ascii_pause" 1363 | fi 1364 | 1365 | # sleep to save cpu 1366 | sleep $cpusavesleep 1367 | fi 1368 | 1369 | # purge dead dynels 1370 | size=${#dynel_alive[*]} 1371 | (( ++lastalive )) 1372 | for ((i=lastalive; i"$comm" 1399 | kill -USR1 "$gamepid" 1400 | ;; 1401 | esac 1402 | ;; 1403 | ' '|p|q|z) 1404 | builtin echo "$c" >"$comm" 1405 | kill -USR1 "$gamepid" 1406 | ;; 1407 | esac 1408 | done 1409 | 1410 | # vim: tabstop=4:softtabstop=4:shiftwidth=4:noexpandtab 1411 | -------------------------------------------------------------------------------- /shellshock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacAnimal/shellshock/6b072c66e4d98bb5a7ed1ebe245c485fde86c8c0/shellshock.png --------------------------------------------------------------------------------