ポンクソフト

オセロの作成 - C/C++言語とDXライブラリでゲーム作成入門

前ページ C/C++言語とDXライブラリでゲーム作成入門 TOP 次ページ

目次

  1. C/C++言語とDXライブラリでゲーム作成入門
  2. シューティングゲームの作成(チュートリアル)
  3. ドットイートゲームの作成(チュートリアル)
  4. 15パズルの作成
  5. 神経衰弱の作成
  6. オセロの作成
  7. オブジェクト指向を活用したシューティングゲーム

今回の目的

ソースファイル

今回の講座のソースを全て含んだプロジェクトファイル(Visual C++ 2010)を以下に置いておきます。
othello.zip

ゲームの説明

黒いコマがプレイヤー、白いコマがコンピュータとなります。
othello_img/game.png

オセロ画像の作成

「othello」というプロジェクトを作成し、盤面を384×384ピクセルのPNG画像で用意し、「back.png」という名前でプロジェクトフォルダに置きます。

back.pngの例
othello_img/back.png
コマはひとつ48×48ピクセルのPNG画像を左が黒コマ、右が白コマになるように並べ、「piece.png」という名前でプロジェクトフォルダに置きます。

piece.pngの例
othello_img/piece.png

プログラムの入力・実行

プロジェクトに「othello.cpp」という名前の新規ソースファイルを追加し以下のプログラムを入力してください。
#include "DxLib.h"
#include <string>

int board[8][8]; // 盤のデータ(0:なし 1:黒コマ 2:白コマ)
std::string msg;
int msg_wait;

// 指定した位置にコマを置く
int putPiece(int x, int y, int turn, bool put_flag) {
  int sum = 0;
  if (board[y][x] > 0) return 0;
  for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) {
    int wx[8], wy[8];
    for (int wn = 0;; wn++) {
      int kx = x + dx * (wn + 1); int ky = y + dy * (wn + 1);
      if (kx < 0 || kx > 7 || ky < 0 || ky > 7 || board[ky][kx] == 0) break;
      if (board[ky][kx] == turn) {
        if (put_flag) for (int i = 0; i < wn; i++) board[wy[i]][wx[i]] = turn;
        sum += wn;
        break;
      }
      wx[wn] = kx; wy[wn] = ky;
    }
  }
  if (sum > 0 && put_flag) board[y][x] = turn;
  return sum;
}

// パスチェック
bool isPass(int turn) {
  for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) {
    if (putPiece(x, y, turn, false)) return false;
  }
  return true;
}

// 思考ルーチン1 プレイヤー
bool think1(int turn) {
  static bool mouse_flag = false;
  if (GetMouseInput() & MOUSE_INPUT_LEFT) {
    if (!mouse_flag) {
      mouse_flag = true;
      int mx, my;
      GetMousePoint(&mx, &my);
      if (putPiece(mx / 48, my / 48, turn, true)) return true;
    }
  } else mouse_flag = false;
  return false;
}


// 思考ルーチン2 最も多く取れるところに置く
bool think2(int turn) {
  int max = 0, wx, wy;
  for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) {
    int num = putPiece(x, y, turn, false);
    if (max < num || (max == num && GetRand(1) == 0)) {
      max = num; wx = x; wy = y;
    }
  }
  putPiece(wx, wy, turn, true);
  return true;
}

// 思考ルーチン3 優先順位の高いところに置く
bool think3(int turn) {
  int priority‎[8][8] = {
    {0, 6, 2, 1, 1, 2, 6, 0},
    {6, 6, 5, 4, 4, 5, 6, 6},
    {2, 5, 2, 3, 3, 2, 5, 2},
    {1, 4, 3, 3, 3, 3, 4, 1},
    {1, 4, 3, 3, 3, 3, 4, 1},
    {2, 5, 2, 3, 3, 2, 5, 2},
    {6, 6, 5, 4, 4, 5, 6, 6},
    {0, 6, 2, 1, 1, 2, 6, 0},
  };
  int max = 0, wx, wy;
  for (int p = 0; p <= 6 && max == 0; p++) {
    for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) {
      if (priority‎[y][x] != p) continue;
      int num = putPiece(x, y, turn, false);
      if (max < num || (max == num && GetRand(1) == 0)) {
        max = num; wx = x; wy = y;
      }
    }
  }
  putPiece(wx, wy, turn, true);
  return true;
}

// メッセージセット
// turn ... 1:BLACK 2:WHITE 3:DRAW
// type ... 0:TURN 1:PASS 2:WIN!
void setMsg(int turn, int type) {
  std::string turn_str[] = {"BLACK", "WHITE", "DRAW"};
  std::string type_str[] = {"TURN", "PASS", "WIN!"};
  msg = turn_str[turn - 1];
  if (turn != 3) msg += " " + type_str[type];
  msg_wait = 50;
}

