Alpha 版

残りのメニューを実装して、α版の完成です。

前田稔の超初心者のプログラム入門

プログラムの説明

  1. Menu でメニューの設定とイベントハンドラーは作成されています。
    今回は残りのメニューを実装して、一応プログラムを完成させます。
    データーの入出力メニューです。
    親メニュー 子メニュー 説明
    File(&F)
    Save(&S) 数字テーブルを保存する
    Load(&L) 数字テーブルとヒントを入力する
    SaveAll(&A) 数字テーブルとヒントを保存する
    Exit(&X) プログラムを終了する
  2. データーの入出力メニューを実装します。
    using を追加して、STAThread を設定して下さい。
    using System.IO;    // for File, StreamReader
    using System.Text;  // for Encoding
    
    class form01
    {
        [STAThread]
        public static void Main()
        {
            MyForm mf = new MyForm();
            Application.Run(mf);
        }
    }
    
  3. プレイ中のファイルを保存する SaveFile() 関数です。
    数字テーブル(m_t[9, 9])に続いてカラーテーブル(m_ct[9, 9])を文字列で書き出します。
    数字が並ぶと見にくくなるので、数字テーブルの「0 を . に変換」して保存します。
        private void SaveFile(object sender, EventArgs e)
        {
            StreamWriter writer;
            string file, str;
            int i, j;
            SaveFileDialog saveDialog = new SaveFileDialog();
            saveDialog.Title = "保存するファイルを選択してください";
            saveDialog.Filter = "Number|*.num|すべてのファイル|*.*";
            saveDialog.RestoreDirectory = true;
            if (saveDialog.ShowDialog(this) == DialogResult.OK)
            {
                file = saveDialog.FileName;
                writer = new StreamWriter(file, false, Encoding.GetEncoding("utf-8"));
                for (i = 0; i < 9; i++)
                {
                    str = "";
                    for (j = 0; j < 9; j++)
                    { str += (char)(m_t[i, j] | 0x30); }
                    str = str.Replace('0', '.');
                    writer.WriteLine(str);
                }
                for (i = 0; i < 9; i++)
                {
                    str = "";
                    for (j = 0; j < 9; j++)
                    { str += (char)(m_ct[i, j] | 0x30); }
                    writer.WriteLine(str);
                }
                writer.Close();
            }
            saveDialog.Dispose();
        }
    
  4. ファイルを入力する LoadFile() 関数です。
    数字テーブル(m_t[9, 9])に続いてカラーテーブル(m_ct[9, 9])を入力します。
    数字が並ぶと見にくくなるので、数字テーブルの「0 を . に変換」した形式も扱います。
    またカラーを省略した次のような数字の並びデータ(一行毎に改行して下さい)もサポートします。
    Text Editor で簡単にタイプ出来るので、ネットに掲載されている問題をタイプして試してみて下さい。
    ...46.2.5
    .465138.9
    .7389..61
    .5824.7..
    ..79.512.
    9..67854.
    13..26984
    7...59.12
    .92184.57
    
    LoadFile() 関数です。
        private void LoadFile(object sender, EventArgs e)
        {
            string file, str;
            int i, j;
            OpenFileDialog openDialog = new OpenFileDialog();
            openDialog.Title = "Number ファイルを選択してください";
            openDialog.Filter = "Number|*.num|すべてのファイル|*.*";
            if (openDialog.ShowDialog() == DialogResult.OK)
            {
                file = openDialog.FileName;
                if (!File.Exists(file)) return; //ファイルの有無をチェック
                for (i = 0; i < 9; i++)
                {   for (j = 0; j < 9; j++)
                    {   m_t[i, j] = 0;
                        m_ct[i, j] = 0;
                    }
                }
                StreamReader reader = new StreamReader(file, Encoding.GetEncoding("utf-8"));
                for (i = 0; i < 9; i++)
                {   if ((str = reader.ReadLine()) == null) break;
                    str = str.Replace('.', '0');
                    for (j = 0; j < 9; j++)
                    {   m_t[i, j] = (int)(str[j] & 0xf);
                        if (m_t[i, j]==0)   m_ct[i, j] = 1;
                    }
                }
                for (i = 0; i < 9; i++)
                {   if ((str = reader.ReadLine()) == null) break;
                    if (str.Length<9)   continue;
                    for (j = 0; j < 9; j++)
                    {   m_ct[i, j] = (int)(str[j] & 0xf); }
                }
                reader.Close();
            }
            openDialog.Dispose();
            Debug(m_t);
            Invalidate();
        }
    
  5. プログラムを終了する Exit() 関数です。
        private void Exit(object sender, EventArgs e)
        {   this.Close();  }
    
  6. 完成を確認する Complete() 関数です。
    m_OK フラグを定義して下さい。
        bool        m_OK = false;       //完成フラグ
    
        // 完成を確認
        private void Complete(object sender, EventArgs e)
        {   int sum;
            if (Error())
            {   Invalidate();
                return;
            }
            for (int i = 0; i < 9; i++)
            {   sum = 0;
                for (int j = 0; j < 9; j++) sum += m_t[i, j];
                if (sum != 45) return;
                sum = 0;
                for (int j = 0; j < 9; j++) sum += m_t[j, i];
                if (sum != 45) return;
            }
            m_OK = true;
            Invalidate();
        }
    
  7. MyHandler() で完成メッセージを表示します。
        private void MyHandler(object sender, PaintEventArgs e)
        {
            ・・・
            if (m_OK)
            {   Font f = new Font("MS ゴシック", 30);
                g.DrawString("完成しました! \(^o^)/", f, Brushes.Red, new PointF(30F, 260F));
                m_OK = false;
            }
        }
    
  8. Complete() から呼ばれる Error() 関数です。
    エラーを見つけたマスの色を 6 に設定します。
    Err3() 関数は小プレートの中を調べる関数です。
        public bool Error()
        {   int[] chk = new int[10];
            int     i,j,w;
            bool    flag = false;
            for(i=0; i<9; i++)
            {   for(j = 0; j < 10; j++) chk[j] = 0;
                for(j = 0; j < 9; j++)
                {   if (m_ct[i, j]!=0)   m_ct[i, j] = 1;
                    chk[m_t[i, j]]++;
                }
                for(j = 0; j < 9; j++)
                {
                    if (m_ct[i, j]>0 && m_ct[i, j]<7)
                    {
                        w = m_t[i, j];
                        if (w>0 && chk[w] > 1)
                        {   m_ct[i, j] = 6;
                            flag = true;
                        }
                    }
                }
                for(j = 0; j < 10; j++) chk[j] = 0;
                for(j = 0; j < 9; j++)  chk[m_t[j, i]]++;
                for(j = 0; j < 9; j++)
                {
                    if (m_ct[j, i]>0 && m_ct[j ,i]<7)
                    {
                        w = m_t[j, i];
                        if (w>0 && chk[w] > 1)
                        {   m_ct[j, i] = 6;
                            flag = true;
                        }
                    }
                }
            }
            for (i = 0; i < 3; i++)
                for (j = 0; j < 3; j++)
                    if (Err3(i*3, j*3)) flag = true;
            return flag;
        }
    
  9. 小プレートの中を調べる関数 Err3() 関数です。
        public bool Err3(int y, int x)
        {   int[]   chk = new int[10];      //3*3 で使われている数字
            int     i,j,w;
            bool    flag = false;
            for(i=0; i<10; i++) chk[i] = 0;
            for(i=0; i<3; i++)
                for(j=0; j<3; j++)
                    chk[m_t[i+y, j+x]]++;
            for(i=0; i<3; i++)
                for(j=0; j<3; j++)
                    if (m_ct[i+y, j+x] > 0 && m_ct[i+y, j+x] < 7)
                    {   w = m_t[i+y, j+x];
                        if (w>0 && chk[w] > 1)
                        {   m_ct[i+y, j+x] = 6;
                            flag = true;
                        }
                    }
            return flag;    
        }
    
  10. Tool メニューです。
    現在の盤面でエラーを調べる Check() 関数です。
    ヒントやカラーのリセットも行います。
        private void Check(object sender, EventArgs e)
        {   Error();
            m_hint = false;
            Invalidate();
        }
    
  11. 正解画面を設定する Play() 関数です。
    自動生成したテーブル(m_b[9, 9])を表示するので、入力したファイルでは正解は得られません。
        private void Play(object sender, EventArgs e)
        {
            for(int y=0; y<9; y++)
                for(int x=0; x<9; x++)
                {   m_t[y, x] = m_b[y, x];
                    if (m_ct[y,x]!=0)   m_ct[y, x] = 1;
                }
            Invalidate();
        }
    
  12. Hint/RunHint を選ぶと、小プレートに候補が一個の数字(赤色)を確定してコンピューターが簡単な問題を解いてくれます。
    「赤の数字」に該当するマスが見つからなくなければ、解析が停止します。
    Hint/Think は RunHint より1段上の思考メニューです。
    Hint/Think を実行すると、難問以外はこのメニュー一発で解けるかも知れません。 (^_^;)
  13. ヒント無しで解けたら良いのですが、上級になると手に負えません。
    そんなときの強い見方が Hint メニューで、マスに入る可能性のある数字を教えてくれます。
    数字が一個のときは、その数字で確定です。
    またグループ(行・列・3*3 の小プレート)で候補が一個のときも確定できます。
    詳細は Number Game からナンプレ(数独)ヒントを参照して下さい。
        int[,,] m_ht = new int[9,9,10]; //Helpテーブル
        bool        m_hint = false;     //Hint Mode
    
        private void Hint(object sender, EventArgs e)
        {
            SetHint();
            m_hint = true;
            Invalidate();
        }
    
  14. Hint() 関数から呼ばれる SetHint() 関数です。
    1~9の数字から、行や列で使われている数字を消し込みます。
    Del33() は小プレートの中を調べる関数です。
        public void SetHint()
        {   int     x,y,k,wn;
            for(y=0; y<9; y++)
                for(x=0; x<9; x++)
                {   if (m_ct[y,x]>4)    m_ct[y,x] = 1;
                    for(k=0; k<10; k++) m_ht[y,x,k] = k;
                }
            for(y=0; y<9; y++)
                for(x=0; x<9; x++)
                {   wn = m_t[y,x];
                    if (wn!=0)
                    {   for(k=0; k<9; k++)
                        {   m_ht[y,k,wn] = 0;   //y行から消す
                            m_ht[k,x,wn] = 0;   //x列から消す
                            Del33(y,x);
                        }
                    }
                }
        }
    
  15. SetHint() 関数から呼ばれる Del33() 関数です。
    y,x が含まれる小プレートから使われている数字を消し込みます。
        public void Del33(int y, int x)
        {   int     i,j,yw,xw,n;
            n = m_t[y,x];
            yw = (y/3)*3; 
            xw = (x/3)*3; 
            for(i=0; i<3; i++)
                for(j=0; j<3; j++)  m_ht[yw+i,xw+j,n] = 0;
        }
    

