1. Lone Ranger


lone ranger는 row, column, minigrid의 각 셀의 possible values 중에서 오로지 한 셀에서만 가능한 값을 말한다. 아래 그림을 참조.



(2, 1) 셀만 값 '8' 이 가능하므로 셀 (2, 1)은 값 '8'로 confirm 된다.



2. minigrid 안에서의 lone ranger



세번째 minigrid 의 셀 (7,2)에서 lone ranger '1'이 발견된다. 이 셀은 '1'로 confirm.



3. Row에서의 lone ranger



5번째 row의 셀 (6,5)에서 lone ranger '2'가 발견된다. 이 셀은 '2'로 confirm.



4. Column 에서의 lone ranger



8번째 column의 셀 (8,5)에서 lone ranger '8'을 발견할 수 있다. 이 셀은 '8'로 confirm 된다.



5. 구현 - minigrid에서 lone ranger 찾기



아래의 서브루틴을 추가한다.



		//
		//	Look for lone rangers in minigrid
		//
		public bool LookForLoneRangersinMinigrids()
		{
			bool changes = false;
			bool NextMiniGrid;
			int occurrence;
			int cPos = 0;
			int rPos = 0;


			//	check for each number from 1 to 9
			for ( int n = 1; n < 10; n++ )
			{
				//	check the 9 minigrids
				for ( int r = 1; r < 10; r = r + 3 )
				{
					for ( int c = 1; c < 10; c = c + 3 )
					{
						NextMiniGrid = false;

						//	check within the minigrid
						occurrence = 0;

						for ( int rr = 0; rr < 3; rr++ )
						{
							for ( int cc = 0; cc < 3; cc++ )
							{
								if ( (actual[c + cc, r + rr] == 0) &&
									possible[c + cc, r + rr].Contains(n.ToString()) )
								{
									occurrence += 1;
									cPos = c + cc;
									rPos = r + rr;

									if ( occurrence > 1 )
									{
										NextMiniGrid = true;
										break;
									}

								}
							}

							if ( NextMiniGrid )
								break;
						}

						if ( ( !NextMiniGrid ) && ( occurrence == 1 ) )
						{
							//	that means number is confirmed
							SetCell( cPos, rPos, n, 1 );
							SetToolTip( cPos, rPos, n.ToString() );

							//	save the moves into the stack
							Moves.Push( cPos.ToString() + rPos.ToString() + n.ToString() );
							DisplayActivity( "Look for Lone Rangers in Minigrid", false );
							DisplayActivity( "===================", false );
							DisplayActivity( "Inserted value " + n.ToString() + " in (" + cPos + ", " + rPos + ")", false );

							Application.DoEvents();
							changes = true;

							// if user clicks the hint button, exit the function
							if (HintMode)
								return true;
						}
					}
				}
			}

			return changes;
		}



6. 구현 - row에서 lone ranger 찾기




아래의 서브루틴을 작성해 추가한다.


		//
		//	Look for Lone Rangers in Rows
		//
		public bool LookForLoneRangersinRows()
		{
			bool changes = false;
			int occurrence;
			int cPos = 0;
			int rPos = 0;

			
			//	check by row
			for ( int r = 1; r < 10; r++ )
			{
				for ( int n = 1; n < 10; n++ )
				{
					occurrence = 0;

					for ( int c = 1; c < 10; c++ )
					{
						if ( ( actual[c, r] == 0 ) &&
							possible[c, r].Contains( n.ToString() ) )
						{
							occurrence += 1;

							//	if multiple occurrence, not a lone any more
							if ( occurrence > 1 )
								break;

							cPos = c;
							rPos = r;

						}
					}

					if ( occurrence == 1 )
					{
						//	number is confirmed
						SetCell( cPos, rPos, n, 1 );
						SetToolTip( cPos, rPos, n.ToString() );

						//	save the move into the stack
						Moves.Push( cPos.ToString() + rPos.ToString() + n.ToString() );

						DisplayActivity( "Look for Lone Ranges in Rows", false );
						DisplayActivity( "===================", false );
						DisplayActivity( "Inserted value " + n.ToString() + " in (" + cPos + rPos + ")", false );

						Application.DoEvents();
						changes = true;

						//	if user clicks the Hint button, exit function
						if ( HintMode )
							return true;

					}
				}
			

			}
			return changes;
		}



7. 구현 - column 에서 lone ranger 찾기




아래의 코드를 추가한다.



		//
		//	Look for Lone Rangers in Columns
		//
		public bool LookForLoneRangersinColumns()
		{
			bool changes = false;
			int occurrence;
			int cPos = 0;
			int rPos = 0;


			//	check by column
			for ( int c = 1; c < 10; c++ )
			{
				for ( int n = 1; n < 10; n++ )
				{
					occurrence = 0;

					for ( int r = 1; r < 10; r++ )
					{
						if ( ( actual[c, r] == 0 ) &&
							possible[c, r].Contains( n.ToString() ) )
						{
							occurrence += 1;

							//	if multiple occurrence, not a lone any more
							if ( occurrence > 1 )
								break;

							cPos = c;
							rPos = r;

						}
					}

					if ( occurrence == 1 )
					{
						//	number is confirmed
						SetCell( cPos, rPos, n, 1 );
						SetToolTip( cPos, rPos, n.ToString() );

						//	save the move into the stack
						Moves.Push( cPos.ToString() + rPos.ToString() + n.ToString() );

						DisplayActivity( "Look for Lone Ranges in Columns", false );
						DisplayActivity( "===================", false );
						DisplayActivity( "Inserted value " + n.ToString() + " in (" + cPos + ", " + rPos + ")", false );

						Application.DoEvents();
						changes = true;

						//	if user clicks the Hint button, exit function
						if ( HintMode )
							return true;

					}
				}
			}
			return changes;
		}



8. SolvePuzzle() 함수 수정



위의 순서도를 아래와 같이 구현했다. 수정한다. 현재 이코드는 chapter 5까지의 변경 사항을 적용했다가 이 글을 위해 다시 되돌렸으므로 이 글이 원한는 대로 동작을 하지 않을 수 있다. 참고하여 문제가 있다면 수정한다.

