横・縦・斜め の値が揃っているか

# はじめに
paizaの問題集にある「五目並べ」の問題で、`横` `縦` `斜め`の値が揃っているかを調べる際、主に`縦`と`斜め`の判定でかなり悩んだので備忘録として残します。

https://paiza.jp/works/mondai/prob60/tic_tac_toe_9


## 追記
2021/9/22 : コメントでより簡潔なコードをご教示頂いたので、最後の項目（[追記](#追記)）に追加しました。

# 問題の内容
問題にも書いていますが、ざっくり言うとこんな感じです。

・盤面の各マスには、`O` か `X` か `.` が書かれている。
・同じ記号が`縦`か`横`か`斜め`に`連続で5つ`並んでいれば、その記号のプレイヤーが勝者となる。
・その勝者の記号を表示させる。勝負がついていなければ`D`を表示。

# もくじ
[1. 値の取得](#1-値の取得)
[2. 縦の判定](#2-縦の判定)
[3. 斜めの判定](#3-斜めの判定)

# 1. 値の取得
いつものように標準入力を取得。
取得した`XXOXO`を `X` `X` `O` `X` `O`というように１文字ずつ分解していく。

```php
  // 標準入力を１行ずつ取得
  while($line = fgets(STDIN)) {
    $tmp[] = trim($line);
    }

// $tmpの中身（イメージ）
// $tmp = {
// [0] => "XXOXO"
// [1] => "OXOXX"
// [2] => "OOOOO"
// [3] => "OXOX."
// [4] => "XOXXO"
// }
    
  // １行の文字列をさらに分解
  foreach ($tmp as $value) {
    $str_split[] = mb_str_split($value);
  }

// $str_splitの中身（イメージ）
// $str_split = {
// [0] => {
//   [0] => "X"
//   [1] => "X"
//   [2] => "O"
//   [3] => "X"
//   [4] => "O"
//  }
// [1] => {
//   [0] => "O"
//   [1] => "X"
//   [2] => "O"
//   [3] => "X"
//   [4] => "X"
//  }
// 以下略
```
![スクリーンショット 2021-09-19 16.21.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/65bd32e0-41ae-9264-e9d0-e0495dc0f8a8.png)


# 2. 縦の判定
縦の判定は、以下のように実装。

```php
    // 縦の判定
    for($i = 0; $i < 5; $i++) {
      foreach ($str_split as $str) {
          // ① 配列の行と列を入れ替える
          $column[$i][] = $str[$i];
      }

      // ② "X"と"O"の数を数える
      $columnCount[] = array_count_values($column[$i]);
      if($columnCount[$i]["X"] === 5) {
          echo "X";
          return;
      } elseif($columnCount[$i]["O"] === 5) {
          echo "O";
          return;
      }
    };
```
① では以下のように新しい配列`$column`に同じ添字同士の値を格納していきました。
![スクリーンショット 2021-09-19 15.35.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/71ffc076-232b-aeac-9a70-886da987ed1b.png)
最終的に以下のように縦の列同士だった値が配列に格納される形となります。
![スクリーンショット 2021-09-19 22.47.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/ec64de59-94f8-5814-c99f-34049c7c5d5a.png)

② では上記で作成した配列の中に、`X`と`O`がそれぞれいくつあるのか判定しています。
この部分については、以下の`左上から右下`を判定のところで触れています。

```php
      $columnCount[] = array_count_values($column[$i]);
      if($columnCount[$i]["X"] === 5) {
          echo "X";
          return;
      } elseif($columnCount[$i]["O"] === 5) {
          echo "O";
          return;
      }
```

# 3. 斜めの判定
斜めの判定は、`左上から右下`と`右上から左下`の2パターンを判定しています。

```php
    // 左上から右下
    for($i = 0; $i < 5; $i++) {
      // ① 左上から右下の列を配列に格納
      $aboveLeft[] = $str_split[$i][$i];
    };
    // ② "X"と"O"の数を数える
    $aboveLeftCount = array_count_values($aboveLeft);
      if($aboveLeftCount["X"] === 5) {
         echo  "X";
         return;
      } elseif($aboveLeftCount["O"] === 5) {
          echo "O";
          return;
      }
      
      
    // 右上から左下
    for($i = 0, $j = 4; $i < 5; $i++, $j--) {
      // ① 右上から左下の列を配列に格納
      $aboveRight[] = $str_split[$i][$j];
    };
    // ② "X"と"O"の数を数える
    $aboveRightCount = array_count_values($aboveRight);
      if($aboveRightCount["X"] === 5) {
          echo  "X";
          return;
      } elseif($aboveRightCount["O"] === 5) {
          echo "O";
          return;
      }
```
## 左上から右下
左上から右下へは、規則的に添字が揃っているため、以下のようにコードを書くことができます。

```php
    for($i = 0; $i < 5; $i++) {
      $aboveLeft[] = $str_split[$i][$i];
    };
```
![スクリーンショット 2021-09-19 15.41.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/d0114ea5-e511-769e-b3a5-f6e54517411e.png)

左上から右下の列を配列に格納した後は、配列の中に`X`と`O`がそれぞれいくつあるかを調べます。
`同じ文字が5つ揃っている = 勝者` となるため条件分岐で、5つ文字が揃っていればその記号を表示させるというようにしています。

```php
      if($aboveLeftCount["X"] === 5) {
         echo  "X";
         return;
      } elseif($aboveLeftCount["O"] === 5) {
          echo "O";
          return;
      }
```

### 配列の中の要素を数える
上記のように`配列の中にある要素がいくつあるか`を調べるにあたり、`array_count_values()`を用いました。

https://www.php.net/manual/ja/function.array-count-values.php

![スクリーンショット 2021-09-19 15.41.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/365d33f0-5224-15c3-120a-fad307b46cb7.png)

## 右上から左下
右上から左下へは、片方の添字は`0から4`へ増加、もう片方の添字は`4から0`へ減少するという法則があります。
![スクリーンショット 2021-09-19 15.41.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/8150a675-ce03-b1f1-8190-ba271f77eda6.png)

この法則を利用して以下のようにコードを書くことができます。

```php
    for($i = 0, $j = 4; $i < 5; $i++, $j--) {
      $aboveRight[] = $str_split[$i][$j];
    };
```

この後は、先ほどのように`X`か`O`が5つあるのか条件分岐を行います。

```php
    $aboveRightCount = array_count_values($aboveRight);
    if($aboveRightCount["X"] === 5) {
        echo  "X";
        return;
    } elseif($aboveRightCount["O"] === 5) {
        echo "O";
        return;
    }
```

# おわり
解答コード例を見てみると、上記とは違う解法であったためとても勉強になりました。
繰り返し処理をうまく使えるようにしていこう！

# 追記
上記のコードより簡潔な書き方を教わったため以下に記載致します。

```php
<?php
function judge($arr) {
  $check = function($str) {
    $str = implode(array_unique(str_split($str)));
    return (strlen($str) === 1 && $str !== '.') ? $str : false;
  };

  $s1 = $s2 = '';
  $size = count($arr);

  for($i = 0; $i < $size; $i++) {
    // 横判定
    $str = $check($arr[$i]);
    if($str) return $str;

    // 縦判定
    $str = implode(array_map(function($m) use($i) { return $m[$i];}, $arr));
    $str = $check($str);
    if($str) return $str;

    // 斜め判定用
    $s1 .= $arr[$i][$i];
    $s2 .= $arr[$i][$size - $i - 1];
  }

  // 斜め判定
  $str = $check($s1);
  if($str) return $str;

  $str = $check($s2);
  if($str) return $str;

  return 'D';
}


echo judge([
  'XO.X.',
  'XOXXO',
  'OOOOX',
  'XOXX.',
  'OOXOO',
]);          // O

echo judge([
  'O..XX',
  'OOXXO',
  'OOXXX',
  'XX.OO',
  'XOOXO',
]);          // X
```
こちら、コメントで@3267fさんより頂いたコードとなります。
本当にありがとうございます。

## 縦・斜めの値を取得
私が記述したやり方では、`縦` `斜め`の値を一度配列に置き直し、繰り返しを用いて文字のカウントをしていました。
が、以下のコードで簡潔に書くことができます。

```php:各列の値の取得
  $s1 = $s2 = '';
  $size = count($arr);

  for($i = 0; $i < $size; $i++) {
    // 横判定
    $str = $check($arr[$i]);
    if($str) return $str;

    // 縦判定
    $str = implode(array_map(function($m) use($i) { return $m[$i];}, $arr));
    $str = $check($str);
    if($str) return $str;

    // 斜め判定用
    $s1 .= $arr[$i][$i];
    $s2 .= $arr[$i][$size - $i - 1];
  }
```
配列に格納せずとも上記のように配列から特定の値を直接指定して、文字数をカウントすることができます。

## 文字のカウント
文字のカウントは以下のように`array_unique`で文字を絞り、残った文字を判定しています。

```php:文字のカウント
  $check = function($str) {
    $str = implode(array_unique(str_split($str)));
    return (strlen($str) === 1 && $str !== '.') ? $str : false;
  };
```


簡潔なコードになり、非常に勉強になりました！
改めてお礼申し上げます。


# 参考
https://moroto1122.hatenadiary.org/entry/20100325/1269519538

https://berrysystems.sakura.ne.jp/karasumaru/2020/09/24/paiza-b%E3%83%A9%E3%83%B3%E3%82%AF%E3%83%AC%E3%83%99%E3%83%AB%E3%82%A2%E3%83%83%E3%83%97%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC%E3%80%80step-55%E3%80%8C%E4%BA%94%E7%9B%AE%E4%B8%A6%E3%81%B9%EF%BC%88/


https://www.softel.co.jp/blogs/tech/archives/2576