Visual Basic 初級講座
VB.NET 2002 対応 VB.NET 2003 対応 VB2005 対応

 

Visual Basic 中学校 > 初級講座 >

第52回 実技4 オセロ

初級講座最後の実技としてオセロを作成します。対戦相手としてコンピュータのAI(人工知能)のプログラムも行います。プログラムの作り方を勉強すると言うよりは、「体験」するつもりで実際に手順どおりやってみることをお勧めします。

概要

・オセロを作成する。

・オセロの対戦相手としてコンピュータのAI(人工知能)をプログラムする。

 

1.はじめに

今回はオセロを作ります。また、対戦相手としてコンピュータのAI(人口知能)を作成します。 日本語で「人工知能」というと大げさな感じがしますが、とりあえずオセロの対戦相手になってくれる程度のものです。

完成したところ

■画像1:完成したところ

プログラムの作成方法は順を追って説明しているため、この記事は長くなっています。

この記事ではオセロの作り方の説明を目的としているのではなく、オセロの作りを「体験」していただくことを目的としています。細かいロジックやVBの構文が理解できなかったとしても手順どおりに進めてプログラムを完成させて経験値を向上させてください。

おそらく途中で、プログラムをどこに書けば良いのかわからなくなったり、うまく動くはずのところがうまく動かなくなったりしてつまずくことでしょう。しかし、実際に私が試した手順・方法は記事の中にしっかりと書いておきましたので、よく読めば必ず完成に至るものと思います。そのような四苦八苦こそがプログラムを上達させる良い経験となるでしょう。

メモ  -  VB.NET(2002)の場合

今回作成するオセロはVB.NET(2002) ,VB.NET2003, VB2005に対応していますが、VB.NET(2002)を使用している場合だけちょっとプログラムを変える必要があります。

説明中に何度か出てくるForループのカウンタ変数の宣言の部分ですが、VB.NET2003とVB2005ではたとえば次のような書き方が可能です。

VB.NET2003対応 VB2005対応

For i As Integer = 0 To 10
    …
Next

■リスト1:VB.NET2003, VB2005の場合

上記のようなコードがあったらVB.NET(2002)の場合だけは次のように書き換えてください。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim i As Integer

For i = 0 To 10
    …
Next

■リスト2:VB.NET(2002)の場合

 

2.プログラムの設計

 

説明に入る前に言葉遣いをまとめておきます。オセロとは8×8の盤面に黒と白の石を置いていくゲームですが、ここでは盤面のことを「グリッド」、升目(ますめ)のことを「セル」を呼ぶことにします。

盤面 グリッド (Grid)
升目 セル (Cell)

■表1:今回使用する用語

この用語は私が勝手に考えたものですが、このページの説明では今後この言葉を使いますので注意して下さい。

 

さらに、具体的なプログラムに着手する前に少しプログラムの構造を考えて見ます。

いろいろな作り方があると思いますが、今回はグリッドとセルをそれぞれ独立したクラスとします。

セルは全部で64個もありますから、クラス化することによってプログラムの効率化が図れます。グリッドをクラスにするメリットは自由にグリッドを作ったり消したりできるからです。グリッドは一度作ればゲーム終了まで変わらないようにも思えますが、AIがグリッド使って自由にシミュレーションを行えるようにするためにはやはりクラスになっていたほうが便利です。

それにグリッドには64個のセルを束ねる機能もあります。

ということでAIも含めてこのプログラムでは3つのクラスを作成することになります。もちろんFormなどのクラスもこれとは別に使用しますし、 これらとは別にちょっとだけもっと小さいクラスを作成することにもなります。

クラス 説明
Cell オセロのセル(升目)を表します。セルの状態(黒・白・なし)の管理や、セルへのフォーカスの移動、グラフィックの描画を行います。
ReverseGrid オセロのグリッド(盤面)を表します。石を置くためのメソッドや、ひっくり返すメソッドとそれに関連するイベント、セルの管理、描画処理を行います。
Computer オセロのプレイヤーとなる人工知能を表します。

■表2:作成する主なクラス

ところで、今この文章を書いている段階で私の目の前にはこのプログラムの完成版があります。だから、このようにすらすらとどういうクラスが必要でどういう機能を持たせるかと言うことが書けるのです。

通常はプログラム作成前は漠然と「こういう風にしようかな」という思いがあるだけで厳密な設計はしません。もちろん大きなシステムを作る場合や厳密な動作が求められている場合はしっかりした設計から行いますが、今回はそうではありません。

 

3.グリッドの描画

 

それでは、まずはグリッドを描画するところを書いてオセロらしい雰囲気を出してみましょう。フォームにPictureBoxを1つ貼り付けてください。このPictureBoxの中でオセロをやるので少し大きめに広げてください。

次にグリッドを表すReverseGridクラスをプロジェクトに追加します。このクラスは少しボリュームのあるクラスになりますので専用のファイルを用意してReverseGrid.vbという名前を付けてください。

ReverseGridクラスにはグリッドを描画するためのDrawメソッドをプログラムします。

次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class ReverseGrid

    Public Const CellSize As Integer = 48
'セルのサイズ
   
Public Const XCount As Integer = 8 '盤の横方向のセル数
   
Public Const YCount As Integer = 8 '盤の縦方向のセル数

   
'■Draw
   
''' <summary>現在の状態を描画します。</summary>
   
''' <param name="g">描画対象のGraphicsオブジェクトを指定します。</param>
   
Public Sub Draw(ByVal g As Graphics)

        Dim
X As
Integer
       
Dim Y As Integer
       
Dim BorderPen As New Pen(Color.Black, 2)

   
    '▼グリッドの描画

        '深緑の四角形
   
    g.FillRectangle(Brushes.DarkGreen, 0, 0, XCount * CellSize, YCount * CellSize)

   
    '縦の9本の線
   
    For X = 0 To XCount
            g.DrawLine(BorderPen, X * CellSize, 0, X * CellSize, YCount * CellSize)
       
Next

   
    '横の9本の線
       
For Y = 0 To YCount
            g.DrawLine(BorderPen, 0, Y * CellSize, XCount * CellSize, Y * CellSize)
       
Next

   
End Sub

End
Class

■リスト3:ReverseGrid.vbに記述する。グリッドの描画。

セルのサイズは48×48としますが後で変えようと思ったときに簡単に変えられるように定数CellSizeを使用することにしました。また、セルの数も定数XCountYCountで定義するようにしています。オセロのセルの数は8×8に決まっているのですが、せっかく自分でプログラムしているのですから少しはオリジナル性のあるものを作りたい方がいらっしゃると思います。そんな 時はこの定数をいじることで簡単にセルの数が変えられるようになります。

Drawメソッドの引数にはGraphicsオブジェクトを指定します。VBのグラフィックス機能はすべてこのGraphicsに集約されますのでこの引数は必須です。

この段階ではDrawメソッドは座標の計算が少し複雑である以外には目新しいことはありません。

フォーム側ではPictureBox1Paintイベントでこのメソッドを呼びます。次の通りです。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Grid As New ReverseGrid

Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint

    Grid.Draw(e.Graphics)

End
Sub

■リスト4:Form1に記述する。描画ロジックの呼び出し。

これで実行すると空のオセロのグリッドがフォームに表示されます。

グリッドの描画

■画像2:グリッドの描画

 

4.クリックしたセルに石を置く

 

次にクリックしたセルに石を置くようにプログラムします。石が置けるようになればあとははさまれば石をひっくり返す処理を書くだけでオセロは完成です。とは言ってもいろいろな細かい処理を書かなければいけないのですが…。

ここでいきなりですが今回の最大の山場を迎えます。64個あるセルはそれぞれの状態を記憶しておく必要があります。状態とは「黒」、「白」、「なし」の3種類です。そしてReverseGridDrawメソッドではそれぞれのセルの状態に応じて黒い円や白い円を描画することになります。

プレイヤーはグリッドのあるPictureBox1をクリックして石を置いていくことになりますが、 注意して欲しいのはPictureBox1Clickイベントでは黒・白・なしの状態をセットするだけで描画は行わないと言うことです。描画を行うのはあくまでもPaintイベントです。

この手法はグラフィックスを扱うほとんどのプログラムで採用されています。つまり、クリックなどの動作は状態の記録を変更するだけで実際の描画は行わないと言う手法です。この方が描画処理が一箇所に集中してわかりやすくなりますし、多くのクラスがこの方式になじむように設計されています。

この作り方の場合Clickイベントでは、クリック後にすぐに描画が行われるようにInvalidateメソッドを使ってPaintイベントを呼び出すようにします。そうしないと クリックしてから実際に描画されるまでのタイミングが制御できず妙な間が空いてしまいます。

話を戻します。その64個のセルの状態をどのように記録するかということですが、ここでは64個のCellクラスを作成しその
Cellクラスがそれぞれ自分の状態を保持するようにします。また、Cellクラス自体にも描画機能をつけてお手軽に描画ができるようにもします。

まずは状態を定義する列挙体を作成して下さい。そのためにConstants.vbという新しいクラスをファイルごと作成してください。このファイルは初期状態では次のようになっているはずです。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class Constants

End Class

■リスト5:Constants.vbの最初の状態。この2行は削除してしまう。

この2行は消してしまってください。代わりに次の列挙体を書いてください。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■CellStatus
'''
<summary>セルの状態を表します。</summary>
Public Enum CellStatus
    [Nothing]
'なし
   
Black '黒
   
White '白
End Enum

■リスト6:Constants.vbに記述する。セルの状態を表す列挙体。

つまり、Constants.vbというファイルの中にCellStatusという列挙体があることになります。このような小さな列挙体やクラスを作成する場合にはよくこのように汎用的なそれとわかるファイル名を付けておいて中に小さな列挙体・クラスをたくさん書き込むという手法をとります。

小さな列挙体やクラス1つにつきいちいちファイル1つを作成していたらファイルが増えすぎて管理が大変ですし、かといってどれか1つの列挙体やクラスの名前をファイル名にしてしまうと、他の同居しているクラスの存在が霞んでしまうからです。

これで状態を表す列挙体が用意できたので、いよいよCellクラスを新しく作成して下さい。このクラスはメインのクラスの1つですので1つのファイル(Cell.vb)を割り当ててください。

Cellクラスには黒・白などの情報のほかに自分がどのグリッドに属するどの位置のセルであるかという情報(論理位置)が必須です。また、描画処理を円滑に行うためには描画対象の実際の範囲(物理位置)も保持するものとします。ですから次の4つのプロパティまたはフィールドが必要です。

  メンバ 説明
フィールド Status CellStatus 黒・白・なしの状態
フィールド Grid ReverseGrid 所属するグリッド
フィールド Position Point グリッド内での論理位置。通常は(0, 0) 〜 (7, 7)のどれか。
フィールド Rectangle Rectangle 実際に描画を行う四角形の領域。物理位置。

■表3:Cellクラスの主なマンバ

このうち、Statusの初期値はCellStatus.Nothing(なし)に決まっていますから、コンストラクタではGridPositionを受け取るようにします。RectanglePositionから計算して求めることができますので必要ありません。

コンストラクタは次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class Cell

    Public
Status As CellStatus
'セルの状態。黒・白・空。
   
Public Grid As ReverseGrid
    Public Position As Point
'論理位置
   
Public Rectangle As Rectangle '物理位置

   
'■コンストラクタ
   
''' <summary>論理位置を指定してセルを作成します。</summary>
   
