File: ttt-sh

Play tic-tac-toe against the computer

The script: ttt

#!/bin/bash
# Sat Jul 12 16:52:30 EDT 2003
# NAME: ttt
# Copyright 2003, Chris F.A. Johnson
# Released under the terms of the GNU General Public License

ESC=$'\e'

tictactoe() {
[ $verbose -ge 1 ] && echo tictactoe $* >&2
    board=`printf "%9.9s" "        "`
    mvnum=1
    remainder=" 1 2 3 4 5 6 7 8 9 "
    show "$board"
    while :
    do
      status
      eval \$player$P
      [ $? -ne 0 ] && return
      put $_MOVE "$board"
      board=$_PUT
#      show "$board"
      mkbold "$_MOVE"
      printat 0 0 $mvnum
      printat $prompt
      sleep $pause &
      mlist="$mlist $_MOVE"
      remainder=${remainder/ $_MOVE / }

      checkboard "$board" && {
	  mkbold $win
	  printat $prompt "$cle"
	  eval winner \$player$P
	  return
      }

      [ $mvnum -eq 9 ] && {
	  B=$U mkbold "$_MOVE"
	  winner "Neither player"
	  return
      }
      wait
      B=$U mkbold "$_MOVE"
      if [ $P = O ]
      then
	  P=X
	  OP=O
      else
	  P=O
          OP=X
      fi
      mvnum=$(( $mvnum + 1 ))
    done
}

winner() {
[ $verbose -ge 1 ] && echo winner $* >&2
     printat $prompt "$cle $* wins\n\n"
}

status() {
[ $verbose -ge 1 ] && echo status $* >&2
    if [ $verbose -ge 1 ]
    then
	printat 4 0
	printf "${cle}Move: %s Player: %s Board: \"%s\"  Last move: %s  Move list: %s" \
	    "$mvnum" "$P" "${board// /-}" "$_MOVE" "$mlist"
    else
	false
    fi
}

show() {
[ $verbose -ge 1 ] && echo show $* >&2
    printat 0 0 $mvnum
    printat 0 $top  ##$(( ($LINES - 5) / 2 ))
    printf "  %${margin}.${margin}s | %1s | %1s\n" "${1:0:1}" "${1:1:1}" "${1:2:1}"
    printf "%${margin}.${margin}s---+---+---\n" " "
    printf "  %${margin}.${margin}s | %1s | %1s\n" "${1:3:1}" "${1:4:1}" "${1:5:1}"
    printf "%${margin}.${margin}s---+---+---\n" " "
    printf "  %${margin}.${margin}s | %1s | %1s\n" "${1:6:1}" "${1:7:1}" "${1:8:1}"
}

put() {
[ $verbose -ge 1 ] && echo put $1 \""$2"\" >&2
   local n=$(( $1 - 1 ))
   _PUT=${2:-$board}
   [ "${_PUT:$n:1}" = " " ] &&
            _PUT=${_PUT:0:$n}$P${_PUT:$1}
}

checkboard() {
[ $verbose -ge 1 ] && echo checkboard \"$*\" >&2
    case $1 in
	$P??$P??$P??) win="1 4 7" ;;
	?$P??$P??$P?) win="2 5 8" ;;
	??$P??$P??$P) win="3 6 9" ;;
	??$P?$P?$P??) win="3 5 7" ;;
        $P???$P???$P) win="1 5 9" ;;
	$P$P$P??????) win="1 2 3" ;;
	???$P$P$P???) win="4 5 6" ;;
	??????$P$P$P) win="7 8 9" ;;
        *) false ;;
    esac
}

mkbold() {
[ $verbose -ge 1 ] && echo mkbold $* >&2
  for sq in $*
  do
    row=$(( ($sq - 1) / 3 ))
    col=$(( ($sq - 1) % 3 + 1 ))
    printat $(( ($col * 4) + $margin - 2 )) $(( ($row * 2) + $top ))
    printf "${B}$P${U}"
  done
}

human() {
[ $verbose -ge 1 ] && echo human $* >&2
   while :
   do
       key "Select ($remainder)"
#       case $_MOVE in
#	  "$ESC") read -sn1; read -sn1 kp; continue ;;
#       esac
       _MOVE=$_KEY
       case $_MOVE in
	   1|2|3|4|5|6|7|8|9) [ "${board:_MOVE-1:1}" = " " ] && break ;;
	   q|Q|x|X) echo; return 5 ;;
	   p) printat $prompt
	      KEYECHO= KEYMAX=$(( $COLUMNS - 10 )) key "Enter delay in seconds"
	      pause=$_KEY
	      ;;
	   "") set -- $remainder
	       _MOVE=$1
	       [ $verbose -ge 1 ] && echo "|$_MOVE|" >&2
	       break ;;
	   *) printat $prompt
	      printf "\r%s%s\r" "  "$cle" $_MOVE: Invalid move"
	      sleep 1
	      printf "%s\r" "$cle" ;;
       esac
   done
}

randy() {
[ $verbose -ge 1 ] && echo randy $* >&2
    randstr $remainder
    _MOVE=$_RANDSTR
}

key() {
[ $verbose -ge 1 ] && echo key $* >&2
    printat $prompt "$cle$CVIS$beep"
    read -${KEYECHO}n$KEYMAX -p "${1:-PAK}: " _KEY 2>&1
    printf "$CINV"
}

