コンピュータとオセロ対戦1 ~普通にオセロ~

https://qiita.com/tt_and_tk/items/068f2afde6db637e189f

#このシリーズについて

いくつかの記事を使って、コンピュータとオセロ対戦ができるようになるまでの過程を書いていく予定です。
「何を使って」とかの縛りはなく、とにかくオセロができるようになるまでです。

#全体の目標

プログラムとしては、

* コンピュータとオセロで対戦したい
* スピーディーに対戦したい
* できるだけ強くしたい

記事としては、

* 全部解説する

頑張ります（笑）
なお、プログラムの内容については説明しますが、言語自体についての説明（「#include」ってなに？　「main」ってなに？　みたいな）はしません。

#今回の目標

人同士でのオセロができるようにする。

#ここから本編

調べてないけどたぶんモジュールなんかないので自作します。
スピーディーな対戦のためC言語を用います。

##mainの上に書くやつ
time.hは、のちのち乱数を使うので後で必要になりますが、いまはいりません。でも雰囲気作りのために最後に試合時間を表示するのに使います。ぶっちゃけいらないです（笑）
stdbool.hを使うことでC言語でもbool型が使えるようになります。

あとは数字に名前つけて、盤面を二次元配列で作って、ターン制のゲームなのでターンをグローバルで作って、あとは関数宣言。
関数についてはおいおい説明します。

```osero.c
#include <stdio.h>
#include <time.h>
#include <stdbool.h>

#define NONE 0
#define WHITE 1
#define BLACK 2
#define SIZE 8

int board[SIZE][SIZE] = {NONE};
bool turn = true;

void setup(void);                   // 最初の四個置くやつ
void print(void);                   // 盤面表示
void que(int * line, int * col);    // 置くところを聞く
void put(int line, int col);        // 置く
void count(void);                   // 黒と白の数を数える
bool check(int line, int col);      // 置けるかチェック
bool check_all(void);               // 置けるところがあるかチェック
bool all_none(void);                // 空きがあるかチェック
```

##まずプリント

これやらないと、setup関数やput関数がうまく動いてるか分からないですからね。
まずプリント。
行番号と列番号が表示されるようにしてます。
白丸と黒丸は普通に文字を使ってます。
自分は平気だったけど環境によってどうなるかはわからない。

```osero.c
void print(void){
    int num[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8};
    int i, j;

    printf("\n  ");
    for (i = 0; i < SIZE; i++) printf(" %d ", num[i]);

    printf("\n -------------------------\n");
    for (i = 0; i < SIZE; i++){
        printf("%d", num[i]);
        for (j = 0; j < SIZE; j++){
            if (board[i][j] == NONE) printf("|  ");
            else if (board[i][j] == WHITE) printf("|●");
            else if (board[i][j] == BLACK) printf("|○");
        }
        printf("|\n -------------------------\n");
    }
}
```

##セットアップ

難しい奴からやるとやる気なくなるので簡単な奴から（笑）
特筆すべきことはないです。

```osero.c
void setup(void){
    printf("黒: ○\n白: ●\n\n");
    board[SIZE / 2 - 1][SIZE / 2 - 1] = WHITE;
    board[SIZE / 2][SIZE / 2] = WHITE;
    board[SIZE / 2 - 1][SIZE / 2] = BLACK;
    board[SIZE / 2 ][SIZE / 2 - 1] = BLACK;
    // print();
}
```

##質問

セットアップと同様、簡単な奴から作っていきます。
ちなみに「que」は「question」の略です。
ややこしいですが、画面に表示される行列の番号と、プログラム内で処理するための行列番号は1つずれます。画面表示は1スタートで8までですが、配列は0スタートで7までだからです。putやcheckでデクリメントしてるのはそのためです。

```osero.c
void que(int * line, int * col){
    printf("行: ");
    scanf("%d", line);
    printf("列: ");
    scanf("%d", col);
}
```

##all_none

もうとにかく簡単な奴からやります。
一か所でも盤面に空きがあればtrueを返します。
オセロではたまに、「空いてるところはあるけど自分が置ける場所はない」って状況になることありますよね。なので「自分が置ける場夜があるかどうか」だけを試合終了条件にはできないのでこの関数が必要になります。

```osero.c
bool all_none(char board[SIZE][SIZE]){
    int i, j;
    bool check = false;

    for (i = 0; i < SIZE; i++)
        for (j = 0; j < SIZE; j++)
            if (board[i][j] == NONE) return true;
    
    return false;
}
```

##count

簡単な奴から。

```osero.c
void count(void){
    int i, j;
    int black = 0, white = 0;

    for (i = 0; i < SIZE; i++){
        for (j = 0; j < SIZE; j++){
            if (board[i][j] == BLACK) black++;
            else if (board[i][j] == WHITE) white++;
        }
    }

    printf("黒の数: %2d\n白の数: %2d\n", black, white);

    if (black > white) printf("黒の勝ちです\n");
    else if (black < white) printf("白の勝ちです\n");
    else printf("引き分けです\n");
}
```

##put

簡単な奴終わったので難しいやつ。
ちなみにその位置に置けることが分かっている前提で作ります。置けるかどうかは後述するcheck関数で確かめます。「置ける」の判断基準も後で解説します。
この関数の動きとしては、

1. 置く場所の上に相手の駒があるか確かめる。
2. もしあったら、その後「空き場所がなく」、「自分の駒に到達する」までの数を数える。
3. 上側も確かめる。
4. その数だけひっくり返せるので、自分の駒にする。
5. 横・斜めも調べる。

