棋譜の再生

SGF ファイルを入力して棋譜を再生します。

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

SGF ファイルの説明

  1. 次のステップでは、棋譜を入力して、マウスのクリックで再生します。
    SGF ファイルでは、括弧でくくられた手順 ( ~ ) が再起構造になっています。
    今回のプログラムで最も難しいのが、棋譜を入力してツリーを構築する関数でしょうか。
    棋譜の記述が再起構造になっているので、関数も再起になります。
    再起を使わずにプログラムを試みましたが、かえって複雑なソースになりました。
  2. 出題図のロード(LoadBAN) に続いて LoadTree() を呼び出します。
    m_SGF の Index を先頭に戻して、Token() で '(' に設定してから呼び出しています。
        string      m_SGF;                      // SGF FILE Source
        int         m_Idx;                      // m_SGF Index
        CELL        m_Top;                      // Top Tree
        CELL        m_PT;                       // 棋譜 Tree
    
            if (LoadBAN()==')') return; // 出題図をロード
            m_Idx = 0;
            Token();
            m_Top = LoadTree(0);        // 棋譜を再起でロード
            m_PT = m_Top;
    
  3. LoadBAN() は SGF FILE の出題図を盤(m_St)に設定する関数です。
        private char LoadBAN()
        {
            Token();        // ; をスキップ
            m_Idx++;
            while(true)
            {
                m_WS = "*";
                switch(Token())
                {
                    case '(':   return '(';
                    case ')':   return ')';
                    case ';':   return ';';
                    case '*':               // 出題図プロパティのチェック
                        KeyCheck();
                        break;
                    case '[':
                        if (m_Ishi!=0 && Convt())
                            m_St[m_Pos.X, m_Pos.Y] = m_Ishi;
                        break;
                }
            }
        }
    
  4. SGF の棋譜を再起で解析する LoadTree(int lev) です。
    '(' が検出されたとき、LoadTree(lev+1); を呼び出していることに注目して下さい。
    '(' は分岐手順(メインの手順も分岐の一種と考える)の始まりで、car, cdr を使ってリンクします。
    基本手順は car から、分岐手順は cdr からリンクします。
        private CELL LoadTree(int lev)
        {
            CELL    PT, TOP, LP, WP;
    
            if (Token()!='(')   return null;
            TOP = new CELL(0);
            PT = TOP;
            m_Idx++;
            while(true)
            {
                switch(Token())
                {
                    case '(':
                        LP = LoadTree(lev+1);       // List Tree を生成
                        for (WP = PT; WP.cdr != null; WP = WP.cdr) ;
                        WP.cdr = LP;
                        PT = WP.cdr;
                        break;
                    case ')':
                        m_Idx++;
                        return TOP.car;
                    case ';':
                        m_Idx++;
                        PT.car = new CELL(m_Num++); // 基本セルを生成
                        PT = PT.car;
                        PT.level = lev;
                        Prpperty(PT);
                        break; 
                }
            }
        }
    
  5. Prpperty() は現在の位置からトークンを切り出して、'(', ')', ';', "N", "C", "B", "W" を検出する関数です。
        private void Prpperty(CELL pt)
        {
            while(true)
            {
                switch(Token())
                {
                    case '(':
                    case ')':
                    case ';':
                        return;
                    case '*':
                        if (m_WS == "N")
                        {
                            Token();
                            pt.id = m_WS;
                            break;
                        }
                        if (m_WS == "C")
                        {
                            Token();
                            pt.msg = m_WS;
                            break;
                        }
                        if (m_WS=="B" || m_WS=="W")
                        {   if (m_WS == "B")    pt.teban = 1;
                            if (m_WS == "W")    pt.teban = -1;
                            Token();
                            if (Convt())    pt.pos = m_Pos;
                            break;
                        }
                        break;
                }
            }
        }
    
  6. 次に難しいのは、マウスのクリックで棋譜を再生する関数でしょうか。
    右クリックで次の手順に、左クリックで前の手順に戻ります。
    複数の候補があるときは、盤上に A,B,C, ... の文字を表示して選択します。
    盤上の石(m_Ban[19,19])は、黒:1, 白:-1, A:2, B:3, C:3, ... で識別します。
    単に右クリックするとAの手順が選択されます。
    選択した手順は CELL 構造体の next でリンクします。
    また、手順を戻したり削除するために、next の逆リンクを back に設定します。
        private void OnMyMouseDown(object sender, MouseEventArgs e)
        {   Point pos = new Point(e.X, e.Y);
    
            ClickPos(ref pos);
            if (e.Button == MouseButtons.Left)
            {   Set_Next(pos);  }
            if (e.Button == MouseButtons.Right)
            {   Set_Back();  }
            Invalidate();
        }
    
    m_PT は再生中の棋譜の現在(最後)の手です。
    選択された手が next と back でリンクされていて、back をたどって1手戻します。
        // m_PT.back で戻った局面を m_Ban[] に設定
        private void Set_Back()
        {
            if (m_PT == null) return;
            if (m_PT.back != null)
            {
                m_PT = m_PT.back;
                m_PT.next = null;
            }
            Play(m_Top);    // 出題図から現在までの手順を再現
        }
    
    Set_Next() 関数では、クリックされた位置に英字(選択手順)が格納されているか調べます。
    英字がクリックされなかったときは ch=0; で先頭の候補(A)を選びます。
    また選択候補が無いときは car のリンクをたどります。
        // 一手進んだ局面を m_Ban[] に設定
        private void Set_Next(Point pos)
        {
            CELL    wp;
            int     ch, lv, i;
    
            if (m_PT == null)   return;
            if (m_PT.car == null && m_PT.cdr != null)   // 選択手順
            {
                ch = 0;
                if (pos.X>=0 && m_Ban[pos.X, pos.Y] > 1)
                {
                    ch = m_Ban[pos.X, pos.Y] - 2;
                    MessageBox.Show("文字が選択されました: " + ch);
                }
                lv = m_PT.level+1;
    
                for (i=0, wp=m_PT; wp!= null; wp=wp.cdr)
                {
                    m_Top.CellPrint(wp);
                    if (wp.level==lv)
                    {   i++;
                        if (i>ch)   break;
                    }
                }
    
                m_PT.next = wp;
                wp.back = m_PT;
                m_PT = wp;
            }
            else    // 選択手順が無いとき
            {
                if (m_PT.car != null)
                {
                    wp = m_PT.car;
                    m_PT.next = wp;
                    wp.back = m_PT;
                    m_PT = wp;
                }
            }
            Play(m_Top);    // 出題図から現在までの手順を再現
        }
    
  7. Play() は m_Top から next をたどり、現在の局面を再現する関数です。
    最後に Bunki() 関数で、分岐文字(A:2, B:3, C:4, ... )を盤(m_Ban)に設定します。
        // m_Ban[] に現在の局面を再現(CELL の next==null)
        private void Play(CELL pt)
        {
            CELL    wp;
            int     i, j;
            string  str;
    
            m_Ban = (short[,])m_St.Clone();
            for (wp = pt; wp != null; wp = wp.next)
            {
                switch (Check(wp.pos, wp.teban))
                {
                    case 0:
                        m_Ban[wp.pos.X, wp.pos.Y] = wp.teban;
                        break;
                    case 1:     // 打ち上げ
                        for (i = 0; i < 19; i++)
                            for (j = 0; j < 19; j++)
                                if (m_W[i, j] == 3) m_Ban[i, j] = 0;
                        m_Ban[wp.pos.X, wp.pos.Y] = wp.teban;
                        break;
                    default:
                        str = "不正な石です " + wp.num + " [" + wp.pos.X + "," + wp.pos.Y + "]";
                        MessageBox.Show(str);
                        Console.WriteLine(str); 
                        break;
                }
            }
            Bunki();
        }
    
  8. 分岐文字(A:2, B:3, C:4, ... )を盤(m_Ban)に設定する Bunki() 関数です。
    cdr の連鎖をたどって、分岐選択手順を探します。
    見つけた順に A,B,C... で表示します。
        private void Bunki()
        {
            int     lv, i;
            if (m_PT==null || m_PT.cdr==null)   return;
            if (m_PT.cdr.level==m_PT.level)     return;
            lv = m_PT.level+1;
            CELL wp = m_PT.cdr;
            for(i=2; wp!=null; wp=wp.cdr)
            {
                if (wp.level == lv)
                {
                    m_Ban[wp.pos.X, wp.pos.Y] = (short)(i);
                    i++;
                }
            }
        }
    
  9. 盤上に一個の石を描画する Put() 関数です。
    typ==1 が黒石で、typ==-1 が白石です。
    typ が2以上のときは英字を描画します。
    kuro が黒石の画像で、shiro が白石の画像です。
    bmpabc には、英字のAからTの画像が格納されています。
        Bitmap      kuro, shiro;
        Bitmap      bmpabc;
    
        private void Put(Graphics g, int x, int y, int typ)
        {
            Rectangle   rect;
            rect = new Rectangle(m_Pitch*x+m_Rect.X+m_Pitch/2, m_Pitch*y+m_Rect.Y+m_Pitch/2, m_Pitch, m_Pitch);
            switch (typ)
            {
                case 0:
                    break;
                case 1:
                    g.DrawImage(kuro, rect);
                    break;
                case -1:
                    g.DrawImage(shiro, rect);
                    break;
                default:
                    g.DrawImage(bmpabc, rect, new Rectangle((typ-2)*32,0,32,32),GraphicsUnit.Pixel);  
                    break;
            }
        }
    

[Next Chapter ↓] 棋譜の保存
[Previous Chapter ↑] 棋譜ツリー

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