この記事はSmartHR Advent Calender 2023の16日目の記事です。
背景
今年もAdvent Calenderの季節がやってきたのでテーマについて考えていました。ふと子供がプラレールで遊んでいるのを眺めていて、プログラミングと電車で何かできないか?と考えてみました。もしRubyで電車を走らせてみたら子供が喜んでくれるかな?と思い、Rubyで電車を走らす何かを作ってみることにしました。
プログラミングと電車といえば sl コマンド
Unix系OSで ls コマンドを実行しようとした際に、 sl とミスタイプすると蒸気機関車がコンソール上に走るという有名なコマンドがあります。
この sl コマンドを参考に、Ruby版を実装してみることにしました。
謝辞
- 本記事内のソースコードはslコマンドのLICENSEに則ります。
- このような機会を与えていただいた slコマンドと作者の
Toyoda Masashi
さんに感謝いたします。 - 以降は若干ネタ風になりますが温かい目で見守ってください。
slコマンドのコードリーディング
sl コマンドはC言語で実装されているため、ChatGPTに実装内容を聞きつつコードリーディングを行いました。その結果、以下のことがわかりました。
蒸気機関車を走らせている間は各種操作をブロックする
SLコマンドを実行して機関車を走らせると、キーボードでCtrl + C
を押しても機関車を止めることができません。これはプログラム上でシグナルを無視する設定が機関車を走らせる前にされていました。
signal(SIGINT, SIG_IGN);
この他にも
- キーボードから入力された文字の非表示
- カーソル非表示
- スクロール無効
も合わせて設定されており、とにかく機関車が走り終えるまでは止めさせないという強い意志とジョークプログラムとしての面白さを感じられました。
cursesライブラリで画面描画
C言語に端末制御をする curses
というライブラリがあります。これを利用することで文字の入出力を制御することで、蒸気機関車の描画を行っていました。
curses
ライブラリを応用すればちょっとしたテキストゲームを作ったり、テキストインターフェースアプリケーションの開発もできるようです。
蒸気機関車のアニメーションは二次元配列を利用
蒸気機関車の本体は1行毎に定数定義されていました。
#define D51STR1 " ==== ________ ___________ "
#define D51STR2 " _D _| |_______/ \\__I_I_____===__|_________| "
#define D51STR3 " |(_)--- | H\\________/ | | =|___ ___| "
#define D51STR4 " / | | H | | | | ||_| |_|| "
#define D51STR5 " | | | H |__--------------------| [___] | "
#define D51STR6 " | ________|___H__/__|_____/[][]~\\_______| | "
#define D51STR7 " |/ | |-----------I_____I [][] [] D |=======|__ "
またアニメーションのコマとなるホイール部が6パターン定数定義されていました。
#define D51WHL11 "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ "
#define D51WHL12 " |/-=|___|= || || || |_____/~\\___/ "
#define D51WHL13 " \\_/ \\O=====O=====O=====O_/ \\_/ "
蒸気機関車の本体部とホイール部を二次元配列内に定義されていました。図に表すと以下のとおりです。cursesライブラリに二次元配列から取り出した蒸気機関車を1コマずつ描画して走らせていました。
Rubyでコンソール上に電車を走らせるには?
Rubyにも curses
ライブラリが提供されていました。これを利用することでコンソール上に特定の文字列を描画することができるので、slコマンドを同じ発想を流用すればRubyでも電車を走らせることができそうです。
Rubyで電車を走らせてみよう
初級編: 絵文字の電車を走らせる
C言語のSLコマンドを参考にRubyで絵文字を右端から左端に走らせるバージョンを実装してみました。完成したコードは以下です。
# ========================================
# sl.c: SL version 5.03
# Copyright 1993,1998,2014-2015
# Toyoda Masashi
# (mtoyoda@acm.org)
# Last Modified: 2014/06/03
# ========================================
require "curses"
Curses.init_screen # Cursesの初期化
begin
Curses.curs_set(0) # カーソルを非表示
Curses.noecho # ユーザーの入力内容を画面に表示しない
0.upto(Curses.cols - 1) do |i|
Curses.clear
train = '🚃🚃🚃🚃🚃🚃🚃🚃🚃🚃🚃'
y = Curses.lines / 2 # 縦軸 中央表示
x = Curses.cols - train.length * 2 - i # 横軸 右端から表示
Curses.setpos(y, x)
Curses.addstr(train)
Curses.refresh # 画面を更新して変更を反映
sleep(0.1)
break if x <= 0 # 画面左まで到達したら終了
end
ensure
Curses.close_screen
end
実装のポイントを解説します。
まず初めにSLコマンドと同様に入力制御・画面の表示制御をします。本家のSLコマンドでは Ctrl + C
によるシグナル制御が入っていますが、 (二歳児) により Ctrl + C
が入力されることはないので、今回は見送りにします。
Curses.curs_set(0) # カーソルを非表示
Curses.noecho # ユーザーの入力内容を画面に表示しない
電車をコンソール画面の右端から左端に一コマずつ移動させるため、カウントアップを作ります。この i
は後ほど使います
0.upto(Curses.cols - 1) do |i|
end
絵文字版の電車を変数定義します。子供は電車がたくさん走っているのが好きなので、10両編成にしました。
train = '🚃🚃🚃🚃🚃🚃🚃🚃🚃🚃'
電車を画面上のどの位置に表示させるかの位置を計算します。 Curses.line
にコンソール画面の行数の値、Curses.cols
に画面上の表示可能な桁数の値が入っています。縦軸においては中央表示にするため /2
にします。横軸においては、右から左に移動させるため、 - i
でxの値をデクリメントさせていきます。
y = Curses.lines / 2 # 縦軸 中央表示
x = Curses.cols - train.length * 2 - i # 横軸 右端から表示
setpos
関数 で文字列を表示させる座標を、addstr
関数で表示させたい文字列を設定します。
Curses.setpos(y, x)
Curses.addstr(train)
refresh
関数を呼ぶことで、先ほど設定したものを画面に表示されます。これで一コマを表示させたことになります。アニメーションさせるためにコマを連続表示させる必要がありますが、sleep
でコマ送りの速度を調整します。
Curses.refresh # 画面を更新して変更を反映
sleep(0.1)
これによりコマ送りのアニメーションが動くようになり、電車の絵文字が右から左に移動します。
実行すると... 電車が走ってる〜!
中級編: アスキーアートの電車を走らせる
初級編で絵文字の🚃を走らせることができましたが、文字ですと小さすぎて子供に電車と認識されない可能性がありますので、大きめの電車も走らせてみたいと思います。
ここでは本家SLコマンドのアスキーアートを利用して蒸気機関車を走らせたいと思います。大枠の仕組みは初級編とほぼ同じです。コードの全容は以下です。
# ========================================
# sl.c: SL version 5.03
# Copyright 1993,1998,2014-2015
# Toyoda Masashi
# (mtoyoda@acm.org)
# Last Modified: 2014/06/03
# ========================================
require "curses"
Curses.init_screen # Cursesの初期化
begin
Curses.curs_set(0) # カーソルを非表示
Curses.noecho # ユーザーの入力内容を画面に表示しない
0.upto(Curses.cols - 1) do |i|
Curses.clear
train = [
" ==== ________ ___________ ",
" _D _| |_______/ \\__I_I_____===__|_________| ",
" |(_)--- | H\\________/ | | =|___ ___| ",
" / | | H | | | | ||_| |_|| ",
" | | | H |__--------------------| [___] | ",
" | ________|___H__/__|_____/[][]~\\_______| | ",
" |/ | |-----------I_____I [][] [] D |=======|__ ",
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\O=====O=====O=====O_/ \\_/ "
]
train.each_with_index do |line, index|
y = Curses.lines / 2 + index
x = Curses.cols - train[0].size - i
Curses.setpos(y, x)
Curses.addstr(line)
end
Curses.refresh # 画面を更新して変更を反映
sleep(0.1)
break if i >= Curses.cols - train[0].size # 画面左まで到達したら終了
end
ensure
Curses.close_screen
end
ポイントは蒸気機関車の全体を定義した lines
配列をループさせて、コンソール画面上に1行ずつ表示する設定をしてから、 refresh
で画面に表示させています。
実行すると... 蒸気機関車が走ってる〜!
ただホイール部が動いていないと違和感がありますね...
上級編: 動くアスキーアートの蒸気機関車を走らせる
親として子供だましは良くないのでちゃんとホイール部も動かしたいと思います。中級編のコードを改良します。コード全体は以下です。
# ========================================
# sl.c: SL version 5.03
# Copyright 1993,1998,2014-2015
# Toyoda Masashi
# (mtoyoda@acm.org)
# Last Modified: 2014/06/03
# ========================================
require "curses"
Curses.init_screen # Cursesの初期化
begin
Curses.curs_set(0) # カーソルを非表示
Curses.noecho # ユーザーの入力内容を画面に表示しない
offset = 0
0.upto(Curses.cols - 1) do |i|
Curses.clear
train = []
train[0] = [
" ==== ________ ___________ ",
" _D _| |_______/ \\__I_I_____===__|_________| ",
" |(_)--- | H\\________/ | | =|___ ___| ",
" / | | H | | | | ||_| |_|| ",
" | | | H |__--------------------| [___] | ",
" | ________|___H__/__|_____/[][]~\\_______| | ",
" |/ | |-----------I_____I [][] [] D |=======|__ ",
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\_O=====O=====O=====O/ \\_/ ",
]
train[1] = [
" ==== ________ ___________ ",
" _D _| |_______/ \\__I_I_____===__|_________| ",
" |(_)--- | H\\________/ | | =|___ ___| ",
" / | | H | | | | ||_| |_|| ",
" | | | H |__--------------------| [___] | ",
" | ________|___H__/__|_____/[][]~\\_______| | ",
" |/ | |-----------I_____I [][] [] D |=======|__ ",
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|= O=====O=====O=====O|_____/~\\___/ ",
" \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ",
]
train[2] = [
" ==== ________ ___________ ",
" _D _| |_______/ \\__I_I_____===__|_________| ",
" |(_)--- | H\\________/ | | =|___ ___| ",
" / | | H | | | | ||_| |_|| ",
" | | | H |__--------------------| [___] | ",
" | ________|___H__/__|_____/[][]~\\_______| | ",
" |/ | |-----------I_____I [][] [] D |=======|__ ",
"__/ =| o |=-~O=====O=====O=====O\\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ",
]
train[3] = [
" ==== ________ ___________ ",
" _D _| |_______/ \\__I_I_____===__|_________| ",
" |(_)--- | H\\________/ | | =|___ ___| ",
" / | | H | | | | ||_| |_|| ",
" | | | H |__--------------------| [___] | ",
" | ________|___H__/__|_____/[][]~\\_______| | ",
" |/ | |-----------I_____I [][] [] D |=======|__ ",
"__/ =| o |=-O=====O=====O=====O \\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ",
]
train[4] = [
" ==== ________ ___________ ",
" _D _| |_______/ \\__I_I_____===__|_________| ",
" |(_)--- | H\\________/ | | =|___ ___| ",
" / | | H | | | | ||_| |_|| ",
" | | | H |__--------------------| [___] | ",
" | ________|___H__/__|_____/[][]~\\_______| | ",
" |/ | |-----------I_____I [][] [] D |=======|__ ",
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|=O=====O=====O=====O |_____/~\\___/ ",
" \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ",
]
train[5] = [
" ==== ________ ___________ ",
" _D _| |_______/ \\__I_I_____===__|_________| ",
" |(_)--- | H\\________/ | | =|___ ___| ",
" / | | H | | | | ||_| |_|| ",
" | | | H |__--------------------| [___] | ",
" | ________|___H__/__|_____/[][]~\\_______| | ",
" |/ | |-----------I_____I [][] [] D |=======|__ ",
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\O=====O=====O=====O_/ \\_/ "
]
train_length = train[0][0].size
train[offset].each_with_index do |line, index|
y = Curses.lines / 2 + index
x = Curses.cols - train_length - i
Curses.setpos(y, x)
Curses.addstr(line)
end
offset = (offset + 1) % 6 # コマ送りさせるため 0 - 5を連番で発番する
Curses.refresh # 画面を更新して変更を反映
sleep(0.1)
break if i >= Curses.cols - train_length # 画面左まで到達したら終了
end
ensure
Curses.close_screen
end
まず train
配列内で蒸気機関車の6コマのアスキーアートを定義します。
each_with_index
でループして蒸気機関車を右から左に動かすのは中級編と仕様は同じです。ポイントは offset
変数で 0 - 5
の連番を発番し、画面の表示の際に train
配列の要素番号を offset
を切り替えることでホイールが動くように見せることができます。
train[offset].each_with_index do |line, index|
y = Curses.lines / 2 + index
x = Curses.cols - train_length - i
Curses.setpos(y, x)
Curses.addstr(line)
end
offset = (offset + 1) % 6 # コマ送りさせるため 0 - 5を連番で発番する
実行すると... 蒸気機関車のホイールが動いてる〜!
さて、準備は整ったかな...。
子供に見せてみた
< パパが作った電車見る?
< みる!
o0 (11月からC言語のSLコマンドのコードリーディングを初めてやっとこの日が来たか...)
初級編: 絵文字の電車
< じゃいくよ
< 🚃🚃🚃🚃🚃🚃🚃🚃🚃🚃🚃
< ん? これなに? おおきんでんしゃがいい
< はい、大きい電車ね
( この反応は予想通り )
中級編: アスキーアートの電車を走らせる
< 次はどうかな
< (大きな蒸気機関車のアスキーアートを右から左に動かす)
< おおきいだ(笑)
o0 (お、笑ってくれた! )
上級編: 動くアスキーアートの電車を走らせる
< 最後にこれはどうかな
< (アニメーションさせた蒸気機関車を右から左に動かす)
< これはけいひんとうほくせん?
< いえ、蒸気機関車です。
< けいひんとうほくせんがいい
< え...
(京浜東北線のプラレールが大好きだもんね...)
どなたか京浜東北線のアスキーアートをお持ちの方がいましたら、コメント欄にて共有いただけると助かります...
まとめ
- Rubyで蒸気機関車を走らせたら子供に喜んでもらえた。
- 子供的には蒸気機関車より京浜東北線の方が良かったみたい。
- SLコマンドをきっかけに
Curses
ライブラリの存在、概要について把握することができた。
おしまい。