여기까지 구현했으면 쉬운 퍼즐은 풀린다.



		//
		//	Steps to solve the puzzle
		//
		public bool SolvePuzzle()
		{
			bool changes = false;
			bool ExitLoop = false;


			//	if game is not started, return
			if ( !GameStarted )
				return false;

			try
			{
				do
				{
					do
					{
						do
						{
							do
							{

								//	perform Col/Row and Minigrid Elimination (CRME)
								changes = CheckColumsAndRows();

								if ( ( HintMode && changes ) || IsPuzzleSolved() )
								{
									ExitLoop = true;
									break;
								}

							} while ( changes );

							if ( ExitLoop )
								break;

							//	look for lone rangers in minigrids
							changes = LookForLoneRangersinMinigrids();

							if ( ( HintMode && changes ) || IsPuzzleSolved() )
							{
								ExitLoop = true;
								break;
							}

						} while ( changes );

						if ( ExitLoop )
							break;

						//	look for lone rangers in rows
						changes = LookForLoneRangersinRows();

						if ( ( HintMode && changes ) || IsPuzzleSolved() )
						{
							ExitLoop = true;
							break;
						}

					} while ( changes );

					if ( ExitLoop )
						break;

					//	look for lone rangers in Columns
					changes = LookForLoneRangersinColumns();

					if ( ( HintMode && changes ) || IsPuzzleSolved() )
					{
						ExitLoop = true;
						break;
					}

				} while ( changes );

			}

			catch
			{
				throw new Exception( "Invalid Move" );
			}

			if ( IsPuzzleSolved() )
			{
				timer1.Enabled = false;
				Console.Beep();
				toolStripStatusLabel1.Text = "**** Puzzle Solved ****";
				MessageBox.Show( "Puzzle Solved" );
				return true;
			}
			else
				return false;

		}




Posted by 쿨한넘

1. Column, Row, and Minigrid Elimination (CRME)

이번 챕터에서는 Column, Row, and Minigrid Elimination (CRME)을 이용하여 퍼즐을 해결한다. 아마도 난이도가 높은 퍼즐은 단순한 CRME 만 가지고는 해결이 안될터지만, 향후에 해결할 것 같다.

일단 기본 생각은 아래와 같다.



Scan each cell in the grid from left to right, top to bottom

	For each cell:

		Set possible values for each cell to 123456789

		Scan its column and eliminate the values already present in the column
		Scan its row and eliminate the values already present in the row
		Scan its minigrid and eliminate the values already present in the minigrid

		If there is only one possible value for the cell,
			the number for the cell is confirmed

Until no more cells can be confirmed






2. 예외

  • 해결 불가능한 퍼즐
  • 플레이어가 입력한 값으로 인해 퍼즐이 해결 불가능해 질 경우

위 두가지 상황에서는 경고를 한다.



3. CRME 구현


아래의 member variable을 Form1 class에 추가한다.



		//	for CRME technique
		private String [,] possible = new String[9, 9];
		private Boolean HintMode;


possible[,]은 각 셀의 가능한 값들을 저장한다. String 클래스의 Replace() 같은 메쏘드를 이용하는 것이 편리하여 String 타입으로 선언. HintMode는 유저가 힌트를 요구했는지를 보관하는 변수. 만약에 HintModetrue 이면, CRME 알고리즘은 현재 셀의 값을 찾아 확정하고, 다른 셀의 값을 찾지 않고 콘트롤을 유저에게 넘긴다.



4. SetCell() 수정


아래 코드와 같이 (추가 부분)을 추가한다. 세을 지울경우 possible value 값을 초기화 하는 루틴이다.



		//
		//	set a cell to a given value
		//
		public void SetCell( int col, int row, int value, int erasable )
		{
			//	locate particular Label control
			Control [] lbl = this.Controls.Find( col.ToString() + row.ToString(), true );
			Label cellLabel = lbl[0] as Label;

			//	save the value in the array
			actual[col, row] = value;

			//	if erasing a cell, you need to reset the possible values
			//		for all cells
			//	(추가 부분)
			if ( value == 0 )
				for ( int r = 1; r < 10; r++ )
					for ( int c = 1; c < 10; c++ )
						if ( actual[c, r] == 0 )
							possible[c, r] = String.Empty;


			//	set the appearance for the Label control
			if ( value == 0 )		//	erasing the cell
			{
				cellLabel.Text = String.Empty;
				cellLabel.Tag = erasable;
				cellLabel.BackColor = DEFAULT_BACKCOLOR;
			}
			else
			{
				if ( erasable == 0 )
				{
					//	means default puzzle values
					cellLabel.BackColor = FIXED_BACKCOLOR;
					cellLabel.ForeColor = FIXED_FORECOLOR;
				}
				else
				{
					//	means user-set value
					cellLabel.BackColor = USER_BACKCOLOR;
					cellLabel.ForeColor = USER_FORECOLOR;
				}

				cellLabel.Text = value.ToString();
				cellLabel.Tag = erasable;
			}
		}



5. ToolTip Control 추가


유저가 퍼즐을 해결하는데 도움을 주기위해 아래 그림과 같이 possible value를 표시해주는 ToolTip Control을 추가한다. 마우스 커서가 해당 셀에 위치할 때 possible value들을 표시한다.



ToolTip Control을 추가하기 위해, ToolBox의 Common Controls -> ToolTip 을 찾아 더블클릭한다.



그리고 아래의 코드를 추가한다.



		//
		//	set the Tooltip for a Label Control
		//

		public void SetToolTip( int col, int row, String possibleValues )
		{
			//	locate the particular Label Control
			Control [] lbl = this.Controls.Find( col.ToString() + row.ToString(), true );
						
			toolTip1.SetToolTip( lbl[0] as Label, possibleValues );

		}


그리고 StartNewGame() 함수에 toolTip1.RemoveAll(); 을 아래와 같이 추가한다.



		//
		//	start a new game
		//
		public void StartNewGame()
		{
			saveFileName = String.Empty;
			txtActivities.Text = String.Empty;
			seconds = 0;

			ClearBoard();

			GameStarted = true;
			timer1.Enabled = true;
			toolStripStatusLabel1.Text = "New game started";

			toolTip1.RemoveAll();
		}