問題の作成

  1. 問題の作成は NewGame メニューで行います。
    親メニュー 説明
    NewGame(&G)
    Lebel-1(&A) 初心者レベルの問題です
    Lebel-2(&B) 初級者レベルの問題です
    Lebel-3(&C) 中級者レベルの問題です
    Init(&R) リセットして出題図に戻す
    Complete(&Q)問題が正しく作成できたことを確認する
  2. 問題の生成では、毎回同じパターンで出題していたのでは芸がありません。
    そこでプログラムでナンプレの問題を作成します。
    問題の作成は一筋縄ではいかず、回答が得られるまで繰り返すことになります。
    我と思わん方はプログラムを解析してみて下さい。
    取り合えず次のソースを組み込むと問題を作成してくれます。
  3. 問題を作成するための領域を定義します。
        // Data 作成領域
        int[,] restcol = new int[9, 10]; //縦列制限テーブル[col][num] = 0 : 埋めれる, 1 : 埋めれない
        int[, ,] restblock = new int[3, 3, 10]; //ブロック制限テーブル[row/3][col/3][num] = 同上
    
  4. Generate() から問題を作成する makeMahoujin() 関数を呼び出します。
    作成された m_b[9, 9] から rate の確率で穴を抜いて出題します。
    娘によれば、ナンプレでは複数の正解が生じる問題は駄目だと言うのですが、今回は目をつぶります。 (^_^;)
        public void Generate(int rate)
        {
            makeMahoujin();
            for(int y=0; y<9; y++)
                for(int x=0; x<9; x++)  m_t[y, x] = m_b[y, x];
            // m_t[y,x]=0  色=1 で穴抜きを設定
            for(int y=0; y<9; y++)
                for(int x=0; x<9; x++)
                {
                    m_ct[y, x] = 0;
                    if (rand.Next(100) < rate)
                    {   m_t[y, x] = 0;
                        m_ct[y, x] = 1;
                    }
                }
            Invalidate();
        }
    
  5. 問題を作成する makeMahoujin() 関数です。
        public void makeMahoujin()
        {   int i,j,k;
            int row,col,num;
      
            do  //回答得られるまで繰り返し
            {   //制限テーブル、回答テーブル初期化
                for(i=0; i<9; i++)
                    for(j=0; j<9; j++)
                        m_b[i,j] = 0;
        
                for(i=0; i<9; i++)
                    for(j=0; j<10; j++)
                        restcol[i,j] = 0;
        
                for(i=0; i<3; i++)
                    for(j=0; j<3; j++)
                        for(k=0; k<10; k++)
                            restblock[i,j,k] = 0;
        
                //埋めようとしてみます
                for(row=0; row<9; row++)
                { //1行あたり 40回 挑戦。
                    for(i=0; i<40; i++)
                    {   if( num_tryfillrow(row)!=0 )    break;  //埋めアルゴリズムここで変更
                        //else  x[row]++;       //つまった回数行ごとにカウント
                    }
          
                    //無理ぽ。最初からやり直し
                    if( i >= 40 )   break;
          
                    //1行埋まった。制限テーブル更新
                    for(col=0; col<9; col++)
                    {   num = m_b[row,col];
                        restcol[col,num] = 1;
                        restblock[row/3,col/3,num] = 1;
                    }
                }
        
            } while(row < 9);//最後の行まで埋まるまで繰り返し
        }
        //引数で渡された未完の行、空にして埋めなおす
        //埋める数字をランダムで決定(0 失敗  1 成功)
        public int num_tryfillrow(int row)
        {
          int k;
          int col,num;
          int[] kouho = new int[9];
          int[] used = new int[10];
     
          //どの数字既に埋めたかテーブル初期化
          for(num=1; num<10; num++) used[num] = 0;
      
          //左から順に埋めていく
          for(col=0; col<9; col++){
        
          //埋めれる数字を kouho に入れる
          k = 0;
          for(num=1; num<10; num++)
            if( restcol[col,num] == 0 && restblock[row/3,col/3,num] == 0 && used[num] == 0 )
                kouho[k++] = num;
        
          //何も埋められない→失敗
          if(k == 0)    { return(0);  }
        
          //ランダムで数字を決定
          num = kouho[rand.Next(k)];
          m_b[row,col] = num;
          used[num] = 1;
        
          }
          return(1); //成功
        }
    
  6. これで一応ゲーム(α版)は完成です。
    Level-1~Level-3 を選んでプレイしてみて下さい。
    またネットに掲載されている問題をタイプして試してみて下さい。
    ファイルから入力した問題は、メニューから Anser を選んでも解答は表示されません。

[Next Chapter ↓] Alpha 版の実行
[Previous Chapter ↑] Menu

超初心者のプログラム入門(C# Frame Work)