9 분 소요

아래 코드들이 실행되면 오른쪽 상단에는 실행시간, 중단에는 9x9빈칸, 그리그 그 아래에는 Start, Quit버튼이 위치된다.

1

하나의 9X9 사각형을 9개의 3X3사각형으로 나누는 선을 아래 코드로 이용해 그린다.

sudoku_easy.jsp
	...
    <script type="text/javascript">
        ...	
        $(document).ready(function() 
        {
            ...	    
            //3x3의 작은 사각형으로 나누는 선 그리기
            var row_add;
            var col_add;
            for(var i=2; i< 8; i+=3)
            {
                for(var j=0; j<9; j++)
                {
                    row_add = "#"
                    col_add = "#"     
                    row_add += (i+j*9);
                    col_add += (i*9+j);
                    //border-left: double 3px blue;
                    $(row_add).css("border-right","4px solid black");
                    $(col_add).css("border-bottom","4px solid black");
                }  
            }
        });
        ...
    </script>

2

Start버튼을 누르면 sciprt태그 의 start 함수가 실행된다.

sudoku_easy.jsp
...
<script type="text/javascript">
	...
	function start() 
	{			
		sudoku_maker();		
		...
	}
	...
</script>	

start함수에서 sudoku_maker함수가 실행되고, sudoku_maker클래스의 객체가 생성된다.

3

package games;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.TreeMap;


public class sudoku_maker 
{
	int cur_idx;
	int cur_num;
	Random rand = new Random();
	ArrayList<Integer> nums;
	ArrayList<Integer> including;
	ArrayList<Integer> total = new ArrayList<Integer>();
	ArrayList<ArrayList<Integer>> board =new ArrayList<ArrayList<Integer>>();
	ArrayList<ArrayList<Integer>> rows = new ArrayList<ArrayList<Integer>>();;
	ArrayList<ArrayList<Integer>> cols = new ArrayList<ArrayList<Integer>>();
	ArrayList<ArrayList<Integer>> small_boards =new ArrayList<ArrayList<Integer>>();
	TreeMap<Integer,Integer> blank = new TreeMap<Integer,Integer>();
	
	long beforeTime; 
	long afterTime;
	long secDiffTime;
	long beforeTime_o; 
	long afterTime_o;
	long secDiffTime_o;	
	boolean stop = false;
	boolean back = false;

	public sudoku_maker(int blank_size)
	{		
		initializer();	
        ...
	}
    ...
}

initializer함수를 이용해 2차원 어레이리스트 board,row,cols,small_boards들의 모든 요소값들을 -1로 만들어 준다.

public class sudoku_maker 
{
	...
	public void initializer()
	{
		total = new ArrayList<Integer>();
		board =new ArrayList<ArrayList<Integer>>();
		rows = new ArrayList<ArrayList<Integer>>();;
		cols = new ArrayList<ArrayList<Integer>>();
		small_boards =new ArrayList<ArrayList<Integer>>();		
		for(int i=0; i<9; i++)
		{
			board.add(new ArrayList<>(Arrays.asList(-1,-1,-1,-1,-1,-1,-1,-1,-1)));
			rows.add(new ArrayList<>(Arrays.asList(-1,-1,-1,-1,-1,-1,-1,-1,-1)));
			cols.add(new ArrayList<>(Arrays.asList(-1,-1,-1,-1,-1,-1,-1,-1,-1)));
			small_boards.add(new ArrayList<>(Arrays.asList(-1,-1,-1,-1,-1,-1,-1,-1,-1)));
			for(int j=0; j<9; j++)
			{
				total.add(j+i*9);
			}
		 }
	  }
     ...
}	

processing함수를 이용해 스도쿠 판을 채워줄 숫자를 규칙에 맞게 배열한다.

public class sudoku_maker 
{
	...
	public sudoku_maker(int blank_size)
	{	
    	...
		processing();
        ...
	}
    ...
}

n_mix함수를 이용해 1~9까지 숫자를 무작위 순서로 배치한 어레이리스트를 받는다.