''' <param name="Grid">セルが属するグリッドを指定します。</param>
   
''' <param name="Position">セルの論理位置を指定します。</param>
   
Public Sub New(ByVal Grid As ReverseGrid, ByVal Position As Point)

       
Me.Grid = Grid
       
Me.Position = Position

        Dim
Rect As New Rectangle

   
    '論理位置から物理位置を求めます。
       
Rect.X = Position.X * ReverseGrid.CellSize
        Rect.Y = Position.Y * ReverseGrid.CellSize
        Rect.Width = ReverseGrid.CellSize
        Rect.Height = ReverseGrid.CellSize

   
    Me.Rectangle = Rect

    End Sub

End Class

■リスト7:Cell.vbに記述する。Cellクラスの骨格。

PositionからRectangleを求めるにはReverseGridで定義されているCellSizeの値を利用します。

 

さて、これで必要な情報を保持できるCellクラスができましたが、これとは別に実際に64個のCellクラスのインスタンスを作成する処理を書かなければなりません。セルはグリッドに属するものなのですからReverseGridが生成されたタイミングでこの処理を行うのが最も適当です。

ReverseGridクラスにコンストラクタを追加して64個のCellクラスを生成するコードを書いてください。また、ReverseGrid側からいつでも64個のセルにアクセスできるように作成した64個のCellクラスを2次元配列に保存します。そのための変数m_Cellsも定義します。この変数を2次元配列にするのはセルの位置を指定して対象のCellクラスを簡単に取得できるようにするためです。2次元配列にしておけば、たとえば左から2番目、上から5番目のセルのCellクラスはm_Cell(1, 4)のように直感的にアクセスできます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim m_Cells(XCount - 1, YCount - 1) As Cell '全セルを表す配列

'■コンストラクタ
''' <summary>全セルを初期化します。</summary>
Public Sub New()

    Dim
X As
Integer
   
Dim Y As Integer

   
For X = 0 To XCount - 1
        For Y = 0 To YCount - 1
            m_Cells(X, Y) = New Cell(Me, New Point(X, Y))
       
Next
    Next

End Sub

■リスト8:ReverseGrid.vbに記述する。64個のセルを生成する処理。

セルの数を表すために先ほど記述した定数XCountYCountを使用しています。

それから、外部からもこのm_Cellにアクセスできるようにm_Cellをプロパティを通じて公開します。配列をそのまま公開するか配列型のプロパティとして公開すると言う方法もありますが、今回はメソッド風に引数を2つ持つプロパティとして公開します。

次のCellsプロパティをReverseGridクラスに追加して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■Cells
''' <summary>セルを取得します。</summary>
''' <param name="X">セルの0から始まるX位置を指定します。Xの最大値はXCount-1です。</param>
''' <param name="Y">セルの0から始まるY位置を指定します。Yの最大値はYCount-1です。</param>
''' <returns>対象のセルを返します。</returns>
Public Property Cells(ByVal X As Integer, ByVal Y As Integer) As Cell
   
Get
        Return
m_Cells(X, Y)
    End
Get
   
Set(ByVal value As Cell)
        m_Cells(X, Y) = value
    End
Set
End Property

■リスト9:ReverseGrid.vbに記述する。外部から64個のセルにアクセスするためのプロパティ。

 

長くなりましたがこれで下準備は整いました。Cellクラスに実際の描画を行うDrawメソッドを追加しましょう。次の通りです。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim FrontBrush As New SolidBrush(Color.Black)

'■Draw
''' <summary>現在の状態を描画します。</summary>
''' <param name="g">描画対象のGraphicsオブジェクトを指定します。</param>
Public Sub Draw(ByVal g As Graphics)

    Dim
CellRect As Rectangle = Me.Rectangle
'描画領域

   
'▼描画状態の設定
   
Select Case Me.Status
       
Case CellStatus.Black
            FrontBrush.Color = Color.Black
'表を黒に設定
       
Case CellStatus.White
            FrontBrush.Color = Color.White
'表を白に設定
   
End Select

   
'▼描画実行
   
If Me.Status <> CellStatus.Nothing Then
       
'表面描画
       
g.FillEllipse(FrontBrush, CellRect)
    End
If

End Sub

■リスト10:Cell.vbに記述する。セルの状態を描画するロジック。

この処理ではStatusプロパティによってブラシの色を黒か白に設定します。そして、StatusプロパティがCellStatus.Nothing、つまり「なし」でなければ設定されたブラシを使って描画領域いっぱいに丸を書きます。

描画領域はCellクラスのコンストラクタでセットしたものをそのまま使用しています。

なお、Drawメソッドで登場する変数FrontBrushDrawメソッドの中でしか使用しないので本来はDrawメソッドの中で宣言すべきなのですが、描画処理のたびにいちいちブラシをインスタンス化していたら処理が遅くなるのではないかと思いましてあえて外で宣言しました。これでインスタンスは1回作成するだけであとは同じインスタンスを使い回しして処理の効率化を図ることができます。

 

今度はこのCellクラスのDrawメソッドをReverseGridクラスのDrawメソッドから呼び出すようにしましょう。セルは64個ありますから、それぞれのCellクラスのDrawメソッドをループを回して全部呼び出すことになります。

ReverseGridクラスのDrawメソッドに「▼セルの状態を描画」の部分のコードを追加して、次のようにして下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■Draw
''' <summary>現在の状態を描画します。</summary>
''' <param name="g">描画対象のGraphicsオブジェクトを指定します。</param>
Public Sub Draw(ByVal g As Graphics)

   
Dim X As Integer
   
Dim Y As Integer
   
Dim BorderPen As New Pen(Color.Black, 2)

   
'▼グリッドの描画

    '深緑の四角形
   
g.FillRectangle(Brushes.DarkGreen, 0, 0, XCount * CellSize, YCount * CellSize)

   
'縦の9本の線
   
For X = 0 To XCount
        g.DrawLine(BorderPen, X * CellSize, 0, X * CellSize + 0, YCount * CellSize)
   
Next

   
'横の9本の線
   
For Y = 0 To YCount
        g.DrawLine(BorderPen, 0, Y * CellSize, XCount * CellSize + 0, Y * CellSize)
   
Next

    '▼セルの状態を描画
   
For Y = 0 To YCount - 1
        For X = -0 To XCount - 1
            Cells(X, Y).Draw(g)
       
Next
    Next

End Sub

■リスト11:ReverseGrid.vbに記述する。セルの状態の描画を追加。

これでクラス側の準備は完了です。この段階で実行して表示を確かめてみてください。

実行してみると先ほどと何もかわっていないはずです。クリックしても何もおこりません。もし、エラーが発生したり表示がおかしかったりする場合はここまでの作業に間違いがあるのでよく確認して下さい。

正常に作動している場合はいよいよクリックしたら石を表示するようにします。

まずは、小手調べでPictureBox1Clickイベントに次のコードを記述して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub PictureBox1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles PictureBox1.Click

    Grid.Cells(2, 4).Status = CellStatus.Black
    PictureBox1.Invalidate()

End
Sub

■リスト12:Form1に記述する。セルの状態描画のテスト。

これでPictureBox1のどこかをクリックすると(3, 5)の位置に黒い丸が描画されます。Invlidateメソッドはすぐに描画を行うために使用しています。

実行してPictureBox1をクリックして次のように表示されるか確かめてみてください。

石の描画

■画像3:石の描画

ちょっと表示がしょぼすぎる感じがしますが、これは後で工夫してもう少しくらいはかっこよくします。今はこれで満足しておきましょう。

さて、これでセルの位置さえ指定すればちゃんと黒い石が表示されるようになることがわかりました。残る問題は位置をどうやって指定するかです。クリックされた位置がセルで言うとどのセルに当たるのか算出するプログラムを行う必要があります。

そこで、座標を指定するとその座標にあるセルを返すCellFromPointメソッドをReverseGridクラスに追加します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■CellFromPoint
''' <summary>指定した座標にあるセルを取得します。</summary>
''' <param name="X">X座標を指定します。</param>
''' <param name="Y">Y座標を指定します。</param>
''' <returns>座標(X, Y)にあるセルを返します。該当するセルがない場合にはNothingを返します。</returns>
Public Function CellFromPoint(ByVal X As Integer, ByVal Y As Integer) As Cell

    Dim
ThisCell As Cell

    If
X < 0 OrElse X >= XCount * CellSize
Then
       
Return Nothing
   
End If

   
If Y < 0 OrElse Y >= YCount * CellSize Then
   
    Return Nothing
   
End If

   
ThisCell = Cells(X \ CellSize, Y \ CellSize)

   
Return ThisCell

End
Function

■リスト13:ReverseGrid.vbに記述する。座標の位置にあるセルを取得する。

後はフォームのPictureBox1ClickイベントをこのCellFromPointメソッドを使用するロジックに書き換えれば完了です。次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub PictureBox1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles PictureBox1.Click

    'マウスの座標をPictureBox1のコントロール座標に変換する。
   
Dim Pos As Point = PictureBox1.PointToClient(Windows.Forms.Cursor.Position)

    Grid.CellFromPoint(Pos.X, Pos.Y).Status = CellStatus.Black

    PictureBox1.Invalidate()

End
Sub

■リスト14:Form1に記述する。クリックした位置に黒い石を置く。

マウスの座標はWindows.Forms.Cursor.Positionプロパティで取得することができますが、この座標は画面の左上を(0, 0)とする座標なのでそのままでは使用できません。いったんPictureBox1の左上を(0, 0)とする座標系に変換する必要があります。

座標変換と言ってもかなりシンプルですし、PictureBox1PointToClientメソッド(読み方:PointToClient = ポイントトゥークライアント)を使用すれば簡単にできます。

そして変換した座標をCellFromPointメソッドに渡せば クリックされたセルを取得することができます。

実行して、クリックしたセルに次々と黒い丸が表示されていく様子を確認してみてください。

クリックした位置に石を置く

■画像4:クリックした位置に石を置く

 

5.かっこよくする

 

これでクリックしたセルに黒い石を表示できるようになりましたが、かなりかっこ悪いので少し改造して見栄えをととのえます。

まず、石をかっこよくします。かっこよくといっても少しましになる程度なので過度に期待しないでください。

石をかっこよくするのは実は簡単です。石を描画しているのはCell.Drawメソッドなのでこのメソッドの中を変更するだけです。このようにクラス・メソッドという構成をとって1つのメソッドに1つの機能を割り当てておくと後からプログラムを変更するのが楽になります。

とりあえず、次のように変更して下さい。余裕がある人はここで画像ファイルを読み込むなど独自のカスタマイズを加えても構いません。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim FrontBrush As New SolidBrush(Color.Black)
Dim BackBrush As New SolidBrush(Color.White)

'■Draw
''' <summary>現在の状態を描画します。</summary>
''' <param name="g">描画対象のGraphicsオブジェクトを指定します。</param>
Public Sub Draw(ByVal g As Graphics)

    Dim
CellRect As Rectangle
'描画領域

   
'▼描画領域の算定

    'セルいっぱいに描画するとぎちぎちになるので範囲を-2する。
   
CellRect = Me.Rectangle
    CellRect.Inflate(-2, -2)

   
'▼描画状態の設定
   
Select Case Me.Status
       
Case CellStatus.Black
            FrontBrush.Color = Color.Black
'表を黒に設定
           
BackBrush.Color = Color.White '裏を白に設定
       
Case CellStatus.White
            FrontBrush.Color = Color.White
'表を白に設定
           
BackBrush.Color = Color.Black '裏を黒に設定
   
End Select

   
'▼描画実行
   
If Me.Status <> CellStatus.Nothing Then

        '裏面描画
       
CellRect.Y += 2 '裏と表をちょっとずらして立体的に見せる
       
g.FillEllipse(BackBrush, CellRect)

        '表面描画
       
CellRect.Y -= 2
        g.FillEllipse(FrontBrush, CellRect)

    End
If

End Sub

■リスト15:Cell.vbに記述する。セルの描画処理の改良版。

これで実行すると次のようになります。

石を少しだけかっこよく描画する

■画像5:石を少しだけかっこよく描画する

それほどかっこよくもありませんが、さっきよりは大分ましです。

 

さらに、アクティブなセルを示すための枠線を描画することにします。アクティブなセルが枠線で表示されることによってプログラムにぐっと動きがでてきて、めりはりのある感じになります。

これには、まずセルがアクティブであるかどうかを示す変数FocusedCellクラスに追加します。そして、Focused = Trueのときに枠線を表示するようにDrawメソッドを改めます。また、外部からFocusedの値を変更できるようにFocusメソッドも追加します。変数FocusedFocusメソッドが別々に存在するわけはすぐ後で説明します。

フォーム側ではPictureBox1MouseMoveイベントでマウスがある位置のセルに対してFocusメソッドを呼び出します。

Cellクラスの変数FocusedおよびFocusメソッドは次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Focused As Boolean

'■Focus
''' <summary>セルをアクティブにします。</summary>
''' <remarks>アクティブなセルとはFocusedプロパティがTrueのセルです。
''' このメソッドを呼び出すと同じ盤に属するその他のセルのFocusedプロパティをFalseにします。
''' アクティブであることにそれ以上の効果はありませんが、
''' 描画の際にFocusedプロパティがTrueのセルに枠線を描画します。
'''
</remarks>
Public Sub Focus()

    Dim
X As
Integer
   
Dim Y As Integer

   
'同じグリッドに属する自分以外のセルを非アクティブにする。
   
For X = 0 To ReverseGrid.XCount - 1
        For Y = 0 To ReverseGrid.YCount - 1
            Grid.Cells(X, Y).Focused =
False
       
Next
    Next

   
'自分自身をアクティブにする。
   
Me.Focused = True

End Sub

■リスト16:Cell.vbに記述する。セルのアクティブ化。

プログラムをみるとすぐにわかるようにあるセルをアクティブにするときには他のセルを非アクティブにする必要があります。それでこのような処理を行うFocusメソッドを変数Focusedとは別に用意しました。

この処理はFocusedプロパティプロシージャを作成することによっても実現できますが、他のButtonTextBoxなどのコントロールではFocusというメソッドが使用できるようになっているのでそれに合わせました。

メソッドやプロパティの名前を付けるときは、他に同じ機能のメソッドやプロパティがあればそれと同じ名前にしておくのが吉です。同じ機能なのにクラスごとにメソッド名が異なっていたら使用するほうは大変です。

次にCellクラスのDrawメソッドにコードを追加して次のようにして下さい。追加するのは一番最後の「▼アクティブな場合は枠を描画する」という3行の部分だけです。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim FrontBrush As New SolidBrush(Color.Black)
Dim BackBrush As New SolidBrush(Color.White)

'■Draw
''' <summary>現在の状態を描画します。</summary>
''' <param name="g">描画対象のGraphicsオブジェクトを指定します。</param>
Public Sub Draw(ByVal g As Graphics)

    Dim
CellRect As Rectangle
'描画領域

   
'▼描画領域の算定

    'セルいっぱいに描画するとぎちぎちになるので範囲を-2する。
   
CellRect = Me.Rectangle
    CellRect.Inflate(-2, -2)

   
'▼描画状態の設定

   
Select Case Me.Status
       
Case CellStatus.Black
            FrontBrush.Color = Color.Black
'表を黒に設定
           
BackBrush.Color = Color.White '裏を白に設定
       
Case CellStatus.White
            FrontBrush.Color = Color.White
'表を白に設定
           
BackBrush.Color = Color.Black '裏を黒に設定
   
End Select

   
'▼描画実行

   
If Me.Status <> CellStatus.Nothing Then

       
'裏面描画
       
CellRect.Y += 2 '裏と表をちょっとずらして立体的に見せる
       
g.FillEllipse(BackBrush, CellRect)

       
'表面描画
       
CellRect.Y -= 2
        g.FillEllipse(FrontBrush, CellRect)

    End
If

   
'▼アクティブな場合は枠を描画する

   
If Me.Focused Then
       
g.DrawRectangle(Pens.Orange, CellRect)
    End
If

End Sub

■リスト17:Cell.vbに記述する。アクティブな場合に枠を描画する処理の追加。

最後にフォームのPictureBox1MouseMoveイベント記述して完了です。意欲的な方はこのMouseMoveイベントの内容は自分で書いてみてください。

次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

''' <summary>マウスの移動に伴ってセルにアクティブを示す枠を描画する</summary>
Private Sub PictureBox1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PictureBox1.MouseMove

    Dim
