Numberplace

Numberplace(ナンバープレイス)の完成です。

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

プログラムの説明

  1. ここまではページの説明に従ってプログラムすれば動きますが、ここから先は読者の力量でプログラムして下さい。
    「数字(問題)テーブル,色テーブル,Helpテーブル」を統合して、シンプルにまとめました。
    m_t[9,9,11] がそのテーブルで、9*9 のマスを11個の要素で管理します。
    11個の要素の内、先頭(0番目)に数字を格納します。
    この値が0の時は、数字が設定されていない(タイプされていない)マスです。
    1~9にはヒント(Helpテーブル)を格納して、各マスごとに選択可能な数字を設定します。
    10番目にはマスの色を格納します。
    マスの色が0のときは、出題文字です。
        int[,,] m_t = new int[9,9,11];  //ナンプレ・テーブル
    
  2. 盤面を描画する MyHandler() 関数です。
    if (m_t[y, x, 10]>0 && m_t[y, x, 10]<7) はマスに色が設定されたときです。
    if (m_t[y, x, 0]>0 && m_t[y, x, 0]<10) はマスに数字が設定されたときです。
    if (m_hint && m_t[y, x, 0]==0) はヒントが設定されたマスです。
    ヒントの文字は6色用意されています。
    if (m_OK) はゲームが完成したときです。
        private void MyHandler(object sender, PaintEventArgs e)
        {
            Graphics g = e.Graphics;
            if (m_back.bmp == null) Application.Exit();
            for (int y = 0; y < 9; y++)
            {
                for (int x = 0; x < 9; x++)
                {
                    if (m_t[y, x, 10]>0 && m_t[y, x, 10]<7)
                        m_color.View(g, m_t[y, x, 10]-1, x*60+10, y*60+30);
                    if (m_t[y, x, 0]>0 && m_t[y, x, 0]<10)
                        m_num.View(g, m_t[y, x, 0], x*60+10, y*60+30);
                    if (m_hint && m_t[y, x, 0]==0)  // Hint
                    {
                        for(int k=0; k<9; k++)
                            if (m_t[y, x, k+1] != 0)
                            {
                                int xw = x * 60 + 10 + (k % 3) * 20;
                                int yw = y * 60 + 30 + (k / 3) * 20;
                                m_mini.View(g, m_t[y, x, k+1], xw, yw);
                            }
                    }
                }
            }
            m_back.View(g, 10, 30);
            m_number.View(g, 10, 580);
            if (m_OK)
            {   Font f = new Font("MS ゴシック", 30);
                g.DrawString("完成しました! \(^o^)/", f, Brushes.Red, new PointF(30F, 260F));
                m_OK = false;
            }
        }
    
  3. マウスのクリックを検出する OnMyMouseDown() 関数です。
    m_col フラグで数字の操作と色の操作を切り替えます。
    m_col がオフのときは、左クリックでマスの数字を右クリックでヒントの数字を操作します。
    ヒントの数字は 1~9 の場所をクリックすると「表示/非表示」を切り替えます。
    m_col がオンのときは、左クリックでマスの色を右クリックでヒントの色を操作します。
    色の設定は注目するマスやヒントの文字に色を付けてマークするためです。
    ヒントの文字は6色の文字を切り分けて0~59で設定します。
        private void OnMyMouseDown(object sender, MouseEventArgs e)
        {
            int xp, yp, zp, k;
            xp = (e.X - 10) / 60;
            yp = (e.Y - 30) / 60;
            zp = ((((e.Y - 30) % 60) / 20) * 3) + (((e.X - 10) % 60) / 20);
            zp++;
            if (e.Y > 580)
            {   m_no = xp;
                return;
            }
            if (xp > 8 || yp > 8)   return;     //0(1)~8(9)
            if (m_t[yp, xp, 10] == 0)   return; //出題文字
            if (e.Button == MouseButtons.Left)  //マウスの左ボタン(マスの設定)
            {
                if (m_col==false)       //数字の設定/削除
                {
                    for(k=1; k<10; k++) m_t[yp,xp,k] = 0;
                    if (m_t[yp, xp, 0] == 0)    m_t[yp, xp, 0] = m_no + 1;
                    else    m_t[yp, xp, 0] = 0;
                }
                else                    //マスの色を設定
                {   if (m_t[yp, xp, 10] != 0)
                    {   m_t[yp, xp, 10] = (m_t[yp, xp, 10] + 1) % 5;
                        if (m_t[yp, xp, 10] == 0) m_t[yp, xp, 10] = 1;
                    }
                }
            }
            if (e.Button == MouseButtons.Right) //マウスの右ボタン(ヒント数字の設定/削除)
            {
                if (zp>10)  return;
                m_hint = true; 
                if (m_col==false)       //Hint 文字を設定
                {
                    if (m_t[yp, xp, zp] == 0)  m_t[yp, xp, zp] = zp;
                    else    m_t[yp, xp, zp] = 0;
                }
                else                    //Hint 文字に色を設定
                {   if (m_t[yp, xp, 0] == 0)
                        m_t[yp, xp, zp] = (m_t[yp, xp, zp]+10) % 60;
                }
            }
            Invalidate();
        }
    
  4. m_ht[9,9,10] にヒントを設定する SetHint() 関数です。
    マスの色を初期化して1~9の数字から行・列・3*3で使われている数字を消し込みます。
        public void SetHint()
        {   int     x,y,k,wn;
            for(y=0; y<9; y++)
                for(x=0; x<9; x++)
                {   if (m_t[y,x,10]>4)  m_t[y,x,10] = 1;
                    for(k=1; k<10; k++) m_t[y,x,k] = k;
                }
            for(y=0; y<9; y++)
                for(x=0; x<9; x++)
                {   wn = m_t[y,x,0];
                    if (wn!=0)
                        for(k=0; k<9; k++)
                        {   m_t[y,k,wn] = 0;    //y行から消す
                            m_t[k,x,wn] = 0;    //x列から消す
                            Del33(y,x);         //3*3から消す
                        }
                }
            //Debug(m_ht);
        }
    
  5. 開発が簡単で、それでいて一応パズルゲームの要件を備えているゲームとしてナンプレ(数独)を選んだのですが、進めて行くうちにゲームの奥深さを知りました。
    初級者のレベルでは簡単に正解出来ても、難問/超難問になると容易には解けません。
    現在問題を解く関数を作成中ですが一苦労しそうです。
    娘が言うには正解が2通り以上ある問題はルール違反で、再帰処理(仮定の設定)も使うなと言います。 (^_^;)
    再帰を使って問題を解くと正解が2通り以上ある問題を判別することが出来ません。
    あなたも問題を解く関数に挑戦してみませんか? (^o^)/