public class sudoku_maker 
{
	public void processing()
	{
		//초기 세팅
		for(int i=0; i<9; i++)
		{			
			outerloop:
			while(true)
			{
				nums=n_mix(true);
				...
			}
		}			
	}
}
public class sudoku_maker 
{
	...
	//9개의 숫자 무작위로 배치하기
	private ArrayList<Integer> n_mix(boolean init) 
	{
		ArrayList<Integer> n_array;
		if(init)
		{
			 n_array = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9));
		}
		else
		{
			 n_array = new ArrayList<Integer>(Arrays.asList(0,1,2,3,4,5,6,7,8));
		}
		ArrayList<Integer> n_mix = new ArrayList<Integer>();
		int arr_size = 9;
		for(int i=0; i<9; i++)
		{
			int cur_idx = rand.nextInt(arr_size);		
			int cur_val = n_array.get(cur_idx);
			n_mix.add(cur_val);
			n_array.remove(Integer.valueOf(cur_val));
			arr_size -=1;
		}		
		return n_mix;	
	}
}
스도쿠에 배치된 숫자는 아래와 같은 규칙을 따른다.
1.각각의 가로줄(row)과 세로줄(column)에 1~9가 중복 없이 하나씩 들어간다. 
2.3×3칸(box) 안에는 1~9가 중복 없이 하나씩 들어간다.
예시를 가지고 위의 규칙하에 숫자를 배치하는 코드의 동작을 이해해보자.
첫번째 행에 숫자를 넣는것은 특별히 고려해야할 것이 없음으로, 두번째 줄에 숫자를 
넣는 상황부터 설명해 보겠다.

4

두번째 행에 넣을 숫자들을 얻어온다.
nums=n_mix(true);	// nums == [8, 7, 3, 6, 5, 1, 9, 2, 4]
두번재 행 첫번째 열에 들어갈 숫자cur_num(=nums.get(0))가 스도쿠 규칙에
적합한 값인지 판단한다.

board의 첫번째 열(=cols.get(0))에는 [-1,-1,-1, 4,-1,-1,-1,-1,-1]가 들어가 있다.
그리고 첫번째 3x3box(=small_boards.get(0))에는 [-1, 2, -1, 4, 5,-1,-1,-1,-1]가 들어있다.
cur_num(=8)이 cols.get(0)과 small_boards.get(0)에 들어있지 않으므로, 8은 이 두배열에
들어갈 수 있다. 

두번째 행의 들어갈 나머지 숫자들(nums.get(1)~nums.get(8))에대해 앞서 살펴본 방법과 
동일한 방식으로 판단한다. 

nums의 모든 숫자들이 위의 조건을 만족했음으로, 아래의 반복문(화살표로 표시된 영역)을
이용해 rows, cols, small_boards, board에 nums값들을 넣어준다.
public class sudoku_maker 
{
	public void processing()
	{
		//초기 세팅
		for(int i=0; i<9; i++)
		{			
			outerloop:
			while(true)
			{
				...
				for(int j=0; j<9; j++)
				{							
					cur_num = nums.get(j);			
					
					//자리가 비어있고, 이미 사용된 숫자가 아닌지 판별한다.
					if(cols.get(j).indexOf(cur_num) <0)
					{
						if(small_boards.get(j/3 + 3*Integer.valueOf(i / 3)).indexOf(cur_num) <0)
						{
							if(j==8)
							{										
									for(int k=0; k<9; k++)
									{	
										cur_num = nums.get(k);	
 										rows.get(i).set(k, cur_num);
										cols.get(k).set(i, cur_num);
										small_boards.get(k/3 + 3*(i / 3)).set(cur_num-1, cur_num);
								    	board.get(i).set(k, cur_num);									
									}	
									break outerloop;								
							}							
						}
						else
						{		
							...
						}	
					}
					else
					{
						...
					}						
				}

			}
		}			
	}
}

5