6. 셀의 입력 가능한 값 계산

CalculatePossibleValues() 함수를 아래와 같이 작성하고 추가한다. CalculatePossibleValues()는 해당 셀의 입력 가능한 값을 String으로 리턴하며, 만약 Empty면 Exception을 발생한다.



		//
		//	Calculate the possible values for a cell
		//

		public string CalculatePossibleValues( int col, int row )
		{
			String str;

			if ( possible[col, row] == String.Empty )
				str = "123456789";
			else
				str = possible[col, row];

			//	step 1. check by column

			for ( int r = 1; r < 10; r++ )
				if ( actual[col, r] != 0 )
					//	that means there is an actual value on it
					str = str.Replace( actual[col, r].ToString(), String.Empty );

			//	step 2. check by row

			for ( int c = 1; c < 10; c++ )
				if ( actual[c, row] != 0 )
					//	that means there is an actual value on it
					str = str.Replace( actual[c, row].ToString(), String.Empty );

			//	step 3. check by minigrid

			int startC = col - ( ( col - 1 ) % 3 );
			int startR = row - ( ( row - 1 ) % 3 );

			for ( int rr=startR; rr < startR + 3; rr++ )
				for ( int cc=startC; cc < startC + 3; cc++ )
					if ( actual[cc, rr] != 0 )
						//	that means there is a actual value on it
						str = str.Replace( actual[cc, rr].ToString(), String.Empty );

			//	if possible value is an empty string then error,
			//	because of invalid move

			if ( str == String.Empty )
				throw new Exception( "Invalid Move" );

			return str;
		}



7. Grid 탐색


CheckColumnsAndRows() 함수는 그리드를 검사하여 각 셀을 검사한다. CalculatePossibleValues()를 호출하고 이를 ToolTip Control과 연계시킨다. 만약에 입력 가능한 값이 한개로 결정되면, 셀의 값으로 저장한다. 만약에 유저가 힌트를 요구했을 경우, 첫번째로 값이 결정된 



		//
		//	Calculates the possible values for all the cells
		//
		public Boolean CheckColumsAndRows()
		{
			Boolean changes = false;

			//	check all cells
			for(int row = 1; row < 10;row++)
			{
				for(int col = 1;col < 10;col++)
				{
					if(actual[col,row] == 0)
					{
						try
						{
							possible[col, row] = CalculatePossibleValues(col,row);
						}

						catch(Exception ex)
						{
							DisplayActivity("Invalid placement, please undo move", false);

							throw new Exception("Invalid Move");
						}

						//	display the possible values in the ToolTip
						SetToolTip( col, row, possible[col, row] );

						if ( possible[col, row].Length == 1 )
						{
							//	that means a number is confirmed
							SetCell( col, row, int.Parse( possible[col, row] ), 1 );

							//	number is confirmed
							actual[col, row] = int.Parse( possible[col, row] );
							DisplayActivity( "Col/Row and Minigrid Elimination", false );
							DisplayActivity( "=========================", false );
							DisplayActivity( "Inserted value " + actual[col, row] + " in"
												+ "(" + col + ", " + row + ")", false );

							//	get the UI of the application to refresh
							//		with the newly confirmed number
							Application.DoEvents();

							//	saves the move into the stack
							Moves.Push( col.ToString() + row.ToString() + possible[col, row] );

							// if user only ask for a hint, stop at this point
							changes = true;

							if ( HintMode )
								return true;

						}
					}
				}
			}

			return changes;
		}


8. Hint / Solve Puzzle 기능 구현

Hint 버튼과 Solve Puzzle 버튼의 이벤트 핸들러를 생성하고 아래와 같이 수정한다.



		//
		//	Hint button
		//
		private void btnHint_Click( object sender, EventArgs e )
		{

			//	show hints one cell at a time
			HintMode = true;

			try
			{
				SolvePuzzle();
			}
			catch
			{
				MessageBox.Show(
					"Please undo your move",
					"Invalid Move",
					MessageBoxButtons.OK,
					MessageBoxIcon.Error );
			}
		}



		//
		//	Solve Puzzle button
		//
		private void btnSolvePuzzle_Click( object sender, EventArgs e )
		{

			//	solve the puzzle
			HintMode = false;

			try
			{
				SolvePuzzle();
			}
			catch
			{
				MessageBox.Show(
					"Pleas undo your move",
					"Invalid Move",
					MessageBoxButtons.OK,
					MessageBoxIcon.Error );
			}
		}


그리고 SolvePuzzle()은 다음과 같이 작성한다.



		//
		//	Steps to solve the puzzle
		//
		public bool SolvePuzzle()
		{

			bool changes;
			bool ExitLoop = false;

			try
			{
				do
				{
					//	perform Col/Row and Minigrid Elimination
					changes = CheckColumsAndRows();

					if ( ( HintMode && changes ) || IsPuzzleSolved() )
					{
						ExitLoop = true;
						break;
					}
				} while ( !changes );
			}
			catch
			{
				throw new Exception( "Invalid Move" );
			}

			if ( IsPuzzleSolved() )
			{
				timer1.Enabled = false;
				Console.Beep();
				toolStripStatusLabel1.Text = "**** Puzzle Solved ****";
				MessageBox.Show( "Puzzle Solved" );
				return true;
			}
			else
				return false;


		}



테스트한다.

여기까지 과정중에 알게된 문제점은, Game start를 하기전에 Hint 나 Solve Puzzle 버튼을 누르면, exception 이 발생하는 점, 그리고 Hint나 Solve Puzzle 버튼을 눌러 셀 값을 찾는데 있어서, 하나의 셀값도 확정값을 찾지 못할경우 무한루프에 빠지는 점 두가지이다.

