mirror of
				git://git.code.sf.net/p/zsh/code
				synced 2025-10-31 06:00:54 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			386 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			386 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
| # I noticed we don't ship any contrib and/or example scripts using the
 | |
| # zcurses module, and also that the builtin tetris is sort of boring, so
 | |
| # I figured I'd port it to curses. It works pretty well, but I noticed
 | |
| # two problems with the zcurses module in the process:
 | |
| # 
 | |
| # 1. the HAVE_USE_DEFAULT_COLORS define seems to never be defined?
 | |
| # 
 | |
| # 2a. resizing the window causes 'zcurses input' to wait forever for a
 | |
| # key, even with a timeout defined.
 | |
| # 
 | |
| # Bart says:
 | |
| # >This probably has something to do with the special-casing around wgetch()
 | |
| # >for signals handled by the "trap" command.  See the big comment in
 | |
| # >Src/Modules/curses.c lines 1073-1103.
 | |
| # 
 | |
| # >It may be problematic to mix curses with the generic signal handling in
 | |
| # >the main shell.  We may need to swap in a SIGWINCH handler wrapper while
 | |
| # >the curses UI is active, and restore the main handler when leaving it.
 | |
| # 
 | |
| # 2b. resizing the window doesn't cause an event while running the
 | |
| # program, but if i resize before starting(?) i get an event RESIZE on
 | |
| # my first input call.
 | |
| # 
 | |
| # Bart says:
 | |
| # >There's probably some state that needs to be cleared on entry to
 | |
| # >zccmd_input() so that curses doesn't see something left over from the
 | |
| # >previous signal.  Unfortunately I don't know what that would be.
 | |
| 
 | |
| if (( $LINES < 22 || $COLUMNS < 46 )); then
 | |
|   echo >&2 'terminal needs to be at least 22 lines and 46 columns'
 | |
|   return
 | |
| fi
 | |
| 
 | |
| emulate -L zsh
 | |
| 
 | |
| typeset -a tetris_shapes
 | |
| tetris_shapes=(
 | |
|   0x0f00 0x4444 0x0f00 0x4444
 | |
|   0x4e00 0x4c40 0x0e40 0x4640
 | |
|   0x6600 0x6600 0x6600 0x6600
 | |
|   0x4620 0x6c00 0x4620 0x6c00
 | |
|   0x2640 0x6300 0x2640 0x6300
 | |
|   0x6440 0x8e00 0x44c0 0x0e20
 | |
|   0xc440 0x0e80 0x4460 0x2e00
 | |
| )
 | |
| typeset -A tetris_rotations
 | |
| tetris_rotations=(
 | |
|   0x0f00 0x4444 0x4444 0x0f00
 | |
|   0x4e00 0x4c40 0x4c40 0x0e40 0x0e40 0x4640 0x4640 0x4e00
 | |
|   0x6600 0x6600
 | |
|   0x4620 0x6c00 0x6c00 0x4620
 | |
|   0x2640 0x6300 0x6300 0x2640
 | |
|   0x6440 0x8e00 0x8e00 0x44c0 0x44c0 0x0e20 0x0e20 0x6440
 | |
|   0xc440 0x0e80 0x0e80 0x4460 0x4460 0x2e00 0x2e00 0xc440
 | |
| )
 | |
| local tetris_vsz=20 tetris_hsz=11
 | |
| local tetris_blankline=${(l:11:: :)}
 | |
| local tetris_blankboard=${(j::):-${(l:11:: :)}${(s: :)^${(l:20:: :)}}}
 | |
| 
 | |
| local tetris_board=$tetris_blankboard
 | |
| local tetris_score=0
 | |
| local tetris_lines=0
 | |
| 
 | |
| local tetris_{block{,_next,_x,_y},i}
 | |
| 
 | |