세번째 행에 넣을 숫자들을 얻어온다.
nums=n_mix(true);	// nums == [9, 6, 2, 4, 1, 8, 7, 5, 3]
세번재 행 세번재 열에 들어갈 숫자cur_num(=nums.get(2))가 cols.get(2)
(=[-1, 2, 3, -1, -1, -1, -1, -1, -1])에 들어있으므로 backtracking
(별로 표시한 부분, 이 때 i=2, j=2)를 실행한다. 
public class sudoku_maker 
{
	public void processing()
	{
		//초기 세팅
		for(int i=0; i<9; i++)
		{			
			outerloop:
			while(true)
			{
				nums=n_mix(true);
				innerloop:
				for(int j=0; j<9; j++)
				{							
					cur_num = nums.get(j);			
					
					//자리가 비어있고, 이미 사용된 숫자가 아닌지 판별한다.
					if(cols.get(j).indexOf(cur_num) <0)
					{
						if(small_boards.get(j/3 + 3*Integer.valueOf(i / 3)).indexOf(cur_num) <0)
						{
							...					
						}
						else
						{		
							i = backtracking(i, j);		
							break innerloop;
						}	
					}
					else
					{
					   i =backtracking(i, j);	//i=2, j=2
						break innerloop;
					}						
				}

			}
		}			
	}
}
public class sudoku_maker 
{	
    ...
    public int backtracking(int i, int j)
	{
		//i=2, j=2
		including = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9)); 
		including.removeAll(rows.get(i));						//=> including = [1,2,3,4,5,6,7,8,9]
		including.removeAll(cols.get(j));						//=> including = [1,4,5,6,7,8,9]
		including.removeAll(small_boards.get(j/3 + 3*(i / 3)));	//=> including = [1,6,9]	
        
		//board의 i행 j열에 들어갈 수 있는 숫자가 하나도 없는경우
		if(including.size() == 0)
		{	
			nums = new ArrayList<>(rows.get(i-1));		
			if(i > 0)
			{
				...					
				return i-1;
			}
			else
			{
				return 0;
			}

		}
        
		//board의 i행 j열에 들어갈 수 있는 숫자가 하나 이상(1,6,9) 있는경우  
		else
		{
			return i;
		}
	}
    ...
}
processing()에서 backtracking(별로 표시된 부분)수행결과 i는 2가 된다. 
그 결과 innerloop:로 표시한 반복문에서 탈출한다. 그 다음 while문이 반복되고 nums에 
1~9까지 숫자를 새롭게 무작위로 배치한 값(=[9, 6, 1, 4, 2, 8, 7, 5, 3])들이 들어간다.
public class sudoku_maker 
{
	public void processing()
	{
		//초기 세팅
		for(int i=0; i<9; i++)
		{			
			outerloop:
			while(true)
			{
				//nums = [9, 6, 1, 4, 2, 8, 7, 5, 3]
				nums=n_mix(true);
				innerloop:
				for(int j=0; j<9; j++)
				{							
					cur_num = nums.get(j);			
					
					//자리가 비어있고, 이미 사용된 숫자가 아닌지 판별한다.
					if(cols.get(j).indexOf(cur_num) <0)
					{
						...
					}
					else
					{
						i =backtracking(i, j);	//i=2, j=2
						 break innerloop;
					}						
				}

			}
		}			
	}
}

5

세번째 행에 넣을 각각의 숫자들(nums=[9, 6, 1, 4, 2, 8, 7, 5, 3])이 3x3box과 숫자의 인덱스에 
해당하는 열에 들어있지 않음으로, 아래의 반복문(화살표로 표시된 영역)을 이용해rows, cols, 
small_boards, board에 nums값들을 넣어준다.
public class sudoku_maker 
{
	public void processing()
	{
		//초기 세팅
		for(int i=0; i<9; i++)
		{			
			outerloop:
			while(true)
			{
				...
				for(int j=0; j<9; j++)
				{							
					cur_num = nums.get(j);			
					
					//자리가 비어있고, 이미 사용된 숫자가 아닌지 판별한다.
					if(cols.get(j).indexOf(cur_num) <0)
					{
						if(small_boards.get(j/3 + 3*Integer.valueOf(i / 3)).indexOf(cur_num) <0)
						{
							if(j==8)
							{										
									for(int k=0; k<9; k++)
									{	
										cur_num = nums.get(k);	
 										rows.get(i).set(k, cur_num);
										cols.get(k).set(i, cur_num);
										small_boards.get(k/3 + 3*(i / 3)).set(cur_num-1, cur_num);
								    	board.get(i).set(k, cur_num);									
									}	
								break outerloop;								
							}							
						}
						else
						{		
							...
						}	
					}
					else
					{
						...
					}						
				}

			}
		}			
	}
}

