C言語でオセロゲームを作成

オセロゲームのC言語ソースコード紹介ページのアイキャッチ

今回は息抜きも兼ねてゲームを作成しました。オセロゲームです。

オセロゲームの図

今回はこのオセロゲームのソースコードを公開、そしてそのソースコードの解説を行いたいと思います。

オセロゲームのソースコード

今回私が作成したオセロゲームのソースコードは下記になります。空行やコメントも含めて約400行程度になります。

main.c
#include <stdio.h>

/* 盤のサイズ */
#define SIZE (8)
#define WIDTH (SIZE)
#define HEIGHT (SIZE)

/* 石の色 */
typedef enum color {
  white,
  black,
  empty
} COLOR;

/* 石を置けるかどうかの判断 */
typedef enum put {
  ok,
  ng
} PUT;

/* 盤を表す二次元配列 */
COLOR b[HEIGHT][WIDTH];

/* 盤を初期化 */
int init(void){
  unsigned char x, y;

  for(y = 0; y < HEIGHT; y++){
    for(x = 0; x < WIDTH; x++){
      b[y][x] = empty;
    }
  }

  /* 盤面の真ん中に石を置く */
  b[HEIGHT / 2][WIDTH / 2] = white;
  b[HEIGHT / 2 - 1][WIDTH / 2 - 1] = white;
  b[HEIGHT / 2 - 1][WIDTH / 2] = black;
  b[HEIGHT / 2][WIDTH / 2 - 1] = black;

  return 0;
}

/* マスを表示 */
int displaySquare(COLOR square){

  switch(square){
  case white:
    /* 白色の石は "o" で表示 */
    printf("o");
    break;
  case black:
    /* 黒色の石は "*" で表示 */
    printf("*");
    break;
  case empty:
    /* 空きは " " で表示 */
    printf(" ");
    break;
  default:
    printf("エラー");
    return -1;
  }
  return 0; 
}

/* 盤を表示 */
int display(void){
  int x, y;

  for(y = 0; y < HEIGHT; y++){
    /* 盤の横方向のマス番号を表示 */
    if(y == 0){
      printf(" ");
      for(x = 0; x < WIDTH; x++){
        printf("%d", x);
      }
      printf("\n");
    }

    for(x = 0; x < WIDTH; x++){
      /* 盤の縦方向のます番号を表示 */
      if(x == 0){
        printf("%d", y);
      }

      /* 盤に置かれた石の情報を表示 */
      displaySquare(b[y][x]);
    }
    printf("\n");
  }

  return 0;
}

/* 指定された場所に石を置く */
int put(int x, int y, COLOR color){
  int i, j;
  int s, n;
  COLOR other;

  /* 相手の石の色 */
  if(color == white){
    other = black;
  } else if(color == black){
    other = white;
  } else {
    return -1;
  }

  /* 全方向に対して挟んだ石をひっくり返す */
  for(j = -1; j < 2; j++){
    for(i = -1; i < 2; i++){

      /* 真ん中方向はチェックしてもしょうがないので次の方向の確認に移る */
      if(i == 0 && j == 0){
        continue;
      }

      /* 隣が相手の色でなければその方向でひっくり返せる石はない */
      if(b[y + j][x + i] != other){
        continue;
      }

      /* 置こうとしているマスから遠い方向へ1マスずつ確認 */
      for(s = 2; s < SIZE; s++){
        /* 盤面外のマスはチェックしない */
        if(
          x + i * s >= 0 &&
          x + i * s < WIDTH &&
          y + j * s >= 0 &&
          y + j * s < HEIGHT
        ){

          if(b[y + j * s][x + i * s] == empty){
            /* 自分の石が見つかる前に空きがある場合 */
            /* この方向の石はひっくり返せないので次の方向をチェック */
            break;;
          }

          /* その方向に自分の色の石があれば石がひっくり返せる */
          if(b[y + j * s][x + i * s] == color){
            /* 石を置く */
            b[y][x] = color;

            /* 挟んだ石をひっくり返す */
            for(n = 1; n < s; n++){
              b[y + j * n][x + i * n] = color;
            }
          }
        }
      }
    }
  }

  return 0;
}

