【C++】初心者のためのオセロプログラミング!【雑記】

スポンサーリンク
スポンサーリンク

はじめに

プログラミング初心者の友達にオセロゲームの作り方を教えて欲しいと頼まれた時に作ったものをちょっとした考え方等の解説付きで紹介します。

ソースコードそのものは以前C言語で作ったものとほとんど同じです(授業中の暇つぶしに作ったものなのでかなり汚く、見辛いので書き直したかったという理由もあります笑)。

ソースコード

ただ単純にソースコードが見たい!という方もいると思うので先にソースコード全体を載せておきます。

#include <iostream>

//盤面 空白(0) 黒(-1) 白(1) 番兵(2)
int board[10][10] = {};
//手番
int player = -1;

//盤面の生成
void make_board(){
	//番兵
	for(int i = 0; i < 10; i++){
		board[0][i] = 2;
		board[9][i] = 2;
		board[i][0] = 2;
		board[i][9] = 2;
	}
	//基本位置
	board[4][4] = 1;
	board[5][5] = 1;
	board[4][5] = -1;
	board[5][4] = -1;
}

//盤面の表示
void show_board(){
	//番兵も含めて表示
	for(int i = 0; i < 10; i++){
		for(int j = 0; j < 10; j++){
			switch(board[i][j]){
				case -1:
					std::cout << "●";
					break;
				case 1:
					std::cout << "○";
					break;
				case 0:
					std::cout << "-";
					break;
				case 2:
					//std::cout << "~";
					break;
				default:
					break;
				}
		}
		std::cout << std::endl;
	}
}

//手番の表示
void show_player(){
	switch(player){
		case -1:
			std::cout << "先手(黒)の手番です" << std::endl;
			break;
		case 1:
			std::cout << "後手(白)の手番です" << std::endl;
			break;
		default:
			//std::cout << "error" << std::endl;
			break;
	}
}

//特定の座標から特定の方向に挟めるか判定
int check_dir(int i, int j, int dir_i, int dir_j){
	//指定方向に相手の石がある場合は次のマスを探索する
	int times = 1;
	while(board[i+dir_i*times][j+dir_j*times] == player*-1){
		times++;
	}
	//指定方向の最後に自分の石がある場合
	if(board[i+dir_i*times][j+dir_j*times] == player){
		//指定方向に相手の石が何個あるかを返す
		return times-1;
	}
	//指定方向の最後に自分の石がなければ0を返す
	return 0;
}

//特定の場所に置くことができるか判定
bool check_plc(int i, int j){
	//場所が空であるかどうか
	if(board[i][j] == 0){
		//全方向を探索
		for(int dir_i = -1; dir_i < 2; dir_i++){
			for(int dir_j = -1; dir_j < 2; dir_j++){
				if(check_dir(i,j,dir_i,dir_j)){
					//配置可能であればtrueを返す
					return true;
				}
			}
		}
	}
	return false;
}

//終了判定
bool flag_fin(){
	//置ける場所があるか判定
	for(int i = 1; i < 9; i++){
		for(int j = 1; j < 9; j++){
			if(check_plc(i,j)){
				return true;
			}
		}
	}

	//プレイヤーを変えて置ける場所があるか判定
	player *= -1;
	for(int i = 1; i < 9; i++){
		for(int j = 1; j < 9; j++){
			if(check_plc(i,j)){
				std::cout << "置く場所がないためPlayerを変更しました" << std::endl;
				return true;
			}
		}
	}

	return false;
}

//石を配置する
void place_stn(int i, int j){
	//方向毎に走査
	for(int dir_i = -1; dir_i < 2; dir_i++){
		for(int dir_j = -1; dir_j < 2; dir_j++){
			//挟んだ石の数
			int change_num = check_dir(i,j,dir_i,dir_j);
			//挟んだ石の数だけ置き換える
			for(int k = 1; k < change_num+1; k++){
				board[i+dir_i*k][j+dir_j*k] = player;
			}
		}
	}
	//配置箇所を置き換える
	board[i][j] = player;
}