要約すると「縦・横・斜めで挟めるところを探し、あったらひっくり返す」です。
長すぎる関数なので縦ぶんだけしか載せませんが全部こんな感じです。
繰り返しになりますが、行列番号をデクリメントしてるのは0スタートの数字にするため。
oppは「相手」の英語「opponent」の略です。

```osero.c
void put(int line, int col){
    int i, j;
    int my, opp;
    int pos1, pos2;

    line--;
    col--;

    if (turn == true){
        my = BLACK, opp = WHITE;
        board[line][col] = BLACK;
    }else{
        my = WHITE, opp = BLACK;
        board[line][col] = WHITE;
    }

    // 縦
    pos1 = -1;
    pos2 = -1;
    i = 2;
    if (line - 1 >= 0 && board[line - 1][col] == opp){
        while (line - i >= 0){
            if (board[line - i][col] == NONE) break;
            if (board[line - i][col] == my){
                pos1 = i;
                break;
            }
            i++;
        }
        i = 2;
    }
    if (line + 1 < SIZE && board[line + 1][col] == opp){
        while (line + i < SIZE){
            if (board[line + i][col] == NONE) break;
            if (board[line + i][col] == my){
                pos2 = i;
                break;
            }
            i++;
        }
        i = 2;
    }
    if (pos1 != -1){
        for (i = 1; i < pos1; i++){
            board[line - i][col] = my;
        }
    }
    if (pos2 != -1){
        for (i = 1; i < pos2; i++){
            board[line + i][col] = my;
        }
    }

    /* 横と斜めも同様 */

}
```

##check

難しいやつその2。
置ける条件としては、以下の3つをすべて満たす必要があります。

* 指定場所に何も置かれていない
* 指定場所が盤上に存在する
* 1つでもひっくり返せる

ということで、putと同じ要領でチェック。
一つでもひっくりかえせることが確定した時点でtrueを返します。
こちらも全部載せると長いので縦チェックだけ。
また繰り返しますが、行列番号をデクリメントしてるのは0スタートの数字にするため。

```osero.c
bool check(int line, int col){
    // 置ける　 true
    // 置けない false
    line--;
    col--;
    if (line >= SIZE || col >= SIZE){
        // printf("その位置には置けません\n");
        return false;
    }
    if (board[line][col] != NONE){
        // printf("その位置には置けません\n");
        return false;
    }

    int my, opp;
    int i = 2;
    if (turn == true) my = BLACK, opp = WHITE;
    else my = WHITE, opp = BLACK;

    // 縦
    if (line - 1 >= 0 && board[line - 1][col] == opp){
        while (line - i >= 0){
            if (board[line - i][col] == NONE) break;
            if (board[line - i][col] == my) return true;
            i++;
        }
        i = 2;
    }
    if (line + 1 < SIZE && board[line + 1][col] == opp){
        while (line + i < SIZE){
            if (board[line + i][col] == NONE) break;
            if (board[line + i][col] == my) return true;
            i++;
        }
        i = 2;
    }

    /* 横と斜めも同様 */

    return false;
}
```

##check_all

置ける場所が一か所でもあるか調べます。
ついでにturnを反転させます。
一か所でもあった時点でtrueを返します。
今思えばall_noneみたいな感じのほうが簡単でよかったかも。

```osero.c
bool check_all(void){
    // 置ける　 true
    // 置けない false
    int i = 0, j = 0;

    turn = !turn;

    while (!check(i + 1, j + 1) && j != SIZE){
        i++;
        if (i == SIZE){
            i = 0;
            j++;
        }
    }

    if (j == SIZE) return false;
    else return true;
}
```

##main

これで（書き洩らしがなければ）すべてのパーツがそろいました。
ついにmain関数です。
今思えば別にlineとcolはグローバル変数でもよかったですね。

試合の終了条件として、以下のどちらかが満たされるまで続きます。

* 盤面にこれ以上空きがない
* 空きはあるが、お互い置ける場所がない

たとえ片方のプレイヤーが置けなくても、空きスペースさえあれば相手は置けるかもしれませんからそれだけでは終了にはなりません。
なので終了条件を満たすまでwhile文でターンを回します。

```osero.c
int main(){
    int line, col;
    bool my_check = true, old_check = true;

    // check_allで反転させられるので
    turn = false;
    setup();
    print();

    long now = time(NULL);
    while ((my_check = check_all()) || all_none()){
        if (turn) printf("\n黒のターンです\n");
        else printf("\n白のターンです\n");
        if (my_check){
            que(&line, &col);
            while (!check(line, col)){
                printf("その位置には置けません\n");
                que(&line, &col);
            }
            put(line, col);
            print();
        }else{
            printf("置ける場所がありません\n");
        }
        // お互い置ける場所がない
        if (!my_check && !old_check) break;
        old_check = my_check;
    }

    printf("\n試合時間: %ld秒\n", time(NULL) - now);
    count();

    return 0;
}
```

#フルバージョン

[この中](https://github.com/TTaisei/osero)から「osero.c」を探してください。それです。

#実際にやってみる

友達が誰も対戦してくれなかったのでセルフ対戦しました。
以下、実行画面の様子（一部）

![スクリーンショット 2021-09-02 140153.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/c74841b9-75b6-d4aa-a802-3c330c75a8df.png)

#次回は

ランダムで打ち返してくるプログラムを作ろうと思います。
[次回の記事(ランダムに返す)](https://qiita.com/tt_and_tk/items/b7aa5f543037ad35a7a2)

https://qiita.com/tt_and_tk/items/068f2afde6db637e189f