この記事は SLP KBIT Advent Calendar 2016 の5日目の記事です。
こういった分野に片足を突っ込み始めた程度の素人に毛がそこそこ生え始めたぐらいの人間ですので、記事の内容に関してはある程度目をつぶっていただけたらと思います。(というかこんな記事を作成することからして初体験だったり)
はじめに
こんにちは、私は香川大学工学部の1年、SLPでGMVに所属しております。
…という自己紹介は必要でしょうか?まあ一応初めての記事なので必要ということにしておきますね。
さて、今回はタイトルの通り、C言語で五目並べを作ってみたという記事でございます。
というのも、現在GMVに所属している1年生なので、近いうちにC言語でオセロを作ることになっているのですが、その予習と称して、比較的単純と思われる五目並べに挑戦してみようとなったわけです。
しかしここで一昨年の SLP KBIT Advent Calendar 2014 にて先輩が五目並べのプログラミングについて解説していらっしゃるではありませんか、ということに後から気づきました。ですがまあ、今回のこの記事に関しては、あくまで「だんだんC言語のプログラミングが分かってきた気がするので実際に自力で五目並べの作成に挑戦してみた」という体でお送りしてますので、(内容のレベル的にも)まったく違うものだと思っていただいて大丈夫だと思います。たぶん。(他の記事で内容がほとんど同じになってる可能性もありますけど)
仕様
とりあえずゲームのルールですが、五目並べといってもなんだかんだで色々とルールに種類があるみたいです。ですが今回はとりあえず「まず遊べること」を念頭に置いているため、複雑なことは考えないようにしました。
- 盤面の大きさは20×20
- 縦横斜めの4方向いずれかにおいて5つ並べれば勝利
- 6連以上は無効
- 禁じ手には非対応
こんな感じですかね。禁じ手に非対応なのは実は私自身が禁じ手の内容についてあまりよく知らないというのもあったりはしますが、そんなのは置いておきます。
コーディング
というわけでソースコードを少しずつピックアップしながら内容を説明してみたいと思います。
まずは盤面の大きさを定義しておきます。
#define BOARD_SQUARE 20 //盤面の広さ設定
本来であればもっといろいろと定義しておくべきなのかもしれませんが、どの部分を変数で、どの部分を定数で行えばよいのかがいまいち不明瞭なので、とりあえずこれだけにしておきました。
続いて関数の定義です。まあメイン関数の前にあらかじめ呼び出しておくだけなので内容は後述ですが。
void Board_Output(void); //盤面の出力
void Game(void); //入力処理
void Board_Scan(int x, int y); //盤面の調査
int Board_Scan_Sub( int x, int y, int move_x, int move_y ); //置いた場所を中心に並ぶ個数の調査
void Finish(void); //ゲーム終了処理
今回は5つ用意しました。これらとあとメイン文ですね。
と、その前に、盤面の二次元配列変数とプレイヤーの手番を示す変数が関数のあちらこちらで使用するために引数等々の記述が少し面倒となり、
int board[BOARD_SQUARE][BOARD_SQUARE] = {{0}};
int player_number = 1;
グローバル変数として宣言しました。 なんという怠け者でしょうか。
では、ここからはそれぞれの関数の内容について記述していきます。
main文
int main(void){
int i;
Board_Output();
printf("ゲームスタート!\n");
for( i = 0; i < (BOARD_SQUARE * BOARD_SQUARE); i++ ){
Game();
Board_Output();
if( player_number < 2 ) player_number++;
else player_number = 1;
}
return 0;
}
基本的にはこんな感じです。
なお、手番を示す変数player_numberの値変更をmain文の中で処理していますが、実際はグローバル変数なのでこんなところで処理させる必要はない気がします。これはもともと手番の変数をグローバル変数でなくローカル変数として扱っていた名残です。とりあえず動いたのでそのまま修正忘れてたとも言います
盤面の出力
//盤面の出力---------------------------------------
void Board_Output(void){
int i, j;
printf(" ");
for( i = 0; i < BOARD_SQUARE; i++ ){
printf("%2d",i);
}
puts("");
for( i = 0; i < BOARD_SQUARE; i++ ){
printf("%2d",i);
for( j = 0; j < BOARD_SQUARE; j++ ){
switch( board[j][i] ){
case 0: printf("・"); break;
case 1: printf("○"); break;
case 2: printf("●"); break;
}
}
puts("");
}
puts("");
}
出力の処理ですね。
後述の入力処理では座標指定となるため、座標が分かるように盤面の上と左にX座標、Y座標の数字が出るようになっています。(ちなみに座標が2桁になるとスペースがなくなるため非常に見づらくなります、これは根本的な解決方法が思いつかなかったため放置しましたそのままです。)
入力処理
//入力処理---------------------------------------
void Game(void){
int x, y;
printf("%dP(",player_number);
switch( player_number ){
case 0: printf("・"); break;
case 1: printf("○"); break;
case 2: printf("●"); break;
}
printf(")のターンです。\n");
while(1){
while(1){
printf("置く場所を決めてください(x y) "); scanf("%d %d",&x ,&y);
if( x >= 0 && x < BOARD_SQUARE && y >= 0 && y < BOARD_SQUARE ) break;
else printf("その場所には置けません\n");
}
if( board[x][y] == 0 ){
board[x][y] = player_number;
break;
} else printf("その場所には置けません\n");
}
Board_Scan(x, y);
}
入力処理ですね。
今思えばこの部分while文で二重ループにしてますけど、if文重ねればよかったんですかね。無意味なことをした気がします。
また、ここで入力した座標データをmain文に返さずにそのまま盤面の調査の関数に渡しています。関数から値を返させるやり方は分かるのですが、関数から複数の値を返させるやり方は分からなかったのでこのような形にしたのですが、何かいい方法はあるのでしょうか?関数から関数へと値を飛ばしていくがために色々あってローカル変数からグローバル変数に変更するに至ったりしたのですが。
盤面の調査
//盤面の調査(5個並んだかの調査)---------------------------------------
void Board_Scan( int x, int y ){
int n[4]; //8方向(直線4本分)に並んだ数
int move_x, move_y;
int i;
move_x = 1; move_y = 1; //[\]方向
n[0] = Board_Scan_Sub( x, y, move_x, move_y );
move_x = 0; move_y = 1; //[│]方向
n[1] = Board_Scan_Sub( x, y, move_x, move_y );
move_x = 1; move_y = 0; //[─]方向
n[2] = Board_Scan_Sub( x, y, move_x, move_y );
move_x = -1; move_y = 1; //[/]方向
n[3] = Board_Scan_Sub( x, y, move_x, move_y );
for( i = 0; i < 4; i++ ){
if(n[i] == 5) Finish();
}
}
int Board_Scan_Sub( int x, int y, int move_x, int move_y ){
int n = 1; //置いた場所の1個分で初期化
int i;
for( i = 1; i < 5; i++ ){
if( board[ x + (move_x * i) ][ y + (move_y * i) ] == player_number ) n += 1;
else break;
}
for( i = 1; i < 5; i++ ){
if( board[ x + (-1*move_x * i) ][ y + (-1*move_y * i) ] == player_number ) n += 1;
else break;
}
return n;
}
盤面の調査と5個並んでいるかの調査の関数の両方ですね。
正直ここが一番自信がないです。特にSubとして関数を使用していますが、これはわざわざ関数にする必要はなかったんじゃないかと今でも思います。最初は繰り返し文を使おうかと思ったのですが、縦横斜めの4パターンを繰り返し文によって調査させるやり方が終始思い浮かばず、臨時処置としてSubとなる関数を作成しました。何かいい方法はないものでしょうか…。
ゲーム終了処理
//決着~ゲーム終了---------------------------------------
void Finish(void){
Board_Output();
printf("%dP(",player_number);
switch( player_number ){
case 0: printf("・"); break;
case 1: printf("○"); break;
case 2: printf("●"); break;
}
printf(")の勝利です!\n");
exit(0);
}
ゲーム終了処理ですね。
この部分に至るには実はmain文から入力処理関数に入り、そこからさらに盤面調査の関数に入り、さらにそこから直接この関数に飛んでくる形になってしまっています。個人的にはゲーム終了処理はmain文に何らかの判定を返してreturn 0で終了させるつもりだったのですが、何分関数の中の関数から飛んできちゃってますから、値を返そうにもなかなか面倒なことになりかねないと思い、とりあえずexit関数とやらに頼ることにしました。
なお、ここだけは本気で分からなかったので調べました。「一から自力で」が目標だったんですが、これは非常にグレーゾーン\(^o^)/
実行
難点はいくつかありましたが、無事に実行できました!
画像はその時に友人と対戦してみたものです。1P(黒)が友人で2P(白)が自分です。まあ見れば分かりますがこの直後に負けました(笑)
+α
さて、「自力で五目並べ作成に挑戦」(途中グレーゾーンありましたが)はここまでなんですが、このあと少し遊び心を加えたりしています。
前置き(?)読まなくても大丈夫です
ボードゲームにおいて、オセロや五目並べ、囲碁、将棋、チェス等などは有名ですが、これらにおいてもさらに分類はできると思います。将棋やチェスは盤面において「プレイヤーの向き」が関係してきます。反してオセロや五目並べ、囲碁は「プレイヤーの向き」はあまり関係がないと思います。ようはオセロや五目並べなんかはプレイヤーが盤面を中心として上下左右どこにいてもとくに変わらないということです。これは同時にプレイヤーが何人いようが問題ないということを意味する、、、かもしれません(?)
本題
細かいことを省くなら、「五目並べはルール上、3人でも4人でも遊べるのではないか」ということです。まあ公式ルール云々いわれるとどうしようもないですが、遊ぶことだけを考えるなら何人いたっていいはずです。
ということで、ゲーム開始時に参加プレイヤーの人数を選択できるようにしてみようと思い至りました。
当初はmain文を少し改良して
int main(void){
int player_totalnumber;
int i;
while(1){
printf("プレイヤーの人数を入力してください:"); scanf("%d",&player_totalnumber);
if( player_totalnumber >= 1 ) break;
}
Board_Output();
printf("ゲームスタート!\n");
for( i = 0; i < (BOARD_SQUARE * BOARD_SQUARE); i++ ){
Game();
Board_Output();
if( player_number < player_totalnumber ) player_number++;
else player_number = 1;
}
return 0;
}
という形にして、後に続くswitch文を
switch( player_number ){
case 0: printf("・"); break;
case 1: printf("○"); break;
case 2: printf("●"); break;
case 3: printf("◎"); break;
case 4: printf("□"); break;
case 5: printf("■"); break;
case 6: printf("◇"); break;
case 7: printf("◆"); break;
case 8: printf("△"); break;
case 9: printf("▲"); break;
case 10: printf("▽"); break;
case 11: printf("▼"); break;
case 12: printf("☆"); break;
case 13: printf("★"); break;
}
という書き方をすることで完成としようとしてたのですが、これは人数を増やすごとに一行一行書き加える必要があるので面倒です。(ふつうそんなに大人数でやることなんてないですが)
ということで
- それぞれのプレイヤーに駒を数値として割り振り
- 出力の際には与えられた数値をコードとする文字を出力
- 割り振る数値には乱数を適用
という形で、出力は文字コードに頼ることにしてみました。
まず新たに数値を定義
#define PIECE_HALF1 0 //コマ表示(全角文字)1バイト目
#define PIECE_HALF2 1 //コマ表示(全角文字)2バイト目
#define PIECE 2 //コマ表示における全角1文字分のデータ量
出力するのは全角文字なので、数値(16進数2桁)を2つ用意する必要があります。
続いて駒を割り振る処理のための関数を定義、プレイヤーそれぞれに対応した駒の数値を格納する二次元配列をめんどいのでグローバル変数として宣言しておきます。
void Player_Piece(int player_totalnumber); //コマ割り振り
int player_piece[PIECE][BOARD_SQUARE * BOARD_SQUARE] = {{0}};
main文には使用する乱数を毎回違うものにするための処理と、プレイヤーの人数を決定した直後に駒を割り振る関数に飛ぶように設定しました。
int main(void){
int player_totalnumber;
int i;
srand((unsigned)time(NULL));
while(1){
printf("プレイヤーの人数を入力してください:"); scanf("%d",&player_totalnumber);
if( player_totalnumber >= 1 ) break;
}
Player_Piece( player_totalnumber );
そして割り振りの関数はこんな感じになりました。
//コマの割り振り(4P以降ランダム)---------------------------------------
void Player_Piece(int player_totalnumber){
int i = 0, j;
int f = 0; //フラッグ
int t1, t2; //一時変数
//何もない部分を・にする
player_piece[PIECE_HALF1][i] = 0x81;
player_piece[PIECE_HALF2][i] = 0x45;
i += 1;
//1Pのコマを○にする
player_piece[PIECE_HALF1][i] = 0x81;
player_piece[PIECE_HALF2][i] = 0x9b;
i += 1;
//2Pのコマを●にする
player_piece[PIECE_HALF1][i] = 0x81;
player_piece[PIECE_HALF2][i] = 0x9c;
i += 1;
//3Pのコマを◎にする
player_piece[PIECE_HALF1][i] = 0x81;
player_piece[PIECE_HALF2][i] = 0x9d;
i += 1;
//4P以降のコマのデータは乱数で適当に割り振り
for(; i < player_totalnumber; i++ ){
while(1){
while(1){
t1 = rand()%110 + 0x81;
if( t1 < 0x85 || t1 > 0x86 && t1 < 0xa0 || t1 > 0xdf && t1 < 0xeb || t1 > 0xec ){
break;
}
}
t2 = rand()%94 + 0x40;
for( j = 0; j < i; j++ ){
if( player_piece[PIECE_HALF1][j] == t1 && player_piece[PIECE_HALF2][j] == t2 ) f += 1;
}
if( f == 0 ){
player_piece[PIECE_HALF1][i] = t1;
player_piece[PIECE_HALF2][i] = t2;
break;
}
f = 0;
}
}
}
分かりにくくなるのもどうかと思ったので3人目までは固定させています。肝心なところに脚注がほとんどありませんが、仕様です。わ、忘れてたわけじゃないんだからねっ
ちなみにこの乱数の取得部分ですが、一応文字コード表(S-JIS)見ながら対応させたので基本的には大丈夫なんですが、文字コードの設定上「・」が大量に存在するのでその値を引いたプレイヤーは必然的に目隠しプレイになります。難易度が跳ね上がります。しかし時間の都合と私の実力不足でその部分の対応はできませんでした。
本当は一度文字コード表とにらめっこしながら対応させようとしたのですが、「・」がかたまってる箇所だけを抜き取っていこうとした結果、if文の条件式がとてもとても長くなりそうだったので諦めました。
何かいい方法はないものか…。
そして出力の際のswitch文があった場所ですが、
printf("%c%c", player_piece[PIECE_HALF1][player_number], player_piece[PIECE_HALF2][player_number]);
こんな感じで随分とスマートになってくれました。
実行するとこうなります。
プレイヤー人数を50人にしてあります。(3人ほど「・」になってますね)
一応ボードのマス目の数だけプレイヤーの人数は設定できるようになってます。まあまずそうなると勝敗がつきませんが。
ちなみに1人プレイも可能です。
これでたとえひとりぼっちでも五目並べができますね!(棒)
コード
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define BOARD_SQUARE 20 //盤面の広さ設定
#define PIECE_HALF1 0 //コマ表示(全角文字)1バイト目
#define PIECE_HALF2 1 //コマ表示(全角文字)2バイト目
#define PIECE 2 //コマ表示における全角1文字分のデータ量
void Board_Output(void); //盤面の出力
void Game(); //入力処理
void Board_Scan(int x, int y); //盤面の調査
int Board_Scan_Sub( int x, int y, int move_x, int move_y ); //置いた場所を中心に並ぶ個数の調査
void Finish(void); //ゲーム終了処理
void Player_Piece(int player_totalnumber); //コマ割り振り
int board[BOARD_SQUARE][BOARD_SQUARE] = {{0}};
int player_piece[PIECE][BOARD_SQUARE * BOARD_SQUARE] = {{0}};
int player_number = 1;
//メイン関数-----------------------------------------
int main(void){
int player_totalnumber;
int i;
srand((unsigned)time(NULL));
while(1){
printf("プレイヤーの人数を入力してください:"); scanf("%d",&player_totalnumber);
if( player_totalnumber >= 1 ) break;
}
Player_Piece( player_totalnumber );
Board_Output();
printf("ゲームスタート!\n");
for( i = 0; i < (BOARD_SQUARE * BOARD_SQUARE); i++ ){
Game();
Board_Output();
if( player_number < player_totalnumber ) player_number++;
else player_number = 1;
}
return 0;
}
//盤面の出力---------------------------------------
void Board_Output(void){
int i, j;
int p; //プレイヤー番号
printf(" ");
for( i = 0; i < BOARD_SQUARE; i++ ){
printf("%2d",i);
}
puts("");
for( i = 0; i < BOARD_SQUARE; i++ ){
printf("%2d",i);
for( j = 0; j < BOARD_SQUARE; j++ ){
p = board[j][i];
printf("%c%c", player_piece[PIECE_HALF1][p], player_piece[PIECE_HALF2][p]);
}
puts("");
}
puts("");
}
//入力部分---------------------------------------
void Game(void){
int x, y;
printf("%dP(",player_number);
printf("%c%c", player_piece[PIECE_HALF1][player_number], player_piece[PIECE_HALF2][player_number]);
printf(")のターンです。\n");
while(1){
while(1){
printf("置く場所を決めてください(x y) "); scanf("%d %d",&x ,&y);
if( x >= 0 && x < BOARD_SQUARE && y >= 0 && y < BOARD_SQUARE ) break;
else printf("その場所には置けません\n");
}
if( board[x][y] == 0 ){
board[x][y] = player_number;
break;
} else printf("その場所には置けません\n");
}
Board_Scan(x, y);
}
//盤面の精査(5個並んだかの調査)---------------------------------------
void Board_Scan( int x, int y ){
int n[4]; //8方向(直線4本分)に並んだ数
int move_x, move_y;
int i;
move_x = 1; move_y = 1; //[\]方向
n[0] = Board_Scan_Sub( x, y, move_x, move_y );
move_x = 0; move_y = 1; //[│]方向
n[1] = Board_Scan_Sub( x, y, move_x, move_y );
move_x = 1; move_y = 0; //[─]方向
n[2] = Board_Scan_Sub( x, y, move_x, move_y );
move_x = -1; move_y = 1; //[/]方向
n[3] = Board_Scan_Sub( x, y, move_x, move_y );
for( i = 0; i < 4; i++ ){
if(n[i] == 5) Finish();
}
}
int Board_Scan_Sub( int x, int y, int move_x, int move_y ){
int n = 1; //置いた場所の1個分で初期化
int i;
for( i = 1; i < 5; i++ ){
if( board[ x + (move_x * i) ][ y + (move_y * i) ] == player_number ) n += 1;
else break;
}
for( i = 1; i < 5; i++ ){
if( board[ x + (-1*move_x * i) ][ y + (-1*move_y * i) ] == player_number ) n += 1;
else break;
}
return n;
}
//決着~ゲーム終了---------------------------------------
void Finish(void){
Board_Output();
printf("%dP(",player_number);
printf("%c%c", player_piece[PIECE_HALF1][player_number], player_piece[PIECE_HALF2][player_number]);
printf(")の勝利です!\n");
exit(0);
}
//コマの割り振り(4P以降ランダム)---------------------------------------
void Player_Piece(int player_totalnumber){
int i = 0, j;
int f = 0; //フラッグ
int t1, t2; //一時変数
//何もない部分を・にする
player_piece[PIECE_HALF1][i] = 0x81;
player_piece[PIECE_HALF2][i] = 0x45;
i += 1;
//1Pのコマを○にする
player_piece[PIECE_HALF1][i] = 0x81;
player_piece[PIECE_HALF2][i] = 0x9b;
i += 1;
//2Pのコマを●にする
player_piece[PIECE_HALF1][i] = 0x81;
player_piece[PIECE_HALF2][i] = 0x9c;
i += 1;
//3Pのコマを◎にする
player_piece[PIECE_HALF1][i] = 0x81;
player_piece[PIECE_HALF2][i] = 0x9d;
i += 1;
//4P以降のコマのデータは乱数で適当に割り振り
for(; i < player_totalnumber; i++ ){
while(1){
while(1){
t1 = rand()%110 + 0x81;
if( t1 < 0x85 || t1 > 0x86 && t1 < 0xa0 || t1 > 0xdf && t1 < 0xeb || t1 > 0xec ){
break;
}
}
t2 = rand()%94 + 0x40;
for( j = 0; j < i; j++ ){
if( player_piece[PIECE_HALF1][j] == t1 && player_piece[PIECE_HALF2][j] == t2 ) f += 1;
}
if( f == 0 ){
player_piece[PIECE_HALF1][i] = t1;
player_piece[PIECE_HALF2][i] = t2;
break;
}
f = 0;
}
}
}
おわりに
こんな感じでしょうか。
個人的には当初の目的は果たせたと思うので満足しています。+αの部分はほとんど遊び心が爆発しただけのものではありますが、思いついたままにコードをつけたして、変更していった先でしっかりと動作してくれると嬉しいものです。
今後の課題としては
- 現状の問題点の解消
- CPUのAIの実装
- コマンドプロンプト上でなく、画面上で動くゲーム化
といったところです。3つ目に関しては今は置いておくとして、AIの実装ができればリアルでは実現しにくい大人数での五目並べ(笑)をさせたりできそうなのでいずれやってみたいですね。あとはコードの読みやすさの改善とかですかね。巷で聞く(?)「マジックナンバー」なるものが発生している気がしますし、最初の方で書いたように定数と変数の使い分けがおぼろげだったりもします。脚注を入れる場所だったりもそうですし、やはり別の人が見たときに見やすい丁寧なコードが組めるようになりたいものです。打ち込んだものを自分で見返してても分からなくなったりしてますし