オセロゲーム盤

オセロのページに戻る

まず考えることは,ゲーム盤の状況をどのようなデータで表すか,ということだ。

ゲーム盤を表す配列

 縦横の升目のゲーム盤であるので,これは単純な2次元配列で表すのが適している。その際,サイズを8×8とすると,添え字が 0, 1, 2, ..., 7 となって,各行に付けられた番号とずれてしまう。添え字を 1, 2, 3, ..., 8 として使うためには,サイズを9×9にとればよいが,ここでは番兵を入れるために,もう一つ大きく,10×10の配列を使用することとする。 また,a, b, c, ..., h という列の名は,添え字 1, 2, 3, ..., 8 で表す。

i行j列の場所のマス目の状態は board[i][j] の次のような値で表される。盤外とは,board[0][...], board[9][...], board[...][0], board[...][9] のことである。

以上のことを決めて,配列のサイズ定数と配列の宣言定義をしておく。

#define B 10       /* 盤を表す配列 board のサイズ */

int board[B][B];   /* 盤の状態を表す配列 */

ゲーム盤初の期化

 ゲーム盤を表す配列 borad を初期状態に初期化する関数を作る。board の初期値は次の通りである。

ゲーム盤を表す配列
void init_board(int board[B][B]) の関数仕様
board の値を上記のように設定する。
void init_board(int board[B][B])
{
    int i, j;
	
    for (i = 0; i < B; i++) {
        for (j = 0; j < B; j++) {
            board[i][j] = -1;
        }
    }
    for (i = 1; i <= 8; i++) {
        for (j = 1; j <= 8; j++) {
            board[i][j] = 0;
        }
    }
    board[4][5] = board[5][4] = 1;
    board[4][4] = board[5][5] = 2;
}

 この関数は,まず -1 を全面に代入し,次に,周囲1周りを残した内側に 0 を代入し,最後に,真ん中4個の要素の値を設定している。直前に設定した値をすぐに変更しているわけである。
 そこで,関数内の最初の for ループは,次のように書きなおしてもよい。その方が,無駄な代入がない。

    for (i = 0; i < B; i++) {
        board[0][i] = board[9][i] = board[i][0] = board[i][9] = -1;
    }

 しかし,どちらのコーディングが良いか,よ〜く,考えてみよう。

書きなおしたときの長所と短所を書き並べてみた。

長所:関数の実行が早い
短所:コーディングを間違えやすい

この関数は,ゲームの最初に1回だけ実行される,ということを考えると,この長所は大したことではない。それに比べて,短所は大きな短所であ る。
実際,次のように書き間違えても,一目ではどこが間違いなのか,分からない(2カ所間違いがある。)

    for (i = 0; i < B; i++) {
        board[0][i] = board[9][j] = board[0][i] = board[i][9] = -1;
    }

したがって,書き直す前の方が良いコーディング法と言える。

 このように,配列を複雑な値に設定するときには,大きく周りから値を設定して,小さい内側の部分は,その値を上書きするようにすれば,コーディングし易いことを覚えておこう。
 ただし,関数の実行速度が要求されるときは,無駄のない代入を考えることも大事である。
 しかし,真ん中の4個の無駄な代入を減らすために,次のようなコーディングを行うと,かえって速度を落とす。

    for (i = 1; i <= 8; i++) {
        for (j = 1; j <= 8; j++) {
            if (i <= 3 || i >= 6 || j <= 3 || j >= 6)
                board[i][j] = 0;
        }
    }

無駄な代入の回数は,確かに4回だけ減らせるが,その代償として,8×8×4=256回の,数値比較判断が必要となる。

教訓 単純さは複雑さに勝る(Simple is Best)

 for 文などの実行分の部分を簡潔にするには,究極の方法がある。それは,データをあらかじめ与えておく方法だ。

void init_board(int board[B][B])
{
    int i, j;
    static int initdata[B][B] = 
        {{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  2,  1,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  1,  2,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}};
	
    for (i = 0; i < B; i++) {
        for (j = 0; j < B; j++) {
            board[i][j] = initdata[i][j];
        }
    }
}

 ここで,static という修飾子は,それに続く変数が静的領域にとられる,すなわち静的変数であることを意味する。静的領域にとられた変数は,関数の呼び出しに関わらず,常 にメモリ上に存在する。そして関数が終了しても,消滅しない。また,その初期化は,プログラム開始時に1度だけ行われる。
 これに対し,関数内で宣言された変数(ローカル変数)で static 修飾子がついていないものは,動的変数と呼ばれ,関数が呼び出されるごとに動的領域内にとられ,関数が終了すると消滅する。また,その初期化も呼び出され る度に行われる。
 また,ANSI 規格に厳密に準拠したコンパイラでは,配列を初期化できるのは,配列が大域変数か静的変数であるときに限られている。(gcc コンパイラだと,動的配列変数でも初期化できる。)どのようなコンパイラでもコンパイルできるプログラムのためには, static 修飾子をつけておくべきだ。
 しかも,今回の場合は,関数呼び出しのごとに initboard を初期化する必要はないから,無駄な初期化を避けるためにも static を付けておくべきだ。

 先ほどのプログラムでは,配列 initdata の内容を配列 board にコピーするのに, for 文で行っているが,ライブラリ関数の memcpy を用いると,さらに簡単である。memcpy はヘッダファイル string.h で宣言されているので,これをインクルードする必要がある。

void *memcpy(void *dest, const void *src, size_t n) の関数仕様
ポインタ src が指す場所を先頭とする n バイトの内容を,dest が指す場所を先頭とする n バイトに複写する。戻り値は dest に等しい。
#include <string.h>

 void init_board(int board[B][B])
{
    static int initdata[B][B] = 
        {{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  2,  1,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  1,  2,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1,  0,  0,  0,  0,  0,  0,  0,  0, -1}, 
         {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}};
	
    memcpy(board, initdata, sizeof(initdata));
}

ゲーム盤の表示

次は,ゲーム盤をどのように表示するか,考えよう。テキストで表すので,あまり綺麗にはできないが,できるだけ見やすくなるよう,工夫しよう。

void print_board(int board[B][B])
{
    int i, j;

    for (i = 1; i <= 8; i++) {
        for (j = 1; j <= 8; j++) {
            printf("%2d", board[i][j]);
        }
        printf("\n");
    }
} 

この関数を実行すると,たとえば board が初期状態のときには,次のように表示される。

 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 2 1 0 0 0
0 0 0 1 2 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

関数が思う通りに動いていることを,次の main を実行して確認しておこう。

main()
{
    init_board(board);
    print_board(board);
}

 しかし,今作った表示関数では,行と列の記号番号がすぐに分からないので,もっと見やすく,次のように表示させたい。

   a b c d e f g h
-----------------
1| 0 0 0 0 0 0 0 0 |
2| 0 0 0 0 0 0 0 0 |
3| 0 0 0 0 0 0 0 0 |
4| 0 0 0 2 1 0 0 0 |
5| 0 0 0 1 2 0 0 0 |
6| 0 0 0 0 0 0 0 0 |
7| 0 0 0 0 0 0 0 0 |
8| 0 0 0 0 0 0 0 0 |
-----------------

練習問題1: 上図のように表示するように, print_board を変更せよ。
解答解説

オセロのページに戻る