2
\$\begingroup\$

I wrote this two-player tic-tac-toe program in bash. To make a move, you enter a number between 1 and 9 which corresponds to the square:

1|2|3
4|5|6
7|8|9

I originally submitted this code for code review here, and I'm resubmitting it now that I have implemented the changes suggested for it.

This is my new code:

#!/usr/bin/env bash

declare -r BOARD_SIZE=9
declare -r DEFAULT_SQUARE_VALUE=-
declare -ra PLAYERS=(O X)
declare -ra WINNING_COMBINATIONS=("1 2 3" "4 5 6" "7 8 9" "1 4 7" "2 5 8" "3 6 9" "1 5 9" "3 5 7")

print_move_indices() {
    echo "Move indices:"
    echo "1|2|3"
    echo "4|5|6"
    echo "7|8|9"
    echo
}

draw_board() {
    shift
    for i in {1..3}
    do
        echo "$1|$2|$3"
        shift 3
    done
}


get_placement() {
shift
    while true
    do
        echo "Please enter a valid move (1-$BOARD_SIZE)"
        read -r n
        echo

        if [[ "$n" -gt 0 ]] && [[ "$n" -le "$BOARD_SIZE" ]] && [[ "${!n}" == "$DEFAULT_SQUARE_VALUE" ]]
        then
            placement=$n
            return
        fi

    done
}

is_draw() {
    shift
    for square in "$@"
    do
        if [[ "$square" == "$DEFAULT_SQUARE_VALUE" ]]
        then
            return 1
        fi
    done
    return 0
}

is_winner() {
    shift

    for combination in "${WINNING_COMBINATIONS[@]}"
    do
        is_win=1
        first_square="${combination:0:1}"
        for square in $combination
        do
            if [[ "${!square}" == "$DEFAULT_SQUARE_VALUE" ]] || [[ "${!square}" != "${!first_square}" ]]
            then
                is_win=0
                break
            fi
        done

        if [[ $is_win -eq 1 ]]
        then
            return 0
        fi
    done

    return 1

} 

board=( "/" $(for i in $(seq 1 $BOARD_SIZE); do echo "$DEFAULT_SQUARE_VALUE"; done) ) # dummy value at index 0 so that indices start from 1
current_turn="O"

print_move_indices

while true
do
    echo -e "$current_turn's turn\n"

    draw_board "${board[@]}"
    get_placement "${board[@]}"
    board[placement]="$current_turn"

    if is_draw "${board[@]}"
    then
        echo "Draw!"
        echo
        draw_board "${board[@]}"
        exit
    elif is_winner "${board[@]}"
    then
        echo "$current_turn is the winner!"
        draw_board "${board[@]}"
        exit
    fi

    [[ "$current_turn" == "${PLAYERS[0]}" ]] && current_turn=${PLAYERS[1]} || current_turn=${PLAYERS[0]}

    echo -en "\n\n\n"
done

Example game:

Move indices:
1|2|3
4|5|6
7|8|9

O's turn

-|-|-
-|-|-
-|-|-
Please enter a valid move (1-9)
1




X's turn

O|-|-
-|-|-
-|-|-
Please enter a valid move (1-9)
3




O's turn

O|-|X
-|-|-
-|-|-
Please enter a valid move (1-9)
9




X's turn

O|-|X
-|-|-
-|-|O
Please enter a valid move (1-9)
5




O's turn

O|-|X
-|X|-
-|-|O
Please enter a valid move (1-9)
7




X's turn

O|-|X
-|X|-
O|-|O
Please enter a valid move (1-9)
4




O's turn

O|-|X
X|X|-
O|-|O
Please enter a valid move (1-9)
8

O is the winner!
O|-|X
X|X|-
O|O|O

I do not know much about bash, so any feedback would be much appreciated.

\$\endgroup\$
3
  • \$\begingroup\$ Your last review says: "shellcheck: Code review submissions should lint clean before being submitted. When writing shell code it is easy to produce a result, but it is a minor challenge to make the script correct, due to quoting concerns and other minutiae. Lint this code, and follow the linter's advice, so it lints clean. 'Nuff said." But your current submission doesn't lint clean. \$\endgroup\$
    – ggorlen
    Commented Apr 3 at 18:21
  • \$\begingroup\$ @ggorlen, I don't understand what: "Prefer mapfile or read -a to split command output (or quote to avoid splitting)." wants me to do. (However it was very useful for finding quoting concerns, etc. prior to submitting the code review.) \$\endgroup\$ Commented Apr 4 at 6:17
  • 1
    \$\begingroup\$ @sbottingota Got it, you might add that to the question. \$\endgroup\$
    – ggorlen
    Commented Apr 4 at 15:55

1 Answer 1

1
\$\begingroup\$

It is easy to understand the user prompts when running the code -- and fun, too. Here are some minor suggestions.


When you described the game as "two-player", I assumed I would be playing against the computer. You should make it clear that the user is playing both X and O. Add some printed out text to print_move_indices.


I would prefer fewer blank lines between turns in the printed output.


When I type in a valid number that has already been guessed, I get the message that I need to type in a valid number:

Please enter a valid move (1-9)
1

X's turn

O|-|-
-|-|-
-|-|-
Please enter a valid move (1-9)
1

Please enter a valid move (1-9)
1

Please enter a valid move (1-9)

This is misleading. It would be better to inform the user that the number was already guessed, and that a new number is required.


Add indentation in this function. Change:

get_placement() {
shift

to:

get_placement() {
    shift

Add space around the .. operator for better readability:

for i in {1 .. 3}

The word "draw" has 2 meanings in the code. In the function named is_draw, draw refers to the case where neither player wins. In the function named draw_board, draw refers to printing the board on the output. This is a little confusing. It might be better to rename draw_board as show_board.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.