khurata’s blog

khurata’s blog

C によるマルバツゲーム実装のサンプル

(もともと「Yahoo!知恵袋」の「知恵ノート」だったものを転載しています)
(最終更新日時:2016/5/23)投稿日:2015/11/3

はじめに

 C プログラミングの「古えの課題」として取り上げられる事のある「マルバツゲーム」について、次のような質問が「知恵袋」に提示されていました(q11152109646)。

f:id:khurata:20190404080450j:plain

 これについて回答例を作っていたのですが、なぜか翌日には質問が消されていて回答が出来ませんでした。 せっかく作ったものを埋もれさせるのももったいないので、ここ「知恵ノート」で披露しようと思います。

 

疑問点

 質問された「お題」には、1つ、謎があります。 「キーボードの数字キーで場所選択」という点です。
 これは、テンキーで盤上のカーソルを動かして決定する、というイメージなのか、それとも、数字で座標入力をさせる、というイメージなのか、もはや質問が消されているので、質問者様に確認する事も出来ません。 しかも質問者様は ID 非公開です。

 ただ、前者のイメージでは、標準 C のライブラリ機能ではおそらく実現が出来ない(あるいはかなり難しい)と私は理解しているので、本作例では後者に近いイメージで勝手に作りました。

 

プログラム例

 行数は約660行に過ぎませんが、説明用に注釈を入れまくって全体で1万字を越えるため、「Yahoo!ボックス」にアップロード致しました。
http://yahoo.jp/box/cmIhla
文字エンコーディングShift_JIS です。

※ いつの間にか「Yahoo!ボックス」の共有機能がサービス終了していたため、下記にアップロードし直しました(2021/01/09)。

http://khurata.dyndns.dk/QA/q11152109646.zip

 

説明

 詳細についてはプログラムの注釈を見ていただくとして、ここでは本作例について、作者自身がポイントと考えているいくつかの留意点について説明を致します。

1.関数分割について

 C は「プログラムを関数に分割して書く」という特色を持つ言語であり、これを上手く使うことがコツです。 この特色は C 以降の多くの言語に受け継がれていったので、このコツを身につけておくことは、現代プログラマの基礎知識とも言えます。
 作例は、全体として見れば短くはないかも知れませんが、1つ1つの関数は短く単純です。 注釈を見れば分かりますが、それぞれの関数は、「その関数でやりたい事1つだけ」の機能を実現するように書かれています。

 本当に簡単で小さなプログラムであれば、main 関数1つに全ての機能を押し込めても良いのですが、少し複雑なプログラムになってくると、「1関数方式」では難しくなってきます。
 C プログラムを作っていて、「どうも、この関数は長いな…」とか、「どうも、この関数はうまく行かないな…」と感じたら、その関数が「実は1つの機能ではなく、複数の機能が組み合わさってしまっている」のではないか、と疑ってみてください。

 また、「1機能1関数」を徹底する事によって、「差し替えが容易になる」という利点も生まれます。 たとえば printBoard関数(194行目~)は、本作例では標準出力に盤を描くプログラムにしていますが、この関数だけをグラフィック表示に差し替えたり、あるいはネットワーク送信に差し替える事も出来ます。 これを差し替えても、他の関数に変更は必要ありません。

2.人間の入力について

 基本的に、プログラムの外部から入力される値については、全て「正しい範囲のものしか受け取らない」ようにプログラミングすべきです。
 特に、人間が入力してくる値(キーボード、音声入力、タブレットペン入力など)は、全て疑ってかからなければなりません。 正しい値だけを受け取り、正しくない値は受け取らない、これを徹底することによって、周囲のプログラムがラクになります。

 たとえば作例の choiceOrder関数(150行目~)は、ただ 1 か 2 かを入力させるだけの関数ですが、この関数は、1 か 2 以外の値を決して返しません(158行目があるから)。 こうする事によって、呼び出し側(main関数)では、これをただ呼び出すだけで事足りています(68行目)。

 同様なことは inputPlace関数(327行目~)についても言えます。 ここでこれだけ慎重な処理をし、正しい結果しか返さないからこそ、これを呼び出す main関数側では、94行目や101行目のように、シンプルなプログラムが書けるのです。

 このように、「下請け」関数が、正しい結果しか返さない(返せない時は明確に異常であることを返す)ことによって、その上位関数では、「やりたい事だけを簡潔に書ける」ようになっていきます。

 もう1度、main関数をよく見てください。 main関数にはゲーム進行についての事しか書かれていない事に気付かれたでしょうか。 ゲーム進行を支える細かいことは、全て他の関数に投げているからです。

