C#で三目並べをつくったのですが、CPU側は次の着手をランダムに選択していただけなので弱すぎました。そこでAIを強くすることを考えます。
リーチがかかっているとき
まず3目並ぶときは勝ちを決め、プレーヤーが3目並びそうなときはこれを阻止します。
そのために縦横ななめに3つそろうCellの組み合わせ8とおりを取得するメソッドを定義します。
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 |
public partial class Form1 : Form { Cell[][] Get8Lines() { Cell[][] ret = new Cell[8][]; for (int i = 0; i < RowMax; i++) ret[i] = cells.Where(_ => _.Row == i).ToArray(); // 横に並ぶ場合 for (int i = 0; i < ColMax; i++) ret[i + 3] = cells.Where(_ => _.Col == i).ToArray(); // 縦に並ぶ場合 // 斜めに並ぶ場合 Cell[] arr = new Cell[3]; arr[0] = cells.First(_ => _.Row == 0 && _.Col == 0); arr[1] = cells.First(_ => _.Row == 1 && _.Col == 1); arr[2] = cells.First(_ => _.Row == 2 && _.Col == 2); ret[6] = arr; arr = new Cell[3]; arr[0] = cells.First(_ => _.Row == 0 && _.Col == 2); arr[1] = cells.First(_ => _.Row == 1 && _.Col == 1); arr[2] = cells.First(_ => _.Row == 2 && _.Col == 0); ret[7] = arr; return ret; } } |
角の位置にあるCellもよく使うのでこれを取得するメソッドを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { Cell[] GetCornerCells() { Cell[] ret = new Cell[4]; ret[0] = cells.First(_ => _.Row == 0 && _.Col == 0); ret[1] = cells.First(_ => _.Row == 2 && _.Col == 2); ret[2] = cells.First(_ => _.Row == 0 && _.Col == 2); ret[3] = cells.First(_ => _.Row == 2 && _.Col == 0); return ret; } } |
コンストラクタ内で上記メソッドを実行して、結果をフィールド変数に格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class Form1 : Form { Cell[][] eightLines = null; Cell[] cornerCells = null; public Form1() { InitializeComponent(); this.DoubleBuffered = true; InitCells(); button1.Enabled = false; checkBox1.Checked = true; checkBox1.Enabled = false; // 追加された部分 eightLines = Get8Lines(); cornerCells = GetCornerCells(); } } |
すでにリーチがかかっているかもしれないので、これをチェックするメソッドを定義します。
CPUの手番のときにCPU側がリーチの場合は勝負を決め、プレーヤー側がリーチのときはこれを阻止します。
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 |
public partial class Form1 : Form { bool CheckReach() { CellStatus own = isPlayerFirst ? CellStatus.Batten : CellStatus.Maru; CellStatus enemy = isPlayerFirst ? CellStatus.Maru : CellStatus.Batten; List<Cell> reachs = GetReach(own); if (reachs.Count > 0) { reachs[random.Next(reachs.Count)].Status = own; return true; } reachs = GetReach(enemy); if (reachs.Count > 0) { reachs[random.Next(reachs.Count)].Status = own; return true; } return false; } // リーチの場合、欠けているものを返す List<Cell> GetReach(CellStatus status) { List<Cell> ret = new List<Cell>(); foreach (Cell[] line in eightLines) { if (line.Count(_ => _.Status == status) != 2) continue; if (line.Count(_ => _.Status == CellStatus.None) != 1) continue; ret.Add(line.First(_ => _.Status == CellStatus.None)); } return ret; } } |
ThinkCpuメソッドを以下のように変更します。
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 |
public partial class Form1 : Form { Random random = new Random(); void ThinkCpu() { // 空白のセルを取得する List<Cell> nextList = cells.Where(_ => _.Status == CellStatus.None).ToList(); if (nextList.Count == 0) return; CellStatus own = isPlayerFirst ? CellStatus.Batten : CellStatus.Maru; CellStatus enemy = isPlayerFirst ? CellStatus.Maru : CellStatus.Batten; // リーチがかかっているときは勝負を決める、または阻止する if (CheckReach()) { Invalidate(); return; } // それ以外の場合は次の一手をランダムに決める if (nextList.Count > 0) nextList[random.Next(nextList.Count)].Status = own; Invalidate(); } } |
複数のリーチをかける手段を探す
もしCPU側から複数のリーチをかける手段が存在するならその手を採用するとともに、それ以外のときはプレーヤーから複数のリーチをかける手段が存在しないような着手を選ぶようにします。
もしCPU側から複数のリーチをかける手段が存在する場合、これを実行する処理を示します。
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 |
public partial class Form1 : Form { bool SetDoubleReach() { CellStatus own = isPlayerFirst ? CellStatus.Batten : CellStatus.Maru; List<Cell> nextList = cells.Where(_ => _.Status == CellStatus.None).ToList(); if (nextList.Count == 0) return false; List<Cell> reachs = new List<Cell>(); foreach (Cell cell in nextList) { cell.Status = own; if (GetReach(own).Count > 1) reachs.Add(cell); cell.Status = CellStatus.None; } if (reachs.Count > 0) { reachs[random.Next(reachs.Count)].Status = own; return true; } else return false; } } |
プレーヤー側から複数のリーチをかける手段が存在する場合、これを防ぐ処理を示します。
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 { bool BlockDoubleReach() { List<Cell> nextList = cells.Where(_ => _.Status == CellStatus.None).ToList(); if (nextList.Count == 0) return false; CellStatus own = isPlayerFirst ? CellStatus.Batten : CellStatus.Maru; CellStatus enemy = isPlayerFirst ? CellStatus.Maru : CellStatus.Batten; // プレーヤーのダブルリーチが可能なら阻止する List<Cell> reachs = new List<Cell>(); foreach (Cell cell in nextList) { cell.Status = enemy; if (GetReach(enemy).Count > 1) reachs.Add(cell); cell.Status = CellStatus.None; } if (reachs.Count == 0) return false; // 単純にその場所に先行すれば阻止できるわけではないので総当たりで調べる List<Cell> ret = new List<Cell>(); nextList = cells.Where(_ => _.Status == CellStatus.None).ToList(); foreach (Cell cell in nextList) { // 試しに置いてみる cell.Status = own; // ダブルリーチは阻止できているか? List<Cell> nextList2 = cells.Where(_ => _.Status == CellStatus.None).ToList(); List<Cell> reachs2 = new List<Cell>(); foreach (Cell cell2 in nextList2) { // プレーヤー側として試しに置いてみる // ダブルリーチを阻止できず、次にCPU側が3目並べられる状態でないなら阻止できていない cell2.Status = enemy; if (GetReach(enemy).Count > 1) { List<Cell> reachs3 = GetReach(own); // この場合はダブルリーチを阻止できていない if (reachs3.Count == 0) reachs2.Add(cell2); } cell2.Status = CellStatus.None; } cell.Status = CellStatus.None; // ダブルリーチを阻止できる手段をリストに格納する if (reachs2.Count == 0) ret.Add(cell); } ret[random.Next(ret.Count)].Status = own; return true; } } |
初手に対して正しく対応しないと負けてしまう場合があります。プレーヤーが先番の場合、初手が角の場合はCPU側は中央に、初手が中央の場合は角に着手しなければ負けてしまいます。それ以外のときもCPU側は中央に着手するのが無難です。
もしプレーヤー側が悪手を放った場合、勝ちにいけるようにします。
プレーヤー側が悪手を放った場合
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 |
public partial class Form1 : Form { bool GetWin() { if (cells.Count(_ => _.Status == CellStatus.Maru) != 1) return false; if(cells.Count(_ => _.Status == CellStatus.Batten) != 1) return false; Cell center = cells.First(_ => _.Row == 1 && _.Col == 1); List<Cell> notCornerCells = new List<Cell>() { cells.First(_ => _.Row == 0 && _.Col == 1), cells.First(_ => _.Row == 1 && _.Col == 0), cells.First(_ => _.Row == 1 && _.Col == 2), cells.First(_ => _.Row == 2 && _.Col == 1), }; Cell cell = notCornerCells.FirstOrDefault(_ => _.Status == CellStatus.Batten); if (cell == null) return false; if (cell.Row == 0 || cell.Row == 2) { int row = cell.Row; if (center.Status == CellStatus.Maru) cornerCells.First(_ => _.Row == row).Status = CellStatus.Maru; else if (cornerCells.Where(_ => _.Row == row).Any(_ => _.Status == CellStatus.Maru)) center.Status = CellStatus.Maru; else return false; return true; } if (cell.Col == 0 || cell.Col == 2) { int col = cell.Col; if (center.Status == CellStatus.Maru) cornerCells.First(_ => _.Col == col).Status = CellStatus.Maru; else if (cornerCells.Where(_ => _.Col == col).Any(_ => _.Status == CellStatus.Maru)) center.Status = CellStatus.Maru; else return false; return true; } return false; } } |
負けない選択をする
まとめると以下のようにすると負けることはなくなりそうです。
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 62 63 64 65 |
public partial class Form1 : Form { void ThinkCpu() { List<Cell> nextList = cells.Where(_ => _.Status == CellStatus.None).ToList(); if (nextList.Count == 0) return; CellStatus own = isPlayerFirst ? CellStatus.Batten : CellStatus.Maru; CellStatus enemy = isPlayerFirst ? CellStatus.Maru : CellStatus.Batten; // リーチがかかっているときは勝負を決める if (CheckReach()) { Invalidate(); return; } // ダブルリーチが可能ならかける if (SetDoubleReach()) { Invalidate(); return; } // プレーヤーのダブルリーチが可能なら阻止する if (BlockDoubleReach()) { Invalidate(); return; } // プレーヤーの着手が悪手だったときは勝ちに行く if (GetWin()) { Invalidate(); return; } // 後攻で中央が空いているときは中央に着手 Cell center = cells.First(_ => _.Row == 1 && _.Col == 1); if (isPlayerFirst && center.Status == CellStatus.None) { center.Status = own; Invalidate(); return; } // プレーヤーが中央に着手したときは角に着手 if (center.Status == enemy) { Cell[] arr = cornerCells.Where(_ => _.Status == CellStatus.None).ToArray(); if(arr.Length > 0) { arr[random.Next(arr.Length)].Status = own; Invalidate(); return; } } if (nextList.Count > 0) nextList[random.Next(nextList.Count)].Status = own; Invalidate(); } } |