C#で三目並べをつくります。
三目並べとは3×3 の格子を用意し、二人が交互に「○」と「×」を書き込んでいき3つ並べるゲームである。まるばつ、まるぺけとも呼ばれる。
まずデザイナで以下のようなものをつくります。
Cellクラスの定義
次にセルを描画するためのCellクラスを定義します。
ここでは四角いマスをどの位置に描画するのか、そこに描画されるのは○なのか×なのか、マス以外なにも描画されないのかというデータの保持、実際の描画の処理などをおこないます。
CellStatus列挙体はセルに描画されるものが○か×か何も無いかを指定するためのものです。
1 2 3 4 5 6 |
public enum CellStatus { None = 0, Maru = 1, Batten = 2, } |
Cellクラスのコンストラクタを示します。
引数はそのセルが上から何行目か、左から何列目か、輪郭部分と○×を描画する矩形です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Cell { public int Row = 0; public int Col = 0; public CellStatus Status = CellStatus.None; Rectangle _outerRectangle = new Rectangle(); Rectangle _innerRectangle = new Rectangle(); public Cell(int row, int col, Rectangle outer, Rectangle inner) { Row = row; Col = col; _outerRectangle = outer; _innerRectangle = inner; } } |
プレーヤーの手番のときにクリックされたらそのセルに○または×をセットしたいので、クリックされた座標がセルの内部かどうかを返すメソッドを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Cell { public bool IsInside(Point point) { if (_outerRectangle.Left > point.X) return false; if (_outerRectangle.Right < point.X) return false; if (_outerRectangle.Top > point.Y) return false; if (_outerRectangle.Bottom < point.Y) return false; return true; } } |
○または×を描画する処理を示します。
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 |
public class Cell { Pen _cellPen = new Pen(new SolidBrush(Color.Black), 1); public void DrawOuter(Graphics graphics) { graphics.DrawRectangle(_cellPen, _outerRectangle); } Pen _circlePen = new Pen(new SolidBrush(Color.Red), 4); public void DrawCircle(Graphics graphics) { graphics.DrawEllipse(_circlePen, _innerRectangle); } Pen _battenPen = new Pen(new SolidBrush(Color.Blue), 5); public void DrawBatten(Graphics graphics) { graphics.DrawLine( _battenPen, _innerRectangle.Left, _innerRectangle.Top, _innerRectangle.Right, _innerRectangle.Bottom ); graphics.DrawLine( _battenPen, _innerRectangle.Right, _innerRectangle.Top, _innerRectangle.Left, _innerRectangle.Bottom ); } } |
Bottomプロパティは、セルの輪郭の下部分のY座標を取得するためのものです。
1 2 3 4 |
public class Cell { public int Bottom { get { return _outerRectangle.Bottom; } } } |
Form1クラスの定義
次にForm1クラスを定義します。コンストラクタと定数は以下のとおりです。
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 |
public partial class Form1 : Form { const int RowMax = 3; // 3目並べなので ともに3 const int ColMax = 3; const int CellSize = 60; // セルの大きさ const int CellGap = 10; // セル同士の間隔 const int FirstCellX = 10; // 左上の表示されるセルのX座標とY座標 const int FirstCellY = 10; const int CircleGap = 8; // セルの輪郭と○が描画される矩形部分との間隔 public Form1() { InitializeComponent(); this.DoubleBuffered = true; InitCells(); // セルを初期化する(後述) // ゲーム再挑戦のボタンは最初は無効にする // アプリが起動した段階でゲームは開始されている button1.Enabled = false; // どちらが先番かを設定するチェックボックスにチェックをいれる // ゲーム中に設定が変わらないように無効にする checkBox1.Checked = true; checkBox1.Enabled = 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 |
public partial class Form1 : Form { bool isPlayerFirst = true; List<Cell> cells = new List<Cell>(); void InitCells() { cells.Clear(); for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) { // Cellクラスのコンストラクタに渡す引数を取得する // 輪郭部分の矩形 Rectangle outer = new Rectangle( FirstCellX + col * (CellSize + CellGap), FirstCellY + row * (CellSize + CellGap), CellSize, CellSize ); // ○×が描画される部分の矩形 Rectangle inner = new Rectangle( FirstCellX + col * (CellSize + CellGap) + CircleGap, FirstCellY + row * (CellSize + CellGap) + CircleGap, CellSize - 2 * CircleGap, CellSize - 2 * CircleGap ); cells.Add(new Cell(row, col, outer, inner)); } } } } |
描画に関する処理
描画に関する処理を示します。セルを描画する処理とゲームの結果を表示する処理をおこなっているだけです。
1 2 3 4 5 6 7 8 9 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { DrawCells(e.Graphics); // 9つのセルを描画 DrawResultOfGame(e.Graphics); // ゲームの結果(勝敗)を描画 base.OnPaint(e); } } |
セルを描画する処理を示します。
セルの輪郭部分を描画したあと○か×が設定されている場合はそれも描画しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class Form1 : Form { void DrawCells(Graphics graphics) { foreach (Cell cell in cells) { cell.DrawOuter(graphics); if(cell.Status == CellStatus.Maru) cell.DrawCircle(graphics); if (cell.Status == CellStatus.Batten) cell.DrawBatten(graphics); } } } |
ゲームの結果を描画する処理を示します。
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 |
public enum ResultOfGame { None = 0, // 決着はついていない PlayerWin = 1, // プレーヤー側の勝ち CpuWin = 2, // CPU側の勝ち Draw = 3, // 引き分け } public partial class Form1 : Form { ResultOfGame resultOfGame = ResultOfGame.None; Font resultFont = new Font("MS ゴシック", 16, FontStyle.Bold); SolidBrush resultBrush = new SolidBrush(Color.Black); void DrawResultOfGame(Graphics graphics) { // 勝敗を描画する位置は一番下のセルの下側よりも少し下がった位置 int x = FirstCellX; int y = cells.Max(_ => _.Bottom) + 16; if(resultOfGame == ResultOfGame.PlayerWin) graphics.DrawString("あなたの勝ちです", resultFont, resultBrush, new Point(x, y)); if (resultOfGame == ResultOfGame.CpuWin) graphics.DrawString("あなたの負けです", resultFont, resultBrush, new Point(x, y)); if (resultOfGame == ResultOfGame.Draw) graphics.DrawString("引き分けです", resultFont, resultBrush, new Point(x, y)); } } |
クリック時の処理
自分の手番のときになにも書かれていないセルをクリックしたら○または×を描画します。
クリック時の処理を示します。クリックされたとき自分の手番で無かった場合やゲームが終了している場合はなにもしません。
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 |
public partial class Form1 : Form { bool ignoreClick = false; protected override async void OnClick(EventArgs e) { if(ignoreClick) return; // どこがクリックされたか? Point point = this.PointToClient(MousePosition); // クリックされた場所が着手可能なセル内でない場合は処理は終了 bool isClicked = CheckCellClicked(point); if (!isClicked) return; // プレーヤーの着手によりプレーヤーの勝利が確定したかもしれない ignoreClick = true; bool isWin = CheckVictory(); if (isWin) { // プレーヤー勝利の場合はそのような描画処理をおこなう resultOfGame = ResultOfGame.PlayerWin; OnGameEnd(); return; } // プレーヤーの着手後、すぐにCPU側が着手すると違和感があるので1秒待つ await Task.Delay(1000); ThinkCpu(); // CPUの着手によりプレーヤーの敗北が確定したかもしれない bool isLost = CheckDefeat(); if (isLost) { // そのような描画処理をおこなう resultOfGame = ResultOfGame.CpuWin; OnGameEnd(); return; } // 着手によりすべてのセルが埋まり引き分けが確定したかもしれない if (!cells.Any(_ => _.Status == 0)) { // そのような描画処理をおこなう resultOfGame = ResultOfGame.Draw; OnGameEnd(); return; } // プレーヤーとCPUが相互に着手してゲーム終了になっていない場合はプレーヤーが着手可能の状態に戻す ignoreClick = false; base.OnClick(e); } } |
プレーヤーがクリックした座標は、着手可能なセルなのかどうかをチェックする処理を示します。クリックされた座標をCell.IsInside(point)メソッドに渡すと最大でひとつだけtrueを返すセルがあるはずです。着手可能なセルとはいうまでもなく○も×も書かれていないセルのことです。着手がおこなわれた場合はtrue、そうでない場合はfalseを返します。trueを返した場合は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 CheckCellClicked(Point point) { foreach (Cell cell in cells) { if (cell.IsInside(point)) { // 着手不能のセルがクリックされた場合 if(cell.Status != CellStatus.None) return false; // 着手可能なセルがクリックされた場合 if (isPlayerFirst) cell.Status = CellStatus.Maru; else cell.Status = CellStatus.Batten; Invalidate(); return true; } } // クリックされた場所はセルではない return false; } } |
勝敗判定
プレーヤーが勝利したかをチェックする処理を示します。
○または×がセットされているセルを集めます。それらのなかに縦横ななめのどれかで3つ並んでいるものがあるか調べます。同じ行、同じ列のものを探して3つみつかればそろっていることになります。ななめの場合は真ん中と対角の関係にある2つの角がそろっているかを調べています。
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 |
public partial class Form1 : Form { bool CheckVictory() { if (isPlayerFirst) return CheckLines(CellStatus.Maru); else return CheckLines(CellStatus.Batten); } // 縦横ななめのいずれかで3つ並んでいるかどうかを調べる bool CheckLines(CellStatus status) { // ○または×がセットされているセルを集める var list = cells.Where(_ => _.Status == status).ToList(); // 横に3つ並んでいるか? for (int i = 0; i < RowMax; i++) { if (list.Count(_ => _.Row == i) == 3) return true; } // 縦に3つ並んでいるか? for (int i = 0; i < ColMax; i++) { if (list.Count(_ => _.Col == i) == 3) return true; } // ななめに3つ並んでいるか? // ななめに3つ並ぶためには真ん中は絶対に必要 if (!list.Any(_ => _.Row == 1 && _.Col == 1)) return false; if ( list.Any(_ => _.Row == 0 && _.Col == 0) && list.Any(_ => _.Row == 2 && _.Col == 2)) return true; if ( list.Any(_ => _.Row == 0 && _.Col == 2) && list.Any(_ => _.Row == 2 && _.Col == 0)) return true; return false; } } |
CPUの着手
CPUに着手させる処理を示します。ここでは着手されていないセルを集めて乱数で着手する場所を決めています。だからメチャクチャ弱いです。この部分は次回改善します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class Form1 : Form { Random random = new Random(); void ThinkCpu() { // 終局しているかもしれない List<Cell> list = cells.Where(_ => _.Status == 0).ToList(); if (list.Count == 0) return; // 空いている場所に適当に着手するだけ int r = random.Next(list.Count); if (isPlayerFirst) list[r].Status = CellStatus.Batten; else list[r].Status = CellStatus.Maru; Invalidate(); } } |
CPUが着手したらプレーヤーの敗北が確定しているかもしれないのでチェックします。
1 2 3 4 5 6 7 8 9 10 |
public partial class Form1 : Form { bool CheckDefeat() { if (isPlayerFirst) return CheckLines(CellStatus.Batten); else return CheckLines(CellStatus.Maru); } } |
終局した場合は結果を表示するために更新処理が必要なのでその処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form1 : Form { void OnGameEnd() { // [再挑戦]のボタンをクリックできるようにする // 先手後手設定用のチェックボックスを操作できるようにする button1.Enabled = true; checkBox1.Enabled = true; Invalidate(); } } |
再挑戦のための処理
[再挑戦]のボタンがクリックされたら新しいゲームを開始します。
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 { private async void button1_Click(object sender, EventArgs e) { // セルの○×をクリア foreach (Cell cell in cells) cell.Status = CellStatus.None; // 勝敗はついていない状態に resultOfGame = ResultOfGame.None; // ゲーム開始、先手後手設定用のチェックボックスを無効に button1.Enabled = false; checkBox1.Enabled = false; // セルの○×をクリアしたことが反映されるように更新処理 Invalidate(); // プレーヤーが後手の場合は先にCPUが着手する if(!isPlayerFirst) { await Task.Delay(1000); ThinkCpu(); } // 着手の処理ができるようにクリックイベントを処理できるようにする ignoreClick = false; } } |
プレーヤーの先手後手を設定するチェックボックスの状態が変更されたときの処理を示します。プレーヤーを先番であればisPlayerFirstフラグをtrueに、そうでない場合はfalseに設定します。
1 2 3 4 5 6 7 |
public partial class Form1 : Form { private void checkBox1_CheckedChanged(object sender, EventArgs e) { isPlayerFirst = checkBox1.Checked ? true : false; } } |