C言語リバーシ 石を置く

シェアする

  • このエントリーをはてなブックマークに追加

こんにちは。AG03-mikuというヤマハのウェブキャスティングミキサー兼オーディオインターフェイスを数日前に購入したのですが、ミク版ということで初音ミクV3のoriginalとdarkが39日!使用可能ということで、ミクちゃんと戯れております。楽しいです。

さて、前回は局面の表示を行いました。今回からついに石を置く操作を実装していきます。

まず、場所をユーザーから指定してもらわないといけません。

指定方法には、前回の局面表示で使用したアルファベットと数字を使用します。

例えば、初手f5なら「f5」と入力してもらいます。

セキュリティ的に問題があるのですが、配布するソフトウェアではないのと解説が面倒なので、使ってはいけないとされているscanf関数を使います。(セキュリティ的には、fgets()で標準入力から文字列を受け取ってsscanf()で変数に代入して標準入力をクリアすべきです。)(ただし、最終的にはGUI(グラフィカルユーザーインターフェース)を作る、つまりウィンドウ上のオセロの盤面をクリックして石を置くようにするのでscanfは使わなくなります。)

ビットボードに石を置くには、受け取った文字と数字からuint64_t型に変換しないといけません。

文字と数字を変数として受け取り、uint64_t型の場所を表す数字を返す関数を作りましょう。

またビットシフトを使うのですが、シフト数の計算に文字コードを利用します。

C言語においてアルファベットのchar型文字はASCIIコードで管理され、aからhはもちろん連続に並んでいます。

ですので、例えば変数fileに入っている文字が'g'のとき、file - 'a'は6になります(fileがaなら0、bなら1、cなら2、……となります。)ですので、「右から何列目か」という数値は、file - 'a' + 1 で計算できます。逆に、「左から何列目か(0からカウント)」は 7 - (file - 'a') で計算できます。(fileがaなら7、bなら6、……hなら0、になります。)

また、「下から何行目か(0からカウント)」は 8 - rankで計算できます。

これを用いて、指定位置のみ1が立っているビットボードを作る関数を作成します。

// 座標をuint64_tのposに変換する関数
uint64_t PosTranslate(char file, int rank){
	int file_num;
	uint64_t pos;

	file_num = (int)(7 - file + 'a');

	pos = ( (uint64_t)1 << ( file_num + 8 * (8 - rank) ) );

	return pos;
}

さらに、座標の入力要求から受け取りまでをまとめた関数も作成します。

// 座標を受け取り、posを返す関数
uint64_t GetPos(){
	char file;	// 列番号(アルファベット)
	int rank;	// 行番号(数字)
	uint64_t pos;	// 指定箇所を示すビットボード

	printf("座標を入力してください。(例:f5)\n");
	scanf(" %c%d", &file, &rank);

	// 受け取った座標からビットボードを生成
	pos = PosTranslate(file, rank);
	return pos;
}

scanfで%cの前に半角スペースを入れてるのは改行の読み飛ばしのためです。(こうしないと入力できなくなることがあるのです。詳しくは「scanf 改行 読み飛ばし」などでググってください。)

見て分かる通り、異常な入力に対する例外処理を行っていません。

なので、例えば「Z99」などの入力がきた時にどうなるかは保証できません。

本来はif(file < 'a' || file > 'h' || rank < 1 || rank > 8) return 3; などとしておいて、3が返ってきたら例外処理、というようにすべきです。(なぜ例外が3なのかというと、符号なし整数なので-1が使えないのですが、場所をあらわすビットボードなので1が二つ以上たっている可能性は本来ありません。ということで、1が二つ立っている3(二進数で11)をエラーのサインに使うことで正常値と判別がつきます。)

手元の原作プログラムではこれは行っていないのですが、せっかくなのでやりましょうか。

で、今度は石を置く関数を作らないといけません。

今回は「どこにでも置けて、石がある場所にも上書きして置ける」という雑な関数を作ります。

ビットボードで石を置くときは、手番が先手番なら board->black = board.black | pos; すなわち board->black |= pos; で実現できます(|はビットORの演算子です。posとビットORを取っているので、blackにおけるposの場所のビットを強制的に1にして他の場所をそのまま保てる)。手番が後手番のときはboard->white &|= pos; です。

// 石を置く関数 posは絶対に合法手
void Put(BOARD *board, uint64_t pos){
	switch(board->teban){
		case SENTE:
			board->black |= pos;
			board->teban = GOTE;
			break;
		case GOTE:
			board->white |= pos;
			board->teban = SENTE;
			break;
		default:
			break;
	}
	board->move_num++;
	return;
}

コメントに「posは絶対に合法手」というのは、ここでは合法判定はしないから事前にしとけよ、という意味のメモです。

これをmain関数に実装します。

複数回繰り返したいので、今回はとりあえずforループで5回繰り返してみます。全ソースコードを掲載します。

#include <stdio.h>

#define INPUT_ERROR 3

// 手番を表す列挙型
typedef enum TEBAN{
	SENTE = -1,
	GOTE = 1,
	GAME_OVER = 0
}TEBAN;

// 局面を表す構造体
typedef struct BOARD{
	uint64_t black, white;	// 黒石・白石のビットボード
	TEBAN teban;		// 手番
	int move_num;			// 何手動いたか(手数)
}BOARD;