ナンプレ(数独)ヒント

  1. ナンプレ(数独)を解くためのヒントメニューをサポートします。
    ナンプレは「9行,9列,3*3に区切った9個」のグループに分かれていて、それぞれのグループに1~9の数を割り当てます。
  2. グループ毎にバッテイングしないように配置出来る数字の候補を表示したのがページ先頭の画像です。
    (かなりいい加減な画像なので信用しないで下さい)
    配置出来る数字の候補が一個のときは、そこにはその数字しか配置できないのですから決定できます。
  3. グループ毎に調べて候補が一か所しかない数字も、その場所に決定することが出来ます。
    ページ先頭の画像では 3-2 の1(3行目の1はここだけ)や 6-1 の9(左中の9はここだけ)や 2-8 の8(8列目の8はここだけ)がこれに相当します。
  4. これらの情報を小さな文字に色を付けて表示する関数をサポートします。
    配置出来る数字の候補が一個の数字は m_t[i, j, pt] = pt + 50; で赤色で表示します。
    グループ内で候補が一か所の数字は m_s[j, k] = k + 40; で緑色で表示します。
        // グループを調べてヒントをカラー表示する
        private bool HintCor()
        {   int[]   chk = new int[10];
            int     i,j,k,wk,pt=0;
            bool    flag = false;
            bool    flg = false;
    
            for(i=0; i<9; i++)
                for(j=0; j<9; j++)
                    for(k=1; k<10; k++) m_t[i, j, k] = m_t[i, j, k] % 10;
            // Hint から候補が一個のマスを調べる(赤色で表示)
            for(i=0; i<9; i++)
                for(j=0; j<9; j++)
                {   if (m_t[i, j, 0] != 0) continue;    //出題文字
                    wk = 0;
                    for(k=1; k<10; k++)
                        if (m_t[i, j, k]!=0)
                        {   wk++;
                            pt = k;
                        }
                    if (wk == 1)
                    {   m_t[i, j, pt] = pt + 50;        //赤色に設定
                        flag = true;
                        //Debug("赤色設定", pt);
                    }
                }
            // グループ内で一個だけの数字を検索(確定)
            for(i=0; i<29; i++)
            {   if (i%10==9)    continue;
                GetLine(i);     //グループを m_s[9,11] に抜き出す
                flg = false;
                for(k=0; k<10; k++) chk[k] = 0;
                for(j=0; j<9; j++)
                    if (m_s[j,0]==0)
                        for(k=1; k<10; k++)
                            if (m_s[j, k] != 0) chk[k]++;
                //Debug(chk);
                for (k = 1; k < 10; k++)
                {
                    if (chk[k] == 1)    //グループ内でkは一個
                        for (j = 0; j < 9; j++)
                            if (m_s[j, 0] == 0 && m_s[j, k] == k)
                            {   m_s[j, k] = k + 40;
                                flg = true;
                                flag = true;
                            }
                }
                if (flg)    SetLine(i);
            }
            return flag;
        }
    
  5. ここまでは少し考えれば解るのですが、難問を解くには配置出来る数字を消し込むことが重要です。
    次の段階として「行,列,3*3」に配置出来る数字を制限することを考えます。
    ページ先頭の1行目の8に注目して下さい。
    1行目に8を配置する箇所は 1-4 と 1-6 しかありません。
    ここに8を配置すると「上中の3*3及び1行目の他のマスから8を消すことが出来ます。
    別の例を挙げれば4列目の6に注目して下さい。
    4列で6を配置できるマスは 7-4, 9-4 しかありません。
    従って4列目の他のマスや下中(3*3)の他のマスから6を消すことが出来ます。
    別の例を挙げれば9行目に1を配置できるマスは 9-4 と 9-5 しかありません。
    従って9行目の他のマスや下中(3*3)のマスから1を消すことが出来ます。
    このようにして候補を絞り込むと、次の手が見えてきます。
  6. 次に2個のペアーに注目しましょう。
    同一グループでペアーを構成すれば、関連するグループから消すことが出来ます。
    4-2 と 6-2 の28のペアーに注目して下さい。
    片方が2ならもう一方は8だと言っているのですから、2と8は確定です。
    従って左中の 3*3 や2列目から2と8を消すことが出来ます。
  7. ここまでの手順を尽くしても、解答が得られないのが難問です。
    後は数字の関連(チェーン)を調べることになります。
    次の画像を見て下さい。
    1. 6-1(1,5,6)から5を消すことが出来ます。
    2. 5に注目して考えると、1行目に5を配置出来るマスは 1-1 と 1-4 です。
      同様に5行目に5を配置出来るマスは 5-2 と 5-4 です。
    3. 1-1(1,5,9) と 1-4(5,8) は同じグループなので、5はどちらか一つしか選べません。
      同様に 1-4(5,8) と 5-4(2,5) も同じグループなので、5はどちらか一つしか選べません。
    4. 1-4 を5とした場合、5行目から5を選べるのは 5-2 に限ります。
      同様に 5-4 を5とした場合、1行目から5を選べるのは 1-1 に限ります。
    5. 従って、1-1 か 5-2 の何れか(ピンクのマス)が5になり、6-1 から5を消すことが出来ます。

  8. 数字の関連(チェーン)は難解なので、もう一つの例を紹介します。
    1. 1-4(2,7,9)から2を消すことが出来ます。
    2. 1-2(2,7)から2を選ぶと同じグループの 1-4 から2を消すことが出来るのは当然です。
      問題は 1-2 から7を選んだときにも 1-4 から2を消すことが出来るかです。
    3. 1-2 から7を選ぶと、4-2(4,7) の7が消えて4が選ばれます。
    4. 左中(3*3)の 4-2 以外から4が消えて 6-1(4,9) の9が選ばれます。
    5. 6-4(2,9) の9が消えて2が選ばれます。
    6. 従って、1-2 か 6-4 の何れか(ピンクのマス)が2になり、1-4 から2を消すことが出来ます。

  9. 数字の関連(チェーン)を調べるのは結構面倒で、関連を調べても全ての問題が解ける訳ではありません。
    そこで私は再帰(禁じ手)を使い、大多数の問題を解くメニューを作成しました。
    ネット上の多くのページでは、複数の正解が生じる問題は解けないのですが、再帰を使えば解くことが出来るのもメリットです。
    再起の説明は 再起関数の基礎 を参照して下さい。
    一応 Version-3 が完成したので私のページのダウンロードから提供します。

[Next Chapter ↓] 世界一数独
[Previous Chapter ↑] Alpha 版

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