/* 指定された場所に置けるかどうかを判断 */
PUT isPuttable(int x, int y, COLOR color){
  int i, j;
  int s;
  COLOR other;
  int count;

  /* 既にそこに石が置いてあれば置けない */
  if(b[y][x] != empty){
    return ng;
  }

  /* 相手の石の色 */
  if(color == white){
    other = black;
  } else if(color == black){
    other = white;
  } else {
    return ng;
  }
  /* 各方向に対してそこに置くと相手の石がひっくり返せるかを確認 */

  /* 1方向でもひっくり返せればその場所に置ける */

  /* 置ける方向をカウント */
  count = 0;

  /* 全方向に対して挟んだ石をひっくり返す */
  for(j = -1; j < 2; j++){
    for(i = -1; i < 2; i++){

      /* 真ん中方向はチェックしてもしょうがないので次の方向の確認に移る */
      if(i == 0 && j == 0){
        continue;
      }

      /* 隣が相手の色でなければその方向でひっくり返せる石はない */
      if(b[y + j][x + i] != other){
        continue;
      }

      /* 置こうとしているマスから遠い方向へ1マスずつ確認 */
      for(s = 2; s < SIZE; s++){
        /* 盤面外のマスはチェックしない */
        if(
          x + i * s >= 0 &&
          x + i * s < WIDTH &&
          y + j * s >= 0 &&
          y + j * s < HEIGHT
        ){

          if(b[y + j * s][x + i * s] == empty){
            /* 自分の石が見つかる前に空きがある場合 */
            /* この方向の石はひっくり返せないので次の方向をチェック */
            break;;
          }

          /* その方向に自分の色の石があれば石がひっくり返せる */
          if(b[y + j * s][x + i * s] == color){
            /* 石がひっくり返る方向の数をカウント */
            count++;
          }
        }
      }
    }
  }

  if(count == 0){
    return ng;
  }

  return ok;
}

/* プレイヤーが石を置く */
void play(COLOR color){
  int x, y;

  /* 置く場所が決まるまで無限ループ */
  while(1){
    /* 置く場所の入力を受付 */
    printf("横方向は?");
    scanf("%d", &x);
    printf("縦方向は?");
    scanf("%d", &y);

    /* 入力された場所におけるならループを抜ける */
    if(isPuttable(x, y, color) == ok){
      break;
    }

    /* 入力された場所に石が置けない場合の処理 */

    printf("そこには置けません!!\n");
    printf("下記に置く事ができます\n");

    /* 置ける場所を表示 */
    for(y = 0; y < HEIGHT; y++){
      for(x = 0; x < WIDTH; x++){
        if(isPuttable(x, y, color) == ok){
          printf("(%d, %d)\n", x, y);
        }
      }
    }
  }

  /* 最後に石を置く */
  put(x, y, black);

}

/* COMが石を置く */
void com(COLOR color){
  int x, y;

  /* 置ける場所を探索 */
  for(y = 0; y < HEIGHT; y++){
    for(x = 0; x < WIDTH; x++){
      if(isPuttable(x, y, color) == ok){
        /* 置けるなら即座にその位置に石を置いて終了 */
        put(x, y, color);
        printf("COMが(%d,%d)に石を置きました\n", x, y);
        return ;
      }
    }
  }
}

/* 結果を表示する */
void result(void){
  int x, y;
  int white_count, black_count;

  /* 盤上の白石と黒石の数をカウント */
  white_count = 0;
  black_count = 0;
  for(y = 0; y < HEIGHT; y++){
    for(x = 0; x < WIDTH; x++){
      if(b[y][x] == white){
        white_count++;
      } else if(b[y][x] == black){
        black_count++;
      }
    }
  }

  /* カウント数に応じて結果を表示 */
  if(black_count > white_count){
    printf("あなたの勝利です!!");
  } else if(white_count > black_count){
    printf("COMの勝利です...");
  } else {
    printf("引き分けです");
  }
  printf("(黒:%d / 白:%d)\n", black_count, white_count);

}

