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;
}
일단 여기까지면 빌드와 테스트를 할 수 있다.