/////////////////////////////////////////////////////////
//迷路ゲームプログラム
//作成：棒倒し法、穴開け法、壁伸ばし法
//解法：https://ja.wikipedia.org/wiki/%E8%BF%B7%E8%B7%AF
/////////////////////////////////////////////////////////
using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Resources;					//ResourceManagerを使う為
using System.Collections.Generic;		//Listを使う為
using System.IO;						//StreamReader/Writerを使う為
using System.Text;						//Encoderを使う為
using Atelier;							//Atelier.dllを使う

namespace Maze_DLL
{
	//////////////////////////////////////////////////////////////
	//		迷路クラス（迷路コントロールをDLL化したもの）
	//使い方：インスタンスをコントロールとしてフォームに付加する。
	//////////////////////////////////////////////////////////////
	public class Maze : Panel	//PictureBoxの枠となるPanelから派生
	{
		//////////////////////////
		//クラス、変数、プロパティ
		//////////////////////////
		//通路・壁情報
		const int Path = 0;
		const int Wall = 1;
		//セル情報
		private class Cell
		{
			public int X {get; set;}
			public int Y {get; set;}
		}
		//方向
		const int dirUp		= 0;
		const int dirRight	= 1;
		const int dirDown	= 2;
		const int dirLeft	= 3;
		/////////////////
		//迷路関連データ
		/////////////////
		//迷路の基準規模
		private int UNIT = 16;			//壁、通路一単位のピクセル数（Bitmapオリジナルは16 x 16）
		private int mzWidth, mzHeight;	//迷路のサイズ
		//2次元配列の迷路情報
		private int[,] MazeData;
		//穴掘り開始候補座標
		private List<Cell> StartCells;
		//迷路表示切替フラグ
		bool mzIs3D = true;
		//迷路出口の位置
		Cell mzExit;
		//迷路探索者の位置
		Cell mzWhereIam;
		//迷路探索者の進行方向
		private int mzWhereToGo;			//上－0、右－1、下－2、左－3
		//迷路データの中断ファイル
		const string mzFileName = "Maze.tmp";
		///////////////////////
		//迷路関連コントロール
		///////////////////////
		private PictureBox picBox;		//ピクチャーボックスコントロール
		private Easel easel;			//描画を行うEaselオブジェクト
		///////////////////////////
		//迷路関連ビットマップ画像
		///////////////////////////
		private Bitmap bmpBack, bmpBall, bmpBrick, bmpExit;
		////////////////////////////////////////////////////
		//コンストラクタ（引数がない場合の初期値は41 x 41）
		////////////////////////////////////////////////////
		public Maze(int width = 41, int height = 41)
		{
			//幅、高さが5未満のサイズはエラーで中断させる
			if(width < 5 || height < 5)
			{
				throw new ArgumentOutOfRangeException();
			}
			//幅、高さが偶数の場合は強制的に+1で奇数に変更する
			if(width % 2 == 0)
				width++;
			if(height % 2 == 0)
				height++;
			//迷路情報を初期化
			if(!IniMaze())
			{
				//迷路サイズの設定
				mzWidth = width;
				mzHeight = height;
				MazeData = new int[mzWidth, mzHeight];
				//迷路探索者の初期位置
				mzWhereIam = new Cell();
				mzWhereIam.X = 1;
				mzWhereIam.Y = 1;
				//迷路出口の初期位置
				mzExit = new Cell();
				mzExit.X = mzWidth - 2;
				mzExit.Y = mzHeight - 2;
				//迷路内の進行方向
				mzWhereToGo = 1;	//初期値は右（1）
			}
			if(mzWidth != 41 || mzHeight != 41)
				mzSetUNIT();					//デフォルト初期値でない場合、UNIT値を変更する必要がある
			//迷路作成用Cellリスト
			StartCells = new List<Cell>();
			//コントロール本体（好みで変更可）
			this.BorderStyle = BorderStyle.Fixed3D;
			this.AutoScroll = true;				//スクロールバー付
			this.Width = mzWidth * UNIT + 4;	//デフォルト初期値は660（656 + 4）
			this.Height = mzHeight * UNIT + 4;	//デフォルト初期値は660（656 + 4）
			//描画先とするEaselオブジェクトを作成する
			easel = new Easel(this.ClientSize.Width, this.ClientSize.Height);
			//ピクチャーボックスコントロール
			picBox = new PictureBox();			//枠の中のPictureオブジェクトの作成
			picBox.Location = new Point(0, 0);	//枠内の位置
			picBox.SizeMode = PictureBoxSizeMode.AutoSize;	//画像サイズでPictureBoxの大きさが変更される
			picBox.Image = easel.Canvas;		//PictureBoxにEaselオブジェクトを貼り付ける
			//Maze（Panel）にPictureBoxを追加
			this.Controls.Add(picBox);			//picBoxをpanelの子にする
			//Mazeサイズ変更時の処理
			this.SizeChanged += mzSizeChanged;
			//このプログラムのアッセンブリーを取得する
			System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
			//ResourceManagerインスタンスの作成
			ResourceManager rm = new ResourceManager("Maze", asm);
			//Bitmapを読み込む
			bmpBack = (Bitmap)rm.GetObject("Back");
			bmpBall = (Bitmap)rm.GetObject("Ball");
			bmpBall.MakeTransparent(Color.Black);
			bmpBrick = (Bitmap)rm.GetObject("Brick");
			bmpExit = (Bitmap)rm.GetObject("Exit");
			bmpExit.MakeTransparent(Color.Black);
		}
		///////////////
		//デストラクタ
		///////////////
		~Maze()
		{
			bmpBack.Dispose();
			bmpBall.Dispose();
			bmpBrick.Dispose();
			bmpExit.Dispose();
		}
		/////////////////////////////
		//Mazeサイズが変更された場合
		/////////////////////////////
		private void mzSizeChanged(object sender, EventArgs e)
		{
			mzSetUNIT();
			easel.Resize(this.ClientSize.Width, this.ClientSize.Height);
			Display(mzIs3D, mzWhereToGo);
			picBox.Image = easel.Canvas;
		}
		/////////////////////////////////
		//Mazeのクライアントエリアサイズ
		//に基づきUNITを決定する
		/////////////////////////////////
		private void mzSetUNIT()
		{
			int w = this.ClientSize.Width / mzWidth;
			int h = this.ClientSize.Height / mzHeight;
			UNIT = (w < h) ? w : h;		//Maze内に表示が収まるように小さい方を選択
		}
		/////////////////////////
		//Mazeのサイズを取得する
		/////////////////////////
		public Size GetSize()
		{
			return new Size(mzWidth, mzHeight);
		}
		///////////////////////////
		//Mazeの進行方向を取得する
		///////////////////////////
		public int GetDir()
		{
			return mzWhereToGo;
		}
		///////////////
		//迷路生成処理
		///////////////
		public int[,] GenerateMaze()
		{
			//既に壁データがあれば戻る
			if(MazeData[0, 0] == Wall)
				return null;
			//全てを壁で埋める
			//穴掘り開始候補(x,yともに偶数)座標を保持しておく
			for(int y = 0; y < this.mzHeight; y++)
			{
				for(int x = 0; x < this.mzWidth; x++)
				{
					if(x == 0 || y == 0 || x == this.mzWidth - 1 || y == this.mzHeight - 1)
					{
						MazeData[x, y] = Path;  // 外壁は判定の為通路にしておく(最後に戻す)
					}
					else
					{
						MazeData[x, y] = Wall;
					}
				}
			}
			//穴掘り開始
			Dig(1, 1);
			//外壁を戻す
			for(int y = 0; y < this.mzHeight; y++)
			{
				for(int x = 0; x < this.mzWidth; x++)
				{
					if(x == 0 || y == 0 || x == this.mzWidth - 1 || y == this.mzHeight - 1)
					{
						MazeData[x, y] = Wall;
					}
				}
			}
			return MazeData;
		}
		/////////////////////////////
		//迷路の座標(x, y)に穴を掘る
		/////////////////////////////
		private void Dig(int x, int y)
		{
			//指定座標から掘れなくなるまで堀り続ける
			Random rnd = new Random();
			while(true)
			{
				//掘り進めることができる方向のリストを作成
				List<int> directions = new List<int>();
				if(this.MazeData[x, y - 1] == Wall && this.MazeData[x, y - 2] == Wall)
					directions.Add(dirUp);
				if(this.MazeData[x + 1, y] == Wall && this.MazeData[x + 2, y] == Wall)
					directions.Add(dirRight);
				if(this.MazeData[x, y + 1] == Wall && this.MazeData[x, y + 2] == Wall)
					directions.Add(dirDown);
				if(this.MazeData[x - 1, y] == Wall && this.MazeData[x - 2, y] == Wall)
					directions.Add(dirLeft);
				//掘り進められない場合、ループを抜ける
				if(directions.Count == 0) break;
				//指定座標を通路とし穴掘り候補座標から削除
				SetPath(x, y);
				//掘り進められる場合はランダムに方向を決めて掘り進める
				int dirIndex = rnd.Next(directions.Count);
				//決まった方向に先2マス分を通路とする
				switch(directions[dirIndex])
				{
					case dirUp:
						SetPath(x, --y);
						SetPath(x, --y);
						break;
					case dirRight:
						SetPath(++x, y);
						SetPath(++x, y);
						break;
					case dirDown:
						SetPath(x, ++y);
						SetPath(x, ++y);
						break;
					case dirLeft:
						SetPath(--x, y);
						SetPath(--x, y);
						break;
				}
			}
			//どこにも掘り進められない場合、穴掘り開始候補座標から掘りなおし
			//候補座標が存在しないとき、穴掘り完了
			Cell cell = GetStartCell();
			if(cell != null)
			{
				Dig(cell.X, cell.Y);
			}
		}
		///////////////////////////////////////////////////
		//座標を通路とする(穴掘り開始座標候補の場合は保持)
		///////////////////////////////////////////////////
		private void SetPath(int x, int y)
		{
			this.MazeData[x, y] = Path;
			if(x % 2 == 1 && y % 2 == 1)
			{
				//穴掘り候補座標
				StartCells.Add(new Cell() { X = x, Y = y });
			}
		}
		/////////////////////////////////////
		//穴掘り開始位置をランダムに取得する
		/////////////////////////////////////
		private Cell GetStartCell()
		{
			if(StartCells.Count == 0) return null;

			//ランダムに開始座標を取得する
			Random rnd = new Random();
			int index = rnd.Next(StartCells.Count);
			Cell cell = StartCells[index];
			StartCells.RemoveAt(index);

			return cell;
		}
		/////////////////
		//迷路を表示する
		/////////////////
		public void Display(bool flag, int dir)
		{
			mzIs3D = flag;		//直近の表示フラグを記録
			mzWhereToGo = dir;	//直近の方向を記録
			if(flag)
				Display3D(mzWhereIam.X, mzWhereIam.Y, dir, 0, 0, this.ClientSize.Width, this.ClientSize.Height, 6);
			else
				Display2D();
		}
		/////////////////////
		//迷路を2Dで表示する
		/////////////////////
		public void Display2D()
		{
			easel.Clear();	//一旦白紙にする
			for(int y = 0; y < mzHeight; y++)		// 高さ
			{
				for(int x = 0; x < mzWidth; x++)	// 幅
				{
					if(MazeData[x, y] == Wall)
					{
						easel.PutBMP(bmpBrick, x * UNIT, y * UNIT, x * UNIT + UNIT - 1, y * UNIT + UNIT - 1);
					}
				}
			}
			easel.PutBMP(bmpExit, mzExit.X * UNIT, mzExit.Y * UNIT, mzExit.X * UNIT + UNIT - 1, mzExit.Y * UNIT + UNIT - 1);
			easel.PutBMP(bmpBall, mzWhereIam.X * UNIT, mzWhereIam.Y * UNIT, mzWhereIam.X * UNIT + UNIT - 1, mzWhereIam.Y * UNIT + UNIT - 1);
			picBox.Image = easel.Canvas;
		}
		////////////////////////////////////////////////////
		//迷路を3Dで表示する（本関数は自らを再帰処理する）
		//迷路内の探索者位置(x, y)、進行方向(dir)、
		//ウィンドウ表示画面サイズ（(x1, y1) - (x2, y2)）、
		//サイズ縮小用分母（Denominator）
		////////////////////////////////////////////////////
		public bool Display3D(int x, int y, int dir, int x1, int y1, int x2, int y2, int den)
		{
			//再帰処理で外周壁に達したら何もしないで戻る
			if(x == 0 || x == mzWidth - 1)	return false;
			if(y == 0 || y == mzHeight - 1)	return false;
			//表示領域を正方形を保つために表示サイズを調整
			if((x1 + y1) == 0)		//再帰中は適用外
				x2 = y2;	//常に高さに合わせる
			//進行方向先の光景を縮小率（1/3）で小さくする（diffはx、yの差（減少）分）
			int diff = (x2 - x1) / den / 2;		//x, y共に同じ
			//進行方向一つ先のセルのサイズ（x、y共に差分ｘ2づつ減少する）
			int nextx1 = x1 + diff;
			int nextx2 = x2 - diff;
			int nexty1 = y1 + diff;
			int nexty2 = y2 - diff;
			//一旦黒で塗潰す
			easel.Clear(Color.Black);
			//白色で描画
			easel.gFtColor = Color.White;
			//正面、左、右が壁か否かのフラグ
			bool FWall = false, LWall = false, RWall = false;
			switch(dir)
			{
				case dirUp:	//上へ進む場合
					if(MazeData[x, y - 1] == Wall)
						FWall = true;
					else
						FWall = false;
					if(MazeData[x - 1, y] == Wall)
						LWall = true;
					else
						LWall = false;
					if(MazeData[x + 1, y] == Wall)
						RWall = true;
					else
						RWall = false;
					y--;
					break;
				case dirRight:	//右へ進む場合
					if(MazeData[x + 1, y] == Wall)
						FWall = true;
					else
						FWall = false;
					if(MazeData[x, y - 1] == Wall)
						LWall = true;
					else
						LWall = false;
					if(MazeData[x, y + 1] == Wall)
						RWall = true;
					else
						RWall = false;
					x++;
					break;
				case dirDown:	//下へ進む場合
					if(MazeData[x, y + 1] == Wall)
						FWall = true;
					else
						FWall = false;
					if(MazeData[x + 1, y] == Wall)
						LWall = true;
					else
						LWall = false;
					if(MazeData[x - 1, y] == Wall)
						RWall = true;
					else
						RWall = false;
					y++;
					break;
				case dirLeft:	//左へ進む場合
					if(MazeData[x - 1, y] == Wall)
						FWall = true;
					else
						FWall = false;
					if(MazeData[x, y + 1] == Wall)
						LWall = true;
					else
						LWall = false;
					if(MazeData[x, y - 1] == Wall)
						RWall = true;
					else
						RWall = false;
					x--;
					break;
			}
			den = 3;	//現在地はセルを半分表示して1/6にしているが、その先はセル毎に1/3づつ減少
			if(!FWall)	//再帰で最後の突き当りから奥を描画させる
				Display3D(x, y, dir, nextx1, nexty1, nextx2, nexty2, den);
			//壁、通路の描画
			if(FWall)	//正面が壁なら四角を描く
				easel.Box(nextx1, nexty1, nextx2, nexty2);
			if(LWall)	//左側の描画処理
			{
				easel.Line(x1, y1, nextx1, nexty1);
				easel.Line(x1, y2, nextx1, nexty2);
			}
			else
			{
				easel.Line(x1, nexty1, nextx1, nexty1);
				easel.Line(x1, y1, x1, y2);
				easel.Line(x1, nexty2, nextx1, nexty2);
				easel.Line(nextx1, nexty1, nextx1, nexty2);
			}
			if(RWall)	//右側の描画処理
			{
				easel.Line(x2, y1, nextx2, nexty1);
				easel.Line(x2, y2, nextx2, nexty2);
			}
			else {
				easel.Line(x2, nexty1, nextx2, nexty1);
				easel.Line(x2, y1, x2, y2);
				easel.Line(x2, nexty2, nextx2, nexty2);
				easel.Line(nextx2, nexty1, nextx2, nexty2);
			}
			picBox.Image = easel.Canvas;
			return true;
		}
		///////////
		//移動－上
		///////////
		public bool GoUp()
		{
			bool CanGo = (MazeData[mzWhereIam.X, mzWhereIam.Y - 1] == Path);
			if(CanGo)
				mzWhereIam.Y--;
			else
				Console.Beep();
			return CanGo;
		}
		///////////
		//移動－下
		///////////
		public bool GoDown()
		{
			bool CanGo = (MazeData[mzWhereIam.X, mzWhereIam.Y + 1] == Path);
			if(CanGo)
				mzWhereIam.Y++;
			else
				Console.Beep();
			return CanGo;
		}
		///////////
		//移動－左
		///////////
		public bool GoLeft()
		{
			bool CanGo = (MazeData[mzWhereIam.X - 1, mzWhereIam.Y] == Path);
			if(CanGo)
				mzWhereIam.X--;
			else
				Console.Beep();
			return CanGo;
		}
		///////////
		//移動－右
		///////////
		public bool GoRight()
		{
			bool CanGo = (MazeData[mzWhereIam.X + 1, mzWhereIam.Y] == Path);
			if(CanGo)
				mzWhereIam.X++;
			else
				Console.Beep();
			return CanGo;
		}
		////////////////
		//出口到着判定
		////////////////
		public bool IsExit()
		{
			return (mzWhereIam.X == mzExit.X && mzWhereIam.Y == mzExit.Y);
		}
		/////////////////////////////
		//Maze.tmpファイルを読み込み
		//迷路を初期化する
		/////////////////////////////
		private bool IniMaze()
		{
			string data = "";
			//ファイル（UTF-8：Encoding.UTF8）を読む
			try
			{
				using(StreamReader file = new StreamReader(mzFileName, Encoding.UTF8))
				{
					data = file.ReadToEnd();
					// ファイルを閉じる
					file.Close();
				}
			}
			catch(Exception e)	//FileNotFoundException e
			{
				//Debug用
				MessageBox.Show("エラーコード：" + e.ToString(), "初期化ファイル読み込みエラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			//文字列の行分割
			string[] lines = data.Split(new[]{"\r\n","\n","\r"},StringSplitOptions.None);
			//第１行（迷路の幅と高さ）の語分割
			string[] widthheight = lines[0].Split(',');
			if(!Int32.TryParse(widthheight[0], out mzWidth))
			{
				MessageBox.Show("mzWidthの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			if(!Int32.TryParse(widthheight[1], out mzHeight))
			{
				MessageBox.Show("mzHeightの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			//迷路の作成
			MazeData = new int[mzWidth, mzHeight];
			//第２行（UNIT、mzIs3D、mzWhereToGo）の語分割
			string[] others = lines[1].Split(',');
			if(!Int32.TryParse(others[0], out UNIT))
			{
				MessageBox.Show("UNITの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			if(!Boolean.TryParse(others[1], out mzIs3D))
			{
				MessageBox.Show("mzIs3Dの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			if(!Int32.TryParse(others[2], out mzWhereToGo))
			{
				MessageBox.Show("mzWhereToGoの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			//第３行（迷路探索者の位置）の語分割
			string[] where = lines[2].Split(',');
			mzWhereIam = new Cell();
			int x, y;	//TryParse用変数
			if(Int32.TryParse(where[0], out x))
				mzWhereIam.X = x;
			else
			{
				MessageBox.Show("mzWhereIam.Xの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			if(Int32.TryParse(where[1], out y))
				mzWhereIam.Y = y;
			else
			{
				MessageBox.Show("mzWhereIam.Xの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			//第４行（迷路出口の位置）の語分割
			string[] exitpos = lines[3].Split(',');
			mzExit = new Cell();
			if(Int32.TryParse(exitpos[0], out x))
				mzExit.X = x;
			else
			{
				MessageBox.Show("mzExit.Xの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			if(Int32.TryParse(exitpos[1], out y))
				mzExit.Y = y;
			else
			{
				MessageBox.Show("mzExit.Yの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			//第５行以降（迷路データ）の語分割
			for(y = 0; y < mzHeight; y++)
			{
				string[] dt = lines[4 + y].Split(',');
				for(x = 0; x < mzWidth; x++)
				{
					if(!Int32.TryParse(dt[x], out MazeData[x, y]))
					{
						MessageBox.Show("MazeDataの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
						return false;
					}
				}
			}
			return true;
		}
		/////////////////////////////
		//Maze.tmpファイルを書き込み
		//中断する処理
		/////////////////////////////
		public void Abort()
		{
			//第１行（迷路の幅と高さ）
			string data = mzWidth.ToString() + "," + mzHeight.ToString() + Environment.NewLine;
			//第２行（UNIT、mzIs3D、mzWhereToGo）
			data += UNIT.ToString() + "," + mzIs3D.ToString() + "," + mzWhereToGo.ToString() + Environment.NewLine;
			//第３行（迷路探索者の位置）
			data += mzWhereIam.X.ToString() + "," + mzWhereIam.Y.ToString() + Environment.NewLine;
			//第４行（迷路出口の位置）
			data += mzExit.X.ToString() + "," + mzExit.Y.ToString() + Environment.NewLine;
			//第５行以降（迷路データ）
			for(int y = 0; y < mzHeight; y++)
			{
				int x = 0;
				while(x < mzWidth - 1)
				{
					data += MazeData[x, y].ToString() + ",";
					x++;
				}
				data += MazeData[x, y].ToString() + Environment.NewLine;
			}
			//ファイル（UTF-8：Encoding.UTF8）を書く
			using(StreamWriter file = new StreamWriter(mzFileName, false, Encoding.UTF8))
			{
				//ファイルを書く
				file.Write(data);
				//ファイルを閉じる
				file.Close();
			}
		}
		/////////////////////////////
		//Maze.tmpファイルを削除する
		/////////////////////////////
		public void DeNovo()
		{
			//ファイルの削除
			File.Delete(mzFileName);
		}
	}
}
