闇シェル芸「さあ、闇のゲーム(シェルゲイム)の始まりだぜ！」

この記事では闇シェル芸と闇のゲームについて扱いません。シェル芸で作るゲームの話をします。

シェル芸でゲームを作りたいと思ったことはありませんか？私はありません。
しかし気の迷いからシェル芸でインタラクティブに入力を処理し動作する簡単なゲームを作ってみたら、できてしまいました。

作成する過程や学べたことを書いていきたいと思います。

## 完成物

![shell_geime_pv.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155890/8d9bef5d-f6d2-0083-dfa0-e371775f9974.gif)

### 説明
PONG の劣化版のようなゲームです。
動いてるボールをラケットで跳ね返しましょう。
ボールは壁にあたって跳ね返ってきます。
どれだけ続くかを競い合いましょう！

### 操作の仕方
「J」キーで下に移動、「K」キーで上に移動します。vimな感じです。
また、CTRL+Cで終了しましょう。

### コード

**awkがgawkでないと動かないです。**
「0.1」の部分を変更することで難易度調整できます。

```bash
$ (f(){ read -s -n1 -t0.1 a;xxd -ps -l1 <<<"$a";f; };f)|awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+5;fflush()}'|xargs -L1 -I_ bash -c "echo -e '\e[J\e[H';seq -w 1 10|tr -d '[0-9]'|sed '_s/^/I/'"|awk 'NR%11{printf("%1s%30sI\n",$0,"");fflush()}!(NR%11)' | awk -F "" -vOFS="" 'x<2{x=2}NR%11==0{print "score:"i" ";i+=1;y+=b;if(y<3||y>9){b*=-1}}NR%11==y{x=x+a;$x="@"}$0~/I@|@I/{a*=-1}{print;fflush()}$1~/@/{b=0;i-=1}' a=1 b=1 x=5 y=5
```

awkがgawkじゃない人向け

```bash
$ sudo apt-get install gawk
$ alias awk=gawk
```


## どうやって作ったかの説明

**シェル芸は1行で書くことが定義**とされていますが、以下の文章では**説明のために改行を入れることをご了承ください。**

文中にでてくるawkはgawkです。
ubuntu 18.04 のawkはおそらく mawk なのでお気をつけてください。

### ゲームの更新・入力処理
まず一定時間で更新ゲームを更新する処理と入力を受け付ける処理をしなければなりません。
この2つの機能はなんとreadコマンドのみで達成することができます。

```bash
$ (f(){ read -s -n1 -t0.2 a;xxd -ps -l1 <<<"$a";f; };f)
```
read コマンドにおいて、
 \-n オプションは受け取る文字数。
 \-t オプションはタイムアウト時間
になっているためこれで一定間隔更新しつつで文字が入力された場合はそれをキャッチできます。
連続入力(キーの長押し)されると更新間隔が早くなる欠点もあります。

![image_0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155890/6853a3dd-e4aa-f4bc-eaed-cd98418dba92.gif)


### キー入力を処理する

あとは特定のキーが押されたときをパラメータに変換します。
簡単ですね。ここではキーボードの「↑」「↓」キーに設定してみます。

```bash
$ (f(){ read -s -n1 -t0.2 a;xxd -ps -l1 <<<"$a";f; };f)|awk '/5b/{a*=1}/41/{a-=1}{print a}'
```
しかしこれはうまくいきませんでした。実行してみればわかりますが、「↓」キーには反応しますが「↑」キーには反応しないです。
理由はわかりませんでしたが、**「↑」キーを押した直後に行が更新されるとターミナルから「↓」キーと同じ5bが入力されてしまう**ようです。私のターミナルのせいかもしれません。

面倒なのでキーを「J」キーと「K」キーに変更します。これはうまくいきました。
1~9の間で値を上下させることができます。

```bash
$ (f(){ read -s -n1 -t0.2 a;xxd -ps -l1 <<<"$a";f; };f)|awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+5}'
```

![image_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155890/21ae0cfa-9785-088b-58c3-5e12726c0e82.gif)

### パイプに処理を流す