//勝敗判定
void judge_board(){
	int count_b = 0; //黒石の数
	int count_w = 0; //白石の数
	for(int i = 1; i < 9; i++){
		for(int j = 1; j < 9; j++){
			if(board[i][j] == -1){
				count_b++;
			}else if(board[i][j] == 1){
				count_w++;
			}
		}
	}
	//結果表示
	std::cout << "先手" << count_b << ":後手" << count_w << std::endl;
	//勝敗判定
	if(count_b > count_w){
		std::cout << "先手の勝利" << std::endl;
	}else if(count_w > count_b){
		std::cout << "後手の勝利" << std::endl;
	}else{
		std::cout << "引き分け" << std::endl;
	}
}

int main(){
	//盤面の生成
	make_board();
	//終了までループ
	while(flag_fin()){
		//盤面の表示
		show_board();
		//手番の表示
		show_player();
		//入力受付
		int i,j;
		do{
			std::cout << "配置場所を入力してください" << std::endl;
			std::cin >> i >> j;
		}while(!check_plc(i,j));
		//石を配置する
		place_stn(i,j);
		//手番を入れ替える
		player *= -1;
	}
	//盤面の表示
	show_board();
	//勝利判定
	judge_board();
	return 0;
}

実行

ターミナル上で実行するとこんな感じでオセロができます。

--------
--------
--------
---○●---
---●○---
--------
--------
--------

先手(黒)の手番です
配置場所を入力してください
3 4

--------
--------
---●----
---●●---
---●○---
--------
--------
--------

後手(白)の手番です
配置場所を入力してください

オセロゲームを作る!

ここから先ほどのオセロゲームのソースコードをどのように作っていくのか考えていきます。

まずはオセロゲームの大きな流れ(フローチャート)から考えて、部分毎に作っていきます。

  1. ゲーム開始
  2. オセロの盤面を作る
  3. 終了するかどうか判定する
    1. 1手進める
  4. 最終結果を表示する
  5. ゲーム終了

大体の人がこんな感じの流れを考えつくと思います。今回はこれをベースに考えていきます。

オセロの盤面を作る

まずは最も簡単そうなオセロの盤面を作ります。

盤面は行列で扱うことにしましょう。

オセロということは盤面の石を走査してひっくり返すかどうか判定するプログラムを後々に作ることが予想されます。これについては番兵を利用することで簡単にできるようにするとします。

結果的に行列のサイズは、オセロの縦横8マスの両端に番兵を加えた縦横10マスになります。

//盤面 空白(0) 黒(-1) 白(1) 番兵(2)
int board[10][10] = {};

//盤面の生成
void make_board(){
	//番兵
	for(int i = 0; i < 10; i++){
		board[0][i] = 2;
		board[9][i] = 2;
		board[i][0] = 2;
		board[i][9] = 2;
	}
	//基本位置
	board[4][4] = 1;
	board[5][5] = 1;
	board[4][5] = -1;
	board[5][4] = -1;
}

グローバル変数として宣言する際に0で初期化を行い、番兵とオセロの初期4マスの配置をします。

今回は空きマスを0、黒石を-1、白石を1、番兵を2として作成しました。

盤面がうまく作れているか確認するために盤面を表示する関数を作成します。

#include <iostream>

//盤面の表示
void show_board(){
	//番兵も含めて表示
	for(int i = 0; i < 10; i++){
		for(int j = 0; j < 10; j++){
			switch(board[i][j]){
				case -1:
					std::cout << "●";
					break;
				case 1:
					std::cout << "○";
					break;
				case 0:
					std::cout << "-";
					break;
				case 2:
					//std::cout << "~";
					break;
				default:
					break;
				}
		}
		std::cout << std::endl;
	}
}

数字をそのまま出力すると醜いのでswitch文で記号を表示するようにしました。

int main(){
	//盤面の生成
	make_board();
	//盤面の表示
	show_board();
	return 0;
}

上記のようなメイン関数を作成してコンパイル。実行してみると次のように盤面が作られていることが確認できます。

--------
--------
--------
---○●---
---●○---
--------
--------
--------

終了するかどうか判定する

次に終了かどうか判定する部分はループの条件として利用する(終了しない場合ループ継続、終了の場合ループを抜けるというようにプログラムすれば良い)ためbool型で作成します。

終了判定は、「盤面が埋まっている」「盤面は埋まっていないけど置くことができない」等たくさんあるように思えて、結局のところ石を置くことができるかどうかのみなので次のように考えることができます。

  1. 手番の石を置くことができるのか判定する
    1. 置くことができる場合:終了しない(1を返す)
  2. 手番の石を置くことができない場合:相手の石を置くことができるのか判定する
    1. 置くことができる場合:終了しない(1を返す)
  3. 終了する(0を返す)