ThisCell As Cell

   
'マウスがある位置のセルを取得
   
ThisCell = Grid.CellFromPoint(e.X, e.Y)

    If
Not IsNothing(ThisCell)
Then

        'セルが取得できた場合は、セルにアクティブを示す枠を描画
       
ThisCell.Focus()

        '現在の状態を描画(PictureBox1のPaintイベントを発生させる)
       
PictureBox1.Invalidate() '←実際の描画はすべてここで行う

   
End If

End Sub

■リスト18:Form1に記述する。マウスの位置にあるセルをアクティブにする。

これで実行するとマウスのある位置のセルにオレンジ色の枠が表示されます。マウスを動かすとなんだか気持ちいいですね。

アクティブなセルをオレンジの枠で描画

■画像6:アクティブなセルをオレンジの枠で描画

なお、この処理でもセルがアクティブであるという情報はFocused変数に記録し、実際の描画はPaintイベントから呼び出されるDrawメソッドで行っていることに注意して下さい。

このような構造になっているのでDrawメソッドさえ修正すればいつでも描画処理を変更することができます。

 

6.黒と白を交互に置く

 

だんだんと下地はととのってきたので、今度は黒の番、白の番というのを作りましょう。最初のクリックでは黒い石をおきます。次のクリックでは白い石をおきます。というように交互に黒 と白を置けるようにします。

この処理は特に技術的に難しいことはないので簡単に書けると思います。

今回はフォームで変数Turnを用意して、現在黒の番か白の番かを記録するようにします。そして黒の番なら黒い石、白の番なら白い石をクリックしたときに置くようにします。そして、プレイヤーの色が黒か白かを表すPlayerColor変数も用意します。プレイヤーというのは人間対コンピュータの場合は人間、自分対他人の場合は自分を表す色です。ただし、 我々が今作っているオセロは完成時には人間対コンピュータという構図になるので自分対他人ということはありえません。

TurnPlayerTurnも白か黒かを表すのでCellStatus型にします。また、現在は両方とも初期値はCellStatus.Blackにしておきます。

さらに黒と白のTurnを切り替えるためのChangeTurnメソッドも記述します。このメソッドはこの時点ではなくてもよいくらいの小さなメソッドですが、後で本格的にオセロが完成していくにつれて重要になっていきます。

フォーム側に次のコードを追加して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Turn As CellStatus = CellStatus.Black '今どっちの順番か
Dim PlayerColor As CellStatus = CellStatus.Black 'プレイヤーの色

