【linux】shellスクリプトで簡単なゲームを作ってみよう

2020年1月17日

アザラシが昔サーバー系の会社に入った際、linuxの研修のようなものがあったのですがあまりに座学が多く寝そうになった時がありました。
(ていうかほぼ寝てた)

その時に、コソコソと一人でshellによるゲームを作っていました。
これが案外shellでスクリプトを書く上で勉強になったので、
役に立てばと思い記事にしました。

この記事の対象者としては、今まさに当時の自分のように
linuxを勉強し始めた人ぐらいに見てもらえたらと思っています。

ゲームを作るにあたってやり方を一つずつ説明しますので、記事は長めですのでゆったり見てくださいね。

0.参考にしたサイト

■ 初心者向けシェルスクリプトの基本コマンドの紹介
■ シェルやシェルスクリプトで乱数を使う2つの方法
■ 予約語
■ コマンドでキーボードからの入力を受け取る
■ シェルスクリプト(bash)のif文とtestコマンド([])自分メモ
■ シェルスクリプトで数字かどうか判断する方法(exprだけじゃない)
■ 終了ステータス
■ 関数の使用方法
■シェルスクリプトwhileの使い方

1.どんなゲーム作ったの?

本当に簡単なゲームですが数字あてゲームです。
0~9までの数字を対話式スクリプトで、数字を当てるようなゲームです。

知識としてはshellスクリプトの作り方、linuxの基本的な操作が
ある程度わかっていればネットで調べつつできるようなレベルです。

2.作ってみよう

linux上で、ユーザーのホームディレクトリ上にファイルを作成していきたいと思います。

$ cd ~ ;ユーザーディレクトリに移動
$ vi numgame.sh ;ゲーム用shellスクリプトファイルをviで作成。

一応 vi で説明していますが、vimが使える環境であるならば
vimのほうが良いです。(見た目がvimの方が見やすい)

#!/bin/sh
echo "Hello World!"

挿入モードを解除 :wq を打ってファイルを保存。
(wqはwirteしてquitの意味)
挿入モードの解除は escキーで 挿入モードに入るのは i です。

とりあえず一旦このようなファイルを作りましょう。

次にこのファイルに実行権限を与えます。実行権限が無いと
実行できないので、付与します。

$ chmod u+x numgame.sh

これでファイルに実行権限ができました。
テラターム等だと緑色の文字で表示されるようになったかと思います。
では実行してみましょう。

$ sh numgame.sh
Hello World!

2行目のように Hello World! と出ていればOKです。

numgame.shの記述について説明します。
#!/bin/sh
このスクリプトは シェルスクリプト(bash)を使用して動きます。という
宣言です。シェルは bash,zsh,fish,csh等色々あり、他のシェルで動かしたい場合は記述を変更する必要があります。
echo
echoと書くことでテキストを標準出力ができます。
結果を出力したいときなどに結構使います。

2-1.数字あてゲームを作る要件

数字あてゲームを作るにあたって、必要な要素は
・0~9のランダムな数字を作ること
・対話形式でゲームを進めること
・ユーザーからの入力を数字のみで受け付ける事
・ユーザーから入力された数字が答えよりも上もしくは下であれば
 その旨を返す

・正解するまで何度も入力できるようにする
・正解したら何回で正解したか表示し、ゲームを終了するか再度遊ぶかの
 選択を出す。

です。これだけズラズラ並べると難しそうに感じますが、
すべて調べれば出る基本的なものを組み合わせて作ることができます。

2-2.ランダムな数字を表示してみよう

ランダムな数字を作るということは乱数を使用するということです。
シェルスクリプトで乱数を作成する方法を調べて見ましょう。

参考サイト : シェルやシェルスクリプトで乱数を使う2つの方法

bashには $RANDOM という変数がデフォルトで用意されています。
とりあえずこれを echo で出力してみましょう。
先程のゲームスクリプトではなく、cd や vim などを打ついつもの画面で
やります。

$ echo $RANDOM
ランダムな自然数

上記の echo $RANDOM と打つことでランダムな自然数が表示されたかと思います。ただこれだと数が 0~32767とかなり多い数になってしまいます。
今回は 0~9で生成したいので少しやり方を変えます。

$ echo $(($RANDOM % 10))

