Visual Basic 初級講座 |
Visual Basic 中学校 > 初級講座 >
第52回 実技4 オセロ
初級講座最後の実技としてオセロを作成します。対戦相手としてコンピュータのAI(人工知能)のプログラムも行います。プログラムの作り方を勉強すると言うよりは、「体験」するつもりで実際に手順どおりやってみることをお勧めします。
概要 ・オセロを作成する。 ・オセロの対戦相手としてコンピュータのAI(人工知能)をプログラムする。 |
今回はオセロを作ります。また、対戦相手としてコンピュータのAI(人口知能)を作成します。 日本語で「人工知能」というと大げさな感じがしますが、とりあえずオセロの対戦相手になってくれる程度のものです。
■画像1:完成したところ
プログラムの作成方法は順を追って説明しているため、この記事は長くなっています。
この記事ではオセロの作り方の説明を目的としているのではなく、オセロの作りを「体験」していただくことを目的としています。細かいロジックやVBの構文が理解できなかったとしても手順どおりに進めてプログラムを完成させて経験値を向上させてください。
おそらく途中で、プログラムをどこに書けば良いのかわからなくなったり、うまく動くはずのところがうまく動かなくなったりしてつまずくことでしょう。しかし、実際に私が試した手順・方法は記事の中にしっかりと書いておきましたので、よく読めば必ず完成に至るものと思います。そのような四苦八苦こそがプログラムを上達させる良い経験となるでしょう。
メモ - VB.NET(2002)の場合 今回作成するオセロはVB.NET(2002) ,VB.NET2003, VB2005に対応していますが、VB.NET(2002)を使用している場合だけちょっとプログラムを変える必要があります。 説明中に何度か出てくるForループのカウンタ変数の宣言の部分ですが、VB.NET2003とVB2005ではたとえば次のような書き方が可能です。
■リスト1:VB.NET2003, VB2005の場合 上記のようなコードがあったらVB.NET(2002)の場合だけは次のように書き換えてください。
■リスト2:VB.NET(2002)の場合 |
説明に入る前に言葉遣いをまとめておきます。オセロとは8×8の盤面に黒と白の石を置いていくゲームですが、ここでは盤面のことを「グリッド」、升目(ますめ)のことを「セル」を呼ぶことにします。
盤面 | グリッド (Grid) |
升目 | セル (Cell) |
■表1:今回使用する用語
この用語は私が勝手に考えたものですが、このページの説明では今後この言葉を使いますので注意して下さい。
さらに、具体的なプログラムに着手する前に少しプログラムの構造を考えて見ます。
いろいろな作り方があると思いますが、今回はグリッドとセルをそれぞれ独立したクラスとします。
セルは全部で64個もありますから、クラス化することによってプログラムの効率化が図れます。グリッドをクラスにするメリットは自由にグリッドを作ったり消したりできるからです。グリッドは一度作ればゲーム終了まで変わらないようにも思えますが、AIがグリッド使って自由にシミュレーションを行えるようにするためにはやはりクラスになっていたほうが便利です。
それにグリッドには64個のセルを束ねる機能もあります。
ということでAIも含めてこのプログラムでは3つのクラスを作成することになります。もちろんFormなどのクラスもこれとは別に使用しますし、 これらとは別にちょっとだけもっと小さいクラスを作成することにもなります。
クラス | 説明 |
Cell | オセロのセル(升目)を表します。セルの状態(黒・白・なし)の管理や、セルへのフォーカスの移動、グラフィックの描画を行います。 |
ReverseGrid | オセロのグリッド(盤面)を表します。石を置くためのメソッドや、ひっくり返すメソッドとそれに関連するイベント、セルの管理、描画処理を行います。 |
Computer | オセロのプレイヤーとなる人工知能を表します。 |
■表2:作成する主なクラス
ところで、今この文章を書いている段階で私の目の前にはこのプログラムの完成版があります。だから、このようにすらすらとどういうクラスが必要でどういう機能を持たせるかと言うことが書けるのです。
通常はプログラム作成前は漠然と「こういう風にしようかな」という思いがあるだけで厳密な設計はしません。もちろん大きなシステムを作る場合や厳密な動作が求められている場合はしっかりした設計から行いますが、今回はそうではありません。
それでは、まずはグリッドを描画するところを書いてオセロらしい雰囲気を出してみましょう。フォームにPictureBoxを1つ貼り付けてください。このPictureBoxの中でオセロをやるので少し大きめに広げてください。
次にグリッドを表すReverseGridクラスをプロジェクトに追加します。このクラスは少しボリュームのあるクラスになりますので専用のファイルを用意してReverseGrid.vbという名前を付けてください。
ReverseGridクラスにはグリッドを描画するためのDrawメソッドをプログラムします。
次のようになります。
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を使用することにしました。また、セルの数も定数XCountとYCountで定義するようにしています。オセロのセルの数は8×8に決まっているのですが、せっかく自分でプログラムしているのですから少しはオリジナル性のあるものを作りたい方がいらっしゃると思います。そんな 時はこの定数をいじることで簡単にセルの数が変えられるようになります。
Drawメソッドの引数にはGraphicsオブジェクトを指定します。VBのグラフィックス機能はすべてこのGraphicsに集約されますのでこの引数は必須です。
この段階ではDrawメソッドは座標の計算が少し複雑である以外には目新しいことはありません。
フォーム側ではPictureBox1のPaintイベントでこのメソッドを呼びます。次の通りです。
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:グリッドの描画
次にクリックしたセルに石を置くようにプログラムします。石が置けるようになればあとははさまれば石をひっくり返す処理を書くだけでオセロは完成です。とは言ってもいろいろな細かい処理を書かなければいけないのですが…。
ここでいきなりですが今回の最大の山場を迎えます。64個あるセルはそれぞれの状態を記憶しておく必要があります。状態とは「黒」、「白」、「なし」の3種類です。そしてReverseGridのDrawメソッドではそれぞれのセルの状態に応じて黒い円や白い円を描画することになります。
プレイヤーはグリッドのあるPictureBox1をクリックして石を置いていくことになりますが、 注意して欲しいのはPictureBox1のClickイベントでは黒・白・なしの状態をセットするだけで描画は行わないと言うことです。描画を行うのはあくまでもPaintイベントです。
この手法はグラフィックスを扱うほとんどのプログラムで採用されています。つまり、クリックなどの動作は状態の記録を変更するだけで実際の描画は行わないと言う手法です。この方が描画処理が一箇所に集中してわかりやすくなりますし、多くのクラスがこの方式になじむように設計されています。
この作り方の場合Clickイベントでは、クリック後にすぐに描画が行われるようにInvalidateメソッドを使ってPaintイベントを呼び出すようにします。そうしないと クリックしてから実際に描画されるまでのタイミングが制御できず妙な間が空いてしまいます。
話を戻します。その64個のセルの状態をどのように記録するかということですが、ここでは64個のCellクラスを作成しその
Cellクラスがそれぞれ自分の状態を保持するようにします。また、Cellクラス自体にも描画機能をつけてお手軽に描画ができるようにもします。
まずは状態を定義する列挙体を作成して下さい。そのためにConstants.vbという新しいクラスをファイルごと作成してください。このファイルは初期状態では次のようになっているはずです。
Public Class
Constants End Class |
■リスト5:Constants.vbの最初の状態。この2行は削除してしまう。
この2行は消してしまってください。代わりに次の列挙体を書いてください。
'■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(なし)に決まっていますから、コンストラクタではGridとPositionを受け取るようにします。RectangleはPositionから計算して求めることができますので必要ありません。
コンストラクタは次のようになります。
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)のように直感的にアクセスできます。
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個のセルを生成する処理。
セルの数を表すために先ほど記述した定数XCountとYCountを使用しています。
それから、外部からもこのm_Cellにアクセスできるようにm_Cellをプロパティを通じて公開します。配列をそのまま公開するか配列型のプロパティとして公開すると言う方法もありますが、今回はメソッド風に引数を2つ持つプロパティとして公開します。
次のCellsプロパティをReverseGridクラスに追加して下さい。
'■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メソッドを追加しましょう。次の通りです。
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メソッドで登場する変数FrontBrushはDrawメソッドの中でしか使用しないので本来はDrawメソッドの中で宣言すべきなのですが、描画処理のたびにいちいちブラシをインスタンス化していたら処理が遅くなるのではないかと思いましてあえて外で宣言しました。これでインスタンスは1回作成するだけであとは同じインスタンスを使い回しして処理の効率化を図ることができます。
今度はこのCellクラスのDrawメソッドをReverseGridクラスのDrawメソッドから呼び出すようにしましょう。セルは64個ありますから、それぞれのCellクラスのDrawメソッドをループを回して全部呼び出すことになります。
ReverseGridクラスのDrawメソッドに「▼セルの状態を描画」の部分のコードを追加して、次のようにして下さい。
'■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に記述する。セルの状態の描画を追加。
これでクラス側の準備は完了です。この段階で実行して表示を確かめてみてください。
実行してみると先ほどと何もかわっていないはずです。クリックしても何もおこりません。もし、エラーが発生したり表示がおかしかったりする場合はここまでの作業に間違いがあるのでよく確認して下さい。
正常に作動している場合はいよいよクリックしたら石を表示するようにします。
まずは、小手調べでPictureBox1のClickイベントに次のコードを記述して下さい。
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クラスに追加します。
'■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に記述する。座標の位置にあるセルを取得する。
後はフォームのPictureBox1のClickイベントをこのCellFromPointメソッドを使用するロジックに書き換えれば完了です。次のようになります。
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)とする座標系に変換する必要があります。
座標変換と言ってもかなりシンプルですし、PictureBox1のPointToClientメソッド(読み方:PointToClient = ポイントトゥークライアント)を使用すれば簡単にできます。
そして変換した座標をCellFromPointメソッドに渡せば クリックされたセルを取得することができます。
実行して、クリックしたセルに次々と黒い丸が表示されていく様子を確認してみてください。
■画像4:クリックした位置に石を置く
これでクリックしたセルに黒い石を表示できるようになりましたが、かなりかっこ悪いので少し改造して見栄えをととのえます。
まず、石をかっこよくします。かっこよくといっても少しましになる程度なので過度に期待しないでください。
石をかっこよくするのは実は簡単です。石を描画しているのはCell.Drawメソッドなのでこのメソッドの中を変更するだけです。このようにクラス・メソッドという構成をとって1つのメソッドに1つの機能を割り当てておくと後からプログラムを変更するのが楽になります。
とりあえず、次のように変更して下さい。余裕がある人はここで画像ファイルを読み込むなど独自のカスタマイズを加えても構いません。
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:石を少しだけかっこよく描画する
それほどかっこよくもありませんが、さっきよりは大分ましです。
さらに、アクティブなセルを示すための枠線を描画することにします。アクティブなセルが枠線で表示されることによってプログラムにぐっと動きがでてきて、めりはりのある感じになります。
これには、まずセルがアクティブであるかどうかを示す変数FocusedをCellクラスに追加します。そして、Focused = Trueのときに枠線を表示するようにDrawメソッドを改めます。また、外部からFocusedの値を変更できるようにFocusメソッドも追加します。変数FocusedとFocusメソッドが別々に存在するわけはすぐ後で説明します。
フォーム側ではPictureBox1のMouseMoveイベントでマウスがある位置のセルに対してFocusメソッドを呼び出します。
Cellクラスの変数FocusedおよびFocusメソッドは次のようになります。
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プロパティプロシージャを作成することによっても実現できますが、他のButtonやTextBoxなどのコントロールではFocusというメソッドが使用できるようになっているのでそれに合わせました。
メソッドやプロパティの名前を付けるときは、他に同じ機能のメソッドやプロパティがあればそれと同じ名前にしておくのが吉です。同じ機能なのにクラスごとにメソッド名が異なっていたら使用するほうは大変です。
次にCellクラスのDrawメソッドにコードを追加して次のようにして下さい。追加するのは一番最後の「▼アクティブな場合は枠を描画する」という3行の部分だけです。
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に記述する。アクティブな場合に枠を描画する処理の追加。
最後にフォームのPictureBox1のMouseMoveイベント記述して完了です。意欲的な方はこのMouseMoveイベントの内容は自分で書いてみてください。
次のようになります。
'''
<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メソッドさえ修正すればいつでも描画処理を変更することができます。
だんだんと下地はととのってきたので、今度は黒の番、白の番というのを作りましょう。最初のクリックでは黒い石をおきます。次のクリックでは白い石をおきます。というように交互に黒 と白を置けるようにします。
この処理は特に技術的に難しいことはないので簡単に書けると思います。
今回はフォームで変数Turnを用意して、現在黒の番か白の番かを記録するようにします。そして黒の番なら黒い石、白の番なら白い石をクリックしたときに置くようにします。そして、プレイヤーの色が黒か白かを表すPlayerColor変数も用意します。プレイヤーというのは人間対コンピュータの場合は人間、自分対他人の場合は自分を表す色です。ただし、 我々が今作っているオセロは完成時には人間対コンピュータという構図になるので自分対他人ということはありえません。
TurnもPlayerTurnも白か黒かを表すのでCellStatus型にします。また、現在は両方とも初期値はCellStatus.Blackにしておきます。
さらに黒と白のTurnを切り替えるためのChangeTurnメソッドも記述します。このメソッドはこの時点ではなくてもよいくらいの小さなメソッドですが、後で本格的にオセロが完成していくにつれて重要になっていきます。
フォーム側に次のコードを追加して下さい。
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メソッドを呼び出していることに注意して下さい。
PictureBox1のClickイベントは次のように書き換えてください。
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に記述する。石を置いたタイミングでターンを交代するようにする。
PictureBox1のInvalidateメソッドはChangeTurnメソッド内で呼び出すことになったのでここでは不要になります。
実行すると黒と白が交互に置けるようになります。画面だけ見ればもうオセロゲームが完成したかのようにも見えます。
■画像7:黒と白を交互に置く
描画処理に関しては一通り完成したのでそろそろオセロのルールをプログラムしていきましょう。どこでも好きなところに石を置けるのはもう終わりにします。
多分ここがこのプログラムの最大の難関です。
よくよく考えてみると石が置けるところと置けないところを区別できればオセロのルールはほぼプログラム完了です。なぜなら、オセロでは石が置けるところとは相手の石をひっくり返せるところだからです。 つまり、石が置けるかどうか判定するには石をひっくり返せるかどうか判定することになります。
さて、 石は左・左上・上・右上・右・右下・下・左下の八方向にひっくり返せる可能性があります。いきなり八方向では大変なのでとりあえず、左側にひっくり返せるかどうかを調べるプログラムを書きましょう。
プログラムの前に考え方を簡単にまとめておきます。実際のプログラムもこの考え方をそのままVBで記述しているだけです。
黒い石をおいたとき、左の石をひっくり返せるか判断する方法
黒い石の左は
このうち、ひっくり返せる可能性があるのは左が白い石の場合のみ。 さらにこの左側は
の場合は、さらに左側をみないと判定できない。 の場合は、ひっくり返せることが確定。 との場合は、ひっくり返せないことが確定。 このようにどんどん左側を見ていくことにより最終的に石がひっくり返せるのか判断できます。 |
実際のコードはReverseGridクラスに次のReversibleCountメソッドを追加して 記述します。このメソッドは左方向にひっくり返せるセルの数を数えます。
'■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に記述する。左方向にひっくり返せる石の数を数える。
プログラムの内容は技術的なものというよりは数学的なものになっています。もっと効率の良い数え方もあるかもしれません。
これで左方向に関しては数えられるようになります。フォームのPictureBox1のClickイベントを次のように修正して正しく判定されるか確か見てください。この時点では左方向にひっくり返せる石の数だけしかわからないので注意して下さい。
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に追加して下さい。
'■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メソッドの オーバーロード(多重定義)も追加します。
次のようになります。
'■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に記述する。全方向に対してひっくり返せる石の合計数を調べる。
PictureBox1のClickイベントでは実際の数は問題ではなく、ただ石を置けるか置けないかだけが必要なので、もう少し整理してReverseGridクラスにCanPutメソッドを追加します。
このメソッドは指定した位置に石を置けるかどうかを返します。もちろん内部ではReversibleCountメソッドを呼び出していますが、ひっくりかえせる石の数を返すのではなく、単純に石がおけるかおけないかをTrueかFalseで返します。
'■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に記述する。石を置くことができるか調べる。
これで、PictureBox1のClickイベントを次のように直せば完璧です。
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個おくオセロの例の配置です。
'■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メソッドを呼ぶようにします。
Private Sub Form1_Load(ByVal
sender As System.Object,
ByVal e As
System.EventArgs) Handles
MyBase.Load Grid.Initialize() End Sub |
■リスト28:Form1に記述する。オセロの最初の状態をセットする。
これで実行するとどうでしょうか!ちゃんと置けるところには置けて、置けないところには置けないようになっています。大分オセロの完成に近づいてきました。後はひっくり返す機能を追加すれば最低限の機能はそろいます。
石を単純にひっくり返すだけならCellクラスのStatusプロパティを変更すれば簡単にできます。
たとえば、次のコードは初期状態で中央に4つ表示される石の一番左上が黒なら白に、白なら黒に変更します。不安のある方はテスト用のボタンでも貼り付けて試してみてください。
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メソッドを作って以下のように記述して下さい。
'■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メソッドを追加して下さい。
'■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メソッドにまとめた影響でフォーム側のPictureBox1のClickイベントは次のようになります。
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に記述する。よりスマートになった石を置く処理。
これで、実際に石を置いてひっくり返せるようになりました。実行して一人オセロを楽しんでください!
この段階では勝敗やパスの判定が組み込まれていませんがそれ以外のルールは正常に作動するはずです。
この段階でReverseGridクラスにイベントを追加します。石を置いたことを示すPutNewイベントと、石をひっくり返したことを示すReversedイベントです。Reversedイベントは石を1つひっくり返すたびに発生するものとします。
この2つのイベントは現在のところ何にも使用しません。何にも使用しないイベントをなぜ追加するのか疑問に思われるかもしれませんが、イベントとはそういったものなのです。何に使用するかはクラスを利用するプログラマが考えるものであって、クラスを設計する側は目的を問わずに柔軟なプログラムが可能な手段としてイベントを提供します。
たとえば、将来石を置くときに効果音を出したくなったらフォーム側でPutNewイベントプロシージャに記述することができます。また、実際に後で使用しますが黒の石の数、白の石の数を画面に表示するときにもこれらのイベントを使用します。
他にもこのようなイベントを公開することでプログラマのさまざまな工夫を手助けする結果となるでしょう。実際、マイクロソフト社の作成したコントロールでは○○Changedというどのような使い方をすべきかわからないイベントが大量にあります。
これらのイベントの中にはマイクロソフト社の設計者もどのような使われ方をするのかはっきり想定していないものもあることでしょう。
イベントを追加するためにReverseGridクラスに次の2つの宣言を追加して下さい。
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行追加します。
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行追加します。
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イベントを発生させる。
今度は画面の表示を整えて少し体裁をつくろいます。また、勝敗判定とパスの機能を追加してすべてのルールを実装します。
まず、画面に白の石の数と、黒の石の数と、現在どちらの番であるかを示す表示を追加します。
下の画像のようにコントロールを配置してプロパティを設定して下さい。
■画像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クラスに追加します。
'■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でも良いのですがここでは石を置いたり、ひっくり変えしたりするたびに表示を更新することにします。そのためにGridのPutNewイベントプロシージャとReversedイベントプロシージャを利用します。
Gridのイベントを受け取るためにGridの宣言にWithEventsを追加して下さい。
そして、次のようにイベントプロシージャを記述します。
'''
<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イベントにも数を表示するプログラムを追加しておいてください。
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メソッド内に表示を更新する処理を記述してしまいます。
'■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イベントにも処理を追加します。
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を調べればわかります。すべての空いているセルのCanPutがFalseの場合、その色の石は置く場所がありません。
このロジックを使って置く場所があるかないか調べるためのPuttableCountメソッドをReverseGridクラスに追加します。
'■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メソッドに組み込むと次のようになります。
'■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人オセロを堪能してみてください。特に「パス」のテストは結構難しいです。パスの状況を作り出さなければいけません。実のところ引き分けのテストはしていません。どうしても引き分けられないのです…。
石を置いた瞬間に該当のすべての石が一瞬でひっくり返るのでどの石がひっくり返ったのかわかりにくいし、ちょっとかっこ悪い感じがします。
複数の石がひっくり返る場合は1つずつ人間の目で終える程度の遅さでひっくり返るように改造してみましょう。
フォームでGridのReversedイベントプロシージャにコードを追加して次のようにして下さい。
'''
<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つずつひっくり返っていくような効果と同時に、石の数もその都度更新されていくようになります。
1人オセロはもう十分ですからそろそろコンピュータに対戦相手になってもらいましょう。Computer1という新しいクラスを追加して下さい。このクラスのために新しいファイルを追加することをお勧めします。
このクラスは次のように記述して下さい。
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クラスを実体化する コードを書いておきましょう。フォームに次の宣言を追加して下さい。
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メソッドの一番後ろにコードを追加して下さい。
'■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メソッドをフォームに追加して下さい。
'■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メソッドを呼び出すようにします。
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に記述する。人間が先手でも後手でも開始できるようにする。
以上で人間が黒か白かを指定できるようになります。すばらしいことにオセロはこれで完成です。
あとやるべきことがあるとしたら弱すぎるコンピュータを何とかするということくらいでしょう。これはオセロというゲームとは関係のないことではありますが、ゲームを面白くする上では重要な作業です。
また、プログラマとしてのあなたの力量を発揮する機会でもあります。
それにしてもオセロはどういう風に打つと強いのでしょうか?諸説あるようですが、結局私にはわかりません。私自身がオセロが強くないせいか私が作るAIもオセロが強くありません。
それでも少しはがんばってみましょう。ここからは私の取り組みを説明します。もっといいオセロの打ち方を知っている方はそれをプログラムしてみてください。
まず、「オセロは角を取ると勝つ」というのは唯一私の知っている強いオセロの打ち方です。実際には角をとっても負けるときがありますが、角を取ったほうが勝ちやすいと言うのは確かなように思えます。
そこで、コンピュータにも角が取れるときは角を取らせたいです。Putメソッドではまっさきに角を取れるかを検証することにしましょう。
角を取る機能を追加して以下のようなComputer2クラスを作ってみました。
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メソッドを追加します。
'■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)に白い石が置けるかどうかは次のように判断できます。
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を作成します。
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ができたら送ってください。