読者です 読者をやめる 読者になる 読者になる

AccessViolation Exception

仕事でもはんだづけ、家でもはんだづけ

c言語でオセロを作る

c 技術ネタ

f:id:kamiyaowl:20140906010412p:plain

夏休みは元気にお過ごしでしょうか(?)、私は最後の夏休みを適当に満喫しています。

ともあれ学校の後輩の課題のようなものでオセロを作るというわけで、ちょっと横槍ながら解説していきます。

オセロ(リバーシ

オセロ (遊戯) - Wikipedia

まぁルールはわかっていると思いますが、白黒石を打って挟まれたところがひっくり返るというものです。

実装を考えてみる

  • オセロ盤の管理

  • 石が置ける場所かどうかの管理

  • 石を置いた時のひっくり返す処理

まぁざっとこれぐらいかと。順番に考えていくと、まずオセロ盤の管理。今回は2次元配列で管理して、何も置いてない、黒石、白石の状態を定義します。

ついでにオセロ盤の大きさも定義しておいて

#define WIDTH 8
#define HEIGHT 8
 
#define EMPTY 0
#define BLACK 1
#define WHITE 2
 
int field[HEIGHT][WIDTH];

残りの2つについては、石が一個もひっくり返らない場所には置くことが出来ないので置くときに判定することにします。

処理の流れ

適当に思いつく感じで書いていくと

  • オセロ盤を初期化する

[A]

  • オセロ盤を表示する

  • ユーザーが石を置く場所を入力

  • 石を置く(置けない場合はやり直し)

[B]

  • おわり

AB間を繰り返せばいいだけですね。

オセロ盤の初期化

for2重ループで、オセロ盤を綺麗にしましょう。あとは初期位置に白黒置いてあげれば終わり。

void reversi_init() {
    int i,j;
    for(j = 0 ; j < HEIGHT ; ++j){
        for(i = 0 ; i < WIDTH ; ++i){
            field[j][i] = EMPTY;
        }
    }
    //initialize position
    field[HEIGHT / 2 - 1][WIDTH / 2 - 1] = BLACK;
    field[HEIGHT / 2    ][WIDTH / 2 - 1]  = WHITE;
    field[HEIGHT / 2 - 1][WIDTH / 2    ] = WHITE;
    field[HEIGHT / 2    ][WIDTH / 2    ] = BLACK;
}

関数にまとめておきましょう。

オセロ盤の表示

適当にループして表示しましょう。これも含め関数にまとめておきましょう。

void reversi_print(){
    int i,j;
    /* header */
    printf("   |");
    for(i = 0 ; i < WIDTH ; ++i){
        printf("%2d |",i);
    }
    printf("\n");
    /* field */
    for(j = 0 ; j < HEIGHT ; ++j){
        printf("%2d |",j);
        for(i = 0 ; i < WIDTH ; ++i){
            switch(field[j][i]){
                case EMPTY:
                    printf("   |");
                    break;
                case BLACK:
                    printf(" B |");
                    break;
                case WHITE:
                    printf(" W |");
                    break;
                default:
                    break;
            }
        }
        printf("\n");
    }
}

ちょいとら見やすいように座標やらスペース入れてますが適当に作っちゃって構いません。きちんと表示できるならまた関数にまとめておきましょう。

石を置く処理

ここが問題ですね。必要なのは置く石の種類、座標、返す値は置けたかどうかということでひとまず関数を作ります。

int reversi_put(int bw,int x,int y){
}

あとここで真偽値も扱いたいので以下も追加

#define TRUE 1
#define FALSE 0
 

(defineでやるのもな~とも思いつつ...。)

石を置けない時を考えてみる
  • 入力値がオセロ盤から外れている

  • すでに石が置かれている場合

  • 他にひっくり返す石がない場合

順番にコードで示せば

int reversi_put(int bw,int x,int y){
    /* area check */
    if(x < 0 || WIDTH <= x) return FALSE;
    if(y < 0 || HEIGHT <= y) return FALSE;
    /* already put */
    if(field[y][x] != EMPTY) return FALSE;

    /* ひっくり返す処理がまだ */

}

これで上2つはクリアされたので、最後にひっくり返す処理。少し長くなりそうなので関数を定義

int reversi_check_others(int bw,int x,int y){

}

checkといいつつひっくり返せるならひっくり返し、だめなら0を返す関数とします。

判定

f:id:kamiyaowl:20140906003530p:plain

Excelで作った雑な図で申し訳ないのですが、赤色の位置に黒石を置こうとした場合を考えます。

オレンジ色の矢印で示した部分の白色は反転できますが、ほかは不可能です。

ここで考えるべきなのは、「置こうとした位置から8方向に伸びた先に同じ色の石があればいい」これだけです。

先ほどreversi_check_othersを作ったのですが、さらにここから8方向に同じ色の石があるか確認し、ひっくり返す関数を作成します。

ひっくり返せない条件
  • x,y座標いずれかがオセロ盤の範囲外

  • 石が何も置かれていない

  • 先の座標に同じ色の石がない

再帰を使った実装

先ほどの箇条書きの3つ目、「先の座標」に注目。一個先の座標がひっくり返せるかどうかを返す関数として実装してみる。

つまり、「1個先がひっくり返るならここもひっくり返る」こういうことをすればいいだけで

int reversi_reverse(int bw,int x,int y){
    /* x,y座標いずれかがオセロ盤の範囲外 */
    if(x < 0 || WIDTH <= x) return FALSE;
    if(y < 0 || HEIGHT <= y) return FALSE;
    /* 石が何も置かれていない */
    if(field[y][x] == EMPTY) return FALSE;
    /* 同じ色の石があった! */
    if(field[y][x] == bw) return TRUE;
    else {
        /* 1個右の座標でひっくり返れば、ひっくり返す */
        if(reversi_reverse(bw,x + 1,y)){
            field[y][x] = bw;
            return TRUE;
        } else return FALSE;
    }
}

そう考えて読めば、まぁやってることはわかるかと。ただしこれでは右方向に対してしか判定出来ていません。

そこで、次の座標に映るときの差分も引数dx,dyで取るようにすれば

int reversi_reverse(int bw,int x,int y,int dx,int dy){
    /* x,y座標いずれかがオセロ盤の範囲外 */
    if(x < 0 || WIDTH <= x) return FALSE;
    if(y < 0 || HEIGHT <= y) return FALSE;
    /* 石が何も置かれていない */
    if(field[y][x] == EMPTY) return FALSE;
    /* 同じ色の石があった! */
    if(field[y][x] == bw) return TRUE;
    else {
        /* 1個先の座標でひっくり返れば、ひっくり返す */
        if(reversi_reverse(bw,x + dx,y + dy,dx,dy)){
            field[y][x] = bw;
            return TRUE;
        } else return FALSE;
    }
}

こんな具合でdx,dyの指定仕方で、タテ・ヨコ・ナナメすべて実現できるようになりました。めでたし。

あとは使うだけ
reversi_reverse(bw,x, y - 1,  0, -1);//up
reversi_reverse(bw,x, y + 1,  0,  1);//down
reversi_reverse(bw,x - 1, y, -1,  0);//left
reversi_reverse(bw,x + 1, y,  1,  0);//right

reversi_reverse(bw,x - 1, y - 1, -1, -1);//up left
reversi_reverse(bw,x + 1, y - 1,  1, -1);//up right
reversi_reverse(bw,x - 1, y + 1, -1,  1);//down left
reversi_reverse(bw,x + 1, y + 1,  1,  1);//down right

これで8方向をひっくり返せます。こいつらの関数自身はTRUE(1)を返しているので、足し合わせれば何方向に対してひっくり返せたかが求められるので、

int reversi_check_others(int bw,int x,int y){
    int dir_count = 0;
    dir_count += reversi_reverse(bw,x, y - 1,  0, -1);//up
    dir_count += reversi_reverse(bw,x, y + 1,  0,  1);//down
    dir_count += reversi_reverse(bw,x - 1, y, -1,  0);//left
    dir_count += reversi_reverse(bw,x + 1, y,  1,  0);//right
 
    dir_count += reversi_reverse(bw,x - 1, y - 1, -1, -1);//up left
    dir_count += reversi_reverse(bw,x + 1, y - 1,  1, -1);//up right
    dir_count += reversi_reverse(bw,x - 1, y + 1, -1,  1);//down left
    dir_count += reversi_reverse(bw,x + 1, y + 1,  1,  1);//down right
 
    return dir_count;
}

これで判定及びひっくり返す処理が完成。

置くところまでさかのぼって

1方向以上に置けたなら、指定された場所にも石を置けば終了ですね。

int reversi_put(int bw,int x,int y){
    /* area check */
    if(x < 0 || WIDTH <= x) return FALSE;
    if(y < 0 || HEIGHT <= y) return FALSE;
    /* already put */
    if(field[y][x] != EMPTY) return FALSE;

    if(reversi_check_others(bw,x,y) > 0){
        field[y][x] = bw;
        return TRUE;
    } else return FALSE;
}
ゲームの流れを記述

あとはmainに数値入力と表示、あとは最初に示した流れを記述していけばいいので

int main(void) {
    int put_x,put_y,bw;
    /* initialize reversi */
    reversi_init();
    reversi_print();
    /* start player */
    bw = BLACK;//or WHITE
    while(1) {
        printf("%s's turn>",bw == WHITE ? "WHITE" : "BLACK");
        /* user input */
        scanf("%d %d",&put_x, &put_y);
        /* put */
        if(reversi_put(bw, put_x, put_y)){
            /* after put */
            bw = bw == WHITE ? BLACK : WHITE;//next turn change
            reversi_print();
        } else {
            /* miss */
            printf("cannot put a stone.\n");
        }
    }
    return 0;
}

reversi_putが置けたかどうかを示しているので、置けたらプレイヤーを入れ替えています。

完成

コード的にいささか問題があるような気もしなくもないですが、ひとまずオセロとして機能するコードが出来ました。

コード一式示しておきます。ターン数のカウントが紹介したコードから増えています。

reversi

残りやるべきこと

まだ未実装なところが幾つか

  • ゲーム終了の判定をしていない

  • ゲーム結果が実装されていない

  • どこにも置けない場合のスキップが実装されていない

これらも判定で置くかどうかなど適当に工夫すればいける?と思います(かなり投げやり

改善したコード自体は載せておくので参考程度に。

reversi kai