上記の記述をすることで 0~9の数字を出力することができます。
何をやっているかというと 0~32767 でランダムに出た数字を
10で割り、余った数字を出しているということです。

なので例えば 25565 であれば 25565 ÷ 10 = 2556 余り 5
ということになり、出力される値は 5 となります。

ただこのやり方の問題は、bashは 0~32767 なので答えの結果として
8,9が気持ち少ない割合になるぐらいですね。

では、これを先程作成した numgame.sh に入れ込んでいきましょう。

$ vim numgame.sh
#!/bin/bash

echo $(($RANDOM % 10))

先程は
#!/bin/sh
としましたが、bash独自の $RANDOM を使用しているので
#!/bin/bash
としてbash独自の機能を使いますと明示化しましょう。
(こうしなくても動くとは思いますが)

では起動してみましょう

$ sh numgame.sh
0~9の乱数

上記のようになりましたでしょうか?
なってない場合はスクリプトを見直して見てください。

数字あてゲームを作りにあたって、乱数を作るという部分は
まずはクリアしましたね。ですが、この場合だと
起動した際に乱数を生成して結果を表示している事になりますので、
変数に格納してみましょう。

#!/bin/bash
randomNumber=$(( $RANDOM % 10 ))
echo $randomNumber

変数は randomNumber のようにわかりやすい名前をつけます。
変数を作る時に重要なのは シェルスクリプトでもともと使用している
名前はつけてはいけないということです。これを予約語といいます。

■ 予約語

上記のリンクにあるような case do done 等の名前はつけてはいけません。
重要なので憶えておいてください。

randomNumber=$(($RANDOM % 10))
とすることで、randomNumber という変数に乱数の値を格納でき
利用しやすい形になります。
変数に値を格納する時に重要なのは
変数名と格納値の間の = の前後にスペースを入れてはいけません。
正しく実行できないので注意してください

echo $randomNumber の部分ですが、変数を出力しています。
変数を使用するときは randomNumber とするのではなく
$randomNumber として使用します。

これでファイルを実行することで、同様に乱数が表示されていれば
OKです。

2-3.対話形式スクリプトを作成してみよう

対話形式スクリプトとは、実行した際に
これでいいですか?(Y/N)
のようなものが出るスクリプトです。
yum install ~~~ などの際に
ok?(Y/n)
のように出るのと同じです。

先程のゲームスクリプトを編集します。

#!/bin/bash
#randomNumber=$(( $RANDOM % 10))

read -p "名前を入力してね : " yourName
echo $yourName

上記のようにします。 変数のrandomNumberの先頭に #がついているのは
コメントアウトということです。
# これは乱数です
のようにコメントを入れたいときは # を入れたりします。
他にも上記のように今はこの記述を使わない場合は
#を先頭につけることで、その行を実行しないということができます。

上記を実行すると

$ sh numgame.sh
名前を入力してね : azarashi <- 自分で入力し、最後にエンターを押します。
azarashi

上記のようになります。

read というコマンドは キーボードからの入力を待ち、入力された内容を変数にセットするコマンドです。

参考サイト : コマンドでキーボードからの入力を受け取る

read コマンドに -p というオプションを付けることで、
名前を入力してね という部分を作成できるようにしています。
つけない場合は 事前に echo “名前を入力してね"
というようにしなければいけません。

2-4. 入力の結果によって出力を変える

先程、readコマンドで対話式形式で動かすことができました。
今度は受け取った値によって、出力を変えるようにしてみましょう。

数字あてゲームを作るということは、受け取った数字が
答えよりも上か?下か?正解か?という返答をする必要があります。
ですので、受け取った数字によって出力を変えてみましょう

参考にしたサイト : シェルスクリプト(bash)のif文とtestコマンド([])自分メモ

#!/bin/bash
randomNumber=$(( $RANDOM % 10))

read -p "数字を入力してね : " reciveNumber

if [ $reciveNumber -gt $randomNumber ] ; then
echo "${reciveNumber} は答えよりも大きいです"
elif [ $reciveNumber -lt $randomNumber ] ; then
echo "${reciveNumber} は答えよりも小さいです"
elif [ $reciveNumber -eq $randomNumber] ; then
echo "${reciveNumber} は答えです!"
fi