6

네, 다섯번째 행들을 위에서의 과정을 반복해 채워준다.

7

이제 여섯번째 행에 대해서 아래의 반복문을 실행되고, j가 2일 때 backtracking(별로 표시됨)이 
수행된다. (nums=[2, 3, 1, 4, 9, 8, 7, 5, 6])
public class sudoku_maker 
{
	public void processing()
	{
		//초기 세팅
		for(int i=0; i<9; i++)
		{			
			outerloop:
			while(true)
			{
				...
				for(int j=0; j<9; j++)
				{			
                    //(nums=[2, 3, 1, 4, 9, 8, 7, 5, 6])
					cur_num = nums.get(j);			
					
					//자리가 비어있고, 이미 사용된 숫자가 아닌지 판별한다.
					if(cols.get(j).indexOf(cur_num) <0)
					{
						if(small_boards.get(j/3 + 3*Integer.valueOf(i / 3)).indexOf(cur_num) <0)
						{
							...
						}												
						else
						{		
							i = backtracking(i, j);		//i=5; j=2
							break innerloop;
						}	
					}
					else
					{
						...
					}						
				}
			}
		}			
	}
}
6행 3열에 넣을수 있는 숫자가 하나도 없음으로, 5행에 넣었던 숫자를 제거한다. 이 때, rows,
cols,small_boards의 관련된 값도 변경해준다. 위에서 'i = backtracking(i, j);'수행결과 
i = 5-1이 수행되어 i엔 4가 들어간다. 즉 5행 새롭게 숫자를 넣는 과정을 반복해야 한다는 것이다.
public class sudoku_maker 
{	
    ...
    public int backtracking(int i, int j)
	{
		//i=5, j=2
		including = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9)); 
		//including.removeAll(rows.get(i));						//=> including = [1,2,3,4,5,6,7,8,9]
		including.removeAll(cols.get(j));						//=> including = [4, 6, 7, 8]
		including.removeAll(small_boards.get(j/3 + 3*(i / 3)));	//=> including = []	
        
		//board의 i행 j열에 들어갈 수 있는 숫자가 하나도 없는경우
		if(including.size() == 0)
		{	
			nums = new ArrayList<>(rows.get(i-1));		
			if(i > 0)
			{
				rows.set(i-1, new ArrayList<>(Arrays.asList(-1,-1,-1,-1,-1,-1,-1,-1,-1)));
				board.set(i-1, new ArrayList<>(Arrays.asList(-1,-1,-1,-1,-1,-1,-1,-1,-1)));
				for(int k=0; k<9; k++)
				{	
					cols.get(k).set(i-1, -1);
					cur_num = nums.get(k);	
					small_boards.get(k/3 + 3*Integer.valueOf((i-1) / 3)).set(cur_num-1, -1);									
				}							
				return i-1;
			}
			else
			{
				...
			}

		}
        
		//board의 i행 j열에 들어갈 수 있는 숫자가 하나 이상(1,6,9) 있는경우  
		else
		{
			...
		}
	}
    ...
}

8

지금까지 예시에서 살펴본 동작을 반복하면 9X9스도쿠가 규칙에 맞게 만들어진다. 숫자가 전부 
다 배치되었음으로, 아래 코드를 통해 빈칸이 될곳을 무작위로 선택한다.
public class sudoku_maker 
{
	...
    ArrayList<Integer> total = new ArrayList<Integer>();
	TreeMap<Integer,Integer> blank = new TreeMap<Integer,Integer>();
	public sudoku_maker(int blank_size)
	{		
		//빈칸이 될곳 선택
		for(int i=0; i<blank_size; i++)
		{
			cur_idx = rand.nextInt(total.size());	
			cur_num = total.get(cur_idx);	
			blank.put(cur_num, board.get(cur_num/9).get(cur_num%9));
			total.remove(Integer.valueOf(cur_num));			
		}
	}
	...
}
board에서 빈칸이 될곳의 값을 0으로 만든다.
public class sudoku_maker 
{
	...
	public sudoku_maker(int blank_size)
	{		
		//빈칸 만들기
		for(int key : blank.keySet())
		{	
			board.get(key/9).set(key%9, 0);
		}	
	}
	...
}