COLOR nextColor(COLOR now){
  COLOR next;
  int x, y;

  /* まずは次の石の色を他方の色の石に設定 */
  if(now == white){
    next = black;
  } else {
    next = white;
  }

  /* 次の色の石が置けるかどうかを判断 */
  for(y = 0; y < HEIGHT; y++){
    for(x = 0; x < WIDTH; x++){
      if(isPuttable(x, y, next) == ok){
        /* 置けるのであれば他方の色の石が次のターンに置く石 */
        return next;
      }
    }
  }

  /* 他方の色の石が置けない場合 */

  /* 元々の色の石が置けるかどうかを判断 */
  for(y = 0; y < HEIGHT; y++){
    for(x = 0; x < WIDTH; x++){
      if(isPuttable(x, y, now) == ok){
        /* 置けるのであれば元々の色の石が次のターンに置く石 */
        return now;
      }
    }
  }

  /* 両方の色の石が置けないのであれば試合は終了 */
  return empty;
}

int main(void){
  COLOR now, next;

  /* 盤を初期化して表示 */
  init();
  display();

  /* 最初に置く石の色 */
  now = black;

  /* 決着がつくまで無限ループ */
  while(1){
    if(now == black){
      /* 置く石の色が黒の場合はあなたがプレイ */
      play(now);
      //com(now);
    } else if(now == white){
      /* 置く石の色が白の場合はCOMがプレイ */
      com(now);
    }

    /* 石を置いた後の盤を表示 */
    display();

    /* 次のターンに置く石の色を決定 */
    next = nextColor(now);
    if(next == now){
      /* 次も同じ色の石の場合 */
      printf("置ける場所がないのでスキップします\n");
    } else if(next == empty){
      /* 両方の色の石が置けない場合 */
      printf("試合終了です\n");

      /* 結果表示して終了 */
      result();
      return 0;
    }

    /* 次のターンに置く石を設定 */
    now = next;
 
  }
  return 0;
}

オセロゲームの遊び方

上記の main.c をコンパイルして実行すると、コンソールに下記のように表示が行われます。

 01234567
0        
1        
2        
3   o*   
4   *o   
5        
6        
7        
横方向は?

縦長ですが、オセロの盤面を表しています。「o」が白色の石、「*」が黒色の石を表しています。ユーザーの石の色は黒色です。

さらにここで横方向のマス番号を押すと、縦方向のマス番号の入力が促されます。

縦方向は?

ここで縦方向のマス番号を入力すると、入力した位置に石が置けない場合(相手の石をひっくり返せない場合)は、再度入力を促されるようになっています。この際には石が置ける位置を表示するようにしています。

そこには置けません!!
下記に置く事ができます
(3, 2)
(2, 3)
(5, 4)
(4, 5)
横方向は?

入力した位置に石が置ける場合は、オセロの盤面に黒色の石(*)が置かれ、さらに挟んだ相手の石(o)がひっくり返されます。

横方向は?3
縦方向は?2
 01234567
0        
1        
2   *    
3   **   
4   *o   
5        
6        
7      

石が置かれると、次は相手(COM)が石を置き、再度置いた結果が表示され、ユーザーに石の置く場所の入力が促されます。

COMが(2,2)に石を置きました
 01234567
0        
1        
2  o*    
3   o*   
4   *o   
5        
6        
7        
横方向は?

こんな感じで石を置く処理を繰り返し、ユーザーと COM 両方が石を置けなくなった場合にゲームが終了し、結果が表示されるようになっています。

 01234567
0ooooooo*
1ooooooo*
2oooooo**
3ooooo*o*
4ooooo***
5ooo*ooo*
6oooo**o*
7******oo
試合終了です
COMの勝利です...(黒:20 / 白:44)

スポンサーリンク

ソースコードの解説

