前回のオセロをつくる コンピューターをもっと強くするでは、過去に作成したオセロゲームを強くする方法を考えました。相手に角を取られるようなところに置かないようにすることで少しは強くなりました。
今回はもうちょっと改良を加えます。
オセロをつくる コンピューターをもっと強くするのアルゴリズムはコンピュータが発見した複数の候補手のなかから「次のターン」でプレイヤーに角を取られる手は一旦候補から排除したのですが、もっと先を読ませます。「次のターン」だけでなく「さらに次のターン」でプレイヤーに角を取られる場合がないかも考えます。もっと数手先も読ませてみたいんですよね。
それからコンピュータの手番で角を取れるときは角をとります。とはいえ実際にはオセロは角さえとってしまえば勝てるという単純なゲームではありません。「オセロはルールを覚えるのにかかる時間は1分、極めるまでにかかる時間は一生」といわれています。単純なルールであるがゆえに奥が深いゲームなのです。
でははじめてみましょう。基本的にオセロをつくる コンピューターをもっと強くするにかかれているコードを手直してつくります。
そして手直しする部分といえばコンピュータが次の一手を考える部分、EnemyThink()メソッドです。長くなってしまったので機能ごとに分割しました。
やっていることは「もし角を取れるのであれば角をとる」。そして前回同様にコンピュータは角を取られない場所を探して着手するのですが、さらにもう一手余分に読むことにしました。
ある本に関数は50行内にまとめよ、100行を超えたら恥ずかしいと思え、と書いていたのですが、あれこれ付け加えて100行を超えてしまい、以下のコードも50行をちょっと超えてしまいました(説明のためのコメントを消せば50行以内におさまっているからいいかな)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public partial class Form1 : Form { async void EnemyThink() { await Task.Delay(1000); bool isComPassed = false; // コンピュータの次の一手 StonePosition nextEnemyHand = null; // コンピュータは角をとれるときは角をとる StonePosition nextHand = GetCanDepriveCornerMax(StonePositions, EnemyColor); if (nextHand != null) nextEnemyHand = nextHand; if (nextEnemyHand == null) { // 石が置かれていない場所で挟むことができる場所を探す。 List<StonePosition> enemyHands = GetRevarsePlace(StonePositions, EnemyColor); // コンピュータは角を取られない場所を探す List<NextCandidate> nextCandidates = SearchEnemyHandNotDepriveCorner(enemyHands); if (nextCandidates.Count > 0) { // 敵はできるだけプレイヤーの次の手が少なくなるような手を選ぶ int min = nextCandidates.Min(x => x.HandsCount); nextEnemyHand = nextCandidates.First(x => x.HandsCount == min).StonePosition; } else { // 候補手がない場合は、角を奪われても仕方がないので適当に選ぶしかない int count = enemyHands.Count; if (count > 0) { Random random = new Random(); int r = random.Next(count); nextEnemyHand = enemyHands[r]; } else { // 次の手がまったく存在しない場合はパス isComPassed = true; } } } // コンピュータの次の手が決まったら着手する if (nextEnemyHand != null) { SetCellColor(nextEnemyHand.PositionX, nextEnemyHand.PositionY, EnemyColor); List<StonePosition> reversedPositions2 = GetRevarseStones(StonePositions, nextEnemyHand.PositionX, nextEnemyHand.PositionY, EnemyColor); foreach (StonePosition pos in reversedPositions2) SetCellColor(pos.PositionX, pos.PositionY, EnemyColor); } // プレイヤーの手番になったとき、次の手は存在するのか? ChangePlayerTurn(isComPassed); } } |
コンピュータに角をとれるときは角をとらせるための処理を示します。四隅に置いてみてひっくり返せる石があるかどうかを調べています。そして複数の角を取れるときはひっくり返せる石が多いほうを選んでいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public partial class Form1 : Form { StonePosition GetCanDepriveCornerMax(List<StonePosition> stonePositions, StoneColor color) { Dictionary<StonePosition, int> keyValuePairs = new Dictionary<StonePosition, int>(); StonePosition posLeftTop = stonePositions.First(x => x.PositionX == 0 && x.PositionY == 0); if (posLeftTop.Color == StoneColor.None) { int reverse = GetRevarseStones(stonePositions, posLeftTop.PositionX, posLeftTop.PositionY, color).Count; if (reverse > 0) keyValuePairs.Add(posLeftTop, reverse); } StonePosition posLeftBottom = stonePositions.First(x => x.PositionX == 0 && x.PositionY == RowMax -1); if (posLeftBottom.Color == StoneColor.None) { int reverse = GetRevarseStones(stonePositions, posLeftBottom.PositionX, posLeftBottom.PositionY, color).Count; if (reverse > 0) keyValuePairs.Add(posLeftBottom, reverse); } StonePosition posRightTop = stonePositions.First(x => x.PositionX == ColumMax - 1 && x.PositionY == 0); if (posRightTop.Color == StoneColor.None) { int reverse = GetRevarseStones(stonePositions, posRightTop.PositionX, posRightTop.PositionY, color).Count; if (reverse > 0) keyValuePairs.Add(posRightTop, reverse); } StonePosition posRightBottom = stonePositions.First(x => x.PositionX == ColumMax - 1 && x.PositionY == RowMax - 1); if (posRightBottom.Color == StoneColor.None) { int reverse = GetRevarseStones(stonePositions, posRightBottom.PositionX, posRightBottom.PositionY, color).Count; if (reverse > 0) keyValuePairs.Add(posRightBottom, reverse); } if (keyValuePairs.Count > 0) { int max = keyValuePairs.Max(x => x.Value); return keyValuePairs.First(x => x.Value == max).Key; } else return null; } } |
自分の手番なら角を取れるかだけなら以下のように短く書けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class Form1 : Form { // 自分の手番なら角を奪えるか? bool CanDepriveCorner(List<StonePosition> stonePositions, StoneColor color) { List<StonePosition> hands = GetRevarsePlace(stonePositions, color); return hands.Any(x => (x.PositionX == 0 && x.PositionY == 0) || (x.PositionX == 0 && x.PositionY == RowMax - 1) || (x.PositionX == ColumMax - 1 && x.PositionY == 0) || (x.PositionX == ColumMax - 1 && x.PositionY == RowMax - 1) ); } } |
試しに石を置くときの処理です。何回も使うのでメソッドとして独立させました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { // ためしに石を置いてみる List<StonePosition> PutStoneTentative(List<StonePosition> sourceStonePositions, int posX, int posY, StoneColor color) { // StonePositionsのコピーをつくる List<StonePosition> copiedPositions = new List<StonePosition>(); foreach (StonePosition pos in sourceStonePositions) copiedPositions.Add(new StonePosition(pos.PositionX, pos.PositionY, pos.Color)); // 石を置いてみる var enemyPos = copiedPositions.First(x => x.PositionX == posX && x.PositionY == posY); enemyPos.Color = color; // ひっくり返せる石を取得して色を変える List<StonePosition> reversedPositions = GetRevarseStones(copiedPositions, posX, posY, color); foreach (StonePosition pos in reversedPositions) pos.Color = color; return copiedPositions; } } |
これが本日の主役である角を取らせないように石を置く場所を調べるメソッドです。コンピュータが着手したあとだけでなく、そのあとプレイヤーがどこかに着手した場合、どうやってもコンピュータが角を取られてしまう場合を事前に見つけ出して候補手からはずしています。
さらにもうひとつ深く読みを入れたいのですが、組み合わせ爆発がおきるからなのか極端に処理が遅くなってしまうので、ここで読みを打ち切っています。
読みの結果、どうやっても角を取られる場合は適当に選びます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
public partial class Form1 : Form { List<NextCandidate> SearchEnemyHandNotDepriveCorner(List<StonePosition> enemyHands) { List<NextCandidate> nextCandidates = new List<NextCandidate>(); foreach (StonePosition hand in enemyHands) { // StonePositionsのコピーをつくり、そこでためしに石を置いてみる List<StonePosition> copiedPositions = PutStoneTentative(StonePositions, hand.PositionX, hand.PositionY, EnemyColor); // このときプレイヤーにはどのような応手があるか? List<StonePosition> yourHands = GetRevarsePlace(copiedPositions, YourColor); // もしプレイヤーに角を奪われてしまうのであればその手は候補からいったん外す if (CanDepriveCorner(copiedPositions, YourColor)) continue; // 最終的にコンピュータが選択する手のひとつ StonePosition retEnemyHand = null; //プレイヤーの応手に角を奪う手がない場合、それぞれどのような進行が想定されるか? foreach (var yourNextHand in yourHands) { List<StonePosition> copiedPositions2 = PutStoneTentative(copiedPositions, yourNextHand.PositionX, yourNextHand.PositionY, YourColor); // その後、コンピュータの着手を考える // これで敵味方1手ずつ打ち合った盤面(copiedPositions2)と // 敵の次の次の手リスト(enemyHand2s)が取得できたことになる List<StonePosition> enemyHand2s = GetRevarsePlace(copiedPositions2, EnemyColor); foreach (StonePosition enemyHand2 in enemyHand2s) { List<StonePosition> copiedPositions3 = PutStoneTentative(copiedPositions2, enemyHand2.PositionX, enemyHand2.PositionY, EnemyColor); // このときプレイヤーは角を奪えるか? if (CanDepriveCorner(copiedPositions3, YourColor)) { // 奪えるならenemyHand2は適切な手ではない continue; } else { // プレイヤーが奪えないのであればenemyHand2は不適切な候補ではないといえる // ループを抜け、handを候補手に追加する retEnemyHand = enemyHand2; break; } } } if (retEnemyHand != null) nextCandidates.Add(new NextCandidate(hand, yourHands.Count)); } return nextCandidates; } } |
最後にコンピュータが着手したあとプレイヤーの手番になるのですが、ほんとうにプレイヤーの次の手は存在するのかと双方がパスをしてゲーム終了になるときの判定をする処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public partial class Form1 : Form { // プレイヤーの手番に変更する。 // しかしプレイヤーの次の手は存在するのか? 存在しない場合はコンピュータの手番に。 // コンピュータもパスしたら終了。 void ChangePlayerTurn(bool isComPassed) { List<StonePosition> yourNextHands = GetRevarsePlace(StonePositions, YourColor); if (yourNextHands.Count > 0) { isYour = true; if (!isComPassed) toolStripStatusLabel1.Text = "あなたの手番です。"; else toolStripStatusLabel1.Text = "コンピュータはパスしました。あなたの手番です。"; } else { if (!isComPassed) { toolStripStatusLabel1.Text = "あなたの手番ですがパスするしかありません。"; EnemyThink(); } else End(); } } } |
実際にやってみると最初より強くなり負かされることも多くなりました。私が下手なだけかもしれませんが・・・・