sudoku_easy.jsp에서 s_d.get_sudo_mix(), s_d.get_blank() 실행해 9X9 board에관한 정보들을 받는다.

9

public class sudoku_maker 
{
	...
	ArrayList<ArrayList<Integer>> board =new ArrayList<ArrayList<Integer>>();
	TreeMap<Integer,Integer> blank = new TreeMap<Integer,Integer>();
    ...
	public TreeMap<Integer,Integer> get_blank(int blank_size) 
	{				
		return blank;		
	}        
	public ArrayList<ArrayList<Integer>> get_sudo_mix()
	{
		return board;
	}
	...
}

sudoku_easy.jsp에서 보드에관한 정보를 바탕으로 숫자 및 빈칸을 만들어주고 초시계 또한 시작된다.

11

sudoku_easy.jsp
...
    <script type="text/javascript">
        ...
        function start() 
        {			
            sudoku_maker();	
            $("#start").attr("disabled", true);
            $("#difficulty").attr("disabled", true);
            $("#end").attr("disabled", false);
            timer = setInterval
            (
            function()
            {
               time++;
               min = Math.floor(time/60);
               hour = Math.floor(min/60);
               sec = time%60;
               min = min%60;

               var th = hour;
               var tm = min;
               var ts = sec;
               if(th<10){
               th = "0" + hour;
               }
               if(tm < 10){
               tm = "0" + min;
               }
               if(ts < 10){
               ts = "0" + sec;
               }
               $('#time').html(th + ":" + tm + ":" + ts);
             }, 1000);		
        }
        ...
    </script>

10

위의 코드에서 보면 onkeypress=”Number_check()”라고 되어있다. 즉 빈칸에 키보드로 값을 입력하면 함수가 실행된다. 하나의 빈칸에 숫자가 새로 입력되거나 바뀔경우, 각 빈칸에 해당되는 행과, 열에 있는 숫자들의 합이 45인가 확인해 본다.