3.より短く書ける記法について

 本作例は初学者向きに書いたので、「カンマ演算子(順序演算子)」と「3項演算子」をあえて使いませんでした。 また、論理式の結果を直に使うこともしませんでした。 しかし、この3つを妥当に使えば、より簡潔な C プログラムが書ける事はよくあります。

 カンマ演算子を使うと、たとえば170行目~173行目は、次の1行だけで書けます。

while ( printf( "... あなたの相手を選んでください. %d.人間、%d.CPU.\n", OTHER_HUMAN, OTHER_CPU ), scanf( " %d", &n ), ( n != OTHER_HUMAN ) && ( n != OTHER_CPU ) );

 3項演算子を使うと、たとえば482~487行目は、次の1行だけで書けます。

player ? printf( "... プレイヤー %d(%c) の勝ちです!\n", player, MARK_STR[ player ] ) : puts( "... 引き分けです." );

 また、論理式の結果を直に使えば、たとえば242~245行目は、次の1行だけで書けます。

return ( cnt >= ( COL_MAX * COL_MAX ) );

 このような短く書ける書法は、むやみやたらと使うと可読性を損なうこともありますが、逆に、短くすることによって可読性を高めることもあり、一長一短です。 自分が書きやすい・読みやすい書き方を、その場に応じて使い分ければ良いと私は思います。

4.独自の型宣言について

 本作例は初学者向きに書いたので、typedef 宣言も使いませんでしたが、たとえば board の型として頻繁に出てくる char [ COL_MAX ][ COL_MAX ] は、typedef した方が、よりスッキリしたプログラムになります。

5.マクロ定数について

 本作例では「古えの C」っぽく、#define マクロを多用していますが、モダンなプログラミングにおいては、enum 列挙や const 変数を使う方が望ましいです。

6.各関数の結合について

 本作例は、コンパイル単位がこれ1つしか無いので、問題にはならないのですが、もし、複数のソースファイルをそれぞれコンパイルしてリンクする、という場合には、「そのソースファイル以外のところから使われない関数」には、全て static を付けるほうが良いでしょう。
 なぜなら、static な関数は、コンパイル単位の中でしか結合しなくなるため、リンク時に名前衝突を避ける事ができ、結果としてグローバルな名前汚染が防げるからです。

7.CPU 側の思考ルーチンについて

 本作例における CPU の思考ルーチンは decidePlace関数(382行目~)および、その下請け関数である searchReach関数(517行目~)、searchBlank関数(613行目~)です。
 本作例では、必要最低限の実装しかしていませんが、それでも3目並べくらいだと(14行目で COL_MAX を 3 にしてコンパイルする)、CPU になかなか勝てず、引き分けになってしまうことが多いでしょう。 おそらく5目並べでも、CPU に負けることは無いにせよ、勝つのは簡単では無いはずです。

 とは言え、この思考ルーチンに改良の余地が無いわけではありません。 最も改良の余地があるのは、searchBlank関数です。 この関数は、現在の空きを乱数で選ぶプログラムになっていて、相手の未来を邪魔するとか、自分の未来を有利に導くという「先読み」が全く入っていません。
 おそらく、マルバツゲーム以外の対 CPU 戦ゲームにおいても、「先を読む」プログラムを持つかどうかが、ゲームの面白味を増す大きなポイントでありましょう。

 

終わりに

 本作例が唯一絶対に正しい、というわけではありません。 本当に様々なやり方がありますし、上項で挙げた以外にも、まだまだ改良の余地があるはずです。 もし100人の C プログラマに本題を課したら、100通り以上の実装例が作られるでしょう。

 本作例が、何らかの参考になれば幸いです。
(転載以上)