'■ChangeTurn
''' <summary>ターン交代</summary>
Public Sub ChangeTurn()

   
'現在の状態を描画(PictureBox1のPaintイベントを発生させる)
   
PictureBox1.Invalidate()

   
'▼次のターンの決定
   
If Turn = CellStatus.Black Then
       
Turn = CellStatus.White
   
Else
       
Turn = CellStatus.Black
    End
If

End Sub

リスト19:Form1に記述する。ターン交代処理。

ChangeTurnメソッドの先頭でInvalidateメソッドを呼び出していることに注意して下さい。

PictureBox1Clickイベントは次のように書き換えてください。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub PictureBox1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles PictureBox1.Click

   
'マウスの座標をPictureBox1のコントロール座標に変換する。
   
Dim Pos As Point = PictureBox1.PointToClient(Windows.Forms.Cursor.Position)

    Grid.CellFromPoint(Pos.X, Pos.Y).Status = Turn

    ChangeTurn()

End
Sub

■リスト20:Form1に記述する。石を置いたタイミングでターンを交代するようにする。

PictureBox1InvalidateメソッドはChangeTurnメソッド内で呼び出すことになったのでここでは不要になります。

実行すると黒と白が交互に置けるようになります。画面だけ見ればもうオセロゲームが完成したかのようにも見えます。

黒と白を交互に置く

■画像7:黒と白を交互に置く

 

7.石を置けないところを判定する

 

描画処理に関しては一通り完成したのでそろそろオセロのルールをプログラムしていきましょう。どこでも好きなところに石を置けるのはもう終わりにします。

多分ここがこのプログラムの最大の難関です。

よくよく考えてみると石が置けるところと置けないところを区別できればオセロのルールはほぼプログラム完了です。なぜなら、オセロでは石が置けるところとは相手の石をひっくり返せるところだからです。 つまり、石が置けるかどうか判定するには石をひっくり返せるかどうか判定することになります。

さて、 石は左・左上・上・右上・右・右下・下・左下の八方向にひっくり返せる可能性があります。いきなり八方向では大変なのでとりあえず、左側にひっくり返せるかどうかを調べるプログラムを書きましょう。

プログラムの前に考え方を簡単にまとめておきます。実際のプログラムもこの考え方をそのままVBで記述しているだけです。

黒い石をおいたとき、左の石をひっくり返せるか判断する方法

黒

黒い石の左は

白黒 黒黒 空黒 外黒
白い石か、 黒い石か、 何もないか、 グリッドの外側かのどれか。

このうち、ひっくり返せる可能性があるのは左が白い石の場合のみ。

さらにこの左側は

白白黒 黒白黒 空白黒 外白黒
白い石か、 黒い石か、 何もないか、 グリッドの外側かのどれか。

白白黒の場合は、さらに左側をみないと判定できない。

黒白黒の場合は、ひっくり返せることが確定。

空白黒外白黒の場合は、ひっくり返せないことが確定。

このようにどんどん左側を見ていくことにより最終的に石がひっくり返せるのか判断できます。

 

実際のコードはReverseGridクラスに次のReversibleCountメソッドを追加して 記述します。このメソッドは左方向にひっくり返せるセルの数を数えます。

VB.NET2003対応 VB2005対応

'■ReversibleCount
''' <summary>石をおいた場合に左方向にひっくり返せる石の数を調べます。</summary>
''' <param name="Status">置こうとしている石の状態を指定します。</param>
''' <param name="X">対象のセルの0から始まるX位置を指定します。Xの最大値はXCount-1です。</param>
''' <param name="Y">対象のセルの0から始まるY位置を指定します。Yの最大値はYCount-1です。</param>
''' <returns>石をおいた場合にひっくり返せる石の数を返します。</returns>
Public Function ReversibleCount(ByVal Status As CellStatus, ByVal X As Integer, ByVal Y As Integer) As Integer

   
Dim AnotherStatus As CellStatus

   
'相手の色を取得(自分が黒ならば相手は白、白ならば黒)
   
If Status = CellStatus.Black Then
       
AnotherStatus = CellStatus.White
   
Else
       
AnotherStatus = CellStatus.Black
    End
If

   
Dim TestX As Integer
   
Dim Count As Integer
   
Dim ThisCell As Cell

   
'一番左端に置こうとしているときは左側がひっくり返せるわけがない
   
If X - 1 < 0 Then
       
Return False
   
End If

   
'左のセルを取得
   
ThisCell = Cells(X - 1, Y + 0)

   
'左のセルの色が相手の色ならばひっくり返せる可能性がある
   
If Cells(X + -1, Y + 0).Status = AnotherStatus Then

       
'左方向への走査
       
For i As Integer = 0 To XCount - 1

           
'もう1個左の座標
           
TestX = X - (i + 1)

           
'もう1個左がグリッドからはみ出るなら
            '結局1個もひっくり返せないと言うこと。
           
If TestX < 0 Then
               
Return 0
            End
If

           
'もう1個左の色が
           
Select Case Cells(TestX, Y).Status
               
Case Status
                   
'自分と同じ色ならばひっくり返せる
                   
Return Count
               
Case CellStatus.Nothing
                   
'何もなければひっくり返せない
                   
Return 0
                Case
Else
                   
'相手の色ならばひっくり返せる可能性がある数が1増える
                   
Count += 1
            End
Select

       
Next

   
End If

   
Return 0

End
Function

■リスト21:ReverseGrid.vbに記述する。左方向にひっくり返せる石の数を数える。

プログラムの内容は技術的なものというよりは数学的なものになっています。もっと効率の良い数え方もあるかもしれません。

これで左方向に関しては数えられるようになります。フォームのPictureBox1Clickイベントを次のように修正して正しく判定されるか確か見てください。この時点では左方向にひっくり返せる石の数だけしかわからないので注意して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub PictureBox1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles PictureBox1.Click

   
'マウスの座標をPictureBox1のコントロール座標に変換する。
   
Dim Pos As Point = PictureBox1.PointToClient(Windows.Forms.Cursor.Position)
    Dim ThisCell As Cell

    ThisCell = Grid.CellFromPoint(Pos.X, Pos.Y)
    ThisCell.Status = Turn

   
'左方向にひっくりかえせる石の数を表示
   
MsgBox(Grid.ReversibleCount(Turn, ThisCell.Position.X, ThisCell.Position.Y))

    ChangeTurn()

End
Sub

■リスト22:Form1に記述する。左方向にひっくり返せる石の数を正しく数えられているか表示して確認する。

後は、上方向や左上方向など八方向分のメソッドを作成すればよいのですが、さすがに似たようなメソッドを8個も作成するのは面倒です。でも、しかし、もし、あなたが他に良い方法が思いつかないならば似たようなメソッドを8個作ると言う選択肢があることは重要です!

幸い私は1つのメソッドで八方向の数を数えるようにReversibleCountメソッドを修正することができました。新しいReversibleCountメソッドはどの方向を数えるか引数で指定します。引数は八方向を表す列挙体です。

ReversibleCountメソッドを記述する前にこの八方向を現すScanDirection列挙体をConstants.vbに追加して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■ScanDirection
'''
<summary>方向を表します。</summary>
Public Enum ScanDirection
    Left
    Right
    Up
    Down
    LeftUp
    LeftDown
    RightUp
    RightDown
End Enum

■リスト23:Constants.vbに記述する。八方向を表す構造体。

新しいReversibleCountメソッドは八方向にひっくり返せる石の数を数えることができますが、一度に数えるのは一方向分です。ですからは八方向分数えるには8回ReversibleCountメソッドを呼び出す必要があります。

それで、8回ReversibleCountメソッドを呼び出してその合計値を返すReversibleCountメソッドの オーバーロード(多重定義)も追加します。

次のようになります。

VB.NET2003対応 VB2005対応

'■ReversibleCount
''' <summary>石をおいた場合にひっくり返せる石の数を調べます。</summary>
''' <param name="Status">置こうとしている石の状態を指定します。</param>
''' <param name="X">対象のセルの0から始まるX位置を指定します。Xの最大値はXCount-1です。</param>
''' <param name="Y">対象のセルの0から始まるY位置を指定します。Yの最大値はYCount-1です。</param>
''' <returns>石をおいた場合にひっくり返せる石の数を返します。</returns>
Public Overloads Function ReversibleCount(ByVal Status As CellStatus, ByVal X As Integer, ByVal Y As Integer) As Integer

   
Dim Count As Integer

   
Count = ReversibleCount(Status, ScanDirection.Left, X, Y)
    Count += ReversibleCount(Status, ScanDirection.Right, X, Y)
    Count += ReversibleCount(Status, ScanDirection.Up, X, Y)
    Count += ReversibleCount(Status, ScanDirection.Down, X, Y)
    Count += ReversibleCount(Status, ScanDirection.RightUp, X, Y)
    Count += ReversibleCount(Status, ScanDirection.RightDown, X, Y)
    Count += ReversibleCount(Status, ScanDirection.LeftUp, X, Y)
    Count += ReversibleCount(Status, ScanDirection.LeftDown, X, Y)

   
Return Count

End
Function
'■ReversibleCount
''' <summary>石をおいた場合に特定の方向にひっくり返せる石の数を調べます。</summary>
''' <param name="Status">置こうとしている石の状態を指定します。</param>
''' <param name="Direction">ひっくり返す方向を指定します。</param>
''' <param name="X">対象のセルの0から始まるX位置を指定します。Xの最大値はXCount-1です。</param>
''' <param name="Y">対象のセルの0から始まるY位置を指定します。Yの最大値はYCount-1です。</param>
''' <returns>石をおいた場合にひっくり返せる石の数を返します。</returns>
Private Overloads Function ReversibleCount(ByVal Status As CellStatus, ByVal Direction As ScanDirection, ByVal X As Integer, ByVal Y As Integer) As Integer

   
Dim AnotherStatus As CellStatus
    Dim XDirection As Integer
'左のとき-1, 右のとき1
   
Dim YDirection As Integer '上のとき-1, 下のとき1

   
'相手の色を取得(自分が黒ならば相手は白、白ならば黒)
   
If Status = CellStatus.Black Then
       
AnotherStatus = CellStatus.White
   
Else
       
AnotherStatus = CellStatus.Black
    End
If

   
Select Case Direction
       
Case ScanDirection.Left
            XDirection = -1
            YDirection = 0
       
Case ScanDirection.Right
            XDirection = 1
            YDirection = 0
       
Case ScanDirection.Up
            XDirection = 0
            YDirection = -1
       
Case ScanDirection.Down
            XDirection = 0
            YDirection = 1
       
Case ScanDirection.RightUp
            XDirection = 1
            YDirection = -1
       
Case ScanDirection.RightDown
            XDirection = 1
            YDirection = 1
       
Case ScanDirection.LeftUp
            XDirection = -1
            YDirection = -1
       
Case ScanDirection.LeftDown
            XDirection = -1
            YDirection = 1
    End
Select

    Dim TestX As Integer
   
Dim TestY As Integer
   
Dim Count As Integer
   
Dim ThisCell As Cell

   
'一番端に置こうとしているときはその方向にひっくり返せるわけがない
   
If X + XDirection < 0 OrElse X + XDirection >= XCount OrElse Y + YDirection < 0 OrElse Y + YDirection >= YCount Then
       
Return False
   
End If

   
'隣のセルを取得
   
ThisCell = Cells(X + XDirection, Y + YDirection)

   
'隣のセルの色が相手の色ならばひっくり返せる可能性がある
   
If Cells(X + XDirection, Y + YDirection).Status = AnotherStatus Then

       
For i As Integer = 0 To XCount - 1

           
'もう1個隣の座標
           
TestX = X + (XDirection * (i + 1))
            TestY = Y + (YDirection * (i + 1))

           
'もう1個隣がグリッドからはみ出るなら
            '結局1個もひっくり返せないと言うこと。
           
If TestX < 0 OrElse TestX > XCount - 1 Then
               
Return 0
            End
If

           
If TestY < 0 OrElse TestY > YCount - 1 Then
               
Return 0
            End
If

           
'もう1個隣の色が
           
Select Case Cells(TestX, TestY).Status
               
Case Status
                   
'自分と同じ色ならばひっくり返せる
                   
Return Count
               
Case CellStatus.Nothing
                   
'何もなければひっくり返せない
                   
Return 0
                Case
Else
                   
'相手の色ならばひっくり返せる可能性がある数が1増える
                   
