Пишу, по большей части, про историю, свою жизнь и немного про программирование.

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

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

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

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

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

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

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

#!/bin/bash
# Network chess by Evgeny Stepanischev //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
21 комментарий
heller.ru/blog/ 2011

Жесть.

malinnikov (malinnikov.livejournal.com) 2011

Клево! Попробовал поиграть: http://www.youtube.com/watch?v=M0V8bWwqWH8

Евгений Степанищев (bolknote.ru) 2011

Комментарий для malinnikov.livejournal.com:

О, это как раз терминал без поддержки 256 цветов :) Кстати, стандартный терминал во «Льве» поддерживает сейчас 256, но тормозит, я пользуюсь iTerm2.

Евгений Степанищев (bolknote.ru) 2011

Хотел попробовать записать видео при помощи Screeny, всё начинает дико тормозить и не все события клавиатуры проходят. Странно, почему программа, предназначенная для записи подкастов с экрана (то есть, которой должно вести себя незаметно) так безобразничает?

malinnikov (malinnikov.livejournal.com) 2011

Я просто QuickTime’ом записываю.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для malinnikov.livejournal.com:

O! А я и не знал, что так можно. Всё равно тормозит сильно, но меньше, чем Screeny, удалось записать:

http://video.yandex.ru/users/bolknote/view/10/

diaryea (diaryea.livejournal.com) 2011

<irony>Немного удивился, что в статье вместо «Мак ОС Икс» пишется «Mac OS X», а вместо «эн си» — «nc».</irony>

Евгений Степанищев (bolknote.ru) 2011

Комментарий для diaryea.livejournal.com:

nc — это название команды в консоли. Это всё равно как писать «если» вместо «if» в листинге программы. Я пишу названия по-английски, если не определился ещё как я их буду называть по-русски.

Евгений Степанищев (bolknote.ru) 2011

Исправленная версия будет вот тут: https://raw.github.com/bolknote/shellgames/master/chess.sh

jimidini (jimidini.ya.ru) 2011

а на zsh ты смотрел?

Евгений Степанищев (bolknote.ru) 2011

Комментарий для jimidini.ya.ru:

Когда-то очень давно. Он не везде есть по-умолчанию, к сожалению.

zg (zg.livejournal.com) 2011

Это всё равно как писать «если» вместо «if» в листинге программы.

что-то переделка в жаворонка болку на пользу не пошла.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для zg.livejournal.com:

К чему это сейчас было? Название команды в консоли («nc») переводить не надо, очевидно же. Там наверху ↑↑↑↑ листинг программы, там других команд консольных полно, ни одна не переведена.

chapson 2011

Ого! Женя, это здорово.

zg (zg.livejournal.com) 2011

Комментарий для Евгения Степанищева:

речь шла о упоминании nc в самом тексте (третий абзац). «эн си» это никак не перевод. применяя вашу же аналогию с иф, это как кто-то бы написал в тексте вместо «if» — «иф».

Евгений Степанищев (bolknote.ru) 2011

Комментарий для zg.livejournal.com:

Ну или так. Это неважно в данном контексте.

zg (zg.livejournal.com) 2011

Комментарий для Евгения Степанищева:

ну как неважно. предложение «Это всё равно как писать „если“ вместо „if“ в листинге программы.» полностью теряет смысл.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для zg.livejournal.com:

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

scyphius 2011

Комментарий для Евгения Степанищева:

Скажите, Евгений, а как реализован выход из программы?
Я вижу trap.
Но никак не могу найти где-же выполняется собственно exit — по какой клавише?

Евгений Степанищев (bolknote.ru) 2011

Комментарий для scyphius:

Как обычно — Ctrl+C.

Александр Юрченко 2012

Огромное душевное спасибо!