SolvePuzzle()에 다음과 같이 내 임의로 수정했다.



		//
		//	Steps to solve the puzzle
		//
		public bool SolvePuzzle()
		{
			bool changes;
			bool ExitLoop = false;

			int loopCounter = 0;


			//	if game is not started, return
			if ( !GameStarted )
				return false;

			try
			{
				do
				{
					//	perform Col/Row and Minigrid Elimination
					changes = CheckColumsAndRows();

					if ( ( HintMode && changes ) || IsPuzzleSolved() )
					{
						ExitLoop = true;
						break;
					}

					if ( loopCounter++ == 3 )
						return false;


				} while ( !changes );
			}
			catch
			{
				throw new Exception( "Invalid Move" );
			}

			if ( IsPuzzleSolved() )
			{
				timer1.Enabled = false;
				Console.Beep();
				toolStripStatusLabel1.Text = "**** Puzzle Solved ****";
				MessageBox.Show( "Puzzle Solved" );
				return true;
			}
			else
				return false;

		}



이렇게 수정해도 툴팁 업데이트 로직이 이해가 가질 않는다. hint 나 solve puzzle 버튼이벤트에 의해 값이 찾아지는 셀까지 툴팁 업데이트가 이뤄진다. 이것은 나중에 내가 해결한다.

Posted by 쿨한넘


1. Undo 와 Redo


움직임의 undo는 Moves stack에서 pop, 그리고 이를 RedoMoves stack에 push, 반대로 redo는 RedoMoves stack에서 pop, 이것을 Moves stack에 push 한다. 디자인 뷰에서 Edit -> Undo를 선택하고 더블클릭하여 이벤트 핸들러 코드를 생성하고 다음과 같이 수정한다.



		//
		//	undo a move
		//
		private void undoToolStripMenuItem_Click( object sender, EventArgs e )
		{
			//	if no previous moves, then exit
			if ( Moves.Count == 0 )
				return;

			//	remove from the Moves stack and push into the RedoMoves stack
			String str = Moves.Pop();
			RedoMoves.Push( str );

			//	save the value in the array
			SetCell( int.Parse( str.Substring( 0, 1 ) ), int.Parse( str.Substring( 1, 1 ) ), 0, 1 );
			DisplayActivity( "Value " + str[2] + " removed at (" + str[0] + ", " + str[1] + ")", false );
		}


마찬가지로 Redo 메뉴의 이벤트 핸들러 코드를 생성하고 아래와 같이 수정한다.



		//
		//	redo the move
		//
		private void redoToolStripMenuItem_Click( object sender, EventArgs e )
		{
			//	if RedoMove stack is empty, the exit
			if ( RedoMoves.Count == 0 )
				return;

			//	remove from the RedoMoves stack and push into the Moves stack
			String str = RedoMoves.Pop();
			Moves.Push( str );

			//	save the value in the array
			SetCell( int.Parse( str.Substring( 0, 1 ) ), int.Parse( str.Substring( 1, 1 ) ), int.Parse( str.Substring( 2, 1 ) ), 1 );
			DisplayActivity( "Value " + str[2] + " reinserted at (" + str[0] + ", " + str[1] + ")", false );
		}



2. 게임의 저장


퍼즐은 숫자로 이루어진 String으로 텍스트화일에 저장된다. SaveGameToDisk()를 다음과 같이 수정한다.



		//
		//	Save the game to disk
		//
		public void SaveGameToDisk( Boolean saveAs )
		{
			//	if saveFileName is empty, means game has not been saved before
			if ( ( saveFileName == String.Empty ) || saveAs )
			{
				SaveFileDialog saveFileDialog1 = new SaveFileDialog();

				saveFileDialog1.Filter = "SDO files (*.sdo)|*.sdo|All files (*.*)|*.*";
				saveFileDialog1.FilterIndex = 1;
				saveFileDialog1.RestoreDirectory = false;

				if ( saveFileDialog1.ShowDialog() == DialogResult.OK )
				{
					//	store the filename first
					saveFileName = saveFileDialog1.FileName;
				}
				else
				{
					return;
				}
			}

			//	formulate the string representing the values to store
			StringBuilder str = new StringBuilder();

			for ( int row = 1; row < 10; row++ )
				for ( int col = 1; col < 10; col++ )
					str.Append( actual[col, row].ToString() );

			//	save the values to file
			Computer myComputer = new Computer();

			try
			{
				Boolean fileExists;

				fileExists = myComputer.FileSystem.FileExists( saveFileName );

				if ( fileExists )
					myComputer.FileSystem.DeleteFile( saveFileName );

				myComputer.FileSystem.WriteAllText( saveFileName, str.ToString(), true );

				toolStripStatusLabel1.Text = "Puzzle saved in " + saveFileName;
			}
			catch ( Exception )
			{
				MessageBox.Show( "Error saving game. Please try again." );
				throw;
			}
		}



코드를 추가하면 Computer 클래스를 알지 못한다고 에러를 띄운다.

솔루션 익스플로러에서 Reference를 마우스 오른쪽 클릭하고 Add Reference 선택하여 .NET 탭에서 Microsoft.VisualBasic를 선택한다.



그리고 코드 맨 위에 아래 코드를 추가한다.



using Microsoft.VisualBasic.Devices;



File->Save As... 메뉴의 이벤트 핸들러를 생성하고 아래와 같이 수정한다.



		//
		//	Save As... menu item
		//
		private void saveAsToolStripMenuItem_Click( object sender, EventArgs e )
		{
			if ( !GameStarted )
			{
				DisplayActivity( "Game not started yet.", true );
				return;
			}

			SaveGameToDisk( true );
		}


File->Save 메뉴의 이벤트 핸들러를 생성하고 아래와 같이 수정한다.



		//
		//	Save menu item
		//
		private void saveToolStripMenuItem_Click( object sender, EventArgs e )
		{
			if ( !GameStarted )
			{
				DisplayActivity( "Game not started yet.", true );
				return;
			}

			SaveGameToDisk( false );
		}



3. 저장된 게임 열기