// 勝敗チェック
int checkResult() {
  int pnum[2] = {};
  int result = 0;
  for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) {
    if (board[y][x] > 0) pnum[board[y][x] - 1]++;
  }
  if (isPass(1) && isPass(2)) {
    if (pnum[0] > pnum[1]) result = 1;
    else if (pnum[0] < pnum[1]) result = 2;
    else result = 3;
  }
  if (result) setMsg(result, 2);
  return result;
}

// WinMain
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
  int pieces[2];
  int back;
  int status = 2; // 1:プレイ中 2:TURNメッセージ中 3:パスメッセージ中 4:終了
  int turn = 1; // 1:黒ターン 2:白ターン
  SetGraphMode(384, 384, 32);
  ChangeWindowMode(TRUE);
  DxLib_Init();
  SetDrawScreen(DX_SCREEN_BACK);
  LoadDivGraph("piece.png", 2, 2, 1, 48, 48, pieces);
  back = LoadGraph("back.png");
  board[3][3] = board[4][4] = 1;
  board[4][3] = board[3][4] = 2;
  setMsg(turn, 0);
  while (!ProcessMessage()) {
    ClearDrawScreen();
    switch (status) {
    case 1:
      if (isPass(turn)) {
        setMsg(turn, 1);
        status = 3;
      } else {
        bool (*think[])(int) = {think1, think2};
        if ((*think[turn - 1])(turn)) {
          turn = 3 - turn; status = 2;
          setMsg(turn, 0);
        }
      }
      if (checkResult()) status = 4;
      break;
    case 2:
      if (msg_wait > 0) msg_wait--;
      else status = 1;
      break;
    case 3:
      if (msg_wait > 0) msg_wait--;
      else {
        turn = 3 - turn; status = 2;
        setMsg(turn, 0);
      }
      break;
    }
    DrawGraph(0, 0, back, FALSE);
    for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) {
      if (board[y][x]) DrawGraph(x * 48, y * 48, pieces[board[y][x] - 1], TRUE);
    }
    if (status > 1) {
      int mw = GetDrawStringWidth(msg.c_str(), msg.size());
      DrawBox(192 - mw / 2 - 30, 172, 192 + mw / 2 + 30, 208, GetColor(200, 180, 150), TRUE);
      DrawString(192 - mw / 2, 182, msg.c_str(), GetColor(255, 255, 255));
    }
    ScreenFlip();
  }
  DxLib_End();
  return 0;
}
入力が完了したらメニューの「デバッグ」→「デバッグなしで開始」を選び、プログラムを実行します。

思考ルーチンの変更

上記のソースは黒が思考ルーチン1(プレイヤー)、白が思考ルーチン2(最も多く取れるところにコマを置く)となっています。WinMainの中ほどの
        bool (*think[])(int) = {think1, think2};
think1とthink2をthink1~think3に入れ替えて他の思考ルーチンを試してみてください。例えば{think1, think1}ならばプレイヤー同士の対決になります。{think2, think3}ならコンピュータの思考ルーチン2と思考ルーチン3(優先順位の高いところにコマを置く)の対決になります。

プログラムの解説

グローバル変数

int board[8][8];
盤面のデータです。0が空きマス、1が黒コマ、2が白コマが置かれている状態です。
std::string msg;
int msg_wait;
msgは画面上に表示する各種メッセージを格納する変数です。std::stringは文字列を使いやすくしたクラスのようなものです。関数を使わなくても文字列のコピーや結合が演算子で可能です。
msg_waitはメッセージを表示する時間です。

putPiece関数