ここまではサクサクいったのですが、awkからの処理をパイプに流そうとすると問題がおきます。
下記のコマンドを実行すればわかりますがパイプに流すと表示されなくなってしまいます。

```bash
$ (f(){ read -s -n1 -t0.2 a;xxd -ps -l1 <<<"$a";f; };f)|\
  awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+5}'|\
  cat
```

awkは出力がパイプだと認識すると、出力をバッファしてしまうためのようです。
普段は陰ながら私を支えてくれるバッファが、今回ばかりは苦しめてきます。
しかし、awkにはこれを解消するfflush()という関数があります。

```bash
$ (f(){ read -s -n1 -t0.2 a;xxd -ps -l1 <<<"$a";f; };f)|\
  awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+5;fflush()}'|\
  cat
```

fflush()のおかげで表示ができるようになります。


### 画面にラケットを描画する

あとは良い感じに画面に描画するだけですね。
ANSIエスケープシーケンスを利用して画面に描画します。

```bash
$ (echo -e 'e\[s';f(){ read -s -n1 -t0.4 a;xxd -ps -l1 <<<"$a";f; };f)|\
  awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+5;fflush()}'|\
  xargs -L1 -I_ bash -c "echo _;seq -w 1 12|sed 's/./ /g'|sed '_s/ /|/';echo -e '\e[u'"
```

![image_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155890/47668ca1-87b0-dec6-70d0-5f5e6e0ae891.gif)


ついに画面にラケットを表示できました。

### 画面に壁を描画する

パイプでつなぐだけですね。

```bash
$ (echo -e 'e\[s';f(){ read -s -n1 -t0.4 a;xxd -ps -l1 <<<"$a";f; };f)|\
  awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+6;fflush()}'|\
  xargs -L1 -I_ bash -c "echo _;seq -w 1 10|sed 's/.*/ /'|sed '_s/ /|/';echo -e '\e[u'"|\
  sed -r ':a s/$/ /;/.{20}/!ba;2,10s/$/|/'
```

![image_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155890/85d79592-a848-c951-f3b6-2a00f1d0046c.gif)


### ボールの描画とゲーム処理の簡単な実装

ボールを描画します。
パラメータを保持したり、壁への跳ね返りなども追加します。

```bash
$ (echo -e 'e\[s';f(){ read -s -n1 -t0.1 a;xxd -ps -l1 <<<"$a";f; };f)|\
  awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+5;fflush()}'|\
  xargs -L1 -I_ bash -c "echo -e '\e[u'_;seq -w 1 10|tr -d '[0-9]'|sed '_s/^/I/';echo -e '\e[u'"|\
  awk '{printf("%1s%20sI\n",$0,"");fflush()}'|\
  awk -F "" -vOFS="" 'b<2{b=2}NR%12==5{b=b+a;$b="@"}$0~/I@|@I/{a*=-1}{print $0,a,b}' a=1 b=5
```

また、正規表現を描くときに「|」(パイプ)だと面倒なので「I」(大文字のI)に変更します。

![image_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155890/a4d99411-0b8a-4ba1-9849-e5607a62438a.gif)


### 上下に跳ね返るように

```bash
$ (echo -e 'e\[s';f(){ read -s -n1 -t0.1 a;xxd -ps -l1 <<<"$a";f; };f)|\
  awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+5;fflush()}'|\
  xargs -L1 -I_ bash -c "echo -e '\e[u'_;seq -w 1 10|tr -d '[0-9]'|sed '_s/^/I/';echo -e '\e[u'"|\
  awk '{printf("%1s%20sI\n",$0,"");fflush()}'|\
  awk -F "" -vOFS="" 'x<2{x=2}x>2&&NR%12==0{y+=b;if(y<2||y>10){b*=-1}}NR%12==y{x=x+a;$x="@"}$0~/I@|@I/{a*=-1}{print $0,a":"x" "b":"y;fflush()}$1~/@/{b=0}' a=1 b=1 x=5 y=5
```

![image_5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155890/9b340553-041f-8a90-da52-10027fd2d566.gif)


ほぼほぼ完成ですね。
ここはデバッグコードが見られるので面白いかもしれません。