File->Open 메뉴의 이벤트 핸들러는 다음과 같다.



		//
		//	Open a saved game
		//
		private void openToolStripMenuItem_Click( object sender, EventArgs e )
		{
			if ( GameStarted )
			{
				var response = MessageBox.Show( "Do you want to save current game?",
												"Save current game",
												MessageBoxButtons.YesNoCancel,
												MessageBoxIcon.Question );

				if ( response == DialogResult.Yes )
					SaveGameToDisk( false );
				else if ( response == DialogResult.Cancel )
					return;

			}

			//	load the game from disk

			String fileContents;
			OpenFileDialog openFileDialog1 = new OpenFileDialog();
			Computer myComputer = new Computer();

			openFileDialog1.Filter = "SDO files (*.sdo)|*.sdo|All files (*.*)|*.*";
			openFileDialog1.FilterIndex = 1;
			openFileDialog1.RestoreDirectory = false;

			if ( openFileDialog1.ShowDialog() == DialogResult.OK )
			{
				fileContents = myComputer.FileSystem.ReadAllText( openFileDialog1.FileName );
				toolStripStatusLabel1.Text = openFileDialog1.FileName;
				saveFileName = openFileDialog1.FileName;
			}
			else
			{
				return;
			}

			StartNewGame();

			//	initialize the board

			int counter = 0;
			int value;

			for ( int row = 1; row < 10; row++ )
			{
				for ( int col = 1; col < 10; col++ )
				{
					try
					{
						value = int.Parse( fileContents[counter].ToString() );

						if ( value != 0 )
							SetCell( col, row, value, 0 );
					}
					catch ( Exception )
					{
						MessageBox.Show( "File does not contain a valid Sudoku puzzle" );

						throw;
					}

					counter++;
				}
			}

		}


4. 게임 종료


File -> Exit 메뉴의 이벤트 핸들러는 다음과 같다.



		//
		//	Exit the application
		//
		private void exitToolStripMenuItem_Click( object sender, EventArgs e )
		{

			if ( GameStarted )
			{
				DialogResult reponse = MessageBox.Show( "Do you want to save current game?",
														"Save current game",
														MessageBoxButtons.YesNoCancel );

				if ( reponse == DialogResult.Yes )
					SaveGameToDisk( false );
				else if ( reponse == DialogResult.Cancel )
					return;

			}

			this.Close();
			Application.Exit();
		}




Posted by 쿨한넘


1. 새로운 게임 시작


design view에서 File 메뉴 -> New를 더블 클릭하여 해당하는 이벤트 핸들러 코드를 생성시킨다. 그리고 아래와 같이 수정한다.


		//
		//	start a new game
		//
		private void newToolStripMenuItem_Click( object sender, EventArgs e )
		{
			if ( GameStarted )
			{
				var response = MessageBox.Show( "Do you want to save current game?",
												"Save current game",
												MessageBoxButtons.YesNoCancel,
												MessageBoxIcon.Question );

				if ( response == DialogResult.Yes )
					SaveGameToDisk( false );
				else if ( response == DialogResult.Cancel )
					return;

			}

			StartNewGame();
		}


SaveGameToDisk()는 일단 아래와 같이 작성해 놓는다.


		//
		//	Save the game to disk
		//
		public void SaveGameToDisk( Boolean saveAs )
		{
			return;
		}


StartNewGame()은  몇가지 변수 업데이트와 statusbar 에 있는 Label control 업데이트, 그리고 ClearBoard() 호출을 담고있다. 코드은 아래와 같다.


		//
		//	start a new game
		//
		public void StartNewGame()
		{
			saveFileName = String.Empty;
			txtActivities.Text = String.Empty;
			seconds = 0;

			ClearBoard();

			GameStarted = true;
			timer1.Enabled = true;
			toolStripStatusLabel1.Text = "New game started";
		}


ClearBoard()는 각 셀을 다시 리셋하고, Moves-RedoMoves 스택의 인스턴스를 새로 생성한다.


		//
		//	clear the board
		//
		public void ClearBoard()
		{
			//	initialize the stacks
			Moves = new Stack<string>();
			RedoMoves = new Stack<string>();

			//	initialize the cells in the board
			for ( int row = 1; row < 10; row++ )
				for ( int col = 1; col < 10; col++ )
					SetCell( col, row, 0, 1 );

		}


SetCell()은 잠시 내버려 둔다.

StartNewGame()에서 timer1.Enabled = true 를 통해 게임을 시작하면 타이머를 활성화 시켰다. 타이머가 활성화 되면 StatusBar의 Label control에 경과한 시간을 타나내고, 이는 timer1_Tick() 이벤트 핸들러에서 처리한다.

Design view에서 timer1의 프라퍼티에서 Interval 을 1000 (1s)로 설정하고, 이벤트 창에서 Tick을 더블클릭하여 timer_Tick() 이벤트 핸들러 코드를 생성한다.


 



		//
		//	increment the time counter
		//
		private void timer1_Tick( object sender, EventArgs e )
		{
			toolStripStatusLabel2.Text = "Elapsed time: " + seconds + " second(s)";

			seconds += 1;
		}



2. 셀에 입력할 수의 선택


SelectedNumber 변수는 입력을 위해 선택된 수를 가지고 있으며, Erase 버튼을 선택하면 값은 0을 가지게 된다.

Design view에서 ToolStrip에 있는 Button 10개를 동시에 선택하고 Click 이벤트 핸들러 항목에 'toolStripButton_Click'를 입력한다. 이러면 각 버튼의 클릭 이벤트를 하나의 핸들러에서 처리가 가능하다.



 



생성된 코드를 아래와 같이 수정한다.



		private void ToolStripButton_Click( object sender, EventArgs e )
		{
			//	ToolStripButton selectedButton = (ToolStripButton)sender;		//	ok
			ToolStripButton selectedButton = sender as ToolStripButton;

			//	uncheck all the Button controls in the ToolStrip
			toolStripButton1.Checked = false;
			toolStripButton2.Checked = false;
			toolStripButton3.Checked = false;
			toolStripButton4.Checked = false;
			toolStripButton5.Checked = false;
			toolStripButton6.Checked = false;
			toolStripButton7.Checked = false;
			toolStripButton8.Checked = false;
			toolStripButton9.Checked = false;
			toolStripButton10.Checked = false;

			//	set the selected button to "checked"
			selectedButton.Checked = true;

			//	set the appropriate number selected
			if ( selectedButton.Text == "Erase" )
				SelectedNumber = 0;
			else
				SelectedNumber = int.Parse( selectedButton.Text );

		}



3. ToolStrip Label의 Click 이벤트 핸들링