手番はグローバル変数で管理することにします(先手が-1、後手が1)。

「置くことができるかどうか」というのは「あるマスに置くことができるか判定する」という関数を作って、全てのマスに適用すれば良いです。

「あるマスに置くことができるか判定する」関数(check_plc)はマスの情報があれば良いはずなので引数をi,jとして盤面の行列の番兵以外の場所全てを判定するようにします。

//手番
int player = -1;

//終了判定
bool flag_fin(){
	//置ける場所があるか判定
	for(int i = 1; i < 9; i++){
		for(int j = 1; j < 9; j++){
			/*if(あるマスに置くことができるか判定する関数(i,j)){
				return true;
			}*/
		}
	}

	//プレイヤーを変えて置ける場所があるか判定
	player *= -1;
	for(int i = 1; i < 9; i++){
		for(int j = 1; j < 9; j++){
			/*if(check_plc(i,j)){
				std::cout << "置く場所がないためPlayerを変更しました" << std::endl;
				return true;
			}*/
		}
	}
	return false;
}

手番の変更は置くことができるか判定する前に行っても、置けない場合そのまま終了なので問題はありません。

手番を変更した結果、置くことができる場合にのみ変更したことを出力するようにすればゲームをする上で支障がありませんよね!

あるマスに置くことができるか判定する関数

「あるマスに置くことができるか」というのは

  • マスが空いていること
  • 既に置いてある手番の石とそのマスで相手の石を挟んでいること

という二つの条件を満たしているかどうかということです。

既に置いてある手番の石とそのマスで相手の石を挟んでいるかというのは、「あるマスから見てある方向で相手の石を挟むことができるのか判定する」関数(check_dir)を作るとします。

//特定の場所に置くことができるか判定
bool check_plc(int i, int j){
	//場所が空であるかどうか
	if(board[i][j] == 0){
		//全方向を探索
		for(int dir_i = -1; dir_i < 2; dir_i++){
			for(int dir_j = -1; dir_j < 2; dir_j++){
				/*if(check_dir(i,j,dir_i,dir_j)){
					//配置可能であればtrueを返す
					return true;
				}*/
			}
		}
	}
	return false;
}

あるマスが空であれば、全方向について探索します。

方向はマスの行列から見た方向(-1,0,1)で管理します。

この関数は「置くことができる/できない」のどちらかなのでbool型で定義します。

そのマスから見てある方向で相手の石を挟むことができるのか判定する関数

「そのマスから見てある方向で相手の石を挟むことができるのか判定する」というのは

  1. ある方向に相手の石が存在するか判定する。
    1. 存在する場合:一つ奥を調べる。
  2. 相手の石が続いた後、手番の石があるか判定する
    1. ある場合:相手の石を何個挟んだのか返す
  3. ない場合:0を返す

というように考えることができます。

ここで相手の石を何個挟んだのか返すようにしておくことで、後々ひっくり返すときに使えそうだなと思えるといいですね!

//特定の座標から特定の方向に挟めるか判定
int check_dir(int i, int j, int dir_i, int dir_j){
	//指定方向に相手の石がある場合は次のマスを探索する
	int times = 1;
	while(board[i+dir_i*times][j+dir_j*times] == player*-1){
		times++;
	}
	//指定方向の最後に自分の石がある場合
	if(board[i+dir_i*times][j+dir_j*times] == player){
		//指定方向に相手の石が何個あるかを返す
		return times-1;
	}
	//指定方向の最後に自分の石がなければ0を返す
	return 0;
}

while文を使って1つずつ指定方向に相手の石があるか判定した後、相手の石の先に手番の石があるか判定しています。変数timesを作って何個先を確認したかなどを管理しています。

これでようやく終了判定ができました!

上の二つの関数のコメントアウトしていた部分を外してメイン関数を次のようにしておきます。

int main(){
	//盤面の生成
	make_board();
	//終了までループ
	while(flag_fin()){
		//盤面の表示
		show_board();
		break;
	}
	return 0;
}

入力操作が全くないため、breakを入れておかないと永遠に盤面を表示し続けます。

一手進める

一手進めるのは先ほどのメイン関数の中にあるwhileループ内の動作になります。