### Scoreの追加と調整

スコアを追加し、デバッグコードを削除します。
またフィールドの幅や更新間隔などを変更し遊べるようにします。

```bash

$ (f(){ read -s -n1 -t0.1 a;xxd -ps -l1 <<<"$a";f; };f)|\
  awk '/6a/{a+=1}/6b/{a-=1}{gsub(5,4,a);print a+5;fflush()}'|\
  xargs -L1 -I_ bash -c "echo -e '\e[J\e[H';seq -w 1 10|tr -d '[0-9]'|sed '_s/^/I/'"|\
  awk 'NR%11{printf("%1s%30sI\n",$0,"");fflush()}!(NR%11)' |\
  awk -F "" -vOFS="" 'x<2{x=2}NR%11==0{print "score:"i" ";i+=1;y+=b;if(y<3||y>9){b*=-1}}NR%11==y{x=x+a;$x="@"}$0~/I@|@I/{a*=-1}{print;fflush()}$1~/@/{b=0;i-=1}' a=1 b=1 x=5 y=5

```

完成ですね。

![shell_geime_pv.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155890/351b7cf5-e932-4002-c56e-5633ac5659e2.gif)


## 勉強になったもの

### gawkのfflush()

知らなかったので学びがありました。
個人的に使う機会は少なく、man読んでいて驚きました。
https://linuxjm.osdn.jp/html/GNU_gawk/man1/gawk.1.html

また、この関数の実装の違いなのかgawkでは動くがmawkでは動かないことに気づかず少し焦りました。

### ANSIエスケープシーケンス (CSI (Control Sequence Introducer (Indicator))コード)

ANSIエスケープシーケンスは、面白いし素晴らしい仕様ですね。

正確にはその中の「CSI (Control Sequence Introducer (Indicator))コード」と呼ばれるものらしいですが、[^1]
これについての仕様や規格などを網羅しているサイトや記事を知っている方はぜひ教えていただければ幸いです。
ワンライナーで無ければ、簡単にゲームとかおもしろいコマンドとか作れそうですね。

## まとめ

完成してみるとほぼawkで書いてますね。。。
私はシェル芸初心者のため簡単なゲームの作成にとどまりましたが、
この記事がシェル芸やターミナルで面白いゲームを作る取っ掛かりになれば幸いです。

## おまけ
世界で最もクリーンで洗練されたOSSプロジェクトの一つである[super_unko](https://github.com/unkontributors/super_unko)にある
unko.pyramidコマンドは知っていますか？[^2]
<a href="https://github.com/unkontributors/super_unko"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/unkontributors/super_unko.png" width="460px"></a> 

```bash
## クリスマス🎄もシェル芸で彩れます！
$ unko.pyramid 8|tr 💩 🎄|sed '1s/🎄/👑/';yes 💩|head -2|xargs printf '% 11s\n'
       👑       
      🎄🎄      
     🎄  🎄     
    🎄🎄🎄🎄    
   🎄      🎄   
  🎄🎄    🎄🎄  
 🎄  🎄  🎄  🎄 
🎄🎄🎄🎄🎄🎄🎄🎄
       💩
       💩
$ banner Merry Christmas!
#     #
##   ##  ######  #####   #####    #   #
# # # #  #       #    #  #    #    # #
#  #  #  #####   #    #  #    #     #
#     #  #       #####   #####      #
#     #  #       #   #   #   #      #
#     #  ######  #    #  #    #     #

 #####                                                                    ###
#     #  #    #  #####      #     ####    #####  #    #    ##     ####    ###
#        #    #  #    #     #    #          #    ##  ##   #  #   #        ###
#        ######  #    #     #     ####      #    # ## #  #    #   ####     #
#        #    #  #####      #         #     #    #    #  ######       #
#     #  #    #  #   #      #    #    #     #    #    #  #    #  #    #   ###
 #####   #    #  #    #     #     ####      #    #    #  #    #   ####    ###
```

[^1]: https://www.mm2d.net/main/prog/c/console-02.html
[^2]: super_unkoのリンクカードは、https://ghlinkcard.com/ で作成しており公式のものでは無いです。