셀을 클릭하게 되면 ToolStrip에 있는 선택된 버튼의 값이 셀에 입력된다. 그러나 셀의 값이 퍼즐의 고유 값, 즉 erasable 하지 않으면 바로 리턴한다(즉, Lable의 Tag 프라퍼티가 "0"이면 바로 리턴). Label의 Tag 값이 "1" (erasable) 이면 이벤트 Sender object의 Name으로 value를 결정한다. 그리고  Moves 스택에 push. 그런후에 puzzle이 완전히 완성이 되었는지도 검사한다. Cell_Click()을 다음과 같이 수정한다.



		//
		//	event handler for cell click
		//
		private void Cell_Click( object sender, EventArgs e )
		{
			//	check to see if game has even started or not

			if ( !GameStarted )
			{
				DisplayActivity( "Click File->New to start a new game or File->Open to load an existing game", true );
				return;
			}

			Label cellLabel = sender as Label;

			//	if cell is not erasable then exit
			if ( cellLabel.Tag.ToString() == "0" )
			{
				DisplayActivity( "Selected cell is not empty", false );
				return;
			}

			//	determine the col and row of the selected cell
			int col = int.Parse( cellLabel.Name.Substring( 0, 1 ) );
			int row = int.Parse( cellLabel.Name.Substring( 1, 1 ) );

			//	if erasing a cell
			if ( SelectedNumber == 0 )
			{
				//	if cell is empty then no need to erase
				if ( actual[col, row] == 0 )
					return;

				//	save the value in the array
				SetCell( col, row, SelectedNumber, 1 );
				DisplayActivity( "Number erased at (" + col + ", " + row + ")", false );
			}
			else if ( cellLabel.Text == String.Empty )
			{
				//	else set a value, check if move is valid
				if ( !IsMoveValid( col, row, SelectedNumber ) )
				{
					DisplayActivity( "Invalid move at (" + col + ", " + row + ")", false );
					return;
				}

				//	save the value in the array
				SetCell( col, row, SelectedNumber, 1 );
				DisplayActivity( "Number " + SelectedNumber.ToString() + " placed at (" + col + ", " + row + ")", false );

				//	save the move into the stack
				Moves.Push( cellLabel.Name.ToString() + SelectedNumber );

				//	check if the puzzle is solved
				if ( IsPuzzleSolved() )
				{
					timer1.Enabled = false;
					Console.Beep();
					toolStripStatusLabel1.Text = "**** Puzzle is Solved ****";
				}
			}

		}



4. 움직임의 유효성 검사


셀에 값을 입력하기 전에 스도쿠 룰에 따라 입력 하려는 값이 유효한지 검사한다. 입력하려는 수는 minigrid, column, row에서 유일한 값이어야 한다. IsMoveValid() 함수는 아래와 같이 구현한다.



		//
		//	check if move is valid
		//

		public Boolean IsMoveValid( int col, int row, int value )
		{
			Boolean puzzleSolved = true;

			//	scan through column
			for ( int i = 1; i < 10; i++ )
				if ( actual[col, i] == value )		//	duplicate
					return false;

			//	scan through row
			for ( int i = 1; i < 10; i++ )
				if ( actual[i, row] == value )		//	duplicate
					return false;

			//	scan through minigrid
			int startCol = col - ( ( col - 1 ) % 3 );
			int startRow = row - ( ( row - 1 ) % 3 );

			for ( int rr = 0; rr < 3; rr++ )
				for ( int cc=0; cc < 3; cc++ )
					if ( actual[startCol + cc, startRow + rr] == value )	//	duplicate
						return false;

			return true;
		}



5. 퍼즐이 해결되었는지를 검사


셀에 값이 입력이 되면 퍼즐이 완성되었는지를 검사한다. IsPuzzleSolved()는 아래와 같다.



		//
		//	check whether a puzzle is solved
		//
		public Boolean IsPuzzleSolved()
		{
			String pattern;

			//	check row by row
			for ( int r = 1; r < 10; r++ )
			{
				pattern = "123456789";

				for ( int c = 1; c < 10; c++ )
					pattern = pattern.Replace( actual[c, r].ToString(), String.Empty );

				if ( pattern.Length > 0 )
					return false;
			}

			//	check column by column
			for ( int c = 1; c < 10; c++ )
			{
				pattern = "123456789";

				for ( int r = 1; r < 10; r++ )
					pattern = pattern.Replace( actual[c, r].ToString(), String.Empty );

				if ( pattern.Length > 0 )
					return false;
			}

			//	check by minigrid

			for ( int c=1; c < 10; c = c + 3 )
			{
				pattern = "123456789";

				for ( int r=1; r < 10; r = r + 3 )
				{
					for ( int cc=0; cc < 3; cc++ )
						for ( int rr=0; rr < 3; rr++ )
							pattern = pattern.Replace( actual[c + cc, r + rr].ToString(), String.Empty );
				}

				if ( pattern.Length > 0 )
					return false;
			}

			return true;
		}



6. 셀의 값 갱신


SetCell() 함수는 지정된 column, row 위치의 cell이 erasable 한지를 검사하고, 그 셀에 값을 지정한다. 셀은 동적으로 생성된 Label 콘트롤로 표현되어 있어서, Controls 클래스의 Find() 메쏘드를 사용한다. SetCell()은 또한 셀의 컬러를 지정한다.



		//
		//	set a cell to a given value
		//
		public void SetCell( int col, int row, int value, int erasable )
		{
			//	locate particular Label control
			Control [] lbl = this.Controls.Find( col.ToString() + row.ToString(), true );
			Label cellLabel = lbl[0] as Label;

			//	save the value in the array
			actual[col, row] = value;

			//	set the appearance for the Label control
			if ( value == 0 )		//	erasing the cell
			{
				cellLabel.Text = String.Empty;
				cellLabel.Tag = erasable;
				cellLabel.BackColor = DEFAULT_BACKCOLOR;
			}
			else
			{
				if ( erasable == 0 )
				{
					//	means default puzzle values
					cellLabel.BackColor = FIXED_BACKCOLOR;
					cellLabel.ForeColor = FIXED_FORECOLOR;
				}
				else
				{
					//	means user-set value
					cellLabel.BackColor = USER_BACKCOLOR;
					cellLabel.ForeColor = USER_FORECOLOR;
				}

				cellLabel.Text = value.ToString();
				cellLabel.Tag = erasable;
			}
		}