numgame.shを以上のように編集してください。
いろいろと不明点があるかと思いますので、一つずつ説明していきます。

if [] ; then
これはプログラムでは基礎的な記述で if 文といいます。
使い方としては
もし AがBよりも大きければ~や
小さければ~や
一致すれば~
等の処理をしたい時に使います。
余談ですが
最近の言語であれば大体は
if( a == b){

}
のような書き方ですが、shellスクリプトでは
if[] ; then という書き方をします。

なので、shellスクリプトの書き方でif文を説明すると

もし [ Aが 大きければ Bよりも ] ; それなら
 ○○します
if [ $A -gt $B ] ; then
echo “hogehoge"
ということです。

ただまだ不明点があるかと思います。
-gt ってなんだよ!
ですよね。これは数式で書くなら
>
ということです。英語で
greater than (よりも大きい)
ということです。

つまり上記のスクリプトで
-lt とあるのは
<
ということで、英語で
less than(よりも小さい)
ということです。

なんとなくわかってきたでしょうか?

次に、 elif の説明をします。
if はわかったけど、elifってなんだよ。

これは else(それ以外の) if(もしも)
他の言語だと elif と略さず
else if(){}
として使ってる場合が多いです。

ちなみにこれを使わず、
if [] ; then
fi
if [] ; then
fi



としてやることも可能です。

じゃあなぜやらないか?
この処理の方法だと何が問題なのか。それは
すべてのifを比較するので処理としてムダで、
なおかつバグる可能性高い書き方だからです。

例えばこんな式があったとします。

数字=10
if [ 数字 -lt 100 ] ; then
echo "数字は100より小さいです!"
fi
if [数字 -gt 0] ; then
echo "数字は0よりも大きいです!"
fi

これを実行するとどうなるでしょうか?
答えは、
数字は100より小さいです!
数字は0よりも大きいです!
と出てしまいます。
出力されてほしいのがどちらかでいい場合はこのように出てしまうのは
よくありません。

それにこのif文が今は2個ですが、3個、4個、5個・・・と
もっと増えて行ったらどうでしょう?
毎回処理をするたび上から一個ずつ処理をしていったらとんでもなく
処理が大変ですよね?

そういうときに elif を使って、一回目の比較で
該当したら他のifは処理せず if文を抜けるということができます。

ただ、必ずしも elif でやったほうが良いというわけではなく、
処理によっては if … fi if … fi のほうがいい場合もあります。
書くスクリプトを良く考えた上で処理を変えましょう。

さらっと説明を飛ばしましたが
fi は ここがif文の終わりだよ。と記述しています。
書かないとバグります。

2-5.数字以外を入力されてしまったら

さて、先程の説明では数字を入れる前提で話してましたが、
実際にユーザーがその意図を汲み取って数字を絶対入れるとは
限りません!

試しに先程のスクリプトで文字を入れてみましょう。