// 関数プロトタイプ宣言
void Init(BOARD *board);						// 局面を初期化する関数
void ShowBoard(BOARD *board);					// 盤面を表示する関数
uint64_t GetPos();								// 座標を入力させ、posを返す関数
uint64_t PosTranslate(char file, int rank);		// 座標をuint64_tのposに変換する関数
void Put(BOARD *board, uint64_t pos);			// 石を置く関数 posは絶対に合法手

int main(void){
	BOARD board;
	uint64_t pos;
	int i;

	// 局面を初期化
	Init(&board);

	// 局面を表示
	ShowBoard(&board);

	// 石を置く
	for (i = 0; i < 5; i++){
		pos = GetPos();
		if( pos == INPUT_ERROR ){
			printf("エラーです。\n");
			continue;
		}
		Put(&board, pos);
		ShowBoard(&board);
	}
	return 0;
}

// 局面を初期化
void Init(BOARD *board){
	board->black = ((uint64_t)1<<28) + ((uint64_t)1<<35);
	board->white = ((uint64_t)1<<27) + ((uint64_t)1<<36);
	board->teban = SENTE;
	board->move_num = 0;
}

// 盤面を表示する関数
void ShowBoard(BOARD *board){
	int i, rank = 1;
	uint64_t pos = (uint64_t)1<<63;
	// 列番号
	printf("  a b c d e f g h\n");
	// 盤面表示
	for ( i = 0; i < 64 ; i++){
		// 行番号
		if(i % 8 == 0) printf("%d", rank++);
		// 盤面状態表示
		if( ( board->black & pos )!= 0) printf("黒");
		else if( ( board->white & pos ) != 0) printf("白");
		else printf("口");
		// 8回表示が終わるごとに改行
		if(i % 8 == 7) printf("\n");
		// posを一つずらす
		pos >>= 1;
	}
	// 手番表示
	printf("\n手番: ");
	switch(board->teban){
		case SENTE: printf("先手\n"); break;
		case GOTE: printf("後手\n"); break;
		default: break;
	}
}

// 座標を入力させ、posを返す関数
uint64_t GetPos(){
	char file;	// 列番号(アルファベット)
	int rank;	// 行番号(数字)
	uint64_t pos;	// 指定箇所を示すビットボード

	printf("座標を入力してください。(例:f5)\n");
	scanf(" %c%d", &file, &rank);

	// 不正値ならエラーを返す
	if(file < 'a' || file > 'h' || rank < 1 || rank > 8) return INPUT_ERROR;

	// 受け取った座標からビットボードを生成
	pos = PosTranslate(file, rank);
	return pos;
}

// 座標をuint64_tのposに変換する関数
uint64_t PosTranslate(char file, int rank){
	int file_num;
	uint64_t pos;

	file_num = 7 - file + 'a';

	pos = ( (uint64_t)1 << ( file_num + 8 * (8 - rank) ) );

	return pos;
}

// 石を置く関数 posは絶対に合法手
void Put(BOARD *board, uint64_t pos){
	switch(board->teban){
		case SENTE:
			board->black |= pos;
			board->teban = GOTE;
			break;
		case GOTE:
			board->white |= pos;
			board->teban = SENTE;
			break;
		default:
			break;
	}
	board->move_num++;
	return;
}

結構長くなりましたね。コンパイルして実行すると確かに指定した場所に石が置かれていると思います。(石の表現は僕の環境の黒・白・口を採用しています。)

不正な入力に対してもエラーです。と表示できています。(ただし、ループ回数は消費します。)

エラーについて、return 3;ではなく最初に#define INPUT_ERROR 3 でマクロ定義してからreturn INPUT_ERROR; をし、if(pos == INPUT_ERROR)で判定をしていますが、これはいわゆる「マジックナンバー」を防いでいるのです。

プログラミングにおけるマジックナンバーとは、「具体的な数字であるが、特定の状況のみにおいて成立するものであったり、他人が見ても意味が分からない数字」というような意味です。(Wikipediaにマジックナンバー (プログラム)というページがあります。)

基本的にマジックナンバーをコード内に書くのは良くないとされています。マクロ定義やenumで定義するか、変数に代入して使いましょう。

さて、今回は場所の入力受け取りをビットボード変換、石を置く処理を実装しました。次回はどうしましょうかね。ついに「合法手一覧の取得」にいきますかね……。

合法手一覧の取得とは、ある局面において「ルール上置ける場所(1つ以上の敵石を挟める場所)」のみに1が立ったビットボードを生成することです。

これができると、合法手一覧をvalidとしたときに、GetPos()で受け取ったposに対してpos&validが0でないときにposが合法手であると判定することができますし、validが0ならパスが発生する局面であるとか、連続パス(どちらの手番で見てもvalidが0)ならゲーム終了など、色々な判定ができるようになります。

でもこれ、コード長いし難しいんですよね……。ビットボードの神髄って感じですが。この関数だけで82行あります。

まあ、頑張っていきましょう。では。

スポンサーリンク
レクタングル(大)
レクタングル(大)

シェアする

  • このエントリーをはてなブックマークに追加

フォローする