7. 메세지 출력


폼의 TextBox 콘트롤에 메세지를 출력하는 DisplayActivity()는 다음과 같다.



		//
		//	Display a message in the Activities text box
		//
		public void DisplayActivity( String str, Boolean soundBeep )
		{
			if ( soundBeep )
				Console.Beep();

			txtActivities.Text += str + Environment.NewLine;
		}



일단 여기까지면 빌드와 테스트를 할 수 있다.





Posted by 쿨한넘

이제는 코드 작업.


1. 멤버 변수 선언




solution explorer에서 Form1.cs를 선택하고 표시된 아이콘을 클릭하면 Form1.cs 소스 화일을 보여준다.

이 에디터에서 작업을 한다.


		//	dimension of each cell in the grid
		const int CellWidth = 32;
		const int CellHeight = 32;

		//	offset from the top-left corner of the window
		const int xOffset = -20;
		const int yOffset = 25;

		//	color for empty cells
		private Color DEFAULT_BACKCOLOR = Color.White;

		//	color for original puzzle values
		private Color FIXED_FORECOLOR = Color.Blue;
		private Color FIXED_BACKCOLOR = Color.LightSteelBlue;

		//	color for user inserted values
		private Color USER_FORECOLOR = Color.Black;
		private Color USER_BACKCOLOR = Color.LightYellow;

		//	the number currently selected for insertion
		private int SelectedNumber;

		//	stacks to keep track of all the moves
		//	private Stack Moves = new Stack();				//	이 표현도 괜찮은 듯
		private Stack<string> Moves = new Stack<string>();	//	generic type?
		private Stack<string> RedoMoves = new Stack<string>();

		//	keep track of filename to save to
		private String saveFileName = String.Empty;

		//	used to represent the values in the grid
		private int [,] actual = new int[10, 10];

		//	used to keep track of elapsed time
		private int seconds = 0;

		//	has the game started?
		private Boolean GameStarted = false;


위 코드를 class Form1안에 추가한다.




2. 그리드(셀) 값의 표현



아래 그림과 같은 값을 표현할 때에는,




actual(1, 1) = 4
actual(2, 1) = 0
actual(3, 1) = 2
actual(4, 1) = 0
actual(5, 1) = 3
...
actual(1, 2) = 7
...


이렇게 표현한다. 따라서 10x10 배열이 필요하고, (0,x), (x, 0) 은 사용하지 않는다.




3. 셀의 이름


각 셀은 동적으로 생성된 Label control로 나타내어진다. 각 Label의 Name 프라퍼티는 column과 row의 조합으로 구성된다. 즉,


cell (1, 1) => 11
cell (2, 1) => 21
...


이렇게 표현된다.




4. Erasability of a Cell



각 셀은 유저가 입력한 값(value)나 퍼즐의 고유한 값이 입력된다. 유저가 입력한 값은 지울 수 있고, 퍼즐 고유의 값은 지울 수 없다. 이 특성은 Label의 Tag 프라퍼티를 이용하여 구현한다.


Label.Tag = "1"    //    value can be erased
Label.Tag = "0"    //    cannot be erased




5. 스택에 Moves 저장하기


셀에 값이 입력 될 때마다 스택에 좌표와 값을 저장한다. 유저가 undo를 할 경우 Moves 스택에서 값을 pop하여 Redo 스택에 push 하여 저장한다. 유저가 redo를 선택하면 redo 스택에서 값을 pop 하여 Moves 스택에 push 한다. 스택에 저장되는 값은 세자리의 숫자로 이루어진 스트링이다. (1, 3)에 값 5 => "135" 이렇게 표현된다.






6. 그리드의 동적 생성


어플리케이션이 실행될 때 처음으로 할 일은 9x9, 총 81개의 그리드를 생성하는 일이다. 다음의 코드를 Form1 class 안에 추가한다.


		public void DrawBoard()
		{
			//  default selected number is 1
			toolStripButton1.Checked = true;
			SelectedNumber = 1;

			//  used to store the location of the cell
			Point location = new Point();

			for ( int row = 1; row < 10; row++ )
			{
				for ( int col = 1; col < 10; col++ )
				{
					location.X = col * ( CellWidth + 1 ) + xOffset;
					location.Y = row * ( CellHeight + 1 ) + yOffset;

					Label lbl = new Label();

					lbl.Name = col.ToString() + row.ToString();
					lbl.BorderStyle = BorderStyle.None;
					lbl.Location = location;
					lbl.Width = CellWidth;
					lbl.Height = CellHeight;
					lbl.TextAlign = ContentAlignment.MiddleCenter;
					lbl.BackColor = DEFAULT_BACKCOLOR;
					lbl.Font = new Font( lbl.Font, lbl.Font.Style | FontStyle.Bold );
					lbl.Tag = "1";


					//	Add Handler
					lbl.Click += new EventHandler( Cell_Click );

					this.Controls.Add( lbl );
				}
			}
		}


lbl.Click += new EventHandler( Cell_Click ) 에서 각 Label이 마우스 클릭 이벤트가 발생할 때마다 Cell_Click을 실행하라는 이벤트핸들러를 등록하는 것을 눈여겨 본다. 그리고 this.Controls.Add( lbl ) 로 생성된 Label control을 Form1 (this) 에 추가시킨다.

Cell_Click() 이 없어서 빌드가 되지 않으니 일단 아래의 코드를 추가하여 둔다.


		//	event handler for cell click
		private void Cell_Click( object sender, EventArgs e )
		{
			return;
		}


빌드를 하고 실행을 하여보면 그리드가 그려지지 않는다. 그리드를 그리는 DraqBoard()가 실행되지 않았기 때문. 초기화는 Form1이 실행될 때 발생하는 이벤트인 Form1_Load()에서 한다. Form1.cs [Design]에서 폼을 더블클릭하면 숨겨져 있던 Form1_Load() 코드를 구현해준다. 다음과 같이 수정한다.


		private void Form1_Load( object sender, EventArgs e )
		{
			//	initialize the status bar
			toolStripStatusLabel1.Text = String.Empty;
			toolStripStatusLabel2.Text = String.Empty;

			//	draw the board
			DrawBoard();
		}


