概要
Excel VBAを用いたオセロゲーム開発も、いよいよ佳境に入ってきました。これまでの連載では、Excelシートを盤面に見立て、セルの背景色や図形オブジェクトを操作して石の配置を視覚化し、石を裏返す基本的なロジックを構築してきました。しかし、ゲームとして成立させるためには、プレイヤーの手番を正確に管理し、ゲームがいつ終了するのか、そして最終的にどちらが勝利したのかを判定する仕組みが不可欠です。
今回の「VBAオセロ実践講座 第7回」では、ゲームの進行を司る「手番管理」と、ゲームの決着をつける「勝敗判定」のロジックに深く切り込みます。これらの実装は、単にオセロゲームを完成させるだけでなく、あらゆる対戦型ゲームやシミュレーションにおける「状態管理」と「条件分岐」の基礎を学ぶ上で極めて重要な要素となります。VBAの制御構造を駆使し、複雑なゲームロジックをいかにシンプルかつ堅牢に構築するか、その実践的なアプローチを詳細に解説していきます。
詳細解説
1. 手番管理のロジック設計
オセロは黒と白が交互に石を置いていくゲームです。現在の手番がどちらのプレイヤーであるかを常に把握し、石が置かれるたびに手番を切り替える必要があります。また、有効な手が一つもない場合(パス)の処理も考慮しなければなりません。
手番を管理する最もシンプルな方法は、グローバル変数やモジュールレベルの変数を使用することです。例えば、`CurrentPlayer`という整数型の変数を用意し、黒を`1`、白を`-1`(あるいは`2`)と定義します。石が置かれた後、この変数の値を反転させることで手番を切り替えます。
しかし、単に手番を切り替えるだけでは不十分です。オセロには「パス」のルールが存在します。あるプレイヤーが石を置ける場所が一つもない場合、そのプレイヤーはパスとなり、手番が相手に移ります。そして、両方のプレイヤーが連続してパスとなった場合、ゲームは終了します。この複雑なフローをVBAで実現するには、以下の要素を考慮したプロシージャが必要です。
* **有効な手を探索する機能**: 盤面上の全ての空きマスに対し、石を置いた場合に裏返せる石があるかどうかを判定する機能が必須です。これは、以前作成した`CanPlaceStone`関数を拡張し、盤面全体で有効な手があるかをチェックする`HasValidMove(ByVal Player As Long)`関数として実装できます。
* **手番の切り替え**: `CurrentPlayer`の値を更新します。
* **パス判定**: `HasValidMove`関数が`False`を返した場合、そのプレイヤーはパスとなります。
* **連続パス判定**: パスが発生した場合、直前の手番もパスだったかを記録するフラグ(例: `LastTurnWasPass`)を設けます。両者が連続してパスした場合、ゲーム終了条件の一つを満たします。
これらのロジックは、石を置くたびに呼び出される`PlaceStone`プロシージャの最後に組み込むことが一般的です。手番管理はゲームの心臓部であり、ここでの実装がゲーム全体の安定性に直結するため、慎重な設計が求められます。
2. 勝敗判定とゲーム終了条件
オセロゲームが終了する条件は複数存在します。
1. **盤面の全てが石で埋まった場合**: 64マス全てに石が置かれた時点。
2. **どちらかのプレイヤーの石がなくなった場合**: 一方のプレイヤーの石が全て裏返され、そのプレイヤーの石が0個になった時点。
3. **両方のプレイヤーが連続して有効な手を打てなかった場合(連続パス)**: 上述の手番管理ロジックで判定します。
ゲームが終了したと判断されたら、最終的な勝敗を決定します。勝敗は、盤面上の黒石と白石の数を比較して決定します。数の多い方が勝利し、同数の場合は引き分けとなります。
この判定ロジックは、手番が切り替わるたびに(または石が置かれるたびに)呼び出される`CheckGameOver`プロシージャに実装します。このプロシージャは、まず上記の終了条件のいずれかが満たされているかをチェックし、満たされていれば石の数を数える`CountStones`関数を呼び出して勝敗を決定し、結果をユーザーに通知(例: `MsgBox`で表示)します。
3. オブジェクト指向的アプローチの導入
これまでの連載で、`CBoard`のようなクラスモジュールを導入している場合、手番管理や勝敗判定のロジックもこのクラスのメソッドとしてカプセル化することを検討すべきです。例えば、`CBoard`クラスに`Turn`プロパティ(読み取り専用)、`ChangeTurn`メソッド、`CheckPass`メソッド、`IsGameOver`メソッド、`GetWinner`メソッドなどを追加することで、コードのモジュール性が向上し、管理が容易になります。
– `CBoard.ChangeTurn()`: 現在の手番を切り替え、パス判定も内部で行う。
– `CBoard.IsGameOver()`: ゲーム終了条件を満たしているかブール値で返す。
– `CBoard.GetWinner()`: ゲーム終了時に勝者(黒、白、引き分け)を返す。
このようにクラスモジュールを活用することで、ゲームの状態(盤面、手番、パスの状態など)とそれらを操作するロジックが一箇所に集約され、コードの見通しが格段に良くなります。
サンプルコード
以下に、手番管理と勝敗判定の主要なロジックを構成するVBAコードの抜粋を示します。これは既存のオセロプロジェクトに組み込むことを想定しています。
`Module1` (標準モジュール)または `CBoard` クラスモジュールに記述します。
' 公開変数(CBoardクラスを使わない場合の例、クラスを使う場合はプロパティに)
Public CurrentPlayer As Long ' 現在の手番 (1:黒, -1:白)
Public LastTurnWasPass As Boolean ' 前の手番がパスだったか
'---------------------------------------------------------------------------------------------------
' 盤面上で指定されたプレイヤーが石を置ける場所があるか判定する関数
'---------------------------------------------------------------------------------------------------
Function HasValidMove(ByVal Player As Long) As Boolean
Dim r As Long, c As Long
HasValidMove = False
For r = 1 To 8
For c = 1 To 8
' CanPlaceStone関数は、指定セルに指定プレイヤーが石を置けるかを判定する既存関数と仮定
If CBoard.CanPlaceStone(r, c, Player) Then
HasValidMove = True
Exit Function ' 一つでも有効な手があればTrueを返して終了
End If
Next c
Next r
End Function
'---------------------------------------------------------------------------------------------------
' 手番を切り替え、パス判定とゲーム終了判定を行うプロシージャ
'---------------------------------------------------------------------------------------------------
Sub NextTurn()
Dim nextPlayer As Long
Dim currentPlayerHasMove As Boolean
Dim nextPlayerHasMove As Boolean
' 現在のプレイヤーの手番を記録
CurrentPlayer = -CurrentPlayer ' 手番を切り替える前に、現在の手番を記録しておく
' 次のプレイヤー(手番が切り替わった後のプレイヤー)が有効な手を持てるか確認
nextPlayer = CurrentPlayer
nextPlayerHasMove = HasValidMove(nextPlayer)
' 現在のプレイヤー(元々のプレイヤー)が有効な手を持てたか確認(パス判定用)
currentPlayerHasMove = HasValidMove(-nextPlayer) ' CurrentPlayerの反転値が元々のプレイヤー
If Not nextPlayerHasMove Then
' 次のプレイヤーがパスの場合
If LastTurnWasPass Then
' 連続パスの場合、ゲーム終了
Call CheckGameOver(True) ' 連続パスによるゲーム終了を通知
Exit Sub
Else
' 今回のプレイヤーがパス
LastTurnWasPass = True
' 手番をもう一度切り替えて、元のプレイヤーに戻す(パスなので相手の番)
CurrentPlayer = -CurrentPlayer
MsgBox "現在のプレイヤー(" & IIf(CurrentPlayer = 1, "黒", "白") & ")はパスです。手番が相手に移ります。", vbInformation
' 再度、次の手番の有効性をチェック
If Not HasValidMove(CurrentPlayer) Then
' 相手もパスの場合、連続パスでゲーム終了
Call CheckGameOver(True)
Exit Sub
End If
End If
Else
' 次のプレイヤーが有効な手を持てる場合
LastTurnWasPass = False ' パスはリセット
End If
' その他のゲーム終了条件も確認
Call CheckGameOver(False)
End Sub
'---------------------------------------------------------------------------------------------------
' 盤面上の石の数を数える関数
'---------------------------------------------------------------------------------------------------
Function CountStones(ByVal Player As Long) As Long
Dim r As Long, c As Long
Dim count As Long
count = 0
For r = 1 To 8
For c = 1 To 8
' CBoard.GetStoneAt(r, c) は、(r, c)の石の色を返す既存関数と仮定 (1:黒, -1:白, 0:空)
If CBoard.GetStoneAt(r, c) = Player Then
count = count + 1
End If
Next c
End For
CountStones = count
End Function
'---------------------------------------------------------------------------------------------------
' ゲーム終了条件をチェックし、終了していれば勝敗を判定・表示するプロシージャ
'---------------------------------------------------------------------------------------------------
Sub CheckGameOver(ByVal IsContinuousPass As Boolean)
Dim blackStones As Long
Dim whiteStones As Long
Dim emptyCells As Long
Dim resultMsg As String
' 空のマス数をカウント
emptyCells = CBoard.CountEmptyCells ' CBoardクラスに空きマスを数えるメソッドがあると仮定
' 終了条件1: 連続パス
If IsContinuousPass Then
GoTo DetermineWinner
End If
' 終了条件2: 盤面が全て埋まった場合
If emptyCells = 0 Then
GoTo DetermineWinner
End If
' 終了条件3: どちらかの石がなくなった場合
blackStones = CountStones(1)
whiteStones = CountStones(-1)
If blackStones = 0 Or whiteStones = 0 Then
GoTo DetermineWinner
End If
Exit Sub ' ゲームはまだ終了していない
DetermineWinner:
' 最終的な石の数をカウント
blackStones = CountStones(1)
whiteStones = CountStones(-1)
If blackStones > whiteStones Then
resultMsg = "ゲーム終了!黒の勝利です! (" & blackStones & "対" & whiteStones & ")"
ElseIf whiteStones > blackStones Then
resultMsg = "ゲーム終了!白の勝利です! (" & whiteStones & "対" & blackStones & ")"
Else
resultMsg = "ゲーム終了!引き分けです! (" & blackStones & "対" & whiteStones & ")"
End If
MsgBox resultMsg, vbInformation, "ゲーム結果"
' ゲーム終了後の処理(例: 盤面をリセットするボタンの表示など)
' Application.EnableEvents = False ' 必要に応じてイベントを一時停止
' Call ResetGame ' ゲームを初期状態に戻すプロシージャを呼び出す
' Application.EnableEvents = True
End Sub
**補足**: 上記コードでは、`CBoard`クラスのメソッド(`CanPlaceStone`, `GetStoneAt`, `CountEmptyCells`)を呼び出している箇所があります。これらは、以前の連載で作成したか、または今後作成するクラスメソッドとして実装されていることを前提としています。`CurrentPlayer`や`LastTurnWasPass`も、クラスのプロパティとして管理することが望ましいです。
実務アドバイス
ゲーム開発における手番管理と勝敗判定は、単なるロジックの実装に留まらず、実務におけるシステム設計の原則が凝縮されています。
1. **状態管理の一元化と明確化**:
ゲームの状態(手番、盤面、パス状況など)は、一元的に管理し、その状態が何を意味するのかを明確に定義することが重要です。グローバル変数の乱用は避けるべきですが、小規模なプロジェクトであれば許容範囲です。しかし、規模が大きくなるにつれて、クラスモジュールを使って関連する状態と振る舞いをカプセル化する「オブジェクト指向」の考え方が不可欠になります。これにより、コードの保守性、拡張性、再利用性が飛躍的に向上します。
2. **イベント駆動型プログラミングへの適応**:
Excel VBAはイベント駆動型のプログラミング環境です。ユーザーがセルをクリックしたり、ボタンを押したりする「イベント」に応じて、適切なロジックが起動するように設計します。今回の手番管理や勝敗判定も、石が置かれるというイベントの後に連鎖的に発生する処理として適切に配置する必要があります。
3. **網羅的な条件分岐とエラーハンドリング**:
ゲームの終了条件は複数存在し、それぞれが特定の状況下でしか発生しません。これらの条件を漏れなく、かつ重複なくチェックするロジックを組むことが求められます。特に、「パス」のような特殊なケースは、無限ループや予期せぬ挙動を引き起こしやすいため、デバッグに時間をかける必要があります。起こりうる全てのシナリオを想定し、適切なメッセージ表示や状態遷移を設計することが堅牢なシステム構築に繋がります。
4. **ユーザーインターフェース (UI) とロジックの分離**:
ゲームロジック(手番管理、勝敗判定など)と、その結果をユーザーに表示するUIロジック(`MsgBox`の表示、セルの色変更など)は、できる限り分離して記述すべきです。これにより、ロジック部分のテストが容易になり、UIを変更する際にもロジックに影響を与えずに済むため、開発効率が向上します。
5. **テストとデバッグの徹底**:
ゲームのロジックは複雑になりがちです。特に、手番管理や勝敗判定のような核心部分は、様々な局面で正確に動作するかを徹底的にテストする必要があります。
* 初期状態からゲーム終了までの一連の流れ。
* パスが発生する局面(片方パス、連続パス)。
* 片方の石が0になる局面。
* 盤面が完全に埋まる局面。
* 引き分け
