#!/bin/bash
# snake - Snake game written in bash, to be used in terminal
# (C) 2026 F-Hauri.ch - http://www.f-hauri.ch
# Licensed under terms of GPL v3. www.gnu.org
# Version: 0.1.2 -- Last update: Tue Jun 30 15:26:36 2026
# Require bash version >= 5.0 !
# This script is mostly pure bash except some call to `tput`, `clear` and `stty`
#   at initialisation and end of game.
# Use strongly ANSI and control sequences from XTerm documentation:
#	https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
# Require debian: ncurses-bin, coreutils.
# Usage:  /path/to/snake.sh
#    Move: 
#	Up:	`w`, `i`, `u` or <Cursor UP>
#	Down:	`x`, `m`, `n`, `,` (comma)  or <Cursor DOWN>
#	Left:	`a`, `j`, `h` or <Cursor LEFT>
#	Right:	`d`, `l`, `k` or <Cursor RIGHT>
#    Quit: `q` or <Escape>

if (($#)); then
    sed -ne "/^# Usage/,/^$/{s@\(Usage: \).*@\1 $0@;s/^# \?//;p}" "$0";
    exit 0;
fi

shapes=( \\U25B6 \\U25BC  \\U25C0 \\U25B2 ) # Triangles for snake's head
dirs=(  "2,0"  "0,1"  "-2,0"  "0,-1"  )	    # x,y move for directions 0-3
body=\\U25CF
keys=()					    # Keyboard buffer

crtApple=''
snake=("3,2")				    # snake body (array of dots)
declare -A iSnake=([${snake[0]}]='')	    # dots indexed body of snake

declare -i crtLen=5 crtDir=0 appleValue=0 appleHit=0 appleCnt=0 appleColor

getBgValue() {		# Get terminal background Value (max of R, G or B).
    local _array _string
    IFS= read -d$'\\' -srp $'\033]11;?\033\\' _string
    IFS=$'\e/' read -ra _array <<<"${_string#*:}"
    _array=("${_array[@]%??}")
    printf -v _string '[%d]= ' "${_array[@]/#/0x}"
    declare -a "_array=($_string)"
    _array=("${!_array[@]}")
    printf -v "${1:-TERM_BG_VALUE}" '%d' "${_array[-1]}"
}
apple() {		# Place new apple
    local -i x y collision=1
    local shape
    if [[ $crtApple ]]; then
	IFS=, read -r x y <<<${crtApple}
	printf '\e[%d;%dH ' $y  $x
    fi
    while (( collision )) ;do
	x=$((RANDOM%40*2+1)) y=$((RANDOM%24+1))
	[[ -v "iSnake[$x,$y]" ]] || collision=0
    done
    appleValue=$(( RANDOM % 10 ))
    printf -v shape '\\U%X' $(( appleValue +10122 )) #  10102
    printf '\e[38;5;%dm\e[%d;%dH%b\e[0m' "$appleColor" "$y" "$x" "$shape"
    crtApple="$x,$y"
    appleCnt+=1
}
eatApple() {		# Snake eat apple
    crtLen+="appleValue+1"
    appleHit+=1
    printf \\a
    crtApple=
    apple
}
step() {		# One snake step
    chgDir || return 1
    local -i x y
    IFS=, read -r x y <<<"${snake[0]}"
    printf '\e[%d;%dH%b' $y  $x "$body"	# Replace head by body char.
    x+=${dirs[crtDir]%,*}		# Place new head with shape
    y+=${dirs[crtDir]#*,}		# regarding direction.
    printf '\e[%d;%dH%b' $y $x "${shapes[crtDir]}"

    (( x < 1 || x > 80  || y < 1 || y > 25 )) ||	# border collision
	[[ -v "iSnake[$x,$y]" ]] &&			# body collision
	    return 1
    
    [[ $crtApple == "$x,$y" ]] && eatApple

    snake=("$x,$y" "${snake[@]}")   iSnake["$x,$y"]=''
    
    if ((${#snake[@]} > crtLen ));then		# Remove last body dot...
	IFS=, read -r x y <<<"${snake[${#snake[@]}-1]}"
	printf '\e[%d;%dH ' $y  $x		# from terminal
	snake=("${snake[@]::${#snake[@]}-1}")	# from snake array
	unset "iSnake[$x,$y]"			# from dot indexed array
    fi
}
buffKeys() {		# Bufferize user interactions
    local crtslp key subkey nextStep nsec=150000
    nextStep=$(( (${EPOCHREALTIME/.}/nsec+1)*nsec ))	# sleep 0.15 sec, while
    while ((${EPOCHREALTIME/.}<nextStep)); do		# reading keyboard
	crtslp=00000$(( nextStep - ${EPOCHREALTIME/.} ))	# entries...
	printf -v crtslp %.6f ${crtslp::-6}.${crtslp: -6}
	if read -rsn 1 -t "$crtslp" key; then
	    [[ $key == $'\e' ]] &&			# trap arrow keys
		while IFS= read -d '' -rsn 1 -t .002 subkey; do
		    key+=$subkey
		done
	    keys+=("$key")
	fi
    done
}
chgDir() {		# Oper one direction change if present in buffer
    local key
    if (( ${#keys[@]} ));then
	key=${keys[0]}
	keys=("${keys[@]:1}")
	case ${key,} in
	    d | l | k |  $'\E[C' ) crtDir=0;;
	    x | m | n | , | $'\E[B' ) crtDir=1;;
	    a | j | h | $'\E[D' ) crtDir=2;;
	    w | i | u | $'\E[A' ) crtDir=3;;
	    q | $'\e' ) return 1 ;;
	esac
    fi
}

# Init term

if [[ $(tput lines cols) != $'24\n80' ]]; then
    printf  "\e[8;%d;%dt" 24 80		# Resize term to 80x24
    read -rsn 1 -t .3 _
fi
clear
if (($(tput lines)>24)); then		# Add bottom border if not
    printf '\e[25;80H\n'
    printf '%.0s\U2592\U2592' {3..42}	# Just like to use 42 somewhere ;-)
fi
printf '\e[%d;80H\U2592\e[H' {0..25}	# Fill last column with grey pattern

getBgValue appleColor			# Choose dark or light apple color
appleColor="appleColor>128?125:213 "	# to match the background color
tput civis				# Hide cursor
oTTy=$(stty -g)				# Stop ECHO on terminal
stty -icanon -echo

# Main
gameTime=$EPOCHREALTIME
apple
while step; do
    ((RANDOM<100)) && apple	# move apple spontaneously, sometime
    buffKeys			# bufferize user interactions.
done

# End of game
gameTime=00000$(( ${EPOCHREALTIME/.} - ${gameTime/.} ))
printf -v gameTime '%.0fm%05.2fsec' $(( 10#${gameTime::-6} / 60 )) \
       $(( 10#${gameTime::-6} % 60 )).${gameTime: -6}

printf '\e[25;80H\n\a'
printf '%.0s\U2592\U2592' {3..42}
filled=000$(( ${#snake[@]} * 100000 /960 ))	
printf '\nYou loose!\nScore: %d, Filled %.2f%%, Apple: %d/%d, time: %s.\n' \
       $((crtLen-5)) ${filled::-3}.${filled: -3} $appleHit $appleCnt "$gameTime"
for _ in 1 2; do	# 2 more beeps
    read -rsn1 -t .18 _
    printf '\a'
done
tput cnorm
stty "$oTTy"
