WPFでオセロ作ってみました

#概要
・オセロの基本ルールは省略。
・プレイヤー同士の対戦は不可。
・プレイヤーは先手番として、白を使う。
・プレイヤーが駒を置くと、自動で相手が駒を置く。
・対戦AIは適当（左上から順番に見て、置けるところに置くだけ）
・置ける場所がわかるようなガイドを入れる。

##開発環境
・メインの開発環境はVisualStudioCode
・言語はwpf(C#,Xaml)

##詳細
**ソース一覧**

| ファイル | 概要 | 
|:--------------------|:------------|
| Constants.cs        | 定数定義をまとめたクラス(いらなかった)          |
| MainWindow.xaml     | メインの画面クラス       |
| MainWindow.xaml.cs  | メインの画面クラスの処理     |
| osero-illust1.png   | オセロの駒(黒)画像|
| osero-illust2.png   | オセロの駒(黒)画像|

**メソッド一覧**
**MainWindow.xaml.cs** 

| メソッド | 概要 | 
|:--------------------|:------------|
| MainWindow()        | コンストラクタ 色々初期化する|
| drawBoard()         | 盤面描画処理 盤面状態の配列を元にGridに駒を配置していく|
| BoardSet()          | 行、列を指定して駒を置く。また、勝利判定、手番入れ替えも行う|
| DoEvents()          | 画面の再描画処理用１※|
| ExitFrames()        | 画面の再描画処理用２※|
| Grid_MouseDown()    | Grid上でのクリック時処理、押された場所に応じて、BoardSet()を呼び出す|
| Check_OseroSet()    | オセロの配置可能か判定する。また、囲んだ場合にひっくり返す|
| ClearGrid()         | Gridの要素をすべて削除する|
| EnemyPlay()         | 相手プレイヤーの動作を行う|

※参考にした記事
https://www.ipentec.com/document/csharp-wpf-implement-application-doevents

```c#:Constants.cs
using System;
namespace OseroTest
{
        public class Constants
        {
                public const int OSERO_SIZE_HEIGHT = 40;
                public const int OSERO_SIZE_WIDTH = 40;
        }
}
```

```xml:MainWindow.xaml
<Window x:Class="OseroTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:OseroTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="600">
<StackPanel  x:Name="StackOut" Height="600" Width="600" HorizontalAlignment="Center">
        <Grid x:Name="GridMessage" Height="50" Width="500" >
                <Label x:Name="MessageLabel" HorizontalAlignment="center" FontSize="36"/>
        </Grid>
        <Grid x:Name="GridBoard" Height="400" Width="400"  Background="Green" MouseDown="Grid_MouseDown" >
        <Grid.ColumnDefinitions>
            <!-- * ：「全体幅」から「幅指定列が確保した幅の合計」を引いた
                      残りの幅を、"*" 指定がある列で 均等割りにします。-->
<!--第1列--><ColumnDefinition Width="*"/>
<!--第2列--><ColumnDefinition Width="*"/>
<!--第3列--><ColumnDefinition Width="*"/>
<!--第4列--><ColumnDefinition Width="*"/>
<!--第5列--><ColumnDefinition Width="*"/>
<!--第6列--><ColumnDefinition Width="*"/>
<!--第7列--><ColumnDefinition Width="*"/>
<!--第8列--><ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
<!--第1行--><RowDefinition Height="*"/>
<!--第2行--><RowDefinition Height="*"/>
<!--第3行--><RowDefinition Height="*"/>
<!--第4行--><RowDefinition Height="*"/>
<!--第5行--><RowDefinition Height="*"/>
<!--第6行--><RowDefinition Height="*"/>
<!--第7行--><RowDefinition Height="*"/>
<!--第8行--><RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        </Grid>
        <Grid x:Name="GridMessage2" Height="50" Width="500" >
                <Label x:Name="MessageLabel2" />
        </Grid>
</StackPanel>    
</Window>
```

```c#:MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections;
using System.Windows.Media.Animation;
using System.Threading;
using System.Windows.Threading;

namespace OseroTest
{
    
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {

        public int[,] arrayBoard = null;
        public int[,] beforeBoard = null;
        public int TurnStatus = 0;//手番ステータス
        public int whiteCount = 0;//白カウント
        public int BlackCount = 0;//黒カウント
        public int ableCount = 0;//置ける数カウント
        public MainWindow()
        {
            InitializeComponent();

            // メッセージ表示
            MessageLabel.Content = "白の手番です";
            TurnStatus = -1;//白
            MessageLabel.FontSize = 20;

            // 盤面状態の配列を初期化
            arrayBoard = new int[8, 8] {
                 { 0, 0 ,0 ,0 ,0 ,0 ,0 ,0},
                 { 0, 0 ,0 ,0 ,0 ,0 ,0 ,0},
                 { 0, 0 ,0 ,0 ,0 ,0 ,0 ,0},
                 { 0, 0 ,0 ,0 ,0 ,0 ,0 ,0},
                 { 0, 0 ,0 ,0 ,0 ,0 ,0 ,0},
                 { 0, 0 ,0 ,0 ,0 ,0 ,0 ,0},
                 { 0, 0 ,0 ,0 ,0 ,0 ,0 ,0},
                 { 0, 0 ,0 ,0 ,0 ,0 ,0 ,0},
                 };

            // 初期配置セット
            arrayBoard[3,3] = (int)OseroKind.Black ; 
            arrayBoard[3,4] = (int)OseroKind.White ; 
            arrayBoard[4,3] = (int)OseroKind.White ; 
            arrayBoard[4,4] = (int)OseroKind.Black ; 

            drawBoard();

        }

        /// <summary>
        /// 盤面描画
        /// </summary>
        /// <returns>なし</returns>    }
        private void drawBoard()
        {
            // Create a name scope for the page.
            NameScope.SetNameScope(this, new NameScope());
            ClearGrid();
            ableCount = 0;
            whiteCount = 0;
            BlackCount = 0;
            for(int row = 0 ; row < 8 ; row++){
                for(int col = 0 ; col < 8 ; col++){

                    // 枠線の設定
                    Border border = new Border();
                    border.BorderBrush = new SolidColorBrush(Colors.Black);
                    border.BorderThickness = new System.Windows.Thickness(1);
                    border.SetValue(Grid.RowProperty, row);
                    border.SetValue(Grid.ColumnProperty, col);
                    GridBoard.Children.Add(border);

                    int kind = arrayBoard[row,col];

                    String imgFileName = "";
                    // Gridの要素削除
                    GridBoard.Children.Remove(this);    
                    Boolean ableFlg = false;

                    // 駒配置
                    if(kind == (int)OseroKind.Black ){
                        // Console.WriteLine(arrayBoard[row,col] + ": Black");
                        imgFileName = "osero-illust1.png";
                        BlackCount ++;
                    }else if(kind == (int)OseroKind.White ){
                        // Console.WriteLine(arrayBoard[row,col] + ": White");
                        imgFileName = "osero-illust2.png";
                        whiteCount ++;
                    }else{
                        // 手番プレイヤーが配置可能かどうかを判定
                        if(Check_OseroSet(col,row,TurnStatus,0)){
                            ableFlg = true;
                            ableCount ++;
                            if(TurnStatus == (int)OseroKind.Black){
                                imgFileName = "osero-illust1.png";
                            }else if(TurnStatus == (int)OseroKind.White){
                                imgFileName = "osero-illust2.png";
                            }
                        }else{
                            continue;
                        }

                    }

                    Image img = new Image();
                    BitmapImage imgSrc = new BitmapImage();
                    imgSrc.BeginInit();
                    imgSrc.UriSource = new Uri(imgFileName,UriKind.Relative);
                    imgSrc.CacheOption = BitmapCacheOption.OnLoad;
                    imgSrc.EndInit();

                    img.Source = imgSrc;
                    img.Name="img"+ row + col;
                    img.Height=Constants.OSERO_SIZE_HEIGHT;
                    img.Width=Constants.OSERO_SIZE_HEIGHT;

                    img.SetValue(Grid.RowProperty, row);
                    img.SetValue(Grid.ColumnProperty, col);

                    this.RegisterName(img.Name, img);
                    if(ableFlg){
                        // 配置可能の場合は、透過した画像を設定
                        img.Opacity = 0.2;
                    }

                    DoubleAnimation myDoubleAnimation = new DoubleAnimation();
                    myDoubleAnimation.From = 0;
                    myDoubleAnimation.To = 200;
                    myDoubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(2));

                    Storyboard.SetTargetName(myDoubleAnimation, img.Name);
                    Storyboard.SetTargetProperty(myDoubleAnimation,
                    new PropertyPath(Rectangle.WidthProperty));

                    Storyboard myStoryboard = new Storyboard();
                    myStoryboard.Children.Add(myDoubleAnimation);

                    BeginStoryboard myBeginStoryboard = new BeginStoryboard();
                    myBeginStoryboard.Storyboard = myStoryboard;

                    EventTrigger myEnterTrigger = new EventTrigger();
                    myEnterTrigger.Actions.Add(myBeginStoryboard);

                   GridBoard.Children.Add(img);

                }   
            }
        }

        /// <summary>
        /// 駒を置く処理
        /// </summary>
        private void BoardSet(int col,int row)
        {
            if(!Check_OseroSet(col,row,TurnStatus,1)){
                // チェック結果false
                //MessageLabel2.Content = "そこはおけません";
                return;
            }
            arrayBoard[row,col] = TurnStatus ; 

            // 駒を置いたら手番を入れ替える
            TurnStatus = TurnStatus * -1;

            drawBoard();

            if(whiteCount + BlackCount == 64){
                // ゲーム終了
                String result = string.Format("白:{0} 黒:{1} ", whiteCount , BlackCount) ;
                if(whiteCount < BlackCount){
                    
                    MessageLabel.Content = result +  "★★黒の勝ちです★★";
                }else if(whiteCount > BlackCount){
                    MessageLabel.Content = result  +"☆☆白の勝ちです☆☆";
                }else{
                    MessageLabel.Content = "！！引き分けです！！";
                }
                return;
            }
            if (ableCount == 0){
                // 置ける数が0なら、手番を再度入れ替え
                MessageBox.Show("置ける場所がないため、パスします");
                TurnStatus = TurnStatus * -1;
                drawBoard();

            } 

            if(TurnStatus == -1){
                MessageLabel.Content = "白の手番です";

            }else{
                MessageLabel.Content = "黒の手番です";
                DoEvents();

                EnemyPlay();
            }
        }

        /// <summary>
        /// 画面再描画処理１
        /// </summary>
        private void DoEvents()
        {
            DispatcherFrame frame = new DispatcherFrame();
            var callback = new DispatcherOperationCallback(ExitFrames);
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, callback, frame);
            Dispatcher.PushFrame(frame);
        }

        /// <summary>
        /// 画面再描画処理２
        /// </summary>
        private object ExitFrames(object obj)
        {
            ((DispatcherFrame)obj).Continue = false;
            return null;
        }

        /// <summary>
        /// Grid上でのマウスのボタン押下イベント(タッチ操作含む)
        /// </summary>
        private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Point pos = e.GetPosition(GridBoard);
            int col = (int)Math.Floor(pos.X / 50);
            int row =(int)Math.Floor(pos.Y / 50);

            BoardSet(col,row);
        }

        /// <summary>
        /// オセロ配置判定処理
        /// </summary>
        /// <param name="col">対象カラム</param>
        /// <param name="rows">対象行</param>
        /// <param name="kind">駒種別</param>
        /// <param name="chkFlg">チェックフラグ（0:チェックのみ、1:チェック+駒返し）</param>
        private Boolean Check_OseroSet(int col,int row, int kind, int chkFlg )
        {

            if(arrayBoard[row ,col] != (int)OseroKind.None){
                Console.WriteLine("駒がすでに存在");
                return false;
                // 駒がおいてたらfalse
            }

            int nowTurn = kind;//今の手番
            int target = 0;
            Boolean chkResult = false;

            // 8方向について、それぞれ配置可能か判定する
            for(int i = 0; i < 8 ; i++){
                // 最大値を決め、その数まで、特定方向に対してチェックする
                int iMax = 0;
                int rowAdd = 0;
                int colAdd = 0;
                switch(i){
                    case 0:  //下　方向
                        iMax = row;
                        rowAdd = 1;
                        colAdd = 0;
                        break;
                    case 1:  //右下方向
                        iMax = (row > col) ? row : col;
                        rowAdd = 1;
                        colAdd = 1;
                        break;
                    case 2:  //右　方向
                        iMax = col;
                        rowAdd = 0;
                        colAdd = 1;
                        break;
                    case 3:  //右上方向
                        iMax = ( (7 - row) > col) ? (7 - row) : col;
                        rowAdd = -1;
                        colAdd = 1;
                        break;
                    case 4:  //上　方向
                        iMax = 7 - row;
                        rowAdd = -1;
                        colAdd = 0;
                        break;
                    case 5:  //左上方向
                        iMax = ( (7 - row) > (7 - col) ) ? (7 - row) : (7 - col);
                        rowAdd = -1;
                        colAdd = -1;
                        break;
                    case 6:  //左　方向
                        iMax = 7 - col;
                        rowAdd = 0;
                        colAdd = -1;
                        break;
                    case 7:  //左下方向
                        iMax = ( row > (7 - col) ) ? row : (7 - col);
                        rowAdd = 1;
                        colAdd = -1;
                        break;
                }
                Console.WriteLine("方向：" + i);
                ArrayList reverseList = new ArrayList();

                for(int j = 1 ; j + iMax < 8 ; j ++){
                    target = arrayBoard[row + (j * rowAdd) , col + (j * colAdd) ];
                    if(j == 1 && target*nowTurn != -1){
                        // 一つ目が相手の駒以外(自分の駒or無し)なら、チェック終了 
                        break;
                    }
                    // ひっくり返しリストに格納しておく
                    int[] stack = new int[2];                    
                    stack[0] = row + (j * rowAdd) ;
                    stack[1] = col + (j * colAdd)  ;
                    reverseList.Add(stack); 
                    if(target == 0){
                        //　2つ目以降、空があればそこで終了
                        break;
                    }
                    if(target == nowTurn ){
                        // 2つ目以降、自分の駒が見つかればtrue(空は除く)
                        chkResult =  true;
                        foreach(int[] stackArray in reverseList){
                            // リストのデータをひっくり返す
                            if(chkFlg == 1 ){
                                arrayBoard[stackArray[0],stackArray[1]] = nowTurn;
                            }
                        }
                    }
                }
            }

            return chkResult;

        }

        /// <summary>
        /// グリッド削除
        /// </summary>
        private void ClearGrid()
        {
            for (int i = GridBoard.Children.Count - 1; i >= 0; i--)
            {
                GridBoard.Children.RemoveAt(i);
            }
        }


        /// <summary>
        /// 相手動作
        /// </summary>
        /// <returns>なし</returns>    }
        private void EnemyPlay()
        {
            
            Thread.Sleep(1000);                

            for(int row = 0 ; row < 8 ; row++){
                for(int col = 0 ; col < 8 ; col++){
                    if(Check_OseroSet(col,row,TurnStatus,0)){
                        // 置くところが見つかったら、無条件で置く
                        BoardSet(col,row);
                        return;
                    }
                }   
            }   
        }
    }


    /// <summary>
    /// enum OseroKind
    /// </summary>
    public enum OseroKind { White=-1, None=0, Black=1 };
}

```

##動作確認

・初期表示画面
　薄い駒画像が、置ける場所候補。
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/834885/a26bdd38-1514-a6b8-8bfb-093a1095e4e6.png)

・駒がおけない場合のメッセージボックス
![画像5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/834885/e9eb3843-839b-e0bf-c84b-d1969f3d6e0e.png)

・結果画面
![画像4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/834885/83c1edb0-04f3-c09b-eff6-78d4501c9cca.png)

##感想

何か作ろうと思って、なんとなくオセロ作ってみました。
wpfを久々に触ったので、リハビリにはちょうど良かった気がします。
最低限の作成なので、気が向いたらViewModelを作成したり、AIをもう少し強化などやってみようと思います。