1

As an exercise, I was trying to "optimize" the code presented for a bash-based game. Unfortunately, I am having issues because I can't seem to build a bash array of boolean values (if/test results) then use those in a subsequent complex logic expression.

I thought I could assign values in this manner:

bmap[0]="dummy"
bmap[1]=$( test ${board[1]} == ${symbol} )
bmap[2]=$( test ${board[2]} == ${symbol} )
bmap[3]=$( test ${board[3]} == ${symbol} )
bmap[4]=$( test ${board[4]} == ${symbol} )
bmap[5]=$( test ${board[5]} == ${symbol} )
bmap[6]=$( test ${board[6]} == ${symbol} )
bmap[7]=$( test ${board[7]} == ${symbol} )
bmap[8]=$( test ${board[8]} == ${symbol} )
bmap[9]=$( test ${board[9]} == ${symbol} )

then use those in this manner

if \
    [[ ${bmap[1]} && ${bmap[2]} && ${bmap[3]} ]] ||
    [[ ${bmap[4]} && ${bmap[5]} && ${bmap[6]} ]] ||
    [[ ${bmap[7]} && ${bmap[8]} && ${bmap[9]} ]] ||
    [[ ${bmap[1]} && ${bmap[4]} && ${bmap[7]} ]] ||
    [[ ${bmap[2]} && ${bmap[5]} && ${bmap[8]} ]] ||
    [[ ${bmap[3]} && ${bmap[6]} && ${bmap[9]} ]] ||
    [[ ${bmap[1]} && ${bmap[5]} && ${bmap[9]} ]] ||
    [[ ${bmap[3]} && ${bmap[5]} && ${bmap[7]} ]]
then
    echo "true"
else
    echo "false"
fi

But the error I am getting is

./test_175.sh: line 29: test: ==: unary operator expected
./test_175.sh: line 30: test: ==: unary operator expected
./test_175.sh: line 31: test: ==: unary operator expected
./test_175.sh: line 32: test: ==: unary operator expected
./test_175.sh: line 33: test: ==: unary operator expected
./test_175.sh: line 34: test: ==: unary operator expected
./test_175.sh: line 35: test: ==: unary operator expected
./test_175.sh: line 36: test: ==: unary operator expected

Is there any way to reuse the bmap array values directly as boolean operation results, and not being forced to perform another test referencing those values?

