Загрузка данных


#!/usr/bin/env bash

# Быстрый режим без пауз: FAST=1 bash 21.sh

setup_terminal() {
    ui=0
    RESET='' GOLD='' CYAN='' GREEN='' RED='' DIM=''
    FACE='' RED_FACE='' BACK=''

    if [[ -t 0 && -t 1 && ${TERM:-dumb} != dumb ]]; then
        ui=1
        RESET=$'\033[0m'
        GOLD=$'\033[1;33m'
        CYAN=$'\033[1;36m'
        GREEN=$'\033[1;32m'
        RED=$'\033[1;31m'
        DIM=$'\033[2m'
        FACE=$'\033[0;30;47m'
        RED_FACE=$'\033[0;31;47m'
        BACK=$'\033[0;97;44m'
        printf '\033[?25l'
    fi

    trap cleanup EXIT
    trap 'exit 130' INT
    trap 'exit 143' TERM
}

cleanup() {
    if (( ui )); then
        printf '\033[0m\033[?25h\n'
    fi
}

pause() {
    if (( ui )) && [[ ${FAST:-0} != 1 ]]; then
        sleep "$1"
    fi
}

# Спиннер и полоса загрузки.
animate() {
    local text=$1 steps=${2:-12}
    local i j filled bar
    local frames=('|' '/' '-' '\')

    if (( ! ui )) || [[ ${FAST:-0} == 1 ]]; then
        return
    fi

    for ((i = 0; i <= steps; i++)); do
        filled=$((i * 16 / steps))
        bar=''

        for ((j = 0; j < 16; j++)); do
            if (( j < filled )); then
                bar+='█'
            else
                bar+='░'
            fi
        done

        printf '\r\033[2K  %s%s [%s] %s%s' \
            "$CYAN" "${frames[i % 4]}" "$bar" "$text" "$RESET"

        pause 0.07
    done

    printf '\r\033[2K'
}

read_command() {
    printf '\n  %s ' "$1"

    if (( ui )); then
        printf '\033[?25h'
    fi

    if ! IFS= read -r REPLY; then
        exit 0
    fi

    if (( ui )); then
        printf '\033[?25l'
    fi
}

# Колода из 52 уникальных карт.
new_deck() {
    local i j temp
    deck=()

    for ((i = 0; i < 52; i++)); do
        deck+=("$i")
    done

    # Перемешивание Фишера — Йетса.
    for ((i = 51; i > 0; i--)); do
        j=$((RANDOM % (i + 1)))
        temp=${deck[i]}
        deck[i]=${deck[j]}
        deck[j]=$temp
    done

    deck_position=0
}

draw_card() {
    DRAWN=${deck[deck_position]}
    deck_position=$((deck_position + 1))
}

# Подсчёт очков: туз стоит 11 или 1.
score() {
    local card rank sum=0 aces=0

    for card in "$@"; do
        rank=$((card % 13))

        if (( rank == 0 )); then
            sum=$((sum + 11))
            aces=$((aces + 1))
        elif (( rank >= 10 )); then
            sum=$((sum + 10))
        else
            sum=$((sum + rank + 1))
        fi
    done

    while (( sum > 21 && aces > 0 )); do
        sum=$((sum - 10))
        aces=$((aces - 1))
    done

    printf '%s' "$sum"
}

card_name() {
    local names=(
        'Туз' '2' '3' '4' '5' '6' '7'
        '8' '9' '10' 'Валет' 'Дама' 'Король'
    )
    local suits=('пик' 'червей' 'бубен' 'треф')

    printf '%s %s' "${names[$1 % 13]}" "${suits[$1 / 13]}"
}

# Рисуем карты по четыре в строке.
# hide=1 скрывает вторую карту дилера.
render_hand() {
    local hide=$1
    shift

    local cards=("$@")
    local ranks=('A' '2' '3' '4' '5' '6' '7' '8' '9' '10' 'J' 'Q' 'K')
    local suits=('♠' '♥' '♦' '♣')
    local start end row index card rank suit ink

    if (( ${#cards[@]} == 0 )); then
        printf '  %sПока нет карт%s\n' "$DIM" "$RESET"
        return
    fi

    for ((start = 0; start < ${#cards[@]}; start += 4)); do
        end=$((start + 4))

        if (( end > ${#cards[@]} )); then
            end=${#cards[@]}
        fi

        for ((row = 0; row < 5; row++)); do
            printf '  '

            for ((index = start; index < end; index++)); do
                card=${cards[index]}
                rank=${ranks[card % 13]}
                suit=${suits[card / 13]}
                ink=$FACE

                if (( card / 13 == 1 || card / 13 == 2 )); then
                    ink=$RED_FACE
                fi

                if (( hide && index == 1 )); then
                    ink=$BACK
                fi

                printf '%s' "$ink"

                if (( row == 0 )); then
                    printf '┌─────────┐'
                elif (( row == 4 )); then
                    printf '└─────────┘'
                elif (( hide && index == 1 )); then
                    printf '│░░░░░░░░░│'
                else
                    case "$row" in
                        1) printf '│%-9s│' "$rank" ;;
                        2) printf '│    %s    │' "$suit" ;;
                        3) printf '│%9s│' "$rank" ;;
                    esac
                fi

                printf '%s ' "$RESET"
            done

            printf '\n'
        done
    done
}

table_text() {
    printf '\n  %s♠  ♥       Д В А Д Ц А Т Ь   О Д Н О       ♦  ♣%s\n' \
        "$GOLD" "$RESET"

    printf '  ──────────────────────────────────────────────────────\n'

    printf '  Партия: %-3s  Победы: %s  Поражения: %s  Ничьи: %s\n' \
        "$round" "$wins" "$losses" "$ties"

    printf '  Карт в колоде: %s\n\n' "$((52 - deck_position))"

    if (( dealer_hidden )); then
        printf '  %sДИЛЕР%s — очки скрыты\n' "$CYAN" "$RESET"
    else
        printf '  %sДИЛЕР%s — очки: %s\n' \
            "$CYAN" "$RESET" "$(score "${dealer[@]}")"
    fi

    render_hand "$dealer_hidden" "${dealer[@]}"

    printf '\n  %sВЫ%s — очки: %s\n' \
        "$GOLD" "$RESET" "$(score "${player[@]}")"

    render_hand 0 "${player[@]}"

    printf '\n  ──────────────────────────────────────────────────────\n'
    printf '  Вы ходите первым. Затем дилер добирает до 17 очков.\n'
    printf '  Туз = 1 или 11. J/Q/K = 10. Блэкджек = 21 с двух карт.\n'
    printf '\n  %s%s%s\n\n' "$notice_color" "$notice" "$RESET"
}

# Подготавливаем экран целиком, затем выводим одним кадром.
render() {
    local screen
    screen=$(table_text)

    if (( ui )); then
        printf '\033[H\033[2J%s\n\n' "$screen"
    else
        printf '\n%s\n\n' "$screen"
    fi
}

give_card() {
    local who=$1

    animate 'Раздаём карту…' 7
    draw_card

    if [[ $who == player ]]; then
        player+=("$DRAWN")
        notice="Ваша карта: $(card_name "$DRAWN")."
    else
        dealer+=("$DRAWN")

        if (( dealer_hidden && ${#dealer[@]} == 2 )); then
            notice='Вторая карта дилера лежит рубашкой вверх.'
        else
            notice="Карта дилера: $(card_name "$DRAWN")."
        fi
    fi

    render
    pause 0.4
}

play_round() {
    local p d

    player=()
    dealer=()
    dealer_hidden=1
    notice_color=$CYAN
    notice='Новая партия. Готовим колоду…'

    new_deck
    render
    animate 'Перемешиваем 52 карты…' 18

    give_card player
    give_card player
    give_card dealer
    give_card dealer

    p=$(score "${player[@]}")
    d=$(score "${dealer[@]}")

    # Блэкджек при начальной раздаче завершает партию сразу.
    if (( p == 21 || d == 21 )); then
        animate 'Проверяем блэкджек…' 12
        dealer_hidden=0

        if (( p == 21 && d == 21 )); then
            outcome=tie
            notice='У обоих блэкджек. Ничья!'
        elif (( p == 21 )); then
            outcome=win
            notice='Блэкджек! Вы выиграли!'
        else
            outcome=loss
            notice='У дилера блэкджек. Дилер выиграл.'
        fi

        return
    fi

    notice='Ваш ход: берите карты или остановитесь.'

    while (( p < 21 )); do
        render
        read_command '1 / д — карту | 2 / н — хватит | q — выход:'

        case "$REPLY" in
            1|д|Д|да|Да|ДА|y|Y)
                give_card player
                p=$(score "${player[@]}")
                ;;
            2|н|Н|нет|Нет|НЕТ|n|N)
                break
                ;;
            q|Q|й|Й)
                exit 0
                ;;
            *)
                notice='Введите 1, 2 или q и нажмите Enter.'
                ;;
        esac
    done

    if (( p > 21 )); then
        dealer_hidden=0
        outcome=loss
        notice="У вас $p — перебор. Дилер выиграл."
        return
    fi

    # Дилер ходит автоматически после игрока.
    # При 17 и выше останавливается, даже если у игрока больше.
    animate 'Дилер открывает вторую карту…' 12
    dealer_hidden=0
    notice="Ваш итог: $p. Теперь ход дилера."

    render
    pause 0.7

    while (( d < 17 )); do
        animate 'Дилер думает…' 9
        give_card dealer
        d=$(score "${dealer[@]}")
    done

    if (( d > 21 )); then
        outcome=win
        notice="У дилера $d — перебор. Вы выиграли!"
    elif (( p > d )); then
        outcome=win
        notice="Вы выиграли! Ваши очки: $p. У дилера: $d."
    elif (( p < d )); then
        outcome=loss
        notice="Дилер выиграл. Ваши очки: $p. У дилера: $d."
    else
        outcome=tie
        notice="Ничья! У обоих по $p очков."
    fi
}

main() {
    setup_terminal

    wins=0
    losses=0
    ties=0
    round=0

    while true; do
        round=$((round + 1))
        play_round

        animate 'Подводим итоги…' 10

        case "$outcome" in
            win)
                wins=$((wins + 1))
                notice_color=$GREEN
                ;;
            loss)
                losses=$((losses + 1))
                notice_color=$RED
                ;;
            tie)
                ties=$((ties + 1))
                notice_color=$GOLD
                ;;
        esac

        render

        while true; do
            read_command 'Enter — ещё партия | q — выход:'

            case "$REPLY" in
                ''|1|д|Д|да|Да|ДА|y|Y)
                    break
                    ;;
                q|Q|й|Й|2|н|Н|нет|Нет|НЕТ|n|N)
                    printf '\n  Спасибо за игру! Побед: %s. Поражений: %s. Ничьих: %s.\n' \
                        "$wins" "$losses" "$ties"
                    return
                    ;;
                *)
                    printf '  Нажмите Enter или введите q.\n'
                    ;;
            esac
        done
    done
}

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
    main
fi