タイルの移動

ボードのデータ構造を考え、盤面を初期化して表示するところまでできている。(はず)
この時点で、Siv3Dを使ってグラフィカルな表示を行うにはどういう表現をしたらいいか、考えながら進んでいこう。
(こういう思考と試行の繰り返しが、ゲーム作りに必要な知識と経験となっていくよ)

次に、「移動できるタイルを番号で指定したら、空白と入れ替える」機能を実装する。

必要なこと一覧

必要なことを考えてみる。

  1. まずタイルを収めるボード
  2. それから数字のついたタイルx15個
  3. タイルを動かす方法
  4. 解ける問題を出題するアルゴリズム(解けない時があるかもなのに注意
  5. 問題が解けたか判定するアルゴリズム
  6. ゲームの進行をするルーチンたち(タイトル、出題、プレイ、クリア)

移動できるタイルとは何か

多分、移動できるタイルを指定して動かすところのソースコード的にはこんな感じになると思う。

"移動させたいタイルの指定"
void タイル移動関数(num, myboard)
{
    if(num番のタイルが動かせる)
        空白と交換;
    else
       return;//なにもしない
}
 
int main()
{
	Board myboard;
	InitBoard(myboard);
	PrintBoard(myboard);
	int num;
 
	while (true)
	{
		cout << "どこをずらしますか(番号で指定):";
		cin >> num;
 
		タイル移動関数(num, myboard);
		cout << "結果:" << endl;
		PrintBoard(myboard);
	}
	return 0;
}

それでは移動できるタイルとは何か?を考えてみる。

移動できるタイル

この盤面で動かせるタイルはどれだろうか?

Fig. 1: この盤面で動かせるタイルは?

この盤面では「6」「9」「10」「11」のタイルが動かせるタイル、となる。
これらのタイルを、プログラム上で判別するにはどうしたらよいだろうか?
(このように、問題をまとめて、余計なものを取り払ったりし数式で表しプログラム上で表現できる形にすることをモデル化という)

これらの「動かせる」タイルは、いずれも空白の4近傍の位置に存在するタイルである。
(4近傍とは格子状にデータを並べたときに、注目する位置の、斜めをのぞいた上下左右の隣接位置のことをいう)
つまり、空白タイルから見たときに。。。

空白タイルの座標 $p_{space}(x, y)$の時に、$p_{left}(x-1, y), p_{right}(x+1, y), p_{up}(x, y+1), p_{down}(x, y-1)$の位置のタイルが、動かせるタイルということになる
言葉で書くと、「x座標か、y座標のいずれか一方が1だけずれているタイル」である。
数式で表すと、空白のタイルの座標を$p_{space}(x_1, y_1)$判定したいタイルの座標を$p_n(x_2,y_2)$とすると2点の関係が
$ | x_1 - x_2| + |y_1 - y_2| = 1 $であるなら、動かすことができる。
と書ける。(この式の値は、格子状に並んだものの距離を表す量の1つでマンハッタン距離(市街地距離)と呼ばれるものである
よって。この距離が1の時に、2つのタイルは入れ替え可能である。というプログラムを書けばよい
このままだと、とてもプログラムがめんどくさくなります。(正負の判定して大きいほうから小さいほう引くとかやってらんねぇ)
この式見たことある人ー?
$ |x|^2 = x^2 $

絶対値記号を付けたまま2乗した数は、絶対値を外したものの2乗と等しい。
さらに、条件は距離が1の時というので、これらを使って以下のように式を変形します。
$(x_1 - x_2)^2+(y_1 - y_2)^2 = 1$

この式は、x方向の座標が1だけずれていると、1になり、x方向もy方向も1ズレがあると値が2になる。
したがって、x方向に1ずれているか、またはy方向に1ずれているかを、この式の値が1かどうかを見ることで判定できる。
これで、マンハッタン距離が1なら、空白の隣なので、交換すればパズル上で移動したことになる。

実際に入れ替えを行う

実際の入れ替えは、空白のタイル(値が16のタイル)と、指定したタイルが交換可能であるなら入れ替える。という作業になる。

    struct Position
    {   //整数で、2次元配列のインデックスや、整数座標を表す構造体
        int x,y;
    };
    Position blank = SearchTileNum(BLANK_POS, _board); //BLANK_POS はいつも16
    Position moveTile = SearchTileNum(_num, _board);  //番号_numの入っているタイルのインデックスを取得

ここでSearchTileNumは、番号を指定すると、全タイルを検索してその番号が入っている配列のインデックスを返す関数

_board.tile[blank.y][blank.x]に16番が、
_board.tile[moveTile.y][moveTile.x]に10番が

はいっているはずなので、これらを交換すれば、タイルの移動が完成するはず!
交換はいつものやつで、

"配列の要素の交換"
     std::swap(_board.tile[moveTile.y][moveTile.x], _board.tile[blank.y][blank.x]);

これを実行すればよい。
これで必要な道具が全部そろいました。必要な道具は、

ぐらいがあれば、ひと仕事片付きそうである。一個ずつ作っていこう。

Position SearchTileNum(int num, Board& _board)の作成

番号を整数で指定して、配列のインデックスを取得する。
エラー処理として、Positionに(-1, -1)が入ってたらエラーってことにする。
(見つからないとか不正な値が入っているとか)

"タイルから番号をサーチするよ"
Position SearchTileNum(int num, Board& _board)
{
	Position p = { -1,-1 };
        //値は1~BOARD_HEIGHT*BOARD_WIDTHまであるはず、4x4なら、1~16
        if(値が、_numの値の範囲からずれてたら)
            return(p);
 
	for (int j = 0; j < ボードの高さ; j++)
	{
		for (int i = 0; i < ボードの幅; i++)
		{
			if (_numを見つけた)
			{
				p = インデックスを返す;
				return(p);
			}
		}
	}
	//ここまでたどり着いたら、そのまま-1,-1をかえす。エラー判定用
        //上でエラー処理してるから、ここまでたどり着くパターンはないはずなのよね。でも一応書く
	return(p);
}

bool CanMoveIt(int _num, Board& _board)の作成

次は、番号で指定したタイルが動かせるかどうかを判定する。
空白の番号は、初めのほうで16が空白を表すって書いてあるので、16番のあるタイル位置=空白の位置になる。

const int BLANK_POS = 16;

そんで、

"指定した番号は動かせるタイルの番号ですか?"
bool CanMoveIt(int _num, Board& _board)
{
	Position blank = BLANK_POSのタイル位置を探す
	Position moveTile = _numのタイル位置を探す
        if(blank.x == -1 || moveTile.x == -1) return false;//エラーならfalse
	int dist = blank と moveTile のマンハッタン距離の2乗を計算;
	if (距離が1なら)
		return true;//空白の隣なので動かせる
	else
		return false;//空白の隣じゃなさそうだから動かせない
}

void SwapTile(int _num, Board& _board)の作成

最後、に交換可能なタイルを交換する。
なんだか、2度手間になってる気がするが、とりあえずこの形でつくってゆくぅ

"タイルの交換"
void SwapTile(int _num, Board& _board)
{
	Position blank = BLANK_POSをサーチ;
	Position moveTile = _numをサーチ;
 
	if (blank, moveTileどっちかがエラー)//-1がどっちかに入ってたらエラーだよね
		return;//抜けちゃう
	if (_numが動かせる)
	{
		std::swap(交換!);
	}
	else {
		cout << "そこは動かせない" << endl;
		return;
	}
}

これで道具が全部そろうはず!

動作確認用ソースコード

これらを使って、動作を確認してみよう!

"動作確認ソースコード"
int main()
{
	Board myboard;
	InitBoard(myboard);//初期化関数
	PrintBoard(myboard);//初期状態を表示
	int num;//パネル番号入力用
 
	while (true)
	{
		cout << "どこをずらしますか(番号で指定):";
		cin >> num;
 
		SwapTile(num, myboard);
		cout << "結果:" << endl;
		PrintBoard(myboard);
	}
	return 0;
}

結果としてこんな感じになればOK

その3へ