Count += 1
            End
Select

       
Next

   
End If

   
Return 0

End
Function

■リスト24:ReverseGrid.vbに記述する。全方向に対してひっくり返せる石の合計数を調べる。

PictureBox1Clickイベントでは実際の数は問題ではなく、ただ石を置けるか置けないかだけが必要なので、もう少し整理してReverseGridクラスにCanPutメソッドを追加します。

このメソッドは指定した位置に石を置けるかどうかを返します。もちろん内部ではReversibleCountメソッドを呼び出していますが、ひっくりかえせる石の数を返すのではなく、単純に石がおけるかおけないかをTrueFalseで返します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■CanPut
''' <summary>セルに石を置くことができるか調べます。</summary>
''' <param name="Status">置こうとしている石の状態を指定します。</param>
''' <param name="X">対象のセルの0から始まるX位置を指定します。Xの最大値はXCount-1です。</param>
''' <param name="Y">対象のセルの0から始まるY位置を指定します。Yの最大値はYCount-1です。</param>
''' <returns>セルに石が置ける場合はTrueを返します。</returns>
Public Function CanPut(ByVal Status As CellStatus, ByVal X As Integer, ByVal Y As Integer) As Boolean

   
'既に目的のセルに石が置かれているかチェック
   
If Cells(X, Y).Status <> CellStatus.Nothing Then
       
Return False
   
End If

   
'このセルに石を置いた場合ひっくり返せる石があるかチェック
   
If ReversibleCount(Status, X, Y) = 0 Then
       
Return False
   
End If

   
Return True

End Function

■リスト25:ReverseGrid.vbに記述する。石を置くことができるか調べる。

 

これで、PictureBox1Clickイベントを次のように直せば完璧です。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub PictureBox1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles PictureBox1.Click

   
'マウスの座標をPictureBox1のコントロール座標に変換する。
   
Dim Pos As Point = PictureBox1.PointToClient(Windows.Forms.Cursor.Position)
    Dim ThisCell As Cell

    ThisCell = Grid.CellFromPoint(Pos.X, Pos.Y)

    If
Grid.CanPut(Turn, ThisCell.Position.X, ThisCell.Position.Y)
Then
       
ThisCell.Status = Turn
        ChangeTurn()
    End
If

End Sub

■リスト26:Form1に記述する。石がひっくり返せる場合にのみ石を置くことを許可する。

これで実行してみると…、どこにも石が置けなくなります。どこにおいてもひっくり返せる石がないから当然です。

石の初期配置を行うInitializeメソッドをReverseGridクラスに追加しましょう。初期配置は真ん中に黒と白の石を交互に2個おくオセロの例の配置です。

VB.NET2003対応 VB2005対応

'■Initialize
''' <summary>ゲームを最初の状態にします。</summary>
Public Sub Initialize()

   
'すべてのセルの状態を初期状態にする。
   
For Each Cell As Cell In m_Cells
        Cell.Status = CellStatus.Nothing
   
Next

   
'初期配置
   
Cells(3, 3).Status = CellStatus.Black
    Cells(3, 4).Status = CellStatus.White
    Cells(4, 3).Status = CellStatus.White
    Cells(4, 4).Status = CellStatus.Black

End
Sub

■リスト27:ReverseGrid.vbに記述する。オセロの最初の状態をセットする。

そして、フォームのLoadイベントでこのInitializeメソッドを呼ぶようにします。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    Grid.Initialize()

End
Sub

■リスト28:Form1に記述する。オセロの最初の状態をセットする。

これで実行するとどうでしょうか!ちゃんと置けるところには置けて、置けないところには置けないようになっています。大分オセロの完成に近づいてきました。後はひっくり返す機能を追加すれば最低限の機能はそろいます。

 

8.石をひっくり返す

 

石を単純にひっくり返すだけならCellクラスのStatusプロパティを変更すれば簡単にできます。

たとえば、次のコードは初期状態で中央に4つ表示される石の一番左上が黒なら白に、白なら黒に変更します。不安のある方はテスト用のボタンでも貼り付けて試してみてください。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim ThisCell As Cell

ThisCell = Grid.Cells(3, 3)

Select
Case ThisCell.Status
    Case CellStatus.Black
        ThisCell.Status = CellStatus.White
   
Case CellStatus.White
        ThisCell.Status = CellStatus.Black
End
Select

PictureBox1.Invalidate() '表示を更新する

■リスト29:石をひっくり返せるかテストするコード。試す場合はボタンか何かを貼り付けてForm1に記述する。

あとは、どの石をひっくり返すべきか判定するロジックを書くだけなのですが、このロジックはすでにできています。先ほど作成したReversibleCountメソッドがそれです。ReversibleCountメソッド自体はひっくり返せる石の数を数えるだけですが、ここで数えるだけではなくて、実際にひっくり返してしまうように改造すればこのロジックは完成です。

ReverseGridクラスに新しくReverseメソッドを作って以下のように記述して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■Reverse
''' <summary>石をひっくり返します。</summary>
''' <param name="Status">ひっくり返す原因となった石の色を指定します。</param>
''' <param name="Direction">ひっくり返す方向を指定します。</param>
''' <param name="X">対象のセルの0から始まるX位置を指定します。Xの最大値はXCount-1です。</param>
''' <param name="Y">対象のセルの0から始まるY位置を指定します。Yの最大値はYCount-1です。</param>
Public Sub Reverse(ByVal Status As CellStatus, ByVal Direction As ScanDirection, ByVal X As Integer, ByVal Y As Integer)

    Dim
AnotherStatus As CellStatus
    Dim i As
Integer
   
Dim XDirection As Integer '左のとき-1, 右のとき1
   
Dim YDirection As Integer '上のとき-1, 下のとき1

   
If Status = CellStatus.Black Then
       
AnotherStatus = CellStatus.White
   
Else
   
    AnotherStatus = CellStatus.Black
    End
If

   
Select Case Direction
       
Case ScanDirection.Left
            XDirection = -1
            YDirection = 0
       
Case ScanDirection.Right
            XDirection = 1
            YDirection = 0
       
Case ScanDirection.Up
            XDirection = 0
            YDirection = -1
       
Case ScanDirection.Down
            XDirection = 0
            YDirection = 1
       
Case ScanDirection.RightUp
            XDirection = 1
            YDirection = -1
       
Case ScanDirection.RightDown
            XDirection = 1
            YDirection = 1
       
Case ScanDirection.LeftUp
            XDirection = -1
            YDirection = -1
       
Case ScanDirection.LeftDown
            XDirection = -1
            YDirection = 1
    End
Select

   
Dim TestX As Integer
   
Dim TestY As Integer

   
'▼ひっくりかえす
   
If ReversibleCount(Status, Direction, X, Y) > 0 Then

       
For i = 1 To XCount - 1

            TestX = X + (XDirection * i)
            TestY = Y + (YDirection * i)

            If
TestX < 0 OrElse TestX >= XCount
Then
               
Exit For
           
End If

           
If TestY < 0 OrElse TestY >= YCount Then
               
Exit For
           
End If

           
If Cells(TestX, TestY).Status = AnotherStatus Then
               
Cells(TestX, TestY).Status = Status
           
Else
               
Exit For
           
End If

       
Next

   
End If

End Sub

■リスト30:ReverseGrid.vbに記述する。石をひっくり返す。

ReversibleCountメソッドと似ているのがお分かりになりますでしょうか?後半の実際にひっくり返す部分はReversibleCountメソッドを呼び出してひっくり返せるのを確認してから、順にCellクラスのStatusプロパティを変更していくだけなのでReversibleCountメソッドよりはシンプルになっています。

ReversibleCountメソッドと同じような構造なので引数も同じです。石の色と、石を置いた場所を指定するのは当然なのですが、ひっくりかえす方向も指定する必要があります。

ここで方向を指定するのは問題があります。というのも実際のゲームでは石は可能であれば全方向に向けてひっくり返すので方向の指定はゲームのルール上は無意味だからです。それに全方向に石をひっくり返すために結局のところこのReverseメソッドを8回呼び出す必要があるのです。ですから、ここで方向を指定しないようにプログラムを改造するのも良いでしょう。

しかし、ReversibleCountメソッドとの連携の観点から今回はこのままの設計の方が説明がしやすいのでこの構造で話を進めます。

Reverseメソッドの完成によってオセロに必要な機能は一通りそろいましたが、このReverseメソッドをどこで呼び出すかがポイントです。グリッドがクリックされた場合にどのような処理が必要であるかまとめてみます。

このうち、処理1と処理2は「石を置く」という行為の判定と効果であると言えますから、この際この「石を置く」という行為を1つのメソッドにまとめてしまいます。

ReverseGridクラスに次のPutメソッドを追加して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■Put
''' <summary>セルに石を置きます。</summary>
''' <param name="Status">置く石の状態を指定します。</param>
''' <param name="X">対象のセルの0から始まるX位置を指定します。Xの最大値はXCount-1です。</param>
''' <param name="Y">対象のセルの0から始まるY位置を指定します。Yの最大値はYCount-1です。</param>
''' <returns>石を置いた場合Trueを返します。置けなかった場合Falseを返します。</returns>
''' <remarks>このメソッドを呼び出すと周辺の石がひっくり返ります。</remarks>
Public Function Put(ByVal Status As CellStatus, ByVal X As Integer, ByVal Y As Integer) As Boolean

   
'▼この位置に石が置けるか確認する
   
If CanPut(Status, X, Y) = False Then
       
Return False
   
End If

   
'▼石を置く
   
Cells(X, Y).Focus()
    Cells(X, Y).Status = Status

   
'▼周辺8方向の石をひっくり返す
   
Call Reverse(Status, ScanDirection.Left, X, Y)
   
Call Reverse(Status, ScanDirection.Right, X, Y)
   
Call Reverse(Status, ScanDirection.Up, X, Y)
   
Call Reverse(Status, ScanDirection.Down, X, Y)
   
Call Reverse(Status, ScanDirection.LeftUp, X, Y)
   
Call Reverse(Status, ScanDirection.LeftDown, X, Y)
   
Call Reverse(Status, ScanDirection.RightUp, X, Y)
   
Call Reverse(Status, ScanDirection.RightDown, X, Y)

    Return
True

End Function

■リスト31:ReverseGrid.vbに記述する。石を置いたときの処理をまとめたもの。

石を置こうとしても置けない場合もありますから、Putメソッドは石が置けなかった場合はFalseを返すようにします。石が置けるか置けないかの判定はCanPutメソッドに一任します。

石を置く機能をPutメソッドにまとめた影響でフォーム側のPictureBox1Clickイベントは次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub PictureBox1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles PictureBox1.Click

   
'マウスの座標をPictureBox1のコントロール座標に変換する。
   
Dim Pos As Point = PictureBox1.PointToClient(Windows.Forms.Cursor.Position)
    Dim ThisCell As Cell

    ThisCell = Grid.CellFromPoint(Pos.X, Pos.Y)

    If
Grid.Put(Turn, ThisCell.Position.X, ThisCell.Position.Y)
Then
   
    ChangeTurn()
    End
If

End Sub

■リスト32:Form1に記述する。よりスマートになった石を置く処理。

これで、実際に石を置いてひっくり返せるようになりました。実行して一人オセロを楽しんでください!

この段階では勝敗やパスの判定が組み込まれていませんがそれ以外のルールは正常に作動するはずです。

 

9.イベントによる布石

 

この段階でReverseGridクラスにイベントを追加します。石を置いたことを示すPutNewイベントと、石をひっくり返したことを示すReversedイベントです。Reversedイベントは石を1つひっくり返すたびに発生するものとします。

この2つのイベントは現在のところ何にも使用しません。何にも使用しないイベントをなぜ追加するのか疑問に思われるかもしれませんが、イベントとはそういったものなのです。何に使用するかはクラスを利用するプログラマが考えるものであって、クラスを設計する側は目的を問わずに柔軟なプログラムが可能な手段としてイベントを提供します。

たとえば、将来石を置くときに効果音を出したくなったらフォーム側でPutNewイベントプロシージャに記述することができます。また、実際に後で使用しますが黒の石の数、白の石の数を画面に表示するときにもこれらのイベントを使用します。

他にもこのようなイベントを公開することでプログラマのさまざまな工夫を手助けする結果となるでしょう。実際、マイクロソフト社の作成したコントロールでは○○Changedというどのような使い方をすべきかわからないイベントが大量にあります。

これらのイベントの中にはマイクロソフト社の設計者もどのような使われ方をするのかはっきり想定していないものもあることでしょう。

イベントを追加するためにReverseGridクラスに次の2つの宣言を追加して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Event Reversed(ByVal sender As Object, ByVal e As EventArgs)
Public Event PutNew(ByVal sender As Object, ByVal e As EventArgs)

■リスト33:ReverseGrid.vbに記述する。イベントの宣言。

イベントを発生させるためのRaiseEventをどこに追加すべきかはちょっと自分で考えてみて欲しいです。

 

正解は次の通りです。

PutNewイベントを発生させるために、Putメソッドにコードを1行追加します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Function Put(ByVal Status As CellStatus, ByVal X As Integer, ByVal Y As Integer) As Boolean

    …(略)…

    '▼石を置く
   
Cells(X, Y).Focus()
    Cells(X, Y).Status = Status

    RaiseEvent
PutNew(Me, New EventArgs)

   
…(略)…

End Function

■リスト34:ReverseGrid.vbに記述する。PutNewイベントを発生させる。

Reversedイベントを発生させるために、Reverseメソッドにコードを1行追加します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Sub Reverse(ByVal Status As CellStatus, ByVal Direction As ScanDirection, ByVal X As Integer, ByVal Y As Integer)

   
…(略)…

    If
Cells(TestX, TestY).Status = AnotherStatus
Then
       
Cells(TestX, TestY).Status = Status
        RaiseEvent Reversed(Me, New EventArgs)
   
Else
       
Exit For
   
End If

   
…(略)…

End Sub

■リスト35:ReverseGrid.vbに記述する。Reversedイベントを発生させる。

 

10.勝敗判定と画面表示

 

今度は画面の表示を整えて少し体裁をつくろいます。また、勝敗判定とパスの機能を追加してすべてのルールを実装します。

まず、画面に白の石の数と、黒の石の数と、現在どちらの番であるかを示す表示を追加します。

下の画像のようにコントロールを配置してプロパティを設定して下さい。

コントロールの配置

■画像8:コントロールの配置

 

コントロール プロパティ
lblBlackCount
(Label)
TextAlign MiddleRight
Text 0
lblBlackTurn
(Label)
BackColor Red
Text (空にする)
lblWhiteCount
(Label)
TextAlign MiddleRight
Text 0
lblWhiteTurn
(Label)
BackColor Red
Text (空にする)

■表4:配置するコントロールのプロパティ

赤い横線はラベルを細長くしたものです。マウスではあまり細くできないので、必要ならばプロパティウィンドウを使って高さを数値入力して下さい。上記の画像ではHeightプロパティの値を5に設定しています。

 

まずは、石の数をラベルに表示するようにしましょう。

石の数を数えるCountプロパティをReverseGridクラスに追加します。

VB.NET2003対応 VB2005対応

'■Count
''' <summary>状態を指定してセルの数を取得します。</summary>
''' <param name="Status">数えるセルの状態を指定します。</param>
''' <returns>Statusで指定した状態であるセルの数を返します。</returns>
Public ReadOnly Property Count(ByVal Status As CellStatus) As Integer
    Get

       