指定した位置にコマを置く関数です。
int putPiece(int x, int y, int turn, bool put_flag) {
x, yはコマを置く位置、turnは順番(1なら黒番、2なら白番)、put_flagは実際に置く場合はtrue、置けるかどうか確認する場合はfalseにします。戻り値は裏返ったコマの数です。
  if (board[y][x] > 0) return 0;
置こうとする場所にコマがあればreturnします。
  for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) {
置いたコマの位置から上下左右とナナメ8方向にチェックをして行くので、dxとdyでチェック方向を示します。
    int wx[8], wy[8];
裏返すことのできるコマの位置を一時的に格納する変数です。
      int kx = x + dx * (wn + 1); int ky = y + dy * (wn + 1);
kx, kyがチェックする場所になります。
      if (kx < 0 || kx > 7 || ky < 0 || ky > 7 || board[ky][kx] == 0) break;
盤面からはみ出すか、または空きマスの場合は裏返すことができないのでループを抜けます。
      if (board[ky][kx] == turn) {
        if (put_flag) for (int i = 0; i < wn; i++) board[wy[i]][wx[i]] = turn;
        sum += wn;
        break;
      }
自コマの場合、間に挟まれた敵コマを実際に裏返している部分です。sumには裏返った数の合計が入ります。
      wx[wn] = kx; wy[wn] = ky;
敵コマの場合裏返すことができるので、一時的に配列に位置を格納しておきます。

isPass関数

パスかどうかチェックする関数です。
bool isPass(int turn) {
turnが1なら黒コマ、2なら白コマをチェックし、パスならばtrueを返します。

think1関数

think1~think3は思考ルーチンです。以下のような同じ形式の引数と戻り値を持ちます。
bool think1(int turn)
turnが1なら黒、2なら白の思考になります。戻り値はコマを置いたならばtrue、まだコマを置いていないときはfalseになります。
思考ルーチン1は実際にはプレイヤーのマウス操作になります。

think2関数

思考ルーチン2は、最も多く取れるところにコマを置く単純な思考になります。
  int max = 0, wx, wy;
maxに取れるコマの最大数が入ります。wx, wyは一番多く取れるコマを置く場所になります。
    int num = putPiece(x, y, turn, false);
盤面を順番に走査して、putPiece関数により取れることのできるコマ数を取得します。
    if (max < num || (max == num && GetRand(1) == 0)) {
      max = num; wx = x; wy = y;
    }
取れるコマ数が現在の最大数よりも大きければ無条件に入れ替えます。また、取れるコマ数と現在の最大数が等しければ、1/2の確率で入れ替えます。

think3関数

思考ルーチン3は、盤面の優先順位の高いところにコマを置く関数になります。優先順位は以下の通り。
othello_img/priority.gif
小さい数字のマスほど優先順位が高くなっています。
  for (int p = 0; p <= 6 && max == 0; p++) {
思考の内部は基本的にthink2と同じですが、優先順位の高い順にチェックしています。

setMsg関数

各種メッセージをセットする関数です。
void setMsg(int turn, int type) {
turnが1なら黒、2なら白に対するメッセージとなります。
typeが0ならターン開始、1ならパス、2なら勝利のメッセージとなります。

checkResult関数

勝敗をチェックする関数です。
  for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) {
    if (board[y][x] > 0) pnum[board[y][x] - 1]++;
  }
まず盤面のコマ数を数え、黒のコマ数をpnum[0]、白のコマ数をpnum[1]にセットします。
  if (isPass(1) && isPass(2)) {
双方ともパスのときに勝敗のチェックをします。
    if (pnum[0] > pnum[1]) result = 1;
    else if (pnum[0] < pnum[1]) result = 2;
    else result = 3;
  }
resultが1のときは黒の勝ち、2のときは白の勝ち、3のときは引き分けとなります。

WinMain関数

  int pieces[2];
  int back;
pieces[0]が黒、pieces[1]が白のコマ画像になります。
  int status = 2; // 1:プレイ中 2:TURNメッセージ中 3:パスメッセージ中 4:終了
現在の状態です。1がプレイ中、2がターン開始メッセージ表示中、3がパスメッセージ表示中、4が勝敗メッセージ表示中となります。
  int turn = 1; // 1:黒ターン 2:白ターン
現在のターンです。1が黒のターン、2が白のターンです。
  board[3][3] = board[4][4] = 1;
  board[4][3] = board[3][4] = 2;
boardはグローバル変数なので中身はすべて0になり、中央部分のみ黒と白のコマにしています。
      if (isPass(turn)) {
        setMsg(turn, 1);
        status = 3;
パスならばパスメッセージ表示中に移行します。
        bool (*think[])(int) = {think1, think2};
関数ポインタの配列を使ってthink[0]にthink1関数、think[1]にthink2関数のポインタをセットしています。
        if ((*think[turn - 1])(turn)) {
思考ルーチンを呼び出しています。
          turn = 3 - turn; status = 2;
          setMsg(turn, 0);
コマを置いたら黒と白のターンを入れ替え、ターン開始メッセージ表示中に移行します。
      if (checkResult()) status = 4;
勝敗が決まったら、勝敗メッセージ表示中に移行します。
      if (msg_wait > 0) msg_wait--;
      else status = 1;
ターン開始メッセージ表示中の処理です。表示期間が終了したら各ターンに移行します。
      if (msg_wait > 0) msg_wait--;
      else {
        turn = 3 - turn; status = 2;
        setMsg(turn, 0);
      }
パスメッセージ表示中の処理です。表示期間が終了したらターンを入れ替え、ターン開始メッセージ表示中に移行します。
    for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) {
      if (board[y][x]) DrawGraph(x * 48, y * 48, pieces[board[y][x] - 1], TRUE);
    }
盤面のコマを描画している部分です。
      int mw = GetDrawStringWidth(msg.c_str(), msg.size());
      DrawBox(192 - mw / 2 - 30, 172, 192 + mw / 2 + 30, 208, GetColor(200, 180, 150), TRUE);
      DrawString(192 - mw / 2, 182, msg.c_str(), GetColor(255, 255, 255));
各種メッセージを表示している部分です。GetDrawStringWidthは文字列のピクセル幅を取得するDXライブラリの関数です。DrawBoxは四角形を描画するDXライブラリの関数です。文字列の幅を取得することによって、画面の中央にメッセージを描画することができます。
前ページ C/C++言語とDXライブラリでゲーム作成入門 TOP 次ページ
このエントリーをはてなブックマークに追加 そっか0