sudoku_easy.jsp
    ...
    <script type="text/javascript">
        ...
        var row_key = new Array(9);
        var col_key = new Array(9);
        var row_val = new Array(9);
        var col_val = new Array(9);

        for (var i = 0; i < row_key.length; i++) 
        {
            // new Array() => 빈배열만 선언
            row_key[i] = new Array();
            col_key[i] = new Array();
            row_val[i] = new Array();
            col_val[i] = new Array();	
        }

        var row;
        var col;
        var sum_r;
        var sum_c;
        var solv;
        ...
        function Number_check(class_num)
        {
            //keyCode를 이용해 숫자만 입력 가능하도록 한다.
            // 숫자	 keyCode
            // 0 	=> 	48
            //		...
            // 9 	=> 	57	

            //숫자가 아닌경우
            if(event.keyCode<49 || event.keyCode>57)
            {
               //숫자가아닌 값을 입력란에서 지우는 역할을 한다.
               event.returnValue=false;
            }

            else
            {
                //자바스크립트에서 나눗샘을 할 경우 소수점 단위로 표시하기에 
                //나눗샘의 몫을 얻기 원한다면 버림을 해야된다.
                row = Math.floor((class_num)/9);
                col = (class_num)%9;		

                solv = true;
                good:
                for(var i=0; i<9; i++)
                {
                    //alert(i);		
                    sum_r = 0;
                    sum_c = 0;

                    //row_c에 row_val[i]를 깊은복사 한다.
                    var row_c = row_val[i].slice();
                    for(var j=0; j<row_key[i].length; j++)
                    {
                        if(class_num != row_key[i][j])
                        {
                            //(event.keyCode-48)의 자료형이 Number가 아닌경우를 대비해 *1을 해준다. 
                            sum_r += ($('.'+row_key[i][j]).val())*1;

                            //row_c.indexOf(($('.'+row_key[i][j]).val())*1)인 인데스에 해당하는 값을
                            //row_c에서 제거한다.					
                            row_c.splice(row_c.indexOf(($('.'+row_key[i][j]).val())*1), 1);
                        }

                        //Number_check는 onKeyPress를 통해 실행된다.
                        //onKeyPress는 키를 누르면 이벤트 발생 후 문자가 입력되게 처리된다.
                        //다시말해, 현재 빈칸에 입력된 숫자는 ($('.'+row_sum_blank[i][j]).val())*1로 
                        //가져오지 못한다. 이런 문제를 해결하기 위해선 아래와 같이 코딩해야 한다.
                        else
                        {
                            //blank에 들어갈수 있는 값들에 event.keyCode-48가 포함되었나
                            //확인한다.					
                            if(row_c.includes(event.keyCode-48))
                            {
                                sum_r += (event.keyCode-48);	

                                //row_c.indexOf(event.keyCode-48)인 인데스에 해당하는 값을
                                //row_c에서 제거한다.
                                row_c.splice(row_c.indexOf(event.keyCode-48), 1);
                            }
                            else
                            {
                                solv = false;
                                break good;
                            }

                        }

                    }

                    //col_c에 col_val[i]를 깊은복사 한다.
                    var col_c = col_val[i].slice();
                    for(var j=0; j<col_key[i].length; j++)
                    {		
                        if(class_num != col_key[i][j])
                        {
                            sum_c += ($('.'+col_key[i][j]).val())*1;	
                            //col_c.indexOf(($('.'+col_key[i][j]).val())*1)인 인데스에 해당하는 값을
                            //col_c에서 제거한다.						
                            col_c.splice(col_c.indexOf(($('.'+col_key[i][j]).val())*1), 1);
                        }
                        else
                        {
                            if(col_c.includes(event.keyCode-48))
                            {					
                                sum_c += (event.keyCode-48);	

                                //col_c.indexOf(event.keyCode-48)인 인데스에 해당하는 값을
                                //col_c에서 제거한다.						
                                col_c.splice(col_c.indexOf(event.keyCode-48), 1);
                            }
                            else
                            {
                                solv = false;						
                                break good;
                            }
                        }				
                    }	

                    //가로행의 합이 45가 아닌경우(1~9까지의 합이 45)	
                    if(sum_r + row_sum[i] != 45)
                    {
                        solv = false;
                        break good;	
                    }

                    //세로열의 합이 45가 아닌경우(1~9까지의 합이 45)		
                    if(sum_c + col_sum[i] != 45)
                    {
                        solv = false;
                        break good;	
                    }

                }

                //스도쿠를 완성한 경우
                if(solv)
                {
                    ...				
                }		
            }
        }
        ...
    </script>

스도쿠가 완성되었다면 ajax로 비동기 통신을 해주고 100ms후 결과를 보여주는 모달창을 실행한다.(숫저퍼즐 페이지에서 비동기 통신 참고)

12

13

모달창을 닫으면 end함수가 실행되고 화면이 초기화된다.

<script type="text/javascript">
	...
	function end() 
	{
		//숫자가 없는 깨끗한 상태로 만든다.
		//location.replace()를 이용해 현재 페이지로 이동하면,
		//히스토리에 저장되지 않아 이전페이지 이동이 불가능하게 된다.
		//location.href => 현재 페이지의 주소
		clearInterval(timer);	
		location.replace(location.href);
	}
	...
</script>

2 마친가지로 스도쿠 완성전 Quit버튼을 누른다면, end()함수가 실행되고 화면이 초기화 된다. 동작영상

※전체 코드는 https://github.com/qlqlzh/Storage에서 my_project.zip과 my_project.z01를 다운받은 후 my_project_mybatis.zip의 압축을 풀으면 볼 수 있다.

댓글남기기