numgame.sh: 7 行: [: あ: 整数の式が予期されます
numgame.sh: 9 行: [: あ: 整数の式が予期されます

(ノ∀`)アチャー
エラーを吐きましたね・・・。
今試しに あ という文字を入れましたがいい感じに怒られました。

ですので、ココに文字を入れられたら
数字を入れてください!(怒)と出るようにしましょう。

参考サイト : シェルスクリプトで数字かどうか判断する方法(exprだけじゃない)

expr を使うことで値が数値か、文字列か判別することができます。
これは与えた引数を式として評価してくれるツールで、数式の足し算とかもできます。

その際に終了ステータスというものが取得でき

  • 0: 式が正しく評価され、評価値が0かnull以外の場合
  • 1: 式が正しく評価され、評価値が0かnullのとき
  • 2: 式が不当なとき
  • 3: (GNU版のみ)その他エラーが起こったとき

となります。入れた値が2未満であれば基本的には
数字を入れられたとして確認できます。

なので、終了ステータスを取得して判別してみましょう。

#!/bin/bash
randomNumber=$(( $RANDOM % 10 ))

read -p "数字を入力してね : " reciveNumber
expr $reciveNumber + 1 > /dev/null 2>&1
errorCode=$?
echo $errorCode

これを実行して  と入力したら
2 と返ってきました。

終了ステータス表に照らし合わせると、
2:式が不当なとき
にあたりましたね。成功です

expr $reciveNumber + 1
として $reciveNumber に + 1をして式を評価しています。
ただ、このまま実行すると

expr : 整数でない引数

というエラー出力が出てしまいます。
この文章を出してもいいとは思うのですが、
数字を入れてください!(怒) という文章にしたいので
これは不要ですね。
そういう場合はこの出力を
/dev/null
に送ることでエラー出力がされず/dev/nullに送られます。
エラーなどの不要な出力を出したくないときは
shellスクリプトでは大抵 /dev/null に送ります。

ちなみに > で出力を送る方法はlinuxでは基本のやり方で
あまり説明する必要もないかと思いますが、一応説明すると
標準出力をファイルとして出力したりする時に使います。

$? についてですが
参考サイト : 終了ステータス
の通りで、expr等の終了ステータスがあるものは
linuxでもともと用意されている終了ステータス用変数があり
そちらに終了ステータスコードが格納されます。
今回はそれを利用しています。

これらを踏まえた上で終了ステータスが 0 もしくは 1ではないときは
数字を入れてください!(怒)
としてみましょう。

#!/bin/bash
randomNumber=$(( $RANDOM % 10 ))
read -p "数字を入力してね : " reciveNumber
expr $reciveNumber + 1 > /dev/null 2>&1
errorCode=$?

if [ $errorCode -ge 2 ] ; then
echo "数字を入れてください(怒)"
fi
$ sh numgame.sh
数字を入力してね : あ
数字を入れてください(怒)

以上のような結果になりました!
無事成功しましたね。

ちなみにif文で使用した、 -ge は
grater equal(以上)
なので、 2を含むそれ以上の値ということです。
数式で表すなら
>=
です。

ちなみに・・・
よく数字を扱う上での以上と以下と未満と超えるを間違える人がいるので書いておくと

以上は 値Xを含むそれよりも大きい値
以下は 値Xを含むそれよりも小さい値 
超えるは 値Xを含まないそれよりも大きい値
未満は 値Xを含まないそれより小さい値

です。間違えてる人がいたら違うよ。と教えて上げてください。

では今までのものをくっつけて、数字が入力されたら
数字を比較して、それに対応する出力をしましょう

#!/bin/bash
randomNumber=$(( $RANDOM % 10 ))
read -p "数字を入力してね : " reciveNumber
expr $reciveNumber + 1 > /dev/null 2>&1
errorCode=$?

if [ $errorCode -ge 2 ] ; then
echo "数字を入れてください(怒)"
else
if [ $reciveNumber -gt $randomNumber ] ; then
echo "${reciveNumber} は答えよりも大きいです"
elif [ $reciveNumber -lt $randomNumber ] ; then
echo "${reciveNumber} は答えよりも小さいです"
elif [ $reciveNumber -eq $randomNumber ] ; then
echo "${reciveNumber} は答えです"
fi
fi

のようになりました、
これを実行してみると、文字を入れたらエラーがでて
数字を入れたらしっかりと数字を比較してくれてます!
やったー!

ただ、このままだと一回一回実行してしかも答えが毎回変わってしまいます。一発で当てるってもうそれは別ゲー感があるので、
当てるまで何度もできるようにして、何回目であてれたか出るようにしましょう!ココまできたらほぼ仕上げです

2-6.数字チェック処理を関数にしよう

参考サイト :
■ 関数の使用方法
■シェルスクリプトwhileの使い方

何度でも入力できるようにするということは、ループ処理を使います。
ループ処理をさせるには while を使用しますが、
その前に数字をチェックして、出力する部分を関数化し、
while を使う時にわかりやすいようにしたいと思います。

#!/bin/bash
randomNumber=$(( $RANDOM % 10))
read -p "数字を入力してね : " reciveNumber

checkNumber(){
if [ $reciveNumber -gt $randomNumber ] ; then
echo "${reciveNumber} は答えよりも大きいです"
elif [ $reciveNumber -lt $randomNumber ] ; then
echo "${reciveNumber} は答えよりも小さいです"
elif [ $reciveNumber -eq $randomNumber ] ; then
echo "${reciveNumber} は答えです!"
fi
}

expr $reciveNumber + 1 > /dev/null 2>&1
errorCode=$?
if [ $errorCode -ge 2 ] ; then
echo "数字を入れてください(怒)"
else
checkNumber
fi

上記のようになりました。
このまま実行しても同様の結果が得られます。
関数を追加したので、そこの説明をしたいと思います。

checkNumber(){
処理を記述する
}
これは関数で、同じような処理を何度も呼び出したい場合
何度も何度も記述するよりもこの関数を呼び出すだけで
良くなる他、コードを見た時に処理がわかりやすくなるので
使えるときは使っていったほうが良いです。

この関数を呼び出すときは
checkNumber
と記述することで使えます。

変数に関してですが

number1=0 ; これはグローバル変数
local number2=0 ; これはグローバル変数
checkNumber(){
  local number3=0 ; これはローカル変数
  number
  
  echo $number1 ; 正しく出力される
  echo $number2 ; 正しく出力される
  echo $number3 ; 正しく出力される 
}
echo $number1 ; 正しく出力される
echo $number2 ; 正しく出力される 
echo $number3 ; 出力されない

number1 はグローバル変数でどこで参照しても出力されます。
local をつけるとローカル変数になりますが
local number2 は numgame.shに対してのローカル変数なので結局グローバル変数です。
local number3 は checkNumber内でのローカル変数なので、
関数を抜けてしまうと参照できません。
スコープが違うと参照できないということです。

これで一旦関数の機能として数字のチェックをすることができましたので、
ループ処理に移りましょう。

2-7.ループ処理

ではいよいよループ処理です。
ループ処理にはwhileを使います。
ので、その処理を書いてみました。

諸々ループで処理させるにあたってコードを変更してるので
確認してくださいね

#!/bin/bash
randomNumber=$(( $RANDOM % 10 ))
loopBreak=false;

checkNumber() {
if [ $reciveNumber -gt $randomNumber ] ; then
echo "${reciveNumber} は答えよりも大きいです"
elif [ $reciveNumber -lt $randomNumber ] ; then
echo "${reciveNumber} は答えよりも小さいです"
elif [ $reciveNumber -eq $randomNumber ] ; then
echo "${reciveNumber} は答えです!"
loopBreak=true
fi
}

while true
do
read -p "数字を入力してね : " reciveNumber
expr $reciveNUmber + 1 > /dev/null 2>&1
errorCode=$?
if [ $errorCode -ge 2 ] ; then
echo "数字を入れてください(怒)"
else
checkNumber
if $loopBreak ; then
break
fi
fi
done

while true
do
done
ですが、while 条件式 で条件式が成立するときはdoの内容を
実行し続けるということです。
今回は無限ループで良いので while true とすることで、
条件式は 真 となり継続し続けます。

ただ、延々と継続していては処理を終わらせることができません。
ですので、正解したら処理を抜けるようにしましょう。

loopBreakという変数に falseを入れ
正解した処理の部分で loopBreak=true
といれます。

do内で

checkNumber
if $loopBreak ; then
break
fi

checkNuberの処理が終わった後に
$loopBreakがtrueであれば breakするという処理を入れました。

breakはwhileループを終了させる記述です。
これで答えが正解になった時にwhileループから抜けます。

これでゲームができました!

ただ、whileをブレークさせる為に loopBreakという
グローバル変数を用意するのは少し強引かな?
とも思ったのですが、他言語であればある程度
やり方をわかっているでできるのですがシェルスクリプトでの
いいやり方が思いつきませんでした。
whileの継続条件式をloopbreakにしておけばよかったかな?
(いい方法があれば教えてください)

試しにゲームをしてみましょう

このようになりました!
ゲームとして遊べていますね

3.何回目で成功したかを作る

これを作るのは割と簡単です。
グローバル変数でカウントする変数を用意して、
while do 内で、カウントを加算していきます。
最後に、正解したコメントを出す部分で、何回目に成功したか?
を書けばいいだけです。

ココまでできれば、たやすいかと思います。
是非、自分で頑張ってみてください。

4.他に

自身が研修期間のときは難易度も作成しました。
最初に難易度を4つ用意し、難易度によって
乱数を0~9ではなく0~50にしたり、
0~10000にしたりして難易度を上げてみましょう。

ちなみにこのゲームは1時間程でで作りました。
ある程度勉強していればできるような内容だと思いますので。