あんみんどうふです。
ハノイの塔をC#.NETで作ったので紹介します。
パッと見て子供のおもちゃみたいなゲームですが、意外と奥深く高難度なパズルです。
ハノイの塔とは
横に並んだ3本の杭に異なる大きさの円盤があり、これらを同じ順番で一番右まで移動させたらクリアです。
杭の一番上にある円盤を移動させることができますが、小さい円盤の下に大きい円盤を移動させるのはNGです。
結構単純なルールですが、円盤の数が増えれば増えるほど難しくなります(杭の数は3本で固定です)。
開発環境
いつも通りです。
- Visual Studio 2019
- C#.NET (.NET Core 3.1)
こんな感じ
移動したい円盤のある杭をクリックし、移動先の杭をクリックして移動させます。
移動したい円盤よりも小さい円盤が下にあると移動できません。
同じ順番で一番右まで移動でクリアです!移動は9回かかりました。
円盤が3枚の場合は最低7回でクリアできます。
ちなみに最低移動回数の計算式は「(2のn乗)-1」(n=円盤の数)となっています。
これが4枚になると15回、5枚になると31回。一応12枚まで設定できるようにしていますが、この場合は4095回必要です。1枚動かすのに1秒かかるとすると、クリアまでの推定時間は約1時間と言われています。
64枚では約5800億年かかるそうです。こんなことに5800億年も使いたくない・・・。
(出典: ハノイの塔 – Itoyama’s Page)
中身
300行弱でさくっと収まりました。ゲームの処理よりも描画処理による見た目の調整が難しかったです・・・。
初期化処理
Load時に呼び出す他、円盤の数を決めて再スタートする時に呼び出します。
円盤は一番小さい円盤が1として順に数値で管理しています。上から「小・中・大」と並んでいる円盤は数値化すると「1・2・3」という感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | int disk = 3; // 円盤の数 int[,] field; // フィールド int select = -1; // 選択中のエリア(-1 = 未選択) int movecount = 0; // 移動回数 // 描画関連 int height = 30; // 円盤Height int rate = 0; // 円盤幅の削り具合 int padding = 6; // 円盤同士の間隔 private void MainForm_Load(object sender, EventArgs e) { Initalize(); } /// <summary> /// 初期化 /// </summary> void Initalize() { select = -1; movecount = 0; // フィールド初期化 field = new int[disk, 3]; // 描画関連 rate = 18 - disk; if (disk > 6) { height = 30 - (disk - 8) * 2; padding = 6 - disk / 4; } // フィールド構築 for (int i = 0; i < disk; i++) { field[i, 0] = i + 1; } labelMessage.Text = ""; labelMessage.ForeColor = SystemColors.ControlText; labelCount.Text = "移動回数: 0"; Drawing(); CalcMinCount(); picHanoi1.Enabled = true; picHanoi2.Enabled = true; picHanoi3.Enabled = true; } |
Initralize関数の「// 描画関連」と書かれた部分、円盤の数によって描画の調整を行っているのですがどうもいまいちで、かといって拘る部分でもないので未完成のままです。
描画処理
円盤の色は全部割り当てるのも面倒なので、5で割った余りの値で決めることで5種類の色でループするようにしています。こんな色でループします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | /// <summary> /// 描画処理 /// </summary> void Drawing() { PictureBox[] pictures = { picHanoi1, picHanoi2, picHanoi3 }; for (int j = 0; j < pictures.Length; j++) { Bitmap canvas = new Bitmap(pictures[j].Width, pictures[j].Height); Graphics graphics = Graphics.FromImage(canvas); // 線 Pen pen = new Pen(Color.LightGray, 3); graphics.DrawLine(pen, pictures[j].Width / 2, 0, pictures[j].Width / 2, pictures[j].Height); // 円盤 for (int i = 0; i < disk; i++) { if (field[i, j] != 0) { //Debug.WriteLine("DRAW[{1}, {2}]: {0}", field[i, j], i, j); Color color = new Color(); switch (field[i, j] % 5) { case 1: color = ColorTranslator.FromHtml("#F2994B"); break; case 2: color = ColorTranslator.FromHtml("#F2BF80"); break; case 3: color = ColorTranslator.FromHtml("#F2DE77"); break; case 4: color = ColorTranslator.FromHtml("#8596A6"); break; case 0: color = ColorTranslator.FromHtml("#2B3240"); break; } SolidBrush brush = new SolidBrush(color); graphics.FillRectangle(brush, 0 + (disk - field[i, j]) * rate, pictures[j].Height - height * (disk - i) - padding * (disk - i - 1), pictures[j].Width - (disk - field[i, j]) * rate * 2, height); } } // 円盤フォーカス線 if (select == j && select >= 0) { for (int i = 0; i < disk; i++) { if (field[i, j] > 0) { Pen pen2 = new Pen(Color.Red, 3); graphics.DrawRectangle(pen2, 0 + (disk - field[i, j]) * rate + pen2.Width / 2, pictures[j].Height - height * (disk - i) - padding * (disk - i - 1) + pen2.Width / 2, pictures[j].Width - (disk - field[i, j]) * rate * 2 - pen2.Width, height - pen2.Width); break; } } } // 描画 pictures[j].Image = canvas; } } |
色はいつも「Color.FromArgb」で指定していますが、今回は「ColorTranslator.FromHtml」で指定してみました。
HTMLのカラーコードのまま使えます。いちいち10進数に戻すのも面倒だしなんとなくこっちにしました。
選択処理
杭をクリックすると呼び出されます。
円盤がある杭をクリックすると杭の一番上にある円盤がフォーカスされ、その状態で他の杭をクリックすると移動する処理を書いてます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | /// <summary> /// 選択処理 /// </summary> /// <param name="target">対象列</param> void Selecting(int target) { PictureBox[] pictures = { picHanoi1, picHanoi2, picHanoi3 }; // 選択元の選択(円盤の取得) if (select == -1 && field[disk - 1, target] != 0) { select = target; } // 選択先の選択 else { // 選択元・選択先が同じであれば選択解除のみ行う if (select == target) { select = -1; labelMessage.Text = ""; } else { for (int i = 0; i < disk && select >= 0; i++) { if (field[i, select] > 0) { // 選択先の一番上に取得した選択元の円盤を移動 for (int k = disk - 1; k >= 0 && select >= 0; k--) { if (field[k, target] == 0) { field[k, target] = field[i, select]; field[i, select] = 0; select = -1; movecount++; labelMessage.Text = ""; } else if (field[k, target] < field[i, select]) { labelMessage.Text = "下の円盤より大きい円盤は移動できません。"; return; } } } } } } labelCount.Text = "移動回数: " + movecount; Drawing(); // クリア判定 if (CheckClear()) { labelMessage.Text = "CLEAR!!"; labelMessage.ForeColor = Color.Orange; for (int i = 0; i < pictures.Length; i++) { pictures[i].Enabled = false; } } } /// <summary> /// クリアチェック /// </summary> /// <returns></returns> bool CheckClear() { for(int i = 0; i < disk; i++) { int value = field[i, field.GetLength(1) - 1]; if (value == 0 || value != i + 1) return false; } return true; } |
CheckClear関数、クリアチェックは「一番右の杭が全て埋まり、かつ1から連番で続いている」を条件としてチェックしているだけです。
最低移動回数
最低移動回数はこっちでちゃちゃっと計算してます。
先程の通り、2の(円盤の数)乗-1です。
1 2 3 4 5 6 7 8 9 10 | /// <summary> /// 最低移動回数計算 /// </summary> void CalcMinCount() { double calc; calc = Math.Pow(2, disk); labelMinMoveCount.Text = "最低移動回数: " + (calc - 1); } |
ほぼ全部のコードを貼ったのでコントロールの種類とNameを合わせてコピペすれば誰でも作れちゃいますね。恥ずかしいのであまりしてほしくない
以上です。また何か作ったら紹介します。