コンピュータがプレイする

コンピュータがプレイする関数を追加します。

前田稔(Maeda Minoru)の超初心者のプログラム入門

プログラムの説明

  1. Think.cs にコンピュータがプレイする関数を追加してオセロゲームを完成します。
    // Think_W() 関数を定義した Think.cs
    using System;
    using System.Collections;
    using System.Windows.Forms;
    using System.Diagnostics;
    
    public partial class MyForm
    {   int[,] m_vt = new int[8, 8]     // 1-1, 2-2 は別途
        {{  0,  -6,  4,  2,  2,  4,  -6,  0 },
         { -6,   0, -2, -1, -1, -2,   0, -6 },
         {  4,  -2,  0,  0,  0,  0,  -2,  4 },
         {  2,  -1,  0,  0,  0,  0,  -1,  2 },
         {  2,  -1,  0,  0,  0,  0,  -1,  2 },
         {  4,  -2,  0,  0,  0,  0,  -2,  4 },
         { -6,   0, -2, -1, -1, -2,   0, -6 },
         {  0,  -6,  4,  2,  2,  4,  -6,  0 }};
    
        int[,] t00 = new int[4, 2]      //0-0 座標テーブル(Y,X)
        {{ 0,0 }, { 0,7 }, { 7,7 }, { 7,0 }};
        int[,] t11 = new int[4, 2]      //1-1 座標テーブル(Y,X)
        {{ 1,1 }, { 1,6 }, { 6,6 }, { 6,1 }};
        int[,] dt =  new int[4, 2]     //上辺→,右辺↓,下辺←,左辺↑
        {{ 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 }};
        int[,] dt5 = new int[4, 2]      //左上,右上,右下,左下
        {{ 1, 1 }, { 1, -1 }, { -1, -1 }, { -1, 1 }};
    
        // t[8][8] から辺を一列取り出す
        ArrayList GetLine(int no, int[,] t)
        {
            ArrayList   array= new ArrayList();
            int xp,yp,k;
            yp= t00[no,0];
            xp= t00[no,1];
            for(k=0; k<8; k++)
            {   array.Add(t[yp,xp]);
                yp+= dt[no,0];
                xp+= dt[no,1];
            }
            return array;
        }
    
        // コーナ(黒or白)から展開する駒をカウント
        int Corner(int no, int[,] t)
        {   int     x,y,xp,yp,cnt;
            int     koma;
            yp = t00[no, 0];
            xp = t00[no, 1];
            if (t[yp, xp] == 0) return 0;
            koma = t[yp, xp];
            //コーナ(no)のt[8,8]をt55にコピー
            int[,] t55 = new int[5, 5];
            for(y=0, yp=t00[no, 0]; y<5; y++, yp+=dt5[no, 0])
            {   for(x=0, xp=t00[no, 1]; x<5; x++, xp+=dt5[no, 1])
                {   t55[y,x] = t[yp,xp]; }
            }
            //コーナの確定した石数を求める
            cnt = 0;
            xp = 4;
            for(y=0; y<4 && t55[y,0]==koma; y++)
            {   for(x=0; x<xp; x++)
                {   if ((t55[y,x]!=koma) ||
                       (y>0 && x>0 && t55[y-1, x+1]!=koma && t55[y+1, x-1]!=koma))
                    {   xp=x;  break; }
                    cnt++;
                }
            }
            return cnt;
        }
    
        // 端の駒(黒or白)の立場で続く駒を評価
        int EvEdge(ArrayList s, int bcnt, int wcnt, int scnt)
        {
            if (scnt<3) return 0;
            if ((int)s[0]==1 && (int)s[1]==-1 && (int)s[2]==0 ) return wcnt*20; // [●○△・・・]
            if ((int)s[0]==-1 && (int)s[1]==1 && (int)s[2]==0 ) return bcnt*20; // [○●△・・・]
            return 0;
        }
    
        // 1-2 に駒(黒or白)があるとき(黒が有利なら+の値)
        int EvPattern(ArrayList s, int sc)
        {
            if (sc == 4)
            {   // [△○●・・・・] 黒(c)の手番で隅を取れる
                if ((int)s[1]==-1 && (int)s[2]==1)  return 50;
                return 20;      // 辺が全て黒になる[△●○・・・]
            }
            if ((int)s[1]==1)   // 1-2 に黒 [△●・・・]
            {
                if ((int)s[2]==-1 && (int)s[3]==1)  return -60;   // [△●○●・・・]
                if ((int)s[2]==-1 && (int)s[3]==0 && (int)s[4]==-1) return -40;  // [△●○  ○・・・]
                return 0;
            }
            if ((int)s[1] == -1)  // 1-2 に白[△○・・・]
            {
                if ((int)s[2] == 1 && (int)s[3] == -1) return 60;      // [△○●○・・・] 
                if ((int)s[2] == 0 && (int)s[3] == -1) return 20;      // [△○※○・・・]
                if ((int)s[2] == 0 && (int)s[3] == 0 && (int)s[4] == 1) return 30; //[△○※・●・・・]
                if ((int)s[2] == 0 && (int)s[3] == 1 && (int)s[4] == -1) return 30; //[△○※●○・・・]
            }
            return 0;
        }
    
        // ●の手番(立場)で取り出した1列の辺を評価(黒が有利なら+)
        int EvLine(ArrayList s)
        {
            ArrayList   ws= new ArrayList();
            int bcnt, wcnt, scnt, rt, val, wk;
            // 黒石, 白石, 列の数をカウント
            bcnt = wcnt = 0;
            scnt = 1;
            wk = (int)s[0];
            foreach(int DAT in s)
            {
                if (DAT==1)     bcnt++;
                if (DAT==-1)    wcnt++;
                if (DAT != wk)
                {
                    scnt++;
                    wk = DAT;
                }
            }
            if (bcnt+wcnt==8)   return (bcnt-wcnt)*20;
            if (scnt<3) return 0;
            //連続する(○,●)を一つに詰める
            wk = 9;
            foreach (int DAT in s)
            {
                if (DAT == 0 || DAT != wk)
                {
                    ws.Add(DAT);
                    wk = DAT;
                }
            }
            rt= ws.Count-1;
            // 端に黒(白)が置かれている
            if ((int)ws[0] != 0)
            {
                val = EvEdge(ws, bcnt, wcnt, scnt);
                if ((int)ws[0]==1)  return val;
                else    return(0-val);
            }
            if ((int)ws[rt]!=0)
            {
                ws.Reverse();
                val = EvEdge(ws, bcnt, wcnt, scnt);
                if ((int)ws[0]==1)  return val;
                else    return(0-val);
            }
            // [△・・・△]
            if (scnt==3)    return (bcnt-wcnt)*10;
            val= 0;
            // 1-2 に駒  [△*・・・△] 
            if ((int)ws[1]!=0 || (int)ws[rt-1]!=0)
            {
                if ((int)ws[1]!=0)
                {
                    if ((int)ws[1]==-1) val+= EvPattern(ws,scnt);
                    else    val-= EvPattern(ws,scnt);
                }
                if ((int)ws[rt-1]!=0)
                {
                    ws.Reverse();
                    if ((int)ws[1]==-1) val+= EvPattern(ws, scnt);
                    else    val-= EvPattern(ws, scnt);
                }
            }
            // 両端は[△△・・・△△]
            if (scnt==3)    return (bcnt-wcnt)*10;  //[△○△][△●△] 
            if (scnt==4)    return (bcnt+wcnt)*10;  //[△○●△]
            return val;
        }
    
        // コンピュータ後手で列挙した手を実行して黒番で評価
        int Eval_B(int[,] wt)
        {   ArrayList   line;
            int     x,y,val;
            bool    flg;
            val= 0;
            for(int i=0; i<4; i++)
            {   y= t00[i,0];
                x= t00[i,1];
                if (wt[y,x]!=0)     // Corner() 関数の評価
                {   if (wt[y,x]==1) val+= Corner(i,wt)*20+30;
                    else    val-= Corner(i,wt)*20+30;
                }
                else
                {
                    flg = false;
                    if (Check(1,x,y,wt))    // 隅を取れる
                    {   val += 30;
                        flg = true;            
                    }
                    if (flg==false)
                    {   y = t11[i, 0];
                        x = t11[i, 1];
                        if (wt[y,x]==-1)  val += 20;
                        if (wt[y,x]==1)   val -= 20;
                    }
                    line = GetLine(i, wt);
                    int wk= EvLine(line);
                    val += wk;
                }
            }
            for(int i=0; i<8; i++)
            {   for(int j=0; j<8; j++)
                {   if (wt[i,j]==1)     val+= m_vt[i,j];
                    if (wt[i,j]==-1)    val-= m_vt[i,j];
                }
            }
            return val;
        }
    
        //★ t[8,8] の局面を c の手番で最後まで読み切る
        int Final(int lev, int c, int[,] t)
        {
            ArrayList array;
            int[,]  w_t;
            int     best,xy,i,ans;
            int     nc;
            if (lev>20)
            {   MessageBox.Show("Final Call  Over Level");
                return(Count(-1,t)-Count(1,t));
            }
            nc= 0-c;
            array= Search(c, t);
            if (array.Count==0)
            {   array= Search(nc, t);           // パスの時反転して検索
                if (array.Count==0)             // ゲーム終了まで到達
                {
                    return(Count(-1,t)-Count(1,t));
                }
                return(Final(lev+1,nc,t));      //★反転して再起コール★
            }
            xy = 0;
            best= 60000;
            if (c==-1)  best= -60000;
            foreach(int DAT in array)
            {
                w_t = (int[,])t.Clone();
                if (Reverse(c,DAT,w_t)==false)
                {   T_Log(8,t);
                    MessageBox.Show("Final Reverce Error");
                }
                ans= Final(lev+1,nc,w_t);       //★再起コール★
                if (best<ans && c==-1)
                {   best= ans;
                    xy= DAT;
                }
                if (best>ans && c==1)
                {   best= ans;
                    xy= DAT;
                }
            }
            array.Clear();
            if (lev==0)  m_pos= xy;     //lev=0 のとき必ずここを通る
            return best;
        }
    
        // 白番で手を列挙して、実行した局面を黒番で評価し、最小値を求める
        int Think_W(int[,] t)
        {
            ArrayList   array;
            int[,]  w_t;
            int     val,pos,wk,i;
            i= Count(0,t);
            if (i<14)
            {
                string str = DateTime.Now.ToString();
                this.Text = str;
                Final(0,-1,t);
                str += "  END:" + DateTime.Now.ToString();
                this.Text = str;
                System.Media.SystemSounds.Asterisk.Play();
                return m_pos;
            }
            val= 10000;
            array= Search(-1, t);
            if (array.Count==0)
            {   MessageBox.Show("コンピューターがプレイ出来ません");
                return 99;
            }
            pos = 99;
            foreach(int DAT in array)
            {   w_t= (int[,])t.Clone();
                Reverse(-1,DAT,w_t);
                wk= Eval_B(w_t);
                if (val>wk)
                {   val = wk;
                    pos = DAT;
                }
            }
            return pos;
        }
    }
    
  2. プロジェクトのフォルダーに Think.cs を格納してプロジェクトに取り込んで下さい。
    Think.cs の class MyForm に partial を宣言していますが、CSForm.cs にも partial を宣言して下さい。
    // Osero Platform
    using System;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Collections;
    using System.Diagnostics;
    
    public partial class MyForm : Form
    {
    
  3. コンピュータが白番で打つ手を考える Think_W() 関数です。
    このプログラムはコンピュータの後手(プレイヤーの先手)でゲームするように作成しています。
    オセロゲームでは、最後の数手で勝負がひっくり返ることも珍しくありません。
    終盤の20手ぐらいが読み切れたら鬼に金棒なのですが、時間の関係で十数手ぐらいが限度です。
    このプログラムでは、残りが14手を切ると Final() 関数を呼び出します。
    Final() 関数の実行は時間がかかるので、開始時刻と終了時刻をタイトルバーに表示します。
    開始時刻の後に「応答なし」が表示されることがありますが、4分前後待って下さい。(5分を超えることもあります)
    ゲームの序盤から中盤では、白番で打てる手を列挙して実行した局面を Eval_B() 関数で評価して、白にとって有利な手を選びます。
        // 白番で手を列挙して、実行した局面を黒番で評価し、最小値を求める
        int Think_W(int[,] t)
        {
            ArrayList   array;
            int[,]  w_t;
            int     val,pos,wk,i;
            i= Count(0,t);
            if (i<14)
            {
                string str = DateTime.Now.ToString();
                this.Text = str;
                Final(0,-1,t);
                str += "  END:" + DateTime.Now.ToString();
                this.Text = str;
                System.Media.SystemSounds.Asterisk.Play();
                return m_pos;
            }
            val= 10000;
            array= Search(-1, t);
            if (array.Count==0)
            {   MessageBox.Show("コンピューターがプレイ出来ません");
                return 99;
            }
            pos = 99;
            foreach(int DAT in array)
            {   w_t= (int[,])t.Clone();
                Reverse(-1,DAT,w_t);
                wk= Eval_B(w_t);
                if (val>wk)
                {   val = wk;
                    pos = DAT;
                }
            }
            return pos;
        }
    
  4. 局面を黒番で評価する Eval_B() 関数です。
    コンピュータが白の手番で列挙した局面を評価するので、Eval_B は黒の手番になります。
    オセロゲームでは4個のコーナーは重要で、まずコーナーから評価します。
    コーナーに駒が置かれているときは Coner() 関数を呼び出します。
    コーナーが空白のときは、コーナーに置くことが出来るかを調べます。
    置くことが出来ないときは、危険な 2-2 に駒があるかを調べます。
    次にコーナーに続いて重要な4個の辺を GetLine() 関数で取り出して評価します。
    最後に盤上の駒を m_vt[8,8] で計算して結果をリターンします。
        int Eval_B(int[,] wt)
        {   ArrayList   line;
            int     x,y,val;
            bool    flg;
            val= 0;
            for(int i=0; i<4; i++)
            {   y= t00[i,0];
                x= t00[i,1];
                if (wt[y,x]!=0)     // Corner() 関数の評価
                {   if (wt[y,x]==1) val+= Corner(i,wt)*20+30;
                    else    val-= Corner(i,wt)*20+30;
                }
                else
                {
                    flg = false;
                    if (Check(1,x,y,wt))    // 隅を取れる
                    {   val += 30;
                        flg = true;            
                    }
                    if (flg==false)
                    {   y = t11[i, 0];
                        x = t11[i, 1];
                        if (wt[y,x]==-1)  val += 20;
                        if (wt[y,x]==1)   val -= 20;
                    }
                    line = GetLine(i, wt);
    //Ary_Log(line);
                    int wk= EvLine(line);
    //Debug.Write("EvLine v:" + wk + "\n");
                    val += wk;
                }
            }
            for(int i=0; i<8; i++)
            {   for(int j=0; j<8; j++)
                {   if (wt[i,j]==1)     val+= m_vt[i,j];
                    if (wt[i,j]==-1)    val-= m_vt[i,j];
                }
            }
            return val;
        }
    
  5. 辺を取り出して評価する EvLine() 関数です。
    黒石, 白石, 列の数をカウントして、連続する黒石, 白石を詰めます。
    両端(コーナー)に駒が置かれているときは Coner() 関数で評価しているのですが EvEdge() 関数で補足します。
    次に重要なのが 1-2 の地点で、EvPattern() で危険なパターンを解析します。
        // ●の手番(立場)で取り出した1列の辺を評価(黒が有利なら+)
        int EvLine(ArrayList s)
        {
            ArrayList   ws= new ArrayList();
            int bcnt, wcnt, scnt, rt, val, wk;
            // 黒石, 白石, 列の数をカウント
    
            ・・・
    
            //連続する(○,●)を一つに詰める
    
            ・・・
    
            // 端に黒(白)が置かれている
            if ((int)ws[0]!=0)
            {
                ・・・
    
            }
            if ((int)ws[rt]!=0)
            {
                ・・・
    
            }
            // 1-2 に駒  [△*・・・△] 
            if ((int)ws[1]!=0 || (int)ws[rt-1]!=0)
            {
                ・・・
    
            }
            // 両端は[△△・・・△△]
            if (scnt==3)    return (bcnt-wcnt)*10;  //[△○△][△●△] 
            if (scnt==4)    return (bcnt+wcnt)*10;  //[△○●△]
            return val;
        }
    
  6. 1-2 の地点に駒が置かれているときに EvPattern() で危険なパターンを解析します。
    コーナーを取られる恐れのあるパターンや取れる可能性のあるパターンを調べます。
        int EvPattern(ArrayList s, int sc)
        {
            if (sc == 4)
            {   // [△○●・・・・] 黒(c)の手番で隅を取れる
                if ((int)s[1]==-1 && (int)s[2]==1)  return 50;
                return 20;      // 辺が全て黒になる[△●○・・・]
            }
            if ((int)s[1]==1)   // 1-2 に黒 [△●・・・]
            {
                if ((int)s[2]==-1 && (int)s[3]==1)  return -60;   // [△●○●・・・]
                if ((int)s[2]==-1 && (int)s[3]==0 && (int)s[4]==-1) return -40;  // [△●○  ○・・・]
                return 0;
            }
            if ((int)s[1] == -1)  // 1-2 に白[△○・・・]
            {
                if ((int)s[2] == 1 && (int)s[3] == -1) return 60;      // [△○●○・・・] 
                if ((int)s[2] == 0 && (int)s[3] == -1) return 20;      // [△○※○・・・]
                if ((int)s[2] == 0 && (int)s[3] == 0 && (int)s[4] == 1) return 30; //[△○※・●・・・]
                if ((int)s[2] == 0 && (int)s[3] == 1 && (int)s[4] == -1) return 30; //[△○※●○・・・]
            }
            return 0;
        }
    
  7. Coner() 関数は隅から連続する駒しか数えないので、隣接する駒を数えて補足する EvEdge() 関数です。
        int EvEdge(ArrayList s, int bcnt, int wcnt, int scnt)
        {
            if (scnt<3) return 0;
            if ((int)s[0]==1 && (int)s[1]==-1 && (int)s[2]==0 ) return wcnt*20; // [●○△・・・]
            if ((int)s[0]==-1 && (int)s[1]==1 && (int)s[2]==0 ) return bcnt*20; // [○●△・・・]
            return 0;
        }
    
  8. 隅から連続する返されることの無い駒を数える Coner() 関数です。
    t[8,8] から no 番目のコーナーを t55[5,5] にコピーして駒を数えます。
        int Corner(int no, int[,] t)
        {   int     x,y,xp,yp,cnt;
            int     koma;
            yp = t00[no, 0];
            xp = t00[no, 1];
            if (t[yp, xp] == 0) return 0;
            koma = t[yp, xp];
            //コーナ(no)のt[8,8]をt55にコピー
            int[,] t55 = new int[5, 5];
            for(y=0, yp=t00[no, 0]; y<5; y++, yp+=dt5[no, 0])
            {   for(x=0, xp=t00[no, 1]; x<5; x++, xp+=dt5[no, 1])
                {   t55[y,x] = t[yp,xp]; }
            }
            //コーナの確定した石数を求める
            cnt = 0;
            xp = 4;
            for(y=0; y<4 && t55[y,0]==koma; y++)
            {   for(x=0; x<xp; x++)
                {   if ((t55[y,x]!=koma) ||
                       (y>0 && x>0 && t55[y-1, x+1]!=koma && t55[y+1, x-1]!=koma))
                    {   xp=x;  break; }
                    cnt++;
                }
            }
            return cnt;
        }
    
  9. [8][8] から no の辺を取り出す GetLine() 関数です。
        ArrayList GetLine(int no, int[,] t)
        {
            ArrayList   array= new ArrayList();
            int xp,yp,k;
            yp= t00[no,0];
            xp= t00[no,1];
            for(k=0; k<8; k++)
            {   array.Add(t[yp,xp]);
                yp+= dt[no,0];
                xp+= dt[no,1];
            }
            return array;
        }
    
  10. 終盤を読み切る Final() 関数です。
    パスが無ければ「白,黒」交互に終局まで再帰コールされます。
    再起のループに陥らないように lev が20を超えると終了しています。
    白の手番で打てる手を列挙して Final() 関数を呼び出します。
    白番がパスのときは、手番を変えて打てる手を調べます。
    両方がパスのときは終局で、盤上の駒をカウントしてリターンします。
    詳細は Final 関数 を参照して下さい。
  11. これで初心者を相手にゲームが出来るようになります。
    これまでにも様々な言語でオセロ・ゲームを作成しています。
    基本的な説明は下記のサイトを参照して下さい。
    Windows オセロ・ゲーム「オセロゲームの盤と駒を表示」
    Java オセロ・ゲーム「オセロゲームの盤と駒を表示」
    Javascript オセロ・ゲーム「オセロ・ゲーム」

[Next Chapter ↓] Base Project
[Previous Chapter ↑] Final 関数

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