Dim ThisCount As Integer

       
For Each Cell As Cell In m_Cells
            If Cell.Status = Status
Then
               
ThisCount += 1
            End
If
       
Next

        Return
ThisCount

    End
Get
End Property

■リスト36:ReverseGrid.vbに記述する。グリッド上にある白い石・黒い石の数を数える。石が置かれていないセルの数を数えることもできる。

このプロパティは単純にループを回して指定した状態にあるセルの数を数えているだけです。

フォーム側からは必要なタイミングでこのCountプロパティの値をラベルに表示するようにします。必要なタイミングとはいつでしょうか?

ターンが変わるタイミング、つまりChangeTurnでも良いのですがここでは石を置いたり、ひっくり変えしたりするたびに表示を更新することにします。そのためにGridPutNewイベントプロシージャとReversedイベントプロシージャを利用します。

Gridのイベントを受け取るためにGridの宣言にWithEventsを追加して下さい。

そして、次のようにイベントプロシージャを記述します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

''' <summary>石を置いたときに発生するイベント</summary>
Private Sub Grid_PutNew(ByVal sender As Object, ByVal e As System.EventArgs) Handles Grid.PutNew

    Call Grid_Reversed(sender, e)

End
Sub
''' <summary>石がひっくり返されたときに発生するイベント</summary>
Private Sub Grid_Reversed(ByVal sender As Object, ByVal e As System.EventArgs) Handles Grid.Reversed

   
'現在の黒と白の石の数を表示する
   
lblBlackCount.Text = Grid.Count(CellStatus.Black)
    lblWhiteCount.Text = Grid.Count(CellStatus.White)

End
Sub

■リスト37:Form1に記述する。黒と白の石の数を画面に表示する。

これで実行すると石を置くたびに現在の黒と白の石の数が表示されるようになります。

でも、ゲームを開始した直後のときは黒2, 白2のはずが黒0, 白0と表示されてしまいます。石の数を数える処理をイベントプロシージャに書いたので仕方ありません。フォームのLoadイベントにも数を表示するプログラムを追加しておいてください。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    Grid.Initialize()
    lblBlackCount.Text = Grid.Count(CellStatus.Black)
    lblWhiteCount.Text = Grid.Count(CellStatus.White)

End
Sub

■リスト38:Form1に記述する。ゲーム開始時にも黒と白の石の数を表示する。

 

石の数が表示できるようになったところで、現在が黒・白どちらの順番であるかを表示するようにしましょう。

順番が変わるのはChangeTurnメソッドが呼ばれたときですからChangeTurnメソッド内に表示を更新する処理を記述してしまいます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■ChangeTurn
''' <summary>ターン交代</summary>
Public Sub ChangeTurn()

   
'現在の状態を描画(PictureBox1のPaintイベントを発生させる)
   
PictureBox1.Invalidate()

   
'▼次のターンの決定
   
If Turn = CellStatus.Black Then
       
Turn = CellStatus.White
        lblBlackTurn.Visible =
False
       
lblWhiteTurn.Visible = True
   
Else
       
Turn = CellStatus.Black
        lblBlackTurn.Visible =
True
       
lblWhiteTurn.Visible = False
   
End If

End Sub

■リスト39:Form1に記述する。ターン交代のタイミングで現在のターンを画面に表示する。

これだけだとゲーム開始直後の表示がおかしいのでフォームのLoadイベントにも処理を追加します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    Grid.Initialize()
    lblBlackCount.Text = Grid.Count(CellStatus.Black)
    lblWhiteCount.Text = Grid.Count(CellStatus.White)
    lblWhiteTurn.Visible =
False

End Sub

■リスト40:Form1に記述する。ゲーム開始時にも現在のターンを表示する。

これで石の数・現在の順番の両方が表示されて体裁が整いゲームっぽくなってきました。

 

ChangeTurnメソッドをさらに改良して勝敗判定とパスを実装します。

パスとは石を置く場所がないときに、相手が連続で石を置くルールのことを指します。勝敗判定はほとんどの場合、すべてのセルに石が置かれたときに石が多い方を勝ちとする処理ですが、その他の場合でも 勝敗が決する場合があります。

勝敗が決定するとき
すべてのセルに石が置かれたとき
すべての石が黒または白になったとき
どちらも石を置く場所がなくなったとき

■表5:勝敗が決定するとき

勝敗判定はReverseGridクラスのCountプロパティを使えばかなり簡単にプログラムできます。すべてのセルに石が置かれた場合はGrid.Count(CellStatus.Nothing) = 0で判定できます。

黒と白のそれぞれの数も簡単に取得できるので特に困ることはないでしょう。

一方パスの方法は少しプログラムを行う必要があります。石を置く場所があるかないかはすべての空いているセルに対してCanPutを調べればわかります。すべての空いているセルのCanPutFalseの場合、その色の石は置く場所がありません。

このロジックを使って置く場所があるかないか調べるためのPuttableCountメソッドをReverseGridクラスに追加します。

VB.NET2003対応 VB2005対応

'■PuttableCount
''' <summary>置くことができる場所の数を調べます。</summary>
''' <param name="Status">調べる石の色を指定します。</param>
''' <returns>Statusで指定された色を置くことができる場所の数を返します。</returns>
Public Function PuttableCount(ByVal Status As CellStatus) As Integer

   
Dim Count As Integer

   
For Each Cell As Cell In m_Cells
        If CanPut(Status, Cell.Position.X, Cell.Position.Y)
Then
       
    Count += 1
        End
If
   
Next

    Return
Count

End
Function

■リスト41:ReverseGrid1.vbに記述する。ひっくり返せる石の数を数える。

これでパスの判定はこのPuttableCountメソッドを使うだけですから楽になります。