ソースコードの処理は下記の処理をゲームが終了するまで main 関数内で無限ループするようになっています。

  • 石の色を判断
    • 黒の場合はユーザーから石の置く位置の指定を受け付け★(play 関数)
      • 指定された位置のマスに石を置けるかどうかを判断(isPuttable 関数)
        • 置けない場合
          • ★の入力受付に戻る
        • 置ける場合
          • 入力された位置のマスに石を置き、挟んだ相手の石をひっくり返す(put 関数)
    • 白の場合は COM に石の置くマスを指定させる(com 関数)
      • 指定された位置のマスに石を置き、挟んだ相手の石をひっくり返す(put 関数)
  • 石を置いた後の盤面を表示(display 関数)
  • 次に置く石の色を判断(nextColor 関数)
    • 次に置ける色の石がない場合
      • 結果を表示(result 関数) 
      • プログラムを終了
    • 次に置ける色の石がある場合
      • 石の色をその色に設定してループの最初に戻る

コメントもある程度は書いていますので、各関数の処理についてはソースコードを参照していただければと思います。

ここではオセロゲームプログラムのポイントになる下記の4点についてのみ解説します。

盤面の石の管理

盤面のどの位置にどの色の石が置かれているかを管理するために、グローバル変数として下記の二次元配列 b を作成しています。

/* 盤を表す二次元配列 */
COLOR b[HEIGHT][WIDTH];

この配列では添字が盤面上のマスの位置(1つ目の添字が縦方向、2つ目の添字が横方向)を表し、その値にはマスに置かれている石の色を表す値を格納するようにしています。

石の色は下記のように列挙型で定義しています。

/* 石の色 */
typedef enum color {
  white,
  black,
  empty
} COLOR;

例えば下記を実行すれば、

b[3][1] = black;

下図のマスに黒色の石が置かれたことになります。

配列に石の色を格納する様子

また下記実行後の color の値が empty であれば、

color = b[4][5];

下図のマスには石が置かれていないことになります。

配列から石の色を取得する様子

こんな感じで二次元配列を用いて盤面のマスに置かれている石を管理しています。

石が置けるかどうかを判断する処理

オセロゲームのプログラミングを行うときに一番難しいのは「石が置けるかとどうかを判断する処理」だと思います。

オセロでは、相手の石をひっくり返せるマスにしか石を置けないというルールがあります。

黒石が置ける場所

なので、石を置く位置が入力されても、その石が置けるかどうかを判断し、置けない場合は再度石の位置の入力を促すような処理が必要です。

で、その判断を行っているのが isPuttable 関数になります。

isPuttable 関数は石を置こうとしている位置(x, y)とその石の色(color)を受け取り、それぞれの方向(左上・上・右上・左・右・左下・下・右下の8方向)に対し、x, y の位置に color の石を置くと相手の石がひっくり返るかどうかを判断しています。

ひっくり返せるかどうかを確認する8方向

各方向に対して判断を行うために、下記の2重ループを構成しています。

/* 全方向に対して置くと石がひっくり返るかを確認 */
for(j = -1; j < 2; j++){
  for(i = -1; i < 2; i++){
    /* 〜略〜 */
  }
}

i, j の値と方向の関係は下記のようになります。

  • j = -1, i = -1:左上
  • j = -1, i = 0:上
  • j = -1, i = 1:右上
  • j = 0, i = -1:左
  • j = 0, i = -1:右
  • j = 1, i = -1:左下
  • j = 1, i = 0:下
  • j = 1, i = 1:右下

そして、各方向に対して下記をチェックし、その方向のマスに置かれている相手の石がひっくり返せるかどうかを判断しています。

①置こうとしているマスに隣接するマスが相手の石かどうか

まずチェックするのは置こうとしているマスの隣のマスです。

そのマスが空、もしくはそのマスに自分の石が置かれている場合、その方向にはひっくり返せる相手の石がないと判断できますので、直ちに次の方向の確認に移ります。

隣接するマスが相手の石かどうかの確認

これを行っているのが下記部分です。

/* 隣が相手の色でなければその方向でひっくり返せる石はない */
if(b[y + j][x + i] != other){
  continue;
}

そのマスに相手の色の石が置かれている場合は、その方向の相手の石をひっくり返せる可能性がありますので、次の②に移ります。