Eric Marceau
  • 1,601
  • 1
  • 8
  • 11
  • 1
    `$()` is for substituting the output of a command into the command line. `test` doesn't produce any output, the result is in its exit status. – Barmar Mar 01 '23 at 23:39
  • 1
    The error message may be due to missing quotes on your variable expansions. Use [Shellcheck](https://www.shellcheck.net/) to find common problems in shell code, including missing quotes. It's a good idea to run [Shellcheck](https://www.shellcheck.net/) on all new or modified shell code. The reports include links to detailed information about problems, including how to fix them. – pjh Mar 01 '23 at 23:47
  • 1
    See [When to wrap quotes around a shell variable?](https://stackoverflow.com/q/10067266/4154375) and the [When Should You Quote?](https://mywiki.wooledge.org/Quotes#When_Should_You_Quote.3F) section of [Quotes - Greg's Wiki](https://mywiki.wooledge.org/Quotes). – pjh Mar 01 '23 at 23:49
  • 1
    for the final 9-way conditional ... something a bit more compact: `win=false; for tuple in '1 2 3' '4 5 6' '7 8 9' 'other six triples'; do read -r i j k <<< "$tuple"; [[ "${bmap[i]}" && "${bmap[j]}" && "${bmap[k]}" ]] && win='true' && break; done; echo "$win"` – markp-fuso Mar 02 '23 at 01:06
  • @Barmar, the bash man page states that "test" value for true is 0 and 1 for false. Since I believe my bmap assigments are doing that, why can't I work with those ? – Eric Marceau Mar 02 '23 at 01:32
  • @markp-fuso, thank you for that different approach. That looks rather elegant. I think I will incorporate that. – Eric Marceau Mar 02 '23 at 01:35
  • @pjh, you were correct about the quotes. I was under the impression that the process RC would be captured ( zero or one ) but that is not being interpreted by the shell the way I conceived it would, so that was critical, but not the only element missing (i.e. the "|| .." part of the testing on each array element. – Eric Marceau Mar 02 '23 at 03:48
  • 1
    "value" is not the same as "output". – Barmar Mar 02 '23 at 16:37

4 Answers4

2

Try

bmap=( dummy )
for i in {1..9}; do
    [[ ${board[i]} == "$symbol" ]] && bmap[i]=1 || bmap[i]=0
done

One way to use the logical (0 (false) and 1(true)) values in the bmap array is:

wins=( 1 2 3   4 5 6   7 8 9   1 4 7   2 5 8   3 6 9   1 5 9   3 5 7 )
result=false
for ((i=0; i<${#wins[*]}; i+=3)); do
    if (( bmap[wins[i]] && bmap[wins[i+1]] && bmap[wins[i+2]] )); then
        result=true
        break
    fi
done
echo "$result"

However, it's not clear to me that using the bmap array is a significant optimization.

This Shellcheck-clean implementation of the game tries to optimize by avoiding command substitution ($(...)), reducing repetition, and using fewer variables:

#!/bin/bash -p

board=( ' '  ' '  ' '  ' '  ' '  ' '  ' '  ' '  ' '  ' ' )

function play_game
{
    draw_board

    local player
    for player in X O X O X O X O X; do
        do_move "$player"
        draw_board

        if is_win_for "$player"; then
            printf '*** WINNER is Player %s ***\n' "$player"
            return 0
        fi
    done

    printf '\n*** DRAW - NO WINNER ***\n'
}

function draw_board
{
    printf '
        +-1-+-2-+-3-+
              X
    +   +---+---+---+
    1   | %s | %s | %s |
    +   +---+---+---+
    2 Y | %s | %s | %s |
    +   +---+---+---+
    3   | %s | %s | %s |
    +   +---+---+---+\n' "${board[@]:1:9}"
}

function do_move
{
    local -r player=$1

    local x y board_pos
    while :; do
        printf "\\nPlayer %s's turn:\\n" "$player"
        read_coordinate x
        read_coordinate y

        board_pos=$(( x + 3*(y-1) ))
        if [[ ${board[board_pos]} == ' ' ]]; then
            board[board_pos]=$player
            return 0
        else
            printf 'That space is taken.  Please choose another ...\n'
        fi
    done
}

function read_coordinate
{
    while read -r -p "    Enter $1 coordinate [1-3]: " "$1"; do
        if [[ -z ${!1} || ${!1} == *[^0-9]* ]]; then
            printf 'Not a valid number: %q.  Try again.\n' "${!1}" >&2
        elif (( $1 < 1 || $1 > 3)); then
            printf '%d is not in range 1-3.  Try again.\n' "${!1}" >&2
        else
            return 0
        fi
    done

    return 1
}

function is_win_for
{
    local -r player=$1

    local -r wins=( 1 2 3   4 5 6   7 8 9       # rows
                    1 4 7   2 5 8   3 6 9       # columns
                    1 5 9   3 5 7           )   # diagonals

     local i j
     for ((i=0; i<${#wins[*]}; i+=3)); do
        for ((j=i; j<(i+3); j++)); do
            [[ ${board[wins[j]]} == "$player" ]] || continue 2
        done

        return 0
    done

    return 1
}

play_game
pjh
  • 6,388
  • 2
  • 16
  • 17
  • \@pjh, nicely done! Thank you for your short-form logic for the **draw_board** function. Also for the approach using the explicit list of player tokens. Also, I need to study your logic for **is_win_for** . That looks very interesting but I have some trouble understanding the "for loop elements ( "i+=3", "continue 2" ). – Eric Marceau Mar 03 '23 at 01:17
1

A few issues with the current design:

  • 'capturing' a boolean usually involves an explicit assignment of a 'value' based on the result of the test (eg, [ test ] && x='true_value' || x='false_value')
  • for ${bmap[?] to evaluate as 'false' it needs to be blank (ie, any non-blank value is treated as 'true')
  • if a variable is blank/unset then when testing it is necessary to wrap the variable reference in double quotes to keep from receiving a runtime syntax error (eg, [[ ${bmap[1]} ]] should be [[ "${bmap[1]}" ]])

Updating OP's current code to set a 'true' (non-blank) or 'false' (blank) value:

for ((i=1;i<=9;i++))
do
    [[ "${board[i]}" == "${symbol}" ]] && bmap[i]=1 || bmap[i]=
    #                                                          ^^ blank == false
    #                                             ^^ non-blank == true
done

And the tests become:

if \
[[ "${bmap[1]}" && "${bmap[2]}" && "${bmap[3]}" ]] ||
....

Sample test run:

# set all board entries to `X` except for #4 (set to `O`):

$ for ((i=1;i<=9;i++)); do board[i]=X; done
$ board[4]=O

# populate bmap[] based on symbol=X

$ symbol=X
$ for ((i=1;i<=9;i++)); do  [[ "${board[i]}" == "${symbol}" ]] && bmap[i]=1 || bmap[i]=; done

# results:

$ typeset -p board bmap
declare -a board=([1]="X" [2]="X" [3]="X" [4]="O" [5]="X" [6]="X" [7]="X" [8]="X" [9]="X")
                                              ^^^
declare -a  bmap=([1]="1" [2]="1" [3]="1" [4]=""  [5]="1" [6]="1" [7]="1" [8]="1" [9]="1")
                                              ^^
       ##### all bmap[] entries are 'true' (non-blank) except for bmap[4] which is 'false' (blank)

# boolean tests:

$ [[ "${bmap[1]}" ]] && echo 'true'
true

$ [[ "${bmap[4]}" ]] && echo 'true'
         -- no output because bmap[4] is blank (aka 'false')

NOTES:

  • from a performance perspective this should be quite a bit faster than the current/new design that spawns 9 sub-processes (bmap[1-9]=$( test ${board[1-9]} == ${symbol} )) though ...
  • OP may not see much of a performance improvement from the original game design ( if [[ ${board[1]} == ${symbol} && ${board[2]} == ${symbol}...)
markp-fuso
  • 28,790
  • 4
  • 16
  • 36
0

Final version of script, incorporating elements chosen from all feedback. Thank you all!

#!/bin/bash

DBG=0
DBGm=0
while [ $# -gt 0 ]
do
    case $1 in
        --debug ) DBG=1 ; shift ;;
        --monitor ) DBGm=1 ; shift ;;
        * ) echo -e "\n\t Invalid parameter used on the command line.  Only valid options: [ --debug | --monitor ] \n Bye!\n" ; exit 1 ;;
    esac
done

#QUESTION:  https://stackoverflow.com/questions/75579778/tictactoe-bash-script

#REVISED:   https://stackoverflow.com/questions/75610032/creating-and-using-bash-array-of-logical-values/75610228#75610228

board=( " "
    " "  " "  " "
        " "  " "  " "
        " "  " "  " " )

bmap=( "" "" "" "" "" "" "" "" "" "" )


function draw_board() {
    echo -e "\n\t    +-1-+-2-+-3-+"
    echo -e   "\t          X"
    echo -e   "\t+   +---+---+---+"
    echo -e   "\t1   | ${board[1]} | ${board[2]} | ${board[3]} |"
    echo -e   "\t+   +---+---+---+"
    echo -e   "\t2 Y | ${board[4]} | ${board[5]} | ${board[6]} |"
    echo -e   "\t+   +---+---+---+"
    echo -e   "\t3   | ${board[7]} | ${board[8]} | ${board[9]} |"
    echo -e   "\t+   +---+---+---+"
}

function boolean_map() {
    test ${DBGm} -eq 1 && set -x
    local symbol="${1}"

    test ${DBG} -eq 1 && echo -e "\t\t symbol = '${symbol}'"

    test "${board[1]}" == "${symbol}" && bmap[1]=1 || bmap[1]=""  ;
    test "${board[2]}" == "${symbol}" && bmap[2]=1 || bmap[2]=""  ; 
    test "${board[3]}" == "${symbol}" && bmap[3]=1 || bmap[3]=""  ; 
    test "${board[4]}" == "${symbol}" && bmap[4]=1 || bmap[4]=""  ; 
    test "${board[5]}" == "${symbol}" && bmap[5]=1 || bmap[5]=""  ; 
    test "${board[6]}" == "${symbol}" && bmap[6]=1 || bmap[6]=""  ; 
    test "${board[7]}" == "${symbol}" && bmap[7]=1 || bmap[7]=""  ; 
    test "${board[8]}" == "${symbol}" && bmap[8]=1 || bmap[8]=""  ; 
    test "${board[9]}" == "${symbol}" && bmap[9]=1 || bmap[9]=""  ; 

    test ${DBGm} -eq 1 && set +x

    test ${DBG} -eq 1 && for val in "${!bmap[@]}"
    do
        echo -e "\t\t bmap[${val}] = '${bmap[${val}]}'" >&2
    done
}

function check_win() {
    # REFERENCE:  https://stackoverflow.com/posts/comments/133395599?noredirect=1

    ###
    ### IMPORTANT:  Approach used here lends itself very well to much more complex truth table logic
    ###

    win='false'
    for tuple in '1 2 3' '4 5 6' '7 8 9' '1 4 7' '2 5 8' '3 6 9' '1 5 9' '3 5 7'
    do
        read -r i j k <<< "${tuple}"
        test ${DBG} -eq 1 && echo -e "\n\t\t ${i} = ${bmap[${i}]} " >&2
        test ${DBG} -eq 1 && echo -e   "\t\t ${j} = ${bmap[${j}]} " >&2
        test ${DBG} -eq 1 && echo -e   "\t\t ${k} = ${bmap[${k}]} " >&2
        [[ "${bmap[${i}]}" && "${bmap[${j}]}" && "${bmap[${k}]}" ]] && win="true" && break
    done
    echo "${win}"
    test ${DBG} -eq 1 && echo -e "\t\t win = ${win}" >&2
}

function is_free() {
    local index=${1}
    if [[ ${board[${index}]} == " " ]]; then
        echo 'true'
    else
        echo 'false'
    fi
}

function game_loop {
    local player="X"
    local move_count=0
    local win=false

    draw_board

    while [[ ${move_count} -lt 9 ]]; do
        #echo "$player következik. Adja meg az x koordinátát [1-3]: "
        echo -e "\nPlayer ${player}'s turn:\n\tEnter your choice of x coordinate [1-3]: \c"
        read xVal
            #echo "Adja meg az y koordinátát [1-3]: "
        echo -e "\tEnter your choice of y coordinate [1-3]: \c"
        read yVal

        if [[ ${xVal} -lt 1 || ${xVal} -gt 3 || ${yVal} -lt 1 || ${yVal} -gt 3 ]]; then
                    #echo "Hibás koordináta!" >&2
            echo -e "\n\t ERROR:  Invalid coordinate entered!  Please try again ..." >&2
        else
            if [[ $(is_free $(( (yVal-1) * 3 + xVal ))) == true ]]; then
                board[$(( (yVal-1) * 3 + xVal ))]="${player}"
                    ((move_count++))
                
                boolean_map "${player}"

                    win=$(check_win $player)
                    if [[ ${win} == "true" ]]; then
                        draw_board
                            #echo "$player győzött!"
                        echo -e "\n\t\t\t\t*** WINNER is Player ${player} ***"
                    break
                    else
                        if [[ ${player} == "X" ]]; then
                                player="O"
                        else
                                player="X"
                    fi
                fi
            else
                    #echo "Lépéshiba!" >&2
                echo -e "\n\t WARNING:  That space is already taken.  Please try again ..."
            fi
        fi
        draw_board
    done

    if [[ ${win} == "false" ]]; then
            #echo "Dontetlen!"
        echo -e "\n\t\t\t\t*** DRAW - NO WINNER ***"
    fi
}

game_loop

Session output/interaction:

        +-1-+-2-+-3-+
              X
    +   +---+---+---+
    1   |   |   |   |
    +   +---+---+---+
    2 Y |   |   |   |
    +   +---+---+---+
    3   |   |   |   |
    +   +---+---+---+

Player X's turn:
    Enter your choice of x coordinate [1-3]: 1
    Enter your choice of y coordinate [1-3]: 1

        +-1-+-2-+-3-+
              X
    +   +---+---+---+
    1   | X |   |   |
    +   +---+---+---+
    2 Y |   |   |   |
    +   +---+---+---+
    3   |   |   |   |
    +   +---+---+---+

Player O's turn:
    Enter your choice of x coordinate [1-3]: 2
    Enter your choice of y coordinate [1-3]: 1

        +-1-+-2-+-3-+
              X
    +   +---+---+---+
    1   | X | O |   |
    +   +---+---+---+
    2 Y |   |   |   |
    +   +---+---+---+
    3   |   |   |   |
    +   +---+---+---+

Player X's turn:
    Enter your choice of x coordinate [1-3]: 1
    Enter your choice of y coordinate [1-3]: 2

        +-1-+-2-+-3-+
              X
    +   +---+---+---+
    1   | X | O |   |
    +   +---+---+---+
    2 Y | X |   |   |
    +   +---+---+---+
    3   |   |   |   |
    +   +---+---+---+

Player O's turn:
    Enter your choice of x coordinate [1-3]: 2
    Enter your choice of y coordinate [1-3]: 2

        +-1-+-2-+-3-+
              X
    +   +---+---+---+
    1   | X | O |   |
    +   +---+---+---+
    2 Y | X | O |   |
    +   +---+---+---+
    3   |   |   |   |
    +   +---+---+---+

Player X's turn:
    Enter your choice of x coordinate [1-3]: 1
    Enter your choice of y coordinate [1-3]: 3

        +-1-+-2-+-3-+
              X
    +   +---+---+---+
    1   | X | O |   |
    +   +---+---+---+
    2 Y | X | O |   |
    +   +---+---+---+
    3   | X |   |   |
    +   +---+---+---+

                *** WINNER is Player X ***
Eric Marceau
  • 1,601
  • 1
  • 8
  • 11
  • in `boolean_map()` the 9 separate `test && bmap[N] || bmap(N)` lines can be replaced with a looping construct (see my answer); this won't make any difference to performance since you'll still be running 9x test/assignments – markp-fuso Mar 02 '23 at 15:41
  • @markp-fuso, thank you. I was focusing more on the optimization of the tuple tests, not the test of individual contents, to see if there was a way to optimize the process if it were scale up significantly (as I mentionned in one of the comments, for truth-tables of undefined purpose (circuit logic, neural nets, etc.). Yes, this would not be the language for those applications, but as proof of concept environment, bash is a "goto" for me. – Eric Marceau Mar 02 '23 at 17:53
  • understood; I toyed, for a few seconds, with how to use 'matrix math' ('karnaugh maps` anyone?) but quickly shelved that idea (in the intervening eons I've forgotten even the basics); for a decent incremental bump up in performance you could look at some rudimentary parallel processing (relatively 'easy' in `bash`) or other languages (I find it much easier to convert `bash` to `awk` ... to an extent) – markp-fuso Mar 02 '23 at 18:18
  • 1
    For the most part, **awk** has become my workhorse. I kept this script in bash because the OP of the other posting was doing it in bash. – Eric Marceau Mar 03 '23 at 01:03
0

Use bash regex. 3x3 is pretty simple, it's like 3 arrays with 3 indexes:

l1=( '' '' '' )
l2=( '' '' '' )
l3=( '' '' '' )

You have to check 8 variants: 3 lines, 3 columns and 2 diagonals. This could be done in single(for each side) regex like this:

for side in o x; {
check="$side $side $side"

#    check              all lines              |                                   all columns                                              |                      and two diagonals
[[ "$check" =~ ^${l1[@]}$|^${l2[@]}$|^${l3[@]}$|^${l1[0]}\ ${l2[0]}\ ${l3[0]}$|^${l1[1]}\ ${l2[1]}\ ${l3[1]}$|^${l1[2]}\ ${l2[2]}\ ${l3[2]}$|^${l1[0]}\ ${l2[1]}\ ${l3[2]}$|^${l1[2]}\ ${l2[1]}\ ${l3[0]}$ ]] && echo $side wins
}

Columns and diagonals could be preset-ted for better view:

l1=( '' '' '' )
l2=( '' '' '' )
l3=( '' '' '' )
c1=( "${l1[0]}" "${l2[0]}" "${l3[0]}" )
c2=( "${l1[1]}" "${l2[1]}" "${l3[1]}" )
c3=( "${l1[2]}" "${l2[2]}" "${l3[2]}" )
d1=( "${l1[0]}" "${l2[1]}" "${l3[2]}" )
d2=( "${l1[2]}" "${l2[1]}" "${l3[0]}" )

for side in o x; {
check="$side $side $side"

#    check                all lines            |          all columns           | and two diagonals
[[ "$check" =~ ^${l1[@]}$|^${l2[@]}$|^${l3[@]}$|^${c1[@]}$|^${c2[@]}$|^${c3[@]}$|^${d1[@]}$|^${d2[@]}$ ]] && echo $side wins
} 
Ivan
  • 6,188
  • 1
  • 16
  • 23