getkey() {
    local OKchars=${1:-${remainder// /}}
    local kp2 kp3
    stty -echo
    while :
      do
      prompt="==>"
      prompt
      IFS= read -r -sn1 -p " " kp 2>&1 || exit 2 #cleanup
      case $OKchars in
          *"$kp"*)
              case $kp in
                  $ESC)
                      read -st1 -n1 kp2
                      case $kp2 in
                          \[) read -st1 -n1 kp3
                              case $kp3 in
                                  A) kp=$UP; break ;;
                                  B) kp=$DN; break ;;
                                  C) kp=$RT; break ;;
                                  D) kp=$LF; break ;;
                              esac
                              ;;
                       esac
                       ;;
                  *) break ;;
              esac
              ;;
       esac
    done
    _KEY=$kp
}

randstr() {
[ $verbose -ge 1 ] && echo randstr $* >&2
#    [ -n "$1" ] || return 1
    n=$(( ($RANDOM % $#) + 1 ))
    eval _RANDSTR=\${$n}
}

isblock() {
[ $verbose -ge 1 ] && echo isblock $* >&2
    win=
    block=
    for _MOVE in $remainder
    do
        P=$OP put $_MOVE
	P=$OP checkboard "$_PUT" && return
    done
    _MOVE=
}

block() {
[ $verbose -ge 1 ] && echo block $* >&2
    block=
    for _MOVE in $remainder
    do
        put $_MOVE
	checkboard "$_PUT" && return
        P=$OP put $_MOVE
	P=$OP checkboard "$_PUT" && block=$_MOVE
    done
    [ "$block" ] && { _MOVE=$block; return; }
    randy
}

computer() {
[ $verbose -ge 1 ] && echo computer $* >&2
   local r c
   case $mvnum in
       1) randstr 1 3 7 9
	  _MOVE=$_RANDSTR ;;
       2)
	   if [ "${board:4:1}" = " " ]
	   then
	       _MOVE=5
	   else
	       randstr 1 3 7 9
	       _MOVE=$_RANDSTR
	   fi
	   ;;
       3)  r=${mlist// /}
	   c='1 3 7 9'
	   randstr ${c//[$r]/}
#	   randstr ${remainder//[$r]/}
	   _MOVE=$_RANDSTR
	   ;;
       4)
	   isblock
	   [ "$_MOVE" ] && return
	    r=${mlist// /}
	    c='2 4 6 8'
	    randstr ${remainder//[$r]/}
	    _MOVE=$_RANDSTR
	    ;;
       *) block ;;
   esac
}

printat() { #== print arguments 3-... at Y=$2 X=$1
[ $verbose -ge 1 ] && echo printat $* >&2
    [ $# -lt 2 ] && return 1
    local y=$2
    local x=$1
    shift 2
    msg="$*"
    printf "\e[%d;%dH%b" $y $x "$msg"
}

version()
{
    echo "    $progname, version $version
    Copyright $copyright, $author $email
    This is free software, released under the terms of the GNU General
    Public License.  There is NO warranty; not even for MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.
"
}

usage()
{
echo "
    ${B}NAME${U}:  $progname - Tic-Tac-Toe

    ${B}USAGE${U}: $progname [OPTIONS]

    ${B}OPTIONS${U}:

        -x player1         - specify first player
        -o player2         - specify second player
        -c player1,player2 - specify both players
        -p seconds         - time to pause between moves

        -h, --help       - help: print this message
        -H, --help_long  - help: print more detailed message (if available)
        -v, --verbose    - sends messsages to \$HOME/tttlog
        -V, --version    - print version information

    Copyright 2003, Chris F.A. Johnson
"
}

verbose=0
longusage=0
version="1.0"
copyright=2003
author="Chris F.A. Johnson"
progname=${0##*/}
P=X
OP=O

playerX=human
playerO=computer
co=
li=

if tput ce >/dev/null 2>&1
then ## e.g. FreeBSD
    co=co
    li=li
elif tput el 2>/dev/null
then ## e.g. Linux
    co=cols
    li=lines
fi

UNBOLD=$'\E[0m'
standout=$'\E[0;1;7m'
cle=$'\E[K'
clb=$'\E[1K'
ULINE=$'\E[0;4m'
REVERSE=$'\E[0;7m'
BLINK=$'\E[0;5m'
BOLD=$'\E[0;1m'
CINV=$'\E[0;8m'

R=$REVERSE
U=$UNBOLD
B=$BOLD
BR=$B$R
Cl=$'\f'
if [ "$co" ]
then
    tput reset
    COLUMNS=${COLUMNS:=`tput $co`}
else
    COLUMNS=${COLUMNS:-80}
fi
top=6
margin=11
prompt="3 $(( $top + 9 ))"
beep=  #$'\a'
pause=1
KEYMAX=1
KEYECHO=s

while getopts vVhH-:xoX:O:c:p: var
do
 case "$var" in

     x) playerX=human
	playerO-computer
	;;
     o) playerO=human
	playerX=computer
	;;
     X) playerX=$OPTARG ;;
     O) playerO=$OPTARG ;;
     c|C) playerO=${OPTARG#*,}
	  playerX=${OPTARG%,*}
	  ;;
     p) pause=$OPTARG ;;

    -) case $OPTARG in
         help) usage; exit ;;
         verbose) verbose=$(( $verbose + 1 )) ;;
         version) version; exit ;;
       esac
       ;;
    h) usage; exit ;;
    H) longusage=1; usage; exit ;;
    v) verbose=$(( $verbose + 1 )) ;;
    V) version; exit ;;
    *);;
 esac
done
shift $(( $OPTIND - 1 ))

if [ "$co" ]
then
    COLUMNS=`tput $co`
else
    COLUMNS=${COLUMNS:-80}
fi
margin=11  ##$(( ($COLUMNS - 11) / 2 ))
clear

if [ $verbose -ge 1 ]
then
    exec 2>$HOME/tttlog
    set -x
fi

tictactoe
printat $prompt "\n$CVIS\n"