勝敗判定・パス判定をChangeTurnメソッドに組み込むと次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■ChangeTurn
''' <summary>ターン交代</summary>
Public Sub ChangeTurn()

   
'現在の状態を描画(PictureBox1のPaintイベントを発生させる)
   
PictureBox1.Invalidate()

   
'▼勝敗判定
   
If Grid.Count(CellStatus.Nothing) = 0 Then
       
'全セルへの配置が終了した場合は勝敗判定して終了
       
If Grid.Count(CellStatus.Black) > Grid.Count(CellStatus.White) Then
           
MsgBox("黒の勝ちです!")
        ElseIf Grid.Count(CellStatus.Black) < Grid.Count(CellStatus.White)
Then
           
MsgBox("白の勝ちです!")
       
Else
           
MsgBox("引き分けです!!")
        End
If
       
Return
    ElseIf
Grid.PuttableCount(CellStatus.Black) = 0 AndAlso Grid.PuttableCount(CellStatus.White) = 0 Then
       
'空いているセルがあるのに黒も白も置けない場合
       
If Grid.Count(CellStatus.Black) > Grid.Count(CellStatus.White) Then
           
MsgBox("黒の勝ちです!")
        ElseIf Grid.Count(CellStatus.Black) < Grid.Count(CellStatus.White)
Then
           
MsgBox("白の勝ちです!")
       
Else
           
MsgBox("引き分けです!!")
        End
If
       
Return
   
ElseIf Grid.Count(CellStatus.Black) = 0 Then
       
'すべての石が白になった場合(=黒の石が0個の場合)
       
MsgBox("白の勝ちです!")
       
Return
   
ElseIf Grid.Count(CellStatus.White) = 0 Then
       
'すべての石が黒になった場合(=白の石が0個の場合)
       
MsgBox("黒の勝ちです!")
       
Return
   
End If

   
'▼次のターンの決定
   
If Turn = CellStatus.Black Then
       
Turn = CellStatus.White
        lblBlackTurn.Visible =
False
       
lblWhiteTurn.Visible = True
   
Else
       
Turn = CellStatus.Black
        lblBlackTurn.Visible =
True
       
lblWhiteTurn.Visible = False
   
End If

   
'▼置ける場所があるか判定
   
If Grid.PuttableCount(Turn) = 0 Then
       
'置く場所がなければパスして次のターン
       
ChangeTurn()
    End
If

End Sub

■リスト42:Form1に記述する。勝敗判定・パス判定。

これでようやくすべてのルールを組み込むことができました。また、表示もちゃんとできていますから1人用オセロとしては完成と言っても良いでしょう。

しばらく1人オセロを堪能してみてください。特に「パス」のテストは結構難しいです。パスの状況を作り出さなければいけません。実のところ引き分けのテストはしていません。どうしても引き分けられないのです…。

 

11.効果

 

石を置いた瞬間に該当のすべての石が一瞬でひっくり返るのでどの石がひっくり返ったのかわかりにくいし、ちょっとかっこ悪い感じがします。

複数の石がひっくり返る場合は1つずつ人間の目で終える程度の遅さでひっくり返るように改造してみましょう。

フォームでGridReversedイベントプロシージャにコードを追加して次のようにして下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

''' <summary>石がひっくり返されたときに発生するイベント</summary>
Private Sub Grid_Reversed(ByVal sender As Object, ByVal e As System.EventArgs) Handles Grid.Reversed

   
'現在の状態を描画(PictureBox1のPaintイベントを発生させる)
   
PictureBox1.Invalidate()

   
'現在の黒と白の石の数を表示する
   
lblBlackCount.Text = Grid.Count(CellStatus.Black)
    lblWhiteCount.Text = Grid.Count(CellStatus.White)

   
'ちょっと時間をおく
   
Application.DoEvents()
    System.Threading.Thread.Sleep(500)

End
Sub

■リスト43:Form1に記述する。石を1つ1つゆっくりひっくり返す。

このコードではThreadクラス(読み方:Thread = スレッド)のSleepメソッド(読み方:Sleep = スリープ)を使ってReversedイベントが発生するたびに500ミリ秒(=0.5秒)の間を空けるようにしています。

注意して欲しいのは単に0.5秒の間をあけるとコンピュータの応答がやけに遅く感じられるだけだということです。言葉で書いても良くわからないかもしれませんが、Sleepメソッドの直前のDoEventsメソッドは重要です。

これがないと、待つだけ待った後で表示はいっぺんに変化してしまい何の意味もありません。ためしにDoEventsをコメントにしてどのような動作になるのか確認してみ ると良いでしょう。

このような状況を回避するためにDoEventsメソッドを使用してSleepの直前までの状況を一旦反映させるようにします。この処理のよって石が1つずつひっくり返っていくような効果と同時に、石の数もその都度更新されていくようになります。

 

12.コンピュータとの対戦

 

1人オセロはもう十分ですからそろそろコンピュータに対戦相手になってもらいましょう。Computer1という新しいクラスを追加して下さい。このクラスのために新しいファイルを追加することをお勧めします。

このクラスは次のように記述して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class Computer1

    Dim
Grid As ReverseGrid
    Public Standard As CellStatus

    Public
Sub New(ByVal Grid As ReverseGrid, ByVal Standard As CellStatus)

        Me.Grid = Grid
       
Me.Standard = Standard

    End
Sub

   
Public Sub Put()

        Dim
X As
Integer
       
Dim Y As Integer

       
'順番に見ていってはじめに置けるところにどこでもいいから置く
       
For Y = 0 To ReverseGrid.YCount - 1
            For X = 0 To ReverseGrid.XCount - 1
                If Grid.CanPut(Standard, X, Y)
Then
                   
Grid.Put(Standard, X, Y)
                   
Return
               
End If
           
Next
        Next

    End
Sub

End
Class

■リスト44:Computer1.vbに記述する。最も単純なAI。

コンピュータの動作は非常に単純です。コンストラクタでGridと色を指定します。色はもちろん白か黒です。ここで指定した色がコンピュータの色になります。

コンピュータに石を置かせたい場合はPutメソッドを呼び出します。Putメソッドを呼び出すとコンピュータは自分で判断して適切な位置に石を置きます。

コンピュータの強さや個性はこの「判断」の内容で表現されます。どこに置くと勝てるのか、どのような置き方をすべきか、すべてこのPutメソッドにプログラムします。

説明を簡単にするためにここではとりあえずもっともシンプルなロジックを組み込みました。つまり、セルを1つずつ見ていって石を置けるところを発見したらそこに石を置くと言うロジックです。要するにこのコンピュータはオセロの最低限のルールは守りますが、何も考えないで適当に石を置くと言う打ち手です。

 

フォーム側では人間の番とコンピュータの番を交互に入れ替える必要があります。また、コンピュータに考えているふりをさせるために人間の番が終わってからコンピュータのPutメソッドを呼び出すまでの間に少し間を空けます。この目的で ここでもThread.Sleepメソッドを使用します。

それから、コンピュータの番なのに人間がグリッドをクリックした場合に何も対策を講じていないと1人オセロの場合のように石が置けてしまいます。ですからコンピュータの番のときはPictureBox1を使用不可にしておきます。

このように、人間の番のときは○○、コンピュータの番のときは××というような区別が発生してきますが、これらはすべてターンの交代を担当しているChangeTurnメソッドに組み込みます。

ChangeTurnメソッドのプログラムを改造する前にComputer1クラスを実体化する コードを書いておきましょう。フォームに次の宣言を追加して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応


Dim
Computer As New Computer1(Grid, CellStatus.White)
 

■リスト45:Form1に記述する。コンピュータの宣言。

この宣言はGridの宣言であるDim WithEvents Grid As New ReverseGrid()よりも下に書く必要があります。もし、これより上に書いてしまうとインスタンスを作成するときにまだGridが生成されていない状態となってしまい、後でNullReferenceExceptionが発生していしまいます。Gridのインスタンスを生成してから、Computerのインスタンスを生成するためにはGridの宣言より下にComputerの宣言を書く必要があります。

メモ

上のここでは宣言と同時にNewを使ってインスタンス化を行っているので宣言の順序が重要になります。宣言とインスタンス化を別々にする場合は宣言の順序は重要ではなく、インスタンス化の順序だけが重要になります。

 

次に、ChangeTurnメソッドを改造して人間とコンピュータの間でターンを交代できるようにします。ChangeTurnメソッドの一番後ろにコードを追加して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■ChangeTurn
''' <summary>ターン交代</summary>
Public Sub ChangeTurn()

   
…(略)…

   
'▼人間かコンピュータかで処理を分岐
   
If Turn = PlayerColor Then
       
'人間の番ならば、PictureBoxを使用可能にする。
       
PictureBox1.Enabled = True
   
Else
        'コンピュータの番ならば、PictureBoxを使用不可にする。
       
PictureBox1.Enabled = False

       
'ちょっと時間をおく
       
Application.DoEvents()
        System.Threading.Thread.Sleep(500)

       
'コンピュータに石を置かせる。どのセルに置くかはコンピュータ(AI)が決定する。
       
Computer.Put()
        ChangeTurn()
'プレイヤーの番へ
   
End If

End Sub

■リスト46:Form1に記述する。人間とコンピュータが交互に石を置くようにする。

これだけです。これだけであなた対コンピュータの対局が実現します。はっきりいってこのコンピュータは何も考えていないのでかなり弱いです。負けてはいけません。さぁ、プログラムを実行して対戦してみてください!

メモ

上述のChangeTurnのコードは人間対人間、人間対コンピュータの場合には問題ありますが、コンピュータ対コンピュータの場合には好ましくありません。なぜならコンピュータの番から相手のターンに移行するためにChangeTurnメソッド内でChangeTurnメソッドを呼び出しているからです。

もし、コンピュータ対コンピュータで対戦したとすると延々とChangeTurnメソッドからChangeTurnメソッドを呼び出し続ける結果となります。もし、これが無限に続けばスタックオーバーフローの例外が発生しますが、オセロのセルは64個しかないため例外にはならずにすみます。しかし、デバッグ作業などが煩雑になってしまうでしょう。

 

ところで、この状態だと常に人間が先手で黒、コンピュータが後手で白になっています。先手が黒というのはオセロのルールらしいので良いのですが、先手か後手かは選べるようにしておく必要があります。

そこで、ゲームの開始処理をフォームのLoadイベントで行うのをやめて、フォームにはゲームを開始するためのStartメソッドを追加します。

まず、Loadイベントを削除してください。

次に以下のStartメソッドをフォームに追加して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■Start
''' <summary>ゲームを開始します。</summary>
''' <param name="PlayerColor">人間の石の色を指定します。</param>
''' <remarks>黒が先手になります。</remarks>
Private Sub Start(ByVal PlayerColor As CellStatus)

    Grid.Initialize()

    Me
.PlayerColor = PlayerColor
'人間の色

    If
PlayerColor = CellStatus.Black
Then
       