다시 Design 창에가서 Form1을 선택하고 프라퍼티 창을 보면 다음과 같이 이벤트 Load에 대한 핸들러를 확인 할 수 있다. 이런 구조를 잘 기억한다.


다시 빌드하고 실행을 해보면 그리드가 그려진다. 3x3 의 minigrid의 outline을 그린다. Form1의 Paint 이벤트 핸들러를 이용하여 그린다. 아래 그림처럼 Form1의 프라퍼티 창, 이벤트 뷰에서 Paint 항목을 찾고, 더블 클릭한다. 그러면 자동으로 Form1_Paint() 이벤트 핸들러 코드를 생성한다.



아래와 같이 Form1_Paint()를 수정한다.


		//
		//	draw the lines outlining the minigrids
		//
		private void Form1_Paint( object sender, PaintEventArgs e )
		{
			int x1, y1, x2, y2;

			//	draw the horizontal lines
			x1 = 1 * ( CellWidth + 1 ) + xOffset - 1;
			x2 = 9 * ( CellWidth + 1 ) + xOffset + CellWidth;

			for ( int i = 1; i <= 10; i = i + 3 )
			{
				y1 = i * ( CellHeight + 1 ) + yOffset - 1;
				y2 = y1;

				e.Graphics.DrawLine( Pens.Black, x1, y1, x2, y2 );
			}

			//	draw the vertical lines
			y1 = 1 * ( CellHeight + 1 ) + yOffset - 1;
			y2 = 9 * ( CellHeight + 1 ) + yOffset + CellHeight;

			for ( int j = 1; j <= 10; j += 3 )
			{
				x1 = j * ( CellWidth + 1 ) + xOffset - 1;
				x2 = x1;

				e.Graphics.DrawLine( Pens.Black, x1, y1, x2, y2 );
			}
		}


빌드를 하고 실행을 하면 아래와 같은 모습을 볼 수 있다.





Posted by 쿨한넘

이 챕터에서 구현할 스도쿠 어플리케이션의 기능은,


  • 스도쿠 어플리케이션의 유저 인터페이스
  • 그리드(셀)의 값을 어레이(배열)을 이용하여 표현
  • 움직임(푸는 과정)을 스택 데이터 스트럭쳐에 저장
  • Label controls를 이용하여 그리드를 동적으로 생성
  • Label controls의 마우스 클릭 이벤트를 핸들링
  • 움직임의 유효성 검사 (스도쿠 룰)
  • 퍼즐이 해결되었는지 검사
  • 셀(cell)의 값 업데이트
  • Undoing/Redoing
  • 게임 저장
  • 저장된 게임 로드
  • 게임 끝마치기


퍼즐 풀기등의 기능은 chapter 3에서 구현.

이 챕터에서 구현될 어플리케이션의 모습은,



이런 모습이다. 디자인에서 책과는 조금 다른 점이 있다.




1. 프로젝트를 만든다.






2. 유저 인터페이스를 만든다.


위와 같은 폼이 생기면 폼의 프라퍼티에서 아래 테이블과 같이 속성을 지정한다.


그러면,





3. MenuStrip Control을 추가한다.


Toolbox에서 Menustrip을 찾아 드래그 또는 더블클릭으로 폼에 추가한다.



추가된 MenuStrip의 smart tag을 클랙해서 'Insert Standard Items'를 선택한다.



아래와 같이 메뉴 항목을 수정한다.


 

Level 메뉴 항목은 다음과 같이 수정하면 된다. 나는 'E&xtremely Difficult'로 했다.




4. ToolStrip Control 추가한다.


MenuStrip Control과 마찬가지로 Toolbox에서 ToolStrip을 찾아 폼에 추가한다. MenuStrip 바로 아래에 붙어서 생성된다. 그리고 1개의 Label과 10개의 Button을 ToolStrip에 추가한다.



Label의 Text 프라퍼티를 'Select Number'로 바꾼다. Button의 'DisplayStyle' 프라퍼티를 'Text'로 바꾸고 각 Button의 'Text'프라퍼티를 1, 2, 3, 4, 5, 6, 7, 8, 9, Ease로 설정한다. Button을 다중선택하고 프라퍼티를 변경할 수 있다.





5. StatusStrip Control을 추가한다.


Toolbox에서 StatusStrip을 찾아 추가한다. 폼의 아래에 생긴다. 추가된 StatusStrip에 두개의 Label을 추가한다.




6. 기타 Control 추가.


아래 그림과 표를 참조하여 각 Control 들을 추가한다.



각 control의 프라퍼티에서 (Name) 속성을 btnHint, btnSolvePuzzle, txtActivities 으로 각각 바꿔준다.



셀 그리드는 Label을 이용하여 동적으로 생성한다. 코드 참조.


그리고 마지막으로 Toolbox에서 Timer를 찾아 추가한다.


여기까지의 모양은 아래와 같다. 각각의 control에서 폰트를 Tahoma로 바꾸었다.



Posted by 쿨한넘

별건 없고, 스도쿠에 관한 설명.



3x3 격자를 이 책에서는 minigrid 라고 부른다. 총 9개의 minigrid.




이 좌표는 프로그래밍 전반에 걸쳐서 사용된다. 일반적으로 좌표는 ( 행(row), 열(column) )로 사용하는 게 내 버릇이지만, 여기서는 ( 열(column), 행(row) ) 형식의 좌표를 사용하므로 주의한다.


책에서 소개한 스도쿠 관련 사이트



Posted by 쿨한넘

우연히 얻게된 "Programming Sudoku" (Wei-Meng Lee, Apress, 2006).



이 책은 Visual Basic을 이용하여 스도쿠 어플리케이션 작성을 설명했다.

이를 기본으로 C#으로 옮기는 과정을 기록한다.

책은 Microsoft Visual Basic 2005를 기준으로 설명을 했고, 나는 Microsoft Visual C# 2010, .NET Framework 4.0을 사용하였다. Visual Basic도 모르고, C#, .NET Framework도 몰라서 나중에라도 더 좋은 구현 방법을 찾을 수 있겠다.

Posted by 쿨한넘