一手進めるということは次のように考えられます。

  1. 手番を表示する
  2. 石を置く位置を入力させる
    1. 配置できない位置であればもう一度入力させる
  3. 石を配置する
  4. 手番を変える

まずは手番がどちらなのかを表示する関数を作っておきます。

//手番の表示
void show_player(){
	switch(player){
		case -1:
			std::cout << "先手(黒)の手番です" << std::endl;
			break;
		case 1:
			std::cout << "後手(白)の手番です" << std::endl;
			break;
		default:
			//std::cout << "error" << std::endl;
			break;
	}
}

流石にこれは説明することがないので割愛します。

石を置く位置を入力させる(配置できる位置が入力させるまでループ)は、先ほどの「あるマスに置くことができるか判定する」関数を使って次のように書くことができます。

//入力受付
int i,j;
do{
	std::cout << "配置場所を入力してください" << std::endl;
	std::cin >> i >> j;
}while(!check_plc(i,j));

石を配置する関数

石を配置する部分は関数を作ることにします。配置するだけなので配置する行と列を引数として作ります。

方向毎に「そのマスから見てある方向で相手の石を挟むことができるのか判定する」関数を使って挟める石の数を取得して、その数だけ石を手番の石で置き換えます。

最後に配置した場所のマスを置き換えて石の配置は完了です。

//石を配置する
void place_stn(int i, int j){
	//方向毎に走査
	for(int dir_i = -1; dir_i < 2; dir_i++){
		for(int dir_j = -1; dir_j < 2; dir_j++){
			//挟んだ石の数
			int change_num = check_dir(i,j,dir_i,dir_j);
			//挟んだ石の数だけ置き換える
			for(int k = 1; k < change_num+1; k++){
				board[i+dir_i*k][j+dir_j*k] = player;
			}
		}
	}
	//配置箇所を置き換える
	board[i][j] = player;
}

手番は「-1」と「1」なので毎ループの最後に「-1」をかけてやればいいですね。

これらをまとめてメイン関数を次のようにします。

int main(){
	//盤面の生成
	make_board();
	//終了までループ
	while(flag_fin()){
		//盤面の表示
		show_board();
		//手番の表示
		show_player();
		//入力受付
		int i,j;
		do{
			std::cout << "配置場所を入力してください" << std::endl;
			std::cin >> i >> j;
		}while(!check_plc(i,j));
		//石を配置する
		place_stn(i,j);
		//手番を入れ替える
		player *= -1;
	}
	return 0;
}

最終結果を表示する

最終結果はそれぞれの石の数を数えて、どちらの石が多いか判定すれば良いですね。

「最終結果を表示する」関数を作ってメイン関数に追加する形にします。

//勝敗判定
void judge_board(){
	int count_b = 0; //黒石の数
	int count_w = 0; //白石の数
	for(int i = 1; i < 9; i++){
		for(int j = 1; j < 9; j++){
			if(board[i][j] == -1){
				count_b++;
			}else if(board[i][j] == 1){
				count_w++;
			}
		}
	}
	//結果表示
	std::cout << "先手" << count_b << ":後手" << count_w << std::endl;
	//勝敗判定
	if(count_b > count_w){
		std::cout << "先手の勝利" << std::endl;
	}else if(count_w > count_b){
		std::cout << "後手の勝利" << std::endl;
	}else{
		std::cout << "引き分け" << std::endl;
	}
}

メイン関数には盤面も表示するようにして完成です。

int main(){
	//盤面の生成
	make_board();
	//終了までループ
	while(flag_fin()){
		//盤面の表示
		show_board();
		//手番の表示
		show_player();
		//入力受付
		int i,j;
		do{
			std::cout << "配置場所を入力してください" << std::endl;
			std::cin >> i >> j;
		}while(!check_plc(i,j));
		//石を配置する
		place_stn(i,j);
		//手番を入れ替える
		player *= -1;
	}
	//盤面の表示
	show_board();
	//勝利判定
	judge_board();
	return 0;
}

終わりに

知識さえあれば簡単に書けてしまう(いかに簡単に書くか)というのがプログラミングだと思います。

これを読んだプログラミング初心者がプログラミングって楽しいな!知識があれば簡単に書けそうだな!と思ってもらえると嬉しいです。

コメント

タイトルとURLをコピーしました