スライドパズル コンソール版を作る
スライドパズルの概要
スライドパズルは、こんなやつ
スライドパズル(15パズル)
結構な人が遊んだことがあるはず。
ランダムに並んだタイルを1~15の順番にお片付けすればクリアなパズル。
作業領域として、1マス空きがあり、その空いているマスを使ってタイルの並べ替えを行う。
プログラム上で表現することを考える
必要なことを考えてみる。
- まずタイルを収めるボード
- それから数字のついたタイルx15個
- タイルを動かす方法
- 解ける問題を出題するアルゴリズム(解けない時があるかもなのに注意)
- 問題が解けたか判定するアルゴリズム
- ゲームの進行をするルーチンたち(タイトル、出題、プレイ、クリア)
こんなもんでござんしょうか?
今回作る完成版に近いやつをここに置いておきます
実行すると、こんな怪しいプログラム実行して大丈夫?的な警告出ますが、無視して実行しちゃって大丈夫です。
データ構造を考える
以下の中から、ボードとタイルをどう表現するか考える。
必要そうなこと一覧
- まずタイルを収めるボード
- それから数字のついたタイルx15個
- タイルを動かす方法
- 解ける問題を出題するアルゴリズム(解けない時があるかもなのに注意)
- 問題が解けたか判定するアルゴリズム
- ゲームの進行をするルーチンたち(タイトル、出題、プレイ、クリア)
単純に考えたデータ構造
多分、とりあえず数字のついたタイルが15枚あればいいので。
- "タイルのデータ"
int tile[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0}; //0が入っているところをスペースにしようかな?
これでいいかな。。。?
これを、ボードに押し込めます。
- "ボード構造体を考える"
struct Board { int tile[16] };
これを、スライドパズルの形に表示してみる。
やってみるとわかるが、分かりにくいし使いにくい。。。
そこで以下の図のように、
ボードに枠があって、数字のタイルをはめ込んでいるんだなって考え方にしてみる。
- "枠に数字を割り当てる形にしてみる"
const int BOARD_WIDTH = 4; const int BOARD_HEIGHT = 4; const int BLANK_POS = 16; struct Board { // 行 列 int tile[BOARD_HEIGHT*BOARD_WIDTH]; };
どの枠に何の数字タイルが入ってるのかな?という形になった。ちょい使いやすそう。(ほんとかな?)
ついでに、パズルで使う数字は1~15までなので16番の入っている枠が空白だという設定にする。
上の
const int BLANK_POS = 16;
がそれ。
初期化と表示をしてみる
ボードの初期配置を設定する関数としてInitBoardと、現在のボードの状態を表示する関数PrintBoardをつくってみる。
初期化関数 InitBoard(C++)
InitBoardは初期化関数である。
ボードに関して初期化するので引数はBoard構造体(への参照)、戻り値は特にないのでvoidとする。
授業でやった通り、関数に渡した引数は、関数スコープを持つので(仮引数に値のみがコピーされて関数内だけで生きている)
そのまま渡しても、実引数のほうは変更されない。
実引数のアドレス内のメモリを直接書き換えるためには、参照渡しを使う。
参照渡しは仮引数の変数の前に&をつけるだけでよい。
参照渡しにすると仮引数のコピーではなく、変数が入っているアドレスの中身(んー。難しい)を直接関数からいじれるようになる。
- "初期化関数"
void InitBoard(Board& board) { for (int j = 0; j < BOARD_HEIGHT;j++) { for (int i = 0; i < BOARD_WIDTH; i++) { board.tile[iとjを使って0~15を順番にアクセス] = 左上から順に1~16までを代入; } } }
初期化関数 InitBoard(C言語)
C言語では、関数に渡した変数の中身を書き換えたいときはポインタを使う(もうこれは暗記っていうかルール)。
C言語の関数は、引数をポインタにしないと、値が関数の中でコピーされて、そのコピーに対して作業をするようになっている。
だから、辺の長さ指定しておいて面積計算しなさいとか、引数varの10乗を計算して返しなさい。とかそういうときは困らないけど、
今回みたいに、渡した構造体を初期化してよ!ってときはコピーされて、コピーが初期化されてしまうので困っちゃう(渡した変数自体は初期化されない)
そこで、ポインタを使って以下のようにします!(難しいけど見様見真似で)
- "ポインタで引数の中身をいじくれる"
void InitBoard(Board* board) { for (int j = 0; j < BOARD_HEIGHT;j++) { for (int i = 0; i < BOARD_WIDTH; i++) { board->tile[C++と同じく位置指定] = C++と同じ; } } } //呼び出すときはこんな感じ // Board myboard; // InitBoard(&myboard);
重要なのは、
- 引数をポインタ変数にすること
- ポインタで引数渡したときは、構造体のメンバには→(アロー演算子)でアクセスすること
- 呼び出すときは、変数のアドレスを渡すこと
枠の位置と数字の計算
枠の位置と、番号の変換はよくやる処理なので覚えておいてほしい。
何行目の何列目にいるかというのを行と列の番号から計算する。
これは、1次元配列と2次元配列の変換にも使えるので必須。
表示関数 PrintBoard(C++)
同様に、Board構造体の参照渡しを使って、現在のボードの中身をそれっぽく表示する関数を作ってゆくぅ
ちなみに、今回のように配列を含む構造体だとか後で出てくるclassだとかを、通常の値渡しの仮引数で渡すと、その構造体やclassが丸っとコピーされるのはもはや言うまでもないと思う。
つまり、でかい画像などを表示する関数に画像を含む構造体などを渡してやると毎回コピーが起こる。さすがの最近のコンピュータでも10M以上の画像を毎フレームコピーされたら追いつかなくなってくる。
このようなコピーを防ぐ意味もあり、関数に大きなデータ構造を渡すときは参照渡しを使うことが推奨されている。(詳しくはポインタとかのところでやるので後で!)
ということで、表示するだけの関数なので、戻り値はなし(void)、引数はBoard構造体への参照、となる。
- "C++版プロトタイプ宣言"
//PrintBoard関数の宣言文 void PrintBoard(Board& board);
- "C言語版プロトタイプ宣言"
//PrintBoard関数の宣言文 void PrintBoard(Board* board);
んで、以下のような表示をすることを目標に書いてみよう。
まんま同じ形じゃなくてもいいのでいろいろ工夫して表示してみよう
- "表示関数(C++)"
void PrintBoard(Board& _board) { string ruledLine = "+--+--+--+--+"; cout << ruledLine << endl; //飾り枠の表示 for (int j = 0; j < BOARD_HEIGHT; j++) { for (int i = 0; i < BOARD_WIDTH; i++) { if (空白の位置じゃなかったら) 2文字幅でそのボード枠の中の数字を表示 例 |13 else 空白なので2文字幅でスペースを表す文字を表示 |** ←スペースでもいいよ } cout << "|" << endl; 飾り枠をつけて改行 cout << ruledLine << endl; 最後の行の飾り枠 } }
- "表示関数(C言語)"
void PrintBoard(Board* _board) { char ruledLine[] = "+--+--+--+--+"; printf("%s\n", ruledLine); //飾り枠の表示 for (int j = 0; j < BOARD_HEIGHT; j++) { for (int i = 0; i < BOARD_WIDTH; i++) { if (空白の位置じゃなかったら) 2文字幅でそのボード枠の中の数字を表示 例 |13 else 空白なので2文字幅でスペースを表す文字を表示 |** ←スペースでもいいよ } printf("|\n"); //飾り枠をつけて改行 printf("%s\n", ruledLine); // 最後の行の飾り枠 } }
確認用ソースコード
- "動作確認用ソースコード(C言語)"
int main() { Board myboard; InitBoard(&myboard); PrintBoard(&myboard); return 0; }