②次に隣接するマスに石が置かれているかどうか

ここでは、①でチェックしたマスのさらに隣のマス(置こうとしているマスから遠い方向のマス)です。

このマスに石が置かれていない場合は、その方向に置いてある相手の石をひっくり返す事ができないため、その時点で次の方向のチェックに移ります。

さらにその隣接するマスが空きかどうかの確認

これを行っているのは下記部分です(s は置こうとしているマスからの距離を表す変数です)。

if(b[y + j * s][x + i * s] == empty){
 /* 自分の石が見つかる前に空きがある場合 */
 /* この方向の石はひっくり返せないので次の方向をチェック */
  break;
}

空でない場合は、その方向の相手の石をひっくり返せる可能性がありますので、次の③に移ります。

③②でチェックしたマスに自分の石が置かれているかどうか

次は②でチェックしたマスが自分の石であるかどうかを確認します。

自分の石である場合、ここまで①②で確認したマスまでの相手の石をひっくり返す事が可能です。

さらにその隣接するマスが自分の石かどうかの確認

したがって、置こうとしているマスには実際に石を置くことができると言えます。

それを示すために、下記部分で count の値をインクリメントしています(この count が 0 よりも大きな値の場合に ok を返却するようにしています)。

/* その方向に自分の色の石があれば石がひっくり返せる */
if(b[y + j * s][x + i * s] == color){
  /* 石がひっくり返る方向の数をカウント */
  count++;
}

自分の色の石が置かれていない場合は、まだその方向に置かれている相手の石がひっくり返せるかどうかが判断できませんので、今チェックしたマスのさらに隣のマスに対し、②と③を行います。

順々に遠くのマスを確認していく様子

盤面外のマスに行き着くまでチェックすることで、その方向に石が置けるかどうかを判断する事が可能です。

スポンサーリンク

石を置く処理

石を置く処理は put 関数で行っています。

よく見ていただければ分かると思いますが、ほぼ isPuttable 関数と同じであることが確認できると思います。

大きく違うのは、isPuttable 関数で下記の処理を行っているのに対し、

/* その方向に自分の色の石があれば石がひっくり返せる */
if(b[y + j * s][x + i * s] == color){
  /* 石がひっくり返る方向の数をカウント */
  count++;
}

put 関数では下記の処理を行っています。

/* その方向に自分の色の石があれば石がひっくり返せる */
if(b[y + j * s][x + i * s] == color){
  /* 石を置く */
  b[y][x] = color;

  /* 挟んだ石をひっくり返す */
  for(n = 1; n < s; n++){
    b[y + j * n][x + i * n] = color;
  }
}

つまり、put 関数では置こうとしている位置に石を置いた場合に相手の石がひっくり返るかどうかを判断するだけでなく、その判断後に、実際にその場所に石を置き、さらに石を置いた場合にひっくり返る石を実際にひっくり返す処理を行っています。

石を置いてひっくり返す様子

この処理も isPuttable 関数同様に各方向に対して繰り返し実行するようにしています。

対戦相手が石を置く場所を決定する処理

対戦相手(COM)が石を置く場所を決定する処理は com 関数で行っています。

ただ、対戦相手は左上方向から右下方向のマス1つ1つに対し、そのマスに石が置けるかどうかを判断し、置ける場合は即座にそのマスに石を置くようになっています。

なので、めちゃめちゃ弱いです。対戦相手が次にどのマスに石を置くかが簡単にわかってしまいます。クソゲーです…。

逆にこの com 関数を作り込めばもっと強い対戦相手にする事ができます。例えばランダムに置くマスを決定するようにすればもうちょっと強くなります。

AI などを駆使して com 関数を作り込めば、めちゃめちゃ強くなる可能性もあると思います。

まとめ

このページではオセロゲームをC言語でプログラミングしたソースコードの紹介と解説を行いました。

一応何回か試して動作確認はしていますが、試行回数少ないのでもしかしたらバグっているかもしれません…。

たまにはこんな感じのゲームでもプログラミングしてみるのもいかがでしょうか?割と楽しくプログラミングできると思います!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です