| function __tetris-next-block {
 | |
|   tetris_block_next=$tetris_shapes[1+RANDOM%$#tetris_shapes]
 | |
| }
 | |
| 
 | |
| function __tetris-new-block {
 | |
|   tetris_block=$tetris_block_next
 | |
|   __tetris-next-block
 | |
|   __tetris-draw-next-block
 | |
|   tetris_block_y=0
 | |
|   tetris_block_x=4
 | |
|   if ! __tetris-block-fits; then
 | |
|     __tetris-game-over
 | |
|   fi
 | |
|   __tetris-place-block "*"
 | |
| }
 | |
| 
 | |
| function __tetris-left {
 | |
|   __tetris-place-block " "
 | |
|   (( tetris_block_x-- ))
 | |
|   __tetris-block-fits || (( tetris_block_x++ ))
 | |
|   __tetris-place-block "*"
 | |
| }
 | |
| 
 | |
| function __tetris-right {
 | |
|   __tetris-place-block " "
 | |
|   (( tetris_block_x++ ))
 | |
|   __tetris-block-fits || (( tetris_block_x-- ))
 | |
|   __tetris-place-block "*"
 | |
| }
 | |
| 
 | |
| function __tetris-rotate {
 | |
|   __tetris-place-block " "
 | |
|   local save_block=$tetris_block
 | |
|   tetris_block=$tetris_rotations[$tetris_block]
 | |
|   __tetris-block-fits || tetris_block=$save_block
 | |
|   __tetris-place-block "*"
 | |
| }
 | |
| 
 | |
| function __tetris-drop {
 | |
|   __tetris-place-block " "
 | |
|   ((tetris_block_y++))
 | |
|   while __tetris-block-fits; do
 | |
|     ((tetris_block_y++))
 | |
|     ((tetris_score+=2))
 | |
|   done
 | |
|   ((tetris_block_y--))
 | |
|   __tetris-block-dropped
 | |
| }
 | |
| 
 | |
| function __tetris-timeout {
 | |
|   __tetris-place-block " "
 | |
|   ((tetris_block_y++))
 | |
|   if __tetris-block-fits; then
 | |
|     __tetris-place-block "*"
 | |
|     return
 | |
|   fi
 | |
|   ((tetris_block_y--))
 | |
|   __tetris-block-dropped
 | |
| }
 | |
| 
 | |
| function __tetris-block-dropped {
 | |
|   integer bonus=1
 | |
|   __tetris-place-block "O"
 | |
|   local fl=${tetris_blankline// /O} i=$((tetris_block_y*tetris_hsz))
 | |
|   repeat 4; do
 | |
|     if [[ $tetris_board[i+1,i+tetris_hsz] == $fl ]]; then
 | |
|       if (( fancygraphics )); then for char in {7..1}; do
 | |
|         tetris_board[i+1,i+tetris_hsz]=${tetris_blankline// /$char}
 | |
|         __tetris-render-screen
 | |
|         zcurses timeout score 50
 | |
|         zcurses input score
 | |
|       done; fi
 | |
|       tetris_board[i+1,i+tetris_hsz]=
 | |
|       tetris_board=$tetris_blankline$tetris_board
 | |
|       ((tetris_score+=100*(bonus++*(tetris_lines/10+10))))
 | |
|       ((tetris_lines+=1))
 | |
|       if ((tetris_lines % 10 == 0)); then
 | |
|         ((timestep = timestep * 0.80))
 | |
|       fi
 | |
|     fi
 | |
|     ((i += tetris_hsz))
 | |
|   done
 | |
|   __tetris-new-block
 | |
| }
 | |
| 
 | |
| function __tetris-block-fits {
 | |
|   local y x i=$((1+tetris_block_y*tetris_hsz+tetris_block_x)) b=0x8000
 | |
|   for ((y=0; y!=4; y++)); do
 | |
|     for ((x=0; x!=4; x++)); do
 | |
|       if ((tetris_block&b)); then
 | |
|         ((x+tetris_block_x >= 0)) || return 1
 | |
|         ((x+tetris_block_x < tetris_hsz)) || return 1
 | |
|         ((y+tetris_block_y >= 0)) || return 1
 | |
|         ((y+tetris_block_y < tetris_vsz)) || return 1
 | |
|         [[ $tetris_board[i] == " " ]] || return 1
 | |
|       fi
 | |
|       ((b >>= 1))
 | |
|       ((i++))
 | |
|     done
 | |
|     ((i+=tetris_hsz-4))
 | |
|   done
 | |
|   return 0
 | |
| }
 | |
| 
 | |
| function __tetris-draw-next-block {
 | |
|   local tetris_preview
 | |
|   local y x i=1 b=0x8000
 | |
|   for ((y=0; y!=4; y++)); do
 | |
|     tetris_preview="    "
 | |
|     for ((x=0; x!=4; x++)); do
 | |
|       ((tetris_block_next&b)) && tetris_preview[i]=\*
 | |
|       ((b >>= 1))
 | |
|       ((i++))
 | |
|     done
 | |
|     i=1
 | |
|     zcurses move preview $((y+1)) 1
 | |
|     zcurses string preview ${${${tetris_preview//O/$filled_block}//\*/$active_block}// /  }
 | |
|   done
 | |
| }
 | |
| 
 | |
| function __tetris-place-block {
 | |
|   local y x i=$((1+tetris_block_y*tetris_hsz+tetris_block_x)) b=0x8000
 | |
|   for ((y=0; y!=4; y++)); do
 | |
|     for ((x=0; x!=4; x++)); do
 | |
|       ((tetris_block&b)) && tetris_board[i]=$1
 | |
|       ((b >>= 1))
 | |
|       ((i++))
 | |
|     done
 | |
|     ((i+=tetris_hsz-4))
 | |
|   done
 | |
| }
 | |
| 
 | |
| function __tetris-render-screen {
 | |
|   local i x piece
 | |
|   setopt localoptions histsubstpattern extendedglob
 | |
|   local -a match mbegin mend
 | |
|   local -A animation
 | |
|   animation=( 7 ▇▇ 6 ▆▆ 5 ▅▅ 4 ▄▄ 3 ▃▃ 2 ▂▂ 1 ▁▁ )
 | |
|   for (( i = 0; i < tetris_vsz; i++ )); do
 | |
|     zcurses move gamearea $(( i + 1 )) 1
 | |
|     zcurses string gamearea ${${${${${tetris_board[1+i*tetris_hsz,(i+1)*tetris_hsz]}//O/$filled_block}//\*/$active_block}// /  }//(#b)([1-7])/$animation[$match[1]]}
 | |
|   done
 | |
| 
 | |
|   zcurses clear score
 | |
|   zcurses move score 1 1
 | |
|   zcurses string score "Score: $tetris_score"$'\
 | |
| '" Lines: $tetris_lines"$'\
 | |
| '" Speed: ${timestep%.*} ms"
 | |
| 
 | |
|   zcurses border gamearea
 | |
|   zcurses border score
 | |
|   zcurses border preview
 | |
|   zcurses refresh gamearea score preview $debug
 | |
| }
 | |
| 
 | |
| function __tetris-game-over {
 | |
|   gameover=1
 | |
| }
 | |
| 
 | |
| function __tetris-new-game {
 | |
|   gameover=0
 | |
|   timestep=1000
 | |
|   tetris_score=0
 | |
|   tetris_lines=0
 | |
|   __tetris-next-block
 | |
|   __tetris-new-block
 | |
|   __tetris-render-screen
 | |
| }
 | |
| 
 | |
| function __tetris-game-over-screen {
 | |
|   __tetris-debug "Died with $tetris_score points!"
 | |
|   tetris_board=$tetris_blankboard
 | |
|   local text="You got $tetris_score points!"
 | |
|   local gameover_height=4 gameover_width=$(( $#text + 2 ))
 | |
|   zcurses addwin gameover $gameover_height $gameover_width \
 | |
|                           $(( off_y + (game_height-gameover_height)/2 )) \
 | |
|                           $(( off_x + (game_width+score_width-gameover_width)/2 ))
 | |
|   zcurses move gameover 1 1
 | |
|   zcurses string gameover $text
 | |
|   text='Play again? [yn]'
 | |
|   zcurses move gameover 2 $(( (gameover_width - $#text)/2 ))
 | |
|   zcurses string gameover $text
 | |
|   zcurses border gameover
 | |
|   keepplaying=
 | |
|   until [[ $keepplaying = [ynq] ]]; do
 | |
|     zcurses input gameover keepplaying
 | |
|   done
 | |
|   zcurses delwin gameover
 | |
|   zcurses refresh stdscr
 | |
|   zcurses timeout gamearea ${timestep%.*}
 | |
|   __tetris-new-game
 | |
| }
 | |
| 
 | |
| function __tetris-debug {
 | |
|   if [[ -z $debug ]]; then
 | |
|     return
 | |
|   fi
 | |
|   zcurses scroll debug -1
 | |
|   zcurses move debug 0 0
 | |
|   zcurses string debug "$1"
 | |
| }
 | |
| 
 | |
| function __tetris-remove-wins {
 | |
|   local delwin
 | |
|   local -a delwins
 | |
|   delwins=(gamearea score debug gameover help preview)
 | |
|   for delwin in ${delwins:*zcurses_windows}; do
 | |
|     zcurses delwin $delwin
 | |
|   done
 | |
| }
 | |
| 
 | |
| function __tetris-help {
 | |
|   local i
 | |
|   local help_height=9 help_width=23
 | |
|   zcurses addwin help $help_height $help_width \
 | |
|                       $(( off_y + (game_height - help_height) / 2 )) \
 | |
|                       $(( off_x + (game_width + score_width - help_width) / 2 ))
 | |
|   zcurses move help 1 1
 | |
|   zcurses string help $'left: h, j, left\
 | |
|  right: right, n, l\
 | |
|  rotate: up, c, i\
 | |
|  soft drop: down, t, k\
 | |
|  hard drop: space\
 | |
|  quit: q\
 | |
|  press space to return'
 | |
|   zcurses border help
 | |
|   until [[ $i == [\ q] ]]; do
 | |
|     zcurses input help i
 | |
|     if [[ $i == q ]]; then
 | |
|       keepplaying=n
 | |
|     fi
 | |
|   done
 | |
|   zcurses delwin help
 | |
|   zcurses refresh stdscr
 | |
| }
 | |
| 
 | |
| zmodload zsh/curses && {
 | |
|   zcurses init
 | |
|   __tetris-remove-wins
 | |
|   zcurses refresh
 | |
|   echoti civis
 | |
|   local debug=
 | |
|   if (( ${@[(I)--debug|-d]} )); then
 | |
|     debug=debug
 | |
|   fi
 | |
|   local off_x off_y
 | |
|   local game_height=22   game_width=25
 | |
|   local score_height=5   score_width=20
 | |
|   local preview_height=6 preview_width=10
 | |
|   local filled_block active_block 
 | |
|   local fancygraphics
 | |
|   if zmodload zsh/langinfo && [[ $langinfo[CODESET] = UTF-8 ]]; then
 | |
|     filled_block=██
 | |
|     active_block=▒▒
 | |
|     fancygraphics=${@[(I)--silly]}
 | |
|   else
 | |
|     filled_block='[]'
 | |
|     active_block='()'
 | |
|     fancygraphics=0
 | |
|   fi
 | |
|   off_x=$(( (COLUMNS-game_width-score_width-1) / 2 ))
 | |
|   off_y=$(( (LINES-game_height) / 2 ))
 | |
|   zcurses clear stdscr redraw
 | |
|   zcurses refresh stdscr
 | |
|   zcurses addwin gamearea $game_height $game_width $off_y $off_x
 | |
|   zcurses scroll gamearea off
 | |
|   zcurses addwin score $score_height $score_width \
 | |
|                        $off_y $(( off_x + game_width + 1 ))
 | |
|   zcurses scroll score off
 | |
|   zcurses addwin preview $preview_height $preview_width \
 | |
|                          $(( off_y + score_height )) $(( off_x + game_width + 1 ))
 | |
|   zcurses scroll preview off
 | |
|   if [[ -n $debug ]]; then
 | |
|     zcurses addwin debug $(( game_height - score_height - preview_height - 1 )) \
 | |
|                          $score_width \
 | |
|                          $(( off_y + score_height + preview_height ))\
 | |
|                          $(( off_x + game_width + 1 ))
 | |
|   fi
 | |
|   typeset -F SECONDS
 | |
|   local now prev timestep timeout key kkey keepplaying=y gameover=0
 | |
|   prev=$SECONDS
 | |
|   __tetris-new-game
 | |
|   zcurses timeout gamearea 0
 | |
|   while [[ $keepplaying == y ]]; do
 | |
|     if zcurses input gamearea key kkey; then
 | |
|       __tetris-debug "got input $key$kkey"
 | |
|       case $key$kkey in
 | |
|         LEFT|h|j)  __tetris-left;;
 | |
|         RIGHT|n|l) __tetris-right;;
 | |
|         UP|c|i)    __tetris-rotate;;
 | |
|         DOWN|t|k)  __tetris-timeout; ((tetris_score++)); prev=$SECONDS;;
 | |
|         " ")       __tetris-drop;;
 | |
|         q)         break;;
 | |
|         F1|H)      __tetris-help;;
 | |
|       esac
 | |
|     else
 | |
|       __tetris-debug "timed out"
 | |
|       __tetris-timeout
 | |
|     fi
 | |
|     now=$SECONDS
 | |
|     if (( prev + timestep/1000. < now )); then
 | |
|       (( prev += timestep/1000. ))
 | |
|     fi
 | |
|     timeout=${$(( 1000.*(prev + timestep/1000. - now) + 1 ))%.*}
 | |
|     if (( timeout < 0 )); then
 | |
|       __tetris-debug "BUG: timeout < 0"
 | |
|       timeout=${timestep%.*}
 | |
|     fi
 | |
|     zcurses timeout gamearea $timeout
 | |
|     __tetris-debug "timeout: $timeout"
 | |
| 
 | |
|     __tetris-render-screen
 | |
|     if [[ $gameover == 1 ]]; then
 | |
|       __tetris-game-over-screen
 | |
|     fi
 | |
|   done
 | |
| } always {
 | |
|   __tetris-remove-wins
 | |
|   echoti cnorm
 | |
|   zcurses end
 | |
| }
 |