Computer.Standard = CellStatus.White 'コンピュータの色は白
   
Else
       
Computer.Standard = CellStatus.Black 'コンピュータの色は黒
   
End If

   
'現在の黒と白の駒の数を表示する
   
lblBlackCount.Text = Grid.Count(CellStatus.Black)
    lblWhiteCount.Text = Grid.Count(CellStatus.White)

   
'ChangeTurnを呼び出して黒の番を開始する。そのために仮に今は白の番であることにする。
   
Turn = CellStatus.White
    ChangeTurn()

End
Sub

■リスト47:Form1に記述する。ゲームを開始するために必要な処理をまとめる。

今度はこのStartメソッドを呼ぶためにフォームにボタンを追加します。「黒(先手)で開始」ボタンと、「白(後手)で開始」ボタンです。ボタンの名前はそれぞれbtnStartBlack, btnStartWhiteとします。

ボタンの追加

■画像9:ボタンの追加

そして、ボタンのClickイベントプロシージャでStartメソッドを呼び出すようにします。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Private Sub btnStartBlack_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStartBlack.Click

    Start(CellStatus.Black)

End
Sub
Private Sub btnStartWhite_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStartWhite.Click

    Start(CellStatus.White)

End
Sub

■リスト48:Form1に記述する。人間が先手でも後手でも開始できるようにする。

以上で人間が黒か白かを指定できるようになります。すばらしいことにオセロはこれで完成です。

 

あとやるべきことがあるとしたら弱すぎるコンピュータを何とかするということくらいでしょう。これはオセロというゲームとは関係のないことではありますが、ゲームを面白くする上では重要な作業です。

また、プログラマとしてのあなたの力量を発揮する機会でもあります。

 

13.AIの作成

 

それにしてもオセロはどういう風に打つと強いのでしょうか?諸説あるようですが、結局私にはわかりません。私自身がオセロが強くないせいか私が作るAIもオセロが強くありません。

それでも少しはがんばってみましょう。ここからは私の取り組みを説明します。もっといいオセロの打ち方を知っている方はそれをプログラムしてみてください。

まず、「オセロは角を取ると勝つ」というのは唯一私の知っている強いオセロの打ち方です。実際には角をとっても負けるときがありますが、角を取ったほうが勝ちやすいと言うのは確かなように思えます。

そこで、コンピュータにも角が取れるときは角を取らせたいです。Putメソッドではまっさきに角を取れるかを検証することにしましょう。

角を取る機能を追加して以下のようなComputer2クラスを作ってみました。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class Computer2

    Dim
Grid As ReverseGrid
    Public Standard As CellStatus

    Public
Sub New(ByVal Grid As ReverseGrid, ByVal Standard As CellStatus)

       
Me.Grid = Grid
       
Me.Standard = Standard

    End
Sub
    Public Sub Put()

        Dim
X As
Integer
       
Dim Y As Integer

       
'角に置けるなら角におく
       
If Grid.CanPut(Standard, 0, 0) Then
           
Grid.Put(Standard, 0, 0)
           
Return
       
End If

       
If Grid.CanPut(Standard, 0, ReverseGrid.YCount - 1) Then
           
Grid.Put(Standard, 0, ReverseGrid.YCount - 1)
       
    Return
       
End If

       
If Grid.CanPut(Standard, ReverseGrid.XCount - 1, 0) Then
           
Grid.Put(Standard, ReverseGrid.XCount - 1, 0)
           
Return
       
End If

       
If Grid.CanPut(Standard, ReverseGrid.XCount - 1, ReverseGrid.YCount - 1) Then
           
Grid.Put(Standard, ReverseGrid.XCount - 1, ReverseGrid.YCount - 1)
           
Return
       
End If

       
'順番に見ていってはじめに置けるところにどこでもいいから置く
       
For Y = 0 To ReverseGrid.YCount - 1
            For X = 0 To ReverseGrid.XCount - 1
                If Grid.CanPut(Standard, X, Y)
Then
                   
Grid.Put(Standard, X, Y)
                   
Return
               
End If
           
Next
        Next

   
End Sub

End
Class

■リスト49:Computer2.vbに記述する。少しだけましになったAI。まだまだ弱い。

これでフォームのComputerの宣言をDim Computer As New Computer2(Grid, CellStatus.White)に変更すればComputer2と対戦できます。

対戦するとまだまだ弱いのがわかります。

だいたい自分が角を取るだけではだめなのです。相手に角を取らせないことが重要なのです。それなのにComputer2は平気で石を置くので人間は楽々と角を取ることができてしまいます。

ですから、さらに機能を追加して、ある場所に石を置くことによって相手に角を取られる危険があるかという判断が必要になります。つまり「1手先を読む」ということです。

「先を読む」という機能は上記の例のような感じでゴリゴリプログラムを記述して行うこともできますが、せっかくグリッドがクラスとして独立しているのですからこのクラスの機能を使用して行います。

ReverseGridクラスに自分自身の情報をコピーした新しいReverseGridクラスのインスタンスを返すCopyメソッドを追加します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'■Copy
''' <summary>現在のReverseGridの状態をコピーして新しいReverseGridを作成します。</summary>
''' <returns>新しいReverseGridを返します。</returns>
Public Function Copy() As ReverseGrid

    Dim NewGrid As New ReverseGrid
    Dim X As
Integer
   
Dim Y As Integer

   
For X = 0 To XCount - 1
        For Y = 0 To YCount - 1
            NewGrid.Cells(X, Y).Status = Cells(X, Y).Status
       
Next
    Next

    Return
NewGrid

End
Function

■リスト50:ReverseGrid.vbに記述する。現在のグリッドと同じ状態を持つ新しいグリッド作成する。

コピーを使えば、実際に石を置いてみてどうなるか調べることができるようになります。コピーを使って試せば実際の画面上のグリッドに変更を加えることなく自由に1手先、2手先をシミュレーションできます。

たとえば、Cell(1,1)に黒い石を置いたときに、Cell(0, 0)に白い石が置けるかどうかは次のように判断できます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim ImageGrid As ReverseGrid = Grid.Copy

ImageGrid.Put(CellStatus.Black, 1, 1)

If
ImageGrid.CanPut(CellStatus.White, 0, 0)
Then
   
MsgBox("置けます")
Else
   
MsgBox("置けません")
End
If

■リスト51:新しいグリッドを使って1手先をシミュレーションする例

これを利用して、人間に角を取られる位置にはできるだけ石を置かないようにAIのロジックを訂正してComputer3を作成します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class Computer3

    Dim
Grid As ReverseGrid
    Public Standard As CellStatus

    Public
Sub New(ByVal Grid As ReverseGrid, ByVal Standard As CellStatus)

       
Me.Grid = Grid
       
Me.Standard = Standard

    End
Sub
    Public Sub Put()

        Dim
X As
Integer
       
Dim Y As Integer
       
Dim PlayerColor As CellStatus

       
'角に置けるなら角におく
       
If Grid.CanPut(Standard, 0, 0) Then
           
Grid.Put(Standard, 0, 0)
           
Return
       
End If

       
If Grid.CanPut(Standard, 0, ReverseGrid.YCount - 1) Then
           
Grid.Put(Standard, 0, ReverseGrid.YCount - 1)
           
Return
       
End If

       
If Grid.CanPut(Standard, ReverseGrid.XCount - 1, 0) Then
           
Grid.Put(Standard, ReverseGrid.XCount - 1, 0)
           
Return
       
End If

       
If Grid.CanPut(Standard, ReverseGrid.XCount - 1, ReverseGrid.YCount - 1) Then
           
Grid.Put(Standard, ReverseGrid.XCount - 1, ReverseGrid.YCount - 1)
           
Return
       
End If

       
If Standard = CellStatus.Black Then
           
PlayerColor = CellStatus.White
       
Else
           
PlayerColor = CellStatus.Black
        End
If

       
'順番に見ていってはじめに置けるところを探す
       
Dim ImageGrid As ReverseGrid
        Dim Puts As New ArrayList

        For
Y = 0 To ReverseGrid.YCount - 1
            For X = 0 To ReverseGrid.XCount - 1

                If
Grid.CanPut(Standard, X, Y)
Then

                    Puts.Add(
New Point(X, Y))

                    '▼まず、コピーのグリッドに石を置いてみる
                   
ImageGrid = Grid.Copy
                    ImageGrid.Put(Standard, X, Y)

                    'この状態で相手に角を取られる可能性があるか検証する
                   
Select Case True
                       
Case ImageGrid.CanPut(PlayerColor, 0, 0)
                           
'左上の角を取られてしまうので何もしない
                       
Case ImageGrid.CanPut(PlayerColor, 0, ReverseGrid.YCount - 1)
           
                '左下の角を取られてしまうので何もしない
                       
Case ImageGrid.CanPut(PlayerColor, ReverseGrid.XCount - 1, 0)
                           
'右上の角を取られてしまうので何もしない
                       
Case ImageGrid.CanPut(PlayerColor, ReverseGrid.XCount - 1, ReverseGrid.YCount - 1)
                           
'右下の角を取られてしまうので何もしない
                       
Case Else
           
                '角を取られる心配がないのでこの位置に石を置く
                           
Grid.Put(Standard, X, Y)
                           
Return
                   
End Select

               
End If

           
Next
        Next

       
'角を取られる位置にしか置けない場合、仕方ないので置く
       
Dim Pos As Point = DirectCast(Puts(0), Point)
        Grid.Put(Standard, Pos.X, Pos.Y)

    End
Sub

End
Class

■リスト52:Computer3.vbに記述する。ちょっと強くなったAI。小学校低学年レベル。

このAIと対戦するためにフォームのComputerの宣言を変更するのを忘れないでください。

私はこのAIとの初回の対戦では4つの角を取られて惨敗しました。ただし、これは私がオセロが弱いと言うことであってこのAIが特別オセロが強いと言うことではありません。(なお、2回目の対戦では3つの角を取って私が勝ちました。)

良く考えてみたらこのロジックは単に、「石を置いたことで角を取られるのを防ぐ」ということだけではなく、角を取られる状況を可能な限り回避すると言うロジックになっているようですね。もちろん1手先までしか読みませんが。これで2手先、3手先を読めるようにしたらかなり強いと思います。60手先まで読めるようにしたらどうなるのか興味があります。きっと膨大な処理時間がかかってゲームとしては使い物にならないでしょう。

ともあれ、みなさんにはどのようにしてAIを作成していくのかこれで理解していただけたことでしょう。さらに検証を続くけてどんでもなく強いAIを開発して下さい。オセロくらいのゲームなら「絶対勝つ」という必勝パターンがあるような気がします。コンピュータを使えばそのくらい強いAIが作成できると思うのですがどうでしょうか。良いAIができたら送ってください。