Сетевая игра на bash: шахматы

    Я давно хотел написать какую-нибудь сетевую игру на bash, причём желательно, чтобы управление было удобное, с клавиатуры, обычными курсорным клавишами. Вообще, тема интерактивного взаимодействия в bash глубже, чем «введите число, нажмите „Enter“» не раскопана. Мне пришлось всё изобретать и исследовать самостоятельно. Я очень старался найти что-то похожее по уровню интерактивности, но не нашёл.

    Поскольку тонкости управления с клавиатурой съели очень много моего времени, я не стал заморачиваться с тонкостями совместимости, поэтому игра тестировалась только под Mac OS X, есть ненулевая вероятность, что она заработает и под Linux и уж точно её можно допилить там до рабочего состояния.

    Для работы игра требует наличия nc (aka Netcat) и терминала с поддержкой 256 цветов (под Mac OS рекомендую iTerm2). При наличие интереса к игре, допилю до терминала на 16 цветов и /dev/tcp. Кстати начал выкладывать все свои шел-поделки на ГитХаб.

    Сетевые шахматы на Bash


    Так как игра сетевая, у неё требуется указать два параметра, о которых она расскажет, если её запустить без них. Первый — адрес машины противника, второй — порт. Порт выбирается одинаковым на обеих машинах. Игру можно запустить и на одной машине, в двух консолях (на скриншоте как раз такой случай).

    Играть просто — в каждый момент времени активна только одна доска (на скриншоте — правая, у неё буквы и цифры вокруг доски ярче), на активной доске курсор двигается курсорными клавишами — ←, →, ↑ и ↓, взять фигуру и поставить её на доску — по клавише пробела или Enter. Как только вы поставили фигуру на доску, ход переходит к сопернику. «Съесть» фигуру соперника проще простого — достаточно поставить свою фигуру на чужую. В игре есть защита — нельзя «съесть» свою фигуру.

    Ничего помимо этого в игре нет — не производится правильность контроля ходов, нет проверки на завершение игры, можно даже ходить фигурами соперника. Было очень сложно придумать как обрабатывать нажатия в shell, так что остальное сделать я просто не успел, не поместилось в формат «игрушка за вечер».

    Я постарался снабдить свой код комментариями и писать достаточно структурировано, чтобы можно было разобраться как всё работает самостоятельно.


    
    #!/bin/bash
    # Network chess by Evgeny Stepanischev http://bolknote.ru 2011
    
    if [ $# -ne 2 ]; then
        echo Usage: $0 host-of-opponent port
        exit
    fi
    
    # Хост оппонента
    HOST="$1"
    
    # Общий порт
    PORT="$2"
    
    # Клавиатурные комбинации извстной длины
    SEQLEN=(1b5b4. [2-7]. [cd]... [89ab].{5} f.{7})
    
    # Фигуры
    WHITE=(♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖)
    BLACK=(♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟)
    
    # Наш ход?
    OURMOVE=
    
    # Я чёрный или белый?
    MYCOLOR=
    
    # Доска
    declare -a XY
    
    # Курсор
    CX=1 CY=7
    TAKEN=
    
    # Необходимые нам клавиатурные коды
    KUP=1b5b41
    KDOWN=1b5b42
    KLEFT=1b5b44
    KRIGHT=1b5b43
    KSPACE=20
    
    # Восстановление экрана
    function Restore {
        echo -ne "\033[5B\033[5B\033[?25h\033[m"
        stty "$ORIG" 2>/dev/null
        (bind '"\r":accept-line' 2>/dev/null)
    }
    
    trap Restore EXIT
    
    # Выключаем Enter
    (bind -r '\r' 2>/dev/null)
    # Выключаем остальную клавиатуру
    ORIG=`stty -g`
    stty -echo
    
    # Убирам курсор
    echo -e "\033[?25l"
    
    # Отдаём события клавиатуры в сеть
    function ToNet {
        echo $1 | nc "$HOST" "$PORT"
    }
    
    # Реакция на клавиши курсора
    function React {
        case $1 in
            $KLEFT)
                  if [ $CX -gt 1 ]; then
                      CX=$(($CX-1))
                      PrintBoard
                  fi
               ;;
    
            $KRIGHT)
                  if [ $CX -lt 8 ]; then
                      CX=$(($CX+1))
                      PrintBoard
                  fi
                ;;
    
            $KUP)
                  if [ $CY -gt 1 ]; then
                      CY=$(($CY-1))
                      PrintBoard
                  fi
               ;;
    
            $KDOWN)
                  if [ $CY -lt 8 ]; then
                      CY=$(($CY+1))
                      PrintBoard
                  fi
        esac
    
        # Отдаём события клавиатуры в сеть
        [ "$OURMOVE" ] && ToNet $1
    }
    
    
    # Проверка совпадения с известной клавиатурной комбинацией
    function CheckCons {
        local i
    
        for i in ${SEQLEN[@]}; do
            if [[ $1 =~ ^$i ]]; then
                return 0
            fi
        done
    
        return 1
    }
    
    # Функция реакции на клавиатуру, вызывает React на каждую нажатую клавишу,
    # кроме KSPACE — на неё возвращается управление
    
    function PressEvents {
        local real code action
    
        # Цикл обработки клавиш, здесь считываются коды клавиш,
        # по паузам между нажатиями собираются комбинации и известные
        # обрабатываются сразу
        while true; do
            # измеряем время выполнения команды read и смотрим код нажатой клавиши
            # akw NR==1||NR==4 забирает только строку №1 (там время real) и №4 (код клавиши)
            eval $( (time -p read -r -s -n1 ch; printf 'code %d\n' "'$ch") 2>&1 |
            awk 'NR==1||NR==4 {print $1 "=" $2}' | tr '\r\n' '  ')
    
            # read возвращает пусто для Enter и пробела, присваиваем им код 20,
            # а так же возвращаются отрицательные коды для UTF8
            if [ "$code" = 0 ]; then
                code=20
            else
                 [ $code -lt 0 ] && code=$((256+$code))
    
                 code=$(printf '%02x' $code)
            fi
    
            if [ $code = $KSPACE ]; then
                [ "$OURMOVE" ] && ToNet $KSPACE
    
                SpaceEvent && return
                continue
            fi
    
            # Если клавиши идут подряд (задержки по времени нет)
            if [ $real = 0.00 ]; then
                seq="$seq$code"
    
                if CheckCons $seq; then
                    React $seq
                    seq=
                fi
    
            # Клавиши идут с задержкой (пользователь не может печатать с нулевой задержкой),
            # значит последовательность собрана, надо начинать новую
            else
                [ "$seq" ] && React $seq
                seq=$code
    
                # возможно последовательность состоит из одного символа
                if CheckCons $seq; then
                    React $seq
                    seq=
                fi
            fi
        done
    }
    
    # Проверяем чёрная или белая фигура
    function CheckColor {
         echo -n ${1:0:1}
    }
    
    # Первичное заполнение доски
    function FillBoard {
         local x y ch
    
         for y in {1..8}; do
             for x in {1..8}; do
                 ch='S '
    
                 if [ $y -le 2 ]; then
                     ch=B${BLACK[$x+8*$y-9]}
                 else
                     if [ $y -ge 7 ]; then
                         ch=W${WHITE[$x+8*$y-57]}
                     fi
                 fi
    
                 XY[$x+100*$y]=$ch
             done
        done
    }
    
    # Вывод букв по краю доски
    function PrintBoardLetters {
         local letters=abcdefgh
    
         [ -z "$OURMOVE" ] && echo -ne "\033[30m" || echo -ne "\033[0m"
    
         echo -n '   '
    
         for x in {0..7}; do
             echo -n "${letters:$x:1} "
         done
         echo
    }
    
    # Вывод цифры по краю доски
    function PrintBoardDigit {
        [ -z "$OURMOVE" ] && echo -ne "\033[30m"
        echo -en " $((9-$1))\033[0m "
    }
    
    # Вывод доски
    function PrintBoard {
         local x y c ch
         local colors=('48;5;209;37;1' '48;5;94;37;1')
    
         PrintBoardLetters
    
         for y in {1..8}; do
            PrintBoardDigit $y
    
            for x in {1..8}; do
                c=${colors[($x+$y) & 1]}
                ch=${XY[$x+100*$y]}
    
                if [[ $CX == $x && $CY == $y ]]; then
                    c="$c;7"
                    [ "$TAKEN" ] && ch=$TAKEN
                    [ $MYCOLOR == B ] && c="$c;38;5;16"
                fi
    
                [[ $(CheckColor "$ch") == "B" ]] && c="$c;38;5;16"
    
                echo -en "\033[${c}m${ch:1:1} \033[m"
            done
    
            PrintBoardDigit $y
            echo
         done
    
         PrintBoardLetters
    
         echo -e "\033[11A"
    }
    
    # Приём событий
    function NetListen {
        nc -l $PORT
    }
    
    # Готовы слушать события сети
    function NetEvents {
        local code
    
        while true; do
            code=$(NetListen)
    
            [[ "$code" == "$KSPACE" ]] && SpaceEvent && return
    
            React $code
        done
    }
    
    # Реакция на нажатие Space и Enter — взять или положить фигуру
    function SpaceEvent {
        local xy
    
        # Проверяем, есть ли фигура под курсором
        let xy="$CX+$CY*100"
    
        # Фигуры нет
        if [ "${XY[$xy]:-S }" = "S " ]; then
            if [ -z "$TAKEN" ]; then
                echo -en "\007"
            else
                # Положили фигуру
                XY[$xy]=$TAKEN
                TAKEN=
                return 0
            fi
        # Фигура есть
        else
            # Мы не должны позволять «съесть» свою фигуру
            if [[ $(CheckColor "$TAKEN") == $(CheckColor "${XY[$xy]}") ]]; then
                echo -en "\007"
            else
    			# Фигура есть «в руке», мы «съедаем» противника
    			if [ "$TAKEN" ]; then
    			    XY[$xy]=$TAKEN
                    TAKEN=
                    return 0    
    			else	
                    # «В руке» ничего не было, мы взяли фигуру
                    TAKEN=${XY[$xy]}
                    XY[$xy]="S "
                fi
            fi
        fi
    
        return 1
    }
    
    # Очистка клавиатурного буфера
    function ClearKeyboardBuffer {
        # Быстро — через zsh
        which zsh &>/dev/null && zsh -c 'while {} {read -rstk1 || break}' && return
    
        # Медленно — через bash
        local delta
        while true; do
            delta=`(time -p read -rs -n1 -t1) 2>&1 | awk 'NR==1{print $2}'`
            [[ "$delta" == "0.00" ]] || break
        done
    }
    
    FillBoard
    
    # Кто будет ходить первым
    ToNet HI
    [[ "$(NetListen)" == "HI" ]] && OURMOVE=1
    sleep 0.2
    ToNet ULOOSE
    
    [ "$OURMOVE" ] && MYCOLOR=W || MYCOLOR=B
    
    PrintBoard
    
    # Основной цикл — обрабатываем события из сети или с клавиатуры
    while true; do
        if [ -n "$OURMOVE" ]; then
            ClearKeyboardBuffer
            PressEvents
            OURMOVE=
        else
             NetEvents
             OURMOVE=1
        fi
    
        PrintBoard
    done
    


    Я записал небольшое видео, чтобы показать как это выглядит, к сожалению, все записывалки сильно нагружают мой компьютер и игра начинает тормозить, но общее представление получить можно.

    Комментарии 46

      +20
      Пока живы такие очумельцы — за нашу Родину можно не беспокоиться ;)
        +5
        Ждём Doom на баше =).
          +16
          Ох, это мне надо пару недель не поспать :)
            0
            Может, косынку? :)
            +3
            Отличный практикум на bash. И комменты исходнике просто отличные. Спасибо.
              +7
              Не думал, что в Unicode есть символы для шахматных фигур. И вправду, они есть en.wikipedia.org/wiki/Chess_symbols_in_Unicode =)))
                +2
                В юникоде есть ВСЁ :)
                  +15
                  Вы даже не представляете насколько вы правы :)

                  www.fileformat.info/info/unicode/char/1f4a9/index.htm
                    +1
                    Фу-у…
                      0
                      Со слепу принял сначала вашу картинку за это.
                      Видимо, стоит задуматься)
                        0
                        Кто подскажет, какой шрифт нужно поставить в Linux, чтобы это начало отображаться?
                    +2
                    торт
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        начал отсыпаться? :)
                          0
                          Если бы, пытаюсь расшевелить себе мозг :)
                          0
                          Круть! ;)

                          С кнопками я тоже как то бился помню… как то так:
                          #!/bin/bash
                          # Прочитать 1 символ. 
                          # -s отключить эхо, -n1 только один символ, -r считывать "\" не дожидаясь экранирования
                          while read -s -n1 key                      
                          do
                          ...
                          done
                          

                            +1
                            Так не получится. Курсорные клавиши выдают по три символа за раз, курсорные с шифтом (на Маке) — шесть, так что надо какой-то определять конец комбинации :) Я придумал два выхода — либо найти все комбинации, которые генерируются и собрать их, либо замерять задержки между нажатиями. У меня используется гибрид — для того, чтобы понимать, что нашёлся конец я использую словарь, чтобы определить, что клавиша нажата в составе комбинации — меряю задержку.
                              0
                              Кстати да, для курсорных и прочих клавиш где передаются более одного символа, я делал счетчик их, и типа конечного автомата для их приема, обрабатывал только небходимые мне.
                                0
                                Вот и получается, что нужно либо знать все комбинации и как-то их хранить (каких только клавиш на компьютерах не бывает и что они только не шлют в консоль, хранить это накладно, обрабатывать — долго), либо делать как-то детектор конца комбинации.

                                Причём конечный автомат тоже не очень-то поможет — есть комбинации, не содержащие признака, что дальше может что-то идти, то есть есть комбинация abcd, а есть abcdef, их никак не различить, если делать конечный автомат.
                                  0
                                  Да, детектор по времени нужен. Я кстати был удивлен, когда писал код для работы с терминалом, ввод и вывод. Куча ESC последовательностей, причем разных для разных терминалов (я поддерживал только vt-100) и не придумали хотя бы признака конца последовательности! А если ты ее не обрабатываешь, то она вылазит левыми символами, т.к. само тело то в ASCII. Это создает массу сложностей.
                                  Понятно конечно что все это тянется от печатных машинок и все это legacy и много рудиментов и обратная совместимость… Зоопарк короче :D Это уже не раз тут обсуждали…
                            +2
                            У нас товарищ перед увольнением сделал сетевой морской бой на виндовых батниках. Хотели даже традицию такую ввести, перед увольнением писать морской бой на чем-нибудь экзотическом)
                              +1
                              Мать моя женщина o_O.
                                0
                                Я тоже годика полтора назад писал морской бой в консоли. Только упор делался как раз на наличие всех проверок, и умный АИ.
                                Но и «дизайн» тоже не оставил в стороне, все цвета и символы были хорошо подобраны (хотя у меня было только ASCII и 16 цветов).
                                Надо бы раскопать код…
                                  0
                                  У меня сразу не заработало. Вот фикс:

                                  diff --git a/chess.sh b/chess.sh
                                  index c992749..0d8ba9d 100644
                                  — a/chess.sh
                                  +++ b/chess.sh
                                  @@ -12,7 +12,7 @@ for i in nc1 netcat ncat pnetcat; do
                                  which $i &>/dev/null && NC=$i && break
                                  done

                                  -[ -z "$NC"] && echo 'Error: you have to install netcat to continue' && exit 1
                                  +[ -z "$NC" ] && echo 'Error: you have to install netcat to continue' && exit 1

                                  # Хост оппонента
                                  HOST="$1"
                                    0
                                    там nc1 убрать надо, я для теста ставил, сейчас пофикшу
                                    0
                                    Ксоникс, шахматы… Что следующее? Неужто и вправду Doom? :D
                                    Нет, наверное таки Diablo IV :-P
                                      0
                                      Повешусь я такое делать :)
                                        0
                                        Мы верим в тебя, падаван ))))
                                          0
                                          ну 3D и правда тут х.з., а вот что-то типа Rogue было бы посильно, наверное = )
                                        0
                                        Даёшь тетрис! =)
                                          0
                                          Может быть :)
                                            +1
                                            uuner.doslash.org/forfun/sedtris.sed — тетрис на Седе
                                              0
                                              Это отличный вариант ненормального программирования, но там блоки надо опускать вручную s+Debug-версия тетриса с выполнением по шагам.
                                            0
                                            Осталось режим игры с компьютером написать :)
                                              +1
                                              Bash против гроссмейстеров? Ж-)
                                              0
                                              Ээм, простите за занудство, но это не самый правильный способ — мерить задержку между нажатиями клавиш для выявления комбинации.
                                              Сугубо говоря, он верен до тех пор, пока число циклов тактового генератора между обработками прерывания первой и второй клавиши последовательности больше единицы. (Сейчас-то это почти всегда так, но вот ранее… :-)
                                              Итак, чтобы не быть болтолологом, предложу своё решение:
                                              во-первых, я бы заменил явно указание кодов клавиш на их эскейп-аналоги, это решение получше, так как ориентируемся на коды клавиш для терминала(см пример из книги abs-guide):
                                              # Коды клавиш.
                                              arrowup='\[A'
                                              arrowdown='\[B'
                                              arrowrt='\[C'
                                              arrowleft='\[D'
                                              insert='\[2'
                                              delete='\[3'
                                              SUCCESS=0
                                              далее, после считывания, сравниваем кодами:
                                              read -n3 key # Прочитать 3 символа.
                                              echo -n "$key" | grep "$insert"
                                              if [ "$?" -eq $SUCCESS ]
                                              then
                                                echo "Нажата клавиша \"Insert\"."
                                                exit $SUCCESS
                                              fi

                                              И теперь, комбинация клавишь в эскейп-последовательности выглядит так (для SHIFT+ARR_DOWN):
                                              '\[1;2B', где '\[1;' — начало escape-последовательности, 2B — коды клавиш, 2 — SHIFT, B — ARR_DOWN
                                              так что, читая по три символа это будет выглядеть как-то так:
                                              seq='\[1'
                                              shift_down=';2B'
                                              read -n3 key # Прочитать 3 символа.
                                              echo -n "$key" | grep "$arrowup"
                                              [ "$?" -eq $SUCCESS ] && : # своя обработка
                                              echo -n "$key" | grep "$arrowdown"
                                              [ "$?" -eq $SUCCESS ] && :
                                              echo -n "$key" | grep "$arrowrt"
                                              [ "$?" -eq $SUCCESS ] && :
                                              echo -n "$key" | grep "$arrowleft"
                                              [ "$?" -eq $SUCCESS ] && :
                                              echo -n "$key" | grep "$seq"
                                              if [ "$?" -eq $SUCCESS ] ; then
                                                read -n3 key
                                                echo -n "$key" | grep "$shift_down"
                                                [ "$?" -eq $SUCCESS ] && echo "Pressed Shift-Down"
                                                #etc
                                              fi
                                                0
                                                Что будет если пользователь нажал не курсорные клавиши?
                                                  0
                                                  тогда ведётся считывание (в моём примере по три символа), заменяя read -n3 key на read -n1 key можно вести обработку нажатия и печатных символов, затем, среди них обнаруживать escape-последовательности.
                                                    0
                                                    Ну то есть вы предлагаете один из способов, который я упоминал — искать известные последовательности. Как уже говорил, он не работает, так как есть последовательности разной длины, но с одним началом.
                                                      0
                                                      мм, а какие две последовательности в ваших комбинациях совпадают?
                                                      (я что-то не заметил, курсорные клавиши, курсорные с зажатой управляющей клавишей и обычные все различаются)
                                                        0
                                                        Если курсорную клавишу зажать с shift на «маке», будет последовательность ровно такая же как курсорная, только длиннее. Это только один пример.

                                                        Мне хотелось сделать обработку в которую я достаточно быстро смогу добавить любые новые клавиши, на длине и маске основываться нельзя — будут ложные нажатия.
                                                          0
                                                          ясно, хотя это странно, в bash под linux они различаются.
                                                          ладно, ваша взяла, хотя по мне это некий «костыль», извините уж.
                                                            +1
                                                            Конечно костыль, ещё бы! Если бы в shell было какое-то нормальное встроенное средство для этого, было бы здорово.
                                                              0
                                                              Ещё пример, кстати, вспомнил. Любая длинная последовательность начинается с Esc (0x1B), если нажать клавишу Esc, то будет выдана именно она. Как различить это начало длинной последовательности или пользователь просто Esc нажал?
                                                                0
                                                                да, на счёт этого я в курсе, я предлагал решение к вашей конкретно задаче, где в используемые клавиши Esc не входит:)
                                                                ps
                                                                вообще, конечно, спасибо, тут вы придумали хоть какое-то решение. Как-то пытался найти функцию, возвращающую состояние буфера ввода, но, может, плохо искал, думаю, это решение было бы слегка красивей.
                                                    0
                                                    Забыл сказать: статья мне понравилась, сам когда-то, админя также развлекался с напарником; вывод был разве что на разные терминалы сервера, а не игра по сети, но уже был и таймер ходов, и лог, и чёрт вспомнит, что ещё!

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                  Самое читаемое