58
Copyright 2001 守屋悦朗 総合課題「コンピュータゲームを作る」 オセロ 趣旨:簡単なコンピュータゲームの作成を通して、実践的にCによるプログラミングを 学習することが目的である。 後期は標記の課題を中心にして講義を進める。コンピュータと人間が対戦する「オセロ ゲーム」のプログラムを作ることを最終目標として、毎回、そのプログラムの一部分を順 次作っていく。その過程で、C言語の機能やプログラム開発の技法について on demand 学んでゆきたい(主たる目標であるデータ構造やアルゴリズムについては、この課題と並 行して学んでゆきたい)。 ほぼ毎回、その日に作ることを目標としたプログラム部分を完成させることを課題とす る。完成しなかった場合でも、翌週、当方から完成したものを配布するので、それを利用 して次回の課題に取り組むこと(ただし、できるだけ自分の力で完成させることが望まし い)。2人でチームを作ってもよいものとする→申し出ること。 グラフィック機能があるコンピュータを使えば、市販のファミコンゲームのようなもの を作ることもできる(難しさはほとんど変わらない)が、現環境ではそれが不可能である。 しかし、オセロゲームでも本質的な所は同じである。授業でやることを参考にして、オセ ロゲームの代わりに、五目並べや碁の対戦プログラムを作ってもよい。 オセロゲームとは オセロ1972 年ころ長谷川五郎が、古くからイギリスにあった類似のゲームをヒントに 考案したものである。1977 年からは毎年、世界選手権大会が開催されている。 縦横8×8のます目に区画された正方形の盤を使い、表裏が白色と黒色になっているコ マ(石)64 個を使用して、二人で争うゲームである。 ゲーム開始前の盤面 ●が先手 〇が後手 1

総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

  • Upload
    others

  • View
    5

  • Download
    0

Embed Size (px)

Citation preview

Page 1: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

Copyright 2001 守屋悦朗

総合課題「コンピュータゲームを作る」 オセロ

趣旨:簡単なコンピュータゲームの作成を通して、実践的にCによるプログラミングを

学習することが目的である。

後期は標記の課題を中心にして講義を進める。コンピュータと人間が対戦する「オセロ

ゲーム」のプログラムを作ることを最終目標として、毎回、そのプログラムの一部分を順

次作っていく。その過程で、C言語の機能やプログラム開発の技法について on demand に

学んでゆきたい(主たる目標であるデータ構造やアルゴリズムについては、この課題と並

行して学んでゆきたい)。

ほぼ毎回、その日に作ることを目標としたプログラム部分を完成させることを課題とす

る。完成しなかった場合でも、翌週、当方から完成したものを配布するので、それを利用

して次回の課題に取り組むこと(ただし、できるだけ自分の力で完成させることが望まし

い)。2人でチームを作ってもよいものとする→申し出ること。

グラフィック機能があるコンピュータを使えば、市販のファミコンゲームのようなもの

を作ることもできる(難しさはほとんど変わらない)が、現環境ではそれが不可能である。

しかし、オセロゲームでも本質的な所は同じである。授業でやることを参考にして、オセ

ロゲームの代わりに、五目並べや碁の対戦プログラムを作ってもよい。

オセロゲームとは

オセロは 1972年ころ長谷川五郎が、古くからイギリスにあった類似のゲームをヒントに

考案したものである。1977年からは毎年、世界選手権大会が開催されている。

縦横8×8のます目に区画された正方形の盤を使い、表裏が白色と黒色になっているコ

マ(石)64個を使用して、二人で争うゲームである。

ゲーム開始前の盤面

〇 ● ●が先手

● 〇 〇が後手

1

Page 2: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

ルール:まず、盤の中央に白黒2個ずつの石を上図のように置く。黒が先手で開始し、

交互に、必ず相手のコマを縦、横、斜めのいずれかに挟むような位置に自分の石を置く。

挟んだ相手の石は裏返して自分の石とする。これを繰返して、全部の石を並べ終わるか、

あるいは双方とも打つ手がなくなったときに対局は終了し、その時点で石が多い方が勝ち

である。勝負の途中で、一方だけが石を置くことができなかった場合は「パス」となり、

打てるところが生じるまで他方が連続して打つ。

課題A:オセロ盤を表示する。

上図のような盤面を画面(グラフィック画面ではなく、文字を書き出すテキスト画面で

あることに注意!)に書き出すことが課題Aである。

まず、盤面がどのようになっているか(白黒の石がどこに置かれているか)を表わすた

めに、2次元配列 board を用いる:

int board[10][10];

各配列要素のとる値は

0 NOT_USED (まだどちらの石も置いてない)

1 HUMAN (「人間」の石が置いてある)

-1 COMPUTER (「コンピュータ」の石が置いてある)

2 OUTSIDE (盤の外である)

のいずれかとする。これらの値は#define指令を使って上記のような名前を付けておく:

#define NOT_USED 0

...

#define OUTSIDE 2

board[x][y]の値が上記のいずれであるかによって、ます目位置(x,y)がどのような状態

であるかを表わす。盤の範囲は 1≦x≦8, 1≦y≦8 であり、盤の上下左右(x=0, x=9, y=0,

y=9の部分)には盤外であることを表わす値 OUTSIDE を入れておく(このようなものを番

兵という。後程、課題Cをやれば分かるが、盤の内と外の境を監視する役割を果たすため、

そのように呼ばれる。配列が 8×8ではなく、10×10にとってあるのは、盤の周囲に番兵を

置くためである)。こうしておくことによって、盤をはみ出たか否かの判定処理を特別扱

いせずに処理でき、プログラムが簡単になる(それが具体的にどのようなことかは、課題

Cをやると分かる)。

次に、どちらが先手かを表わす変数 first_playerを宣言しておく:

int first_player;

この課題においては、人間、コンピュータのどちらを先手としてもよいが、仮にコンピュ

ータを先手とするのであれば

first_player=COMPUTER;

2

Page 3: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

とする。

まだ、main関数の書き方しかやっていないので、上記のことを行なう main関数だけから

なるプログラムを書け。出力する盤面の状況は上図と同じではなくてもよい(適当に設定

すればよい)。盤面の設定は、配列の初期値として与えるか、または代入文で設定するこ

と。例えば、上図と同じにするのなら、先ずはじめに、盤の内側の各ます目をクリア(値

NOT_USEDを設定)し、かつ、盤の周囲を値 OutsideBOARDに設定する:

for (x=1; x<=8; x++)

for (y=1; y<=8; y++)

board[x][y]=NOT_USED;

for (x=0; x<10; x++)

board[x][0]=board[x][9]=OUTSIDE;

for (y=0; y<10; y++)

board[0][y]=board[9][y]=OUTSIDE;

次に、代入文を使って

board[4][4]=board[5][5]=HUMAN;

board[4][5]=board[5][4]=COMPUTER;

とすると、対局前の盤の初期状況を設定したことになる(この場合、先手=●を COMPUTER

としている)。あるいは、変数 first_playerの値を使って

board[4][4] = board[5][5] = first_player;

board[4][5] = board[5][4] = -first_player;

とすると、もっとエレガントである。

盤を表示するためには、グラフィック画面のように線を引くことができないので、次の

ように文字を使って表わす:

x→ 1 2 3 4 5 6 7 8

+--+--+--+--+--+--+--+--+

1 | | | | | | | | |

+--+--+--+--+--+--+--+--+

2 | | | | | | | | |

+--+--+--+--+--+--+--+--+

3 | | | | | | | | |

+--+--+--+--+--+--+--+--+

4 | | | |〇|●| | | |

+--+--+--+--+--+--+--+--+

5 | | | |●|〇| | | |

. . .

+--+--+--+--+--+--+--+--+

8 | | | | | | | | |

↑ +--+--+--+--+--+--+--+--+

y

このように、ます目の位置が分かるように、座標を振るとよい。

3

Page 4: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

できれば、

〇=コンピュータ

●=人間

のように、石の所有者を盤の右側に表示するとなおよい。それは、first_player が HUMAN

であるか COMPUTERであるかを判定するだけで簡単にできる。

作ったプログラムは

~/c/game/board.c

にセーブせよ。今回作るプログラムは、次の課題Bにおいて修正し、いくつかの独立した

関数とする。

(課題Aでの習得項目)

配列(内部配列のみ)

#defineによる定数名定義

標準関数 systemとヘッダファイル<stdlib.h>

#include <stdio.h>

#include <stdlib.h>

#include "decl.h"

extern int board[10][10];

extern int first_player;

void clear_board()

/************************************************************************

盤をクリアする

*************************************************************************/

{

int x,y;

clrscr(); /* system("clr"); */

for (x=0; x<=9; x++)

{

board[x][0]=board[x][9]=OUTofBOARD; /* 盤の上縁と下縁に置く番兵 */

}

4

Page 5: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

for (y=1; y<=8; y++)

{

board[y][0]=board[y][9]=OUTofBOARD; /* 盤の左右の縁に置く番兵 */

for (x=1; x<=8; x++)

{

board[x][y]=NOT_USED;

}

}

}

void print_board()

/************************************************************************

盤面を描く(先手・後手が誰かも表示する)

*************************************************************************/

{

int x,y;

clrscr(); /* 端末画面をクリアする */

printf("\n");

printf("%9cx→ 1 2 3 4 5 6 7 8\n", ' ');

for (y=1; y<=8; y++)

{

printf("%10c +---+---+---+---+---+---+---+---+\n", ' ');

printf("%10c%2d", ' ',y);

for (x=1; x<=8; x++)

{

switch(board[x][y])

{

case NOT_USED: printf("| "); break;

case SUSPENDED: printf("| ?"); break;

default:

if (board[x][y] == first_player) printf("| ●");

else printf("| 〇");

break;

}

}

printf("|");

5

Page 6: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

/* 先手・後手の表示 */

if (y==4)

{

switch (first_player)

{

case HUMAN: printf(" ●=あなた"); break;

case COMPUTER: printf(" ●=私"); break;

}

}

if (y==5)

{

switch (first_player)

{

case HUMAN: printf(" 〇=私"); break;

case COMPUTER: printf(" 〇=あなた"); break;

}

}

printf("\n");

}

printf("%10c +---+---+---+---+---+---+---+---+\n", ' ');

printf("%9cy↑\n\n", ' ');

}

6

Page 7: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

課題B:ソーティングを行なう関数を作る。

この時点では配列と関数についてよく理解してもらいたい。そのため、実際に使うのは

ずっと先になるが、標記の「関数」を作ることを課題とする。

ソーティング(sorting、整列化)とは、データを大きさの順に並びかえることでる。小さ

い順に並べかえることを昇順にソートするといい、大きい順に並びかえることを降順にソ

ートするという。ソーティングについてはいろんなアルゴリズムが知られているが、それ

については§12できちんと扱うこととし、ここでは効率については考えず、とりあえず動

くプログラムが作れればよい。

ソートしたいデータは配列

int s[N];

で与えられるものとする。N は配列のサイズであるが、とりあえず

#define N 100

としておこう。次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの

で、ソートしたいデータがソートされている状態に近いと効率がよい:

0 1 m m+1 i-1 i N-1

s0 s1 ... sm sm+1 ... si-1 si ... sN-1

この部分は昇順にソート済み

いま、s[0]~s[i-1]の部分はすでに昇順にソートされているとする。このとき、s[i]をこ

の中の適当な位置に挿入して s[0]~s[i]を昇順にソートされた状態にするには、次のよう

にすればよい。まず、

t=s[i]; j=i-1;

while (j>=0 && t<s[j]) {

s[j+1]=s[j]; j--;

}

として、s[m]≦t=s[i]となる最初の mを探す(この while文を抜け出たときの jの値+1が m

である)。こうして

0 1 m m+1 i-1 i N-1

s0 s1 ... sm * ... si-2 si-1 ... sN-1

この部分は昇順にソート済み

t=s[i]の入るべき位置*を求めたら、

s[j+1]=t;

として、そこに s[i]を入れれば s[0]~s[i]がソートされた状態になる。このことを i=1~

7

Page 8: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

N-1について行なえばよい:

for (i=1; i<N; i++) {

t=s[i]; j=i-1;

while (j>=0 && t<s[j]) {

s[j+1]=s[j]; j--;

}

s[j+1]=t;

}

配列 aとそのサイズ nを引数として受取り、以上のことを行なう関数

sort(int a[], int n)

を作れ。sortは aと nをパラメータとする関数であり、aは上記説明の sにあたるもので、

n は N にあたるものである。s と N は関数 sort を呼び出す側のものである(すなわち、s

をソートしたいとき、

sort(s,N);

と呼び出す)。

作成したプログラムは

~/c/game/sort.c

にセーブせよ。将来は、関数 sortの部分だけをファイルとして独立させる。

(課題Bでの習得項目)

配列と関数の基本

ソーティングとは何か

8

Page 9: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

課題C:盤に関する基本的処理を行なう関数を独立させる。

課題Aで作ったプログラムから以下の処理を行なう部分を関数として独立させよ:

void clear_board(void)

オセロ盤をクリアする(盤内を NOT_USEDに、盤外を OUTSIDEに設定する)

void initialize(void)

オセロ盤を初期設定する(盤をクリアしてから、中央に4個の石を置く)

void print_board(void)

端末画面をクリア後、オセロ盤の現局面を端末画面に書き出す

課題Aで作った board.c を使って上記の関数群を作れ。これらの関数の定義を含み、そ

れらが正しく動作するかどうかを確認する部分を main関数としたプログラムを作り、改め

~/c/game/board.c

にセーブし直せ。

求めるプログラムは、およそ次のような形となろう:

#define NOT_USED 0 /* 各種の定数名の定義 */

...

#define OUTSIDE 2

int board[10][10]; /* オセロ盤 */

int first_player; /* 先手プレーヤ */

void clear_board()

{

...

}

void initialize()

{

clear_board();

board[4][4] = board[5][5] = first_player;

board[4][5] = board[5][4] = -first_player;

}

void print_board()

{

...

}

9

Page 10: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

void main()

{

first_player=HUMAN; /* 仮に、人間を先手とする */

initialize(); print_board();

}

上例のように、盤を表わす配列 boardと先手を表わす変数 first_playerは他のどの関数

からも参照されるものなので、外部配列・外部変数とするのがよい。

(課題Cでの習得項目)

関数の定義と参照(引数も、返す値もない関数)

外部配列と外部変数

関数のプロトタイプ宣言

#include <stdio.h>

#include "decl.h"

extern int board[][10];

extern int first_player;

void print_board()

{

int x,y;

clrscr();

printf("\n");

printf("%9cx→ 1 2 3 4 5 6 7 8\n", ' ');

10

Page 11: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

for (y=1; y<=8; y++)

{

printf("%10c +---+---+---+---+---+---+---+---+\n", ' ');

printf("%10c%2d", ' ',y);

for (x=1; x<=8; x++)

{

switch(board[x][y])

{

case NOT_USED: printf("| "); break;

case SUSPENDED: printf("| ?"); break;

default:

if (board[x][y] == first_player) printf("| ●");

else printf("| 〇");

break;

}

}

printf("|");

if (y==4)

{

switch (first_player)

{

case HUMAN: printf(" ●=あなた"); break;

case COMPUTER: printf(" ●=私"); break;

}

}

if (y==5)

{

switch (first_player)

{

case HUMAN: printf(" 〇=私"); break;

case COMPUTER: printf(" 〇=あなた"); break;

}

}

printf("\n");

}

printf("%10c +---+---+---+---+---+---+---+---+\n", ' ');

printf("%9cy↑\n\n", ' ');

}

11

Page 12: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

課題D:相手の石をいくつ挟むかな?

座標 (x,y) で与えられるます目位置(x,yは int型変数で、1≦x≦8, 1≦y≦8)には、

まだどちらの石も置かれていないとし、いま、変数 player(playerは int型変数で、COMPUTER

または HUMAN だけを値としてとる)で示される方がプレーする順番だとする。このとき、

ます目位置 (x,y) に player の石を置くと、相手プレーヤの石を何個挟むことになるかを

計算したい。このことを行なう関数

int sandwitched_stones(int player, int x, int y)

を作れ。挟んだ相手石の個数を sandwitched_stones() の関数値とする。

1 2 3 4 5 6 7 8

1 〇 ●

2 ● ● 〇

3 〇 ● ● 〇

4 ● 〇 〇

5 ●

6 〇

7

8

例えば上図において、COMPUTERが先手(●)であり、いま、HUMAN(〇)の手番であるとする。

このとき、 player=HUMAN が (x,y)=(3,3) に〇を置くと(3,2), (4,3), (5,3), (4,4), (5,5)

の 5 個の●が挟まれるので、sandwitched_stones(HUMAN,3,3) の値は 5 である。同様に、

sandwitched_stones(COMPUTER,7,4) の値は 4 である。

さて、(x,y) に自分の石を置く(すなわち、board[x][y]=player; とする)ことによっ

て挟むことになる相手石の個数を数えるためには、(x,y) を始点として次図の8通りの方

向について「相手の石が何個続くか」を調べなければならない。このためには各方向ごと

に、(x,y) の隣りの点(例えば右方向なら (x+1,y), 左斜め上なら (x-1,y+1), etc.)を

始 点 と し て 調 べ る べ き 座 標 (xx,yy) を 1 ず つ そ の 方 向 に ず ら し な が ら

board[xx][yy]==-player が false になるまで、その個数をカウントする。false になっ

たとき board[xx][yy]==player が true ならば求めたカウントは挟んだ相手石の個数であ

り、そうでない場合は相手石を一つも挟んでいないのでカウントは0である(この後者の

場合、相手石が尽きた点は、未だどちらの石も置かれていない点か盤外かである。盤の周

囲に盤兵を置いてあることがこの部分のプログラムを簡単にしていることに注意する)。

このようにして8方向について求めたカウントを合計したものが sandwiched_stones

(player,x,y) の値である。

12

Page 13: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

1 2 3 4 5 6 7 8

1 〇 ●

2 ● ● 〇

3 ● ● ● ● 〇 (3,3)に〇を置くと

4 ● 〇 挟む相手石の個数は5

5 ●

6 〇

7

8

8方向の座標 (xx,yy) は次のようにすれば簡単に1ずつずらすことができる:

for (k=0; k<=8; k++) /* 各方向(8通り)について調べる */

{

if (k==4) continue; /* 除外ケース */

dx = k/3-1; dy = k%3-1; /* x,y方向の増分を計算する */

xx = x+dx; yy = y+dy; /* (xx,yy)が現在位置 */

/* 挟んでいる相手石の個数をカウントする */

この部分を作れ

}

上記の関数 sandwitched_stones を作ったらファイル utility.c にセーブせよ。また、

関数 sandwitched_stones の動作を確認するための main関数を作り、それは別のファイル

main.c にセーブせよ。動作の確認のためには、オセロの局面がある程度進んだ状態を作っ

ておく必要がある。

ついでに課題Cで作った3つの関数だけを独立させて(すなわち、main関数を削除して)

ファイル board.cとし、これを使ってオセロの盤面もプリントせよ。

3つのファイル main.cと utility.cと board.cを合わせたものが1つのプログラムであ

る。関数 main, sandwitched_stones, clear_board, print_boardのどれもが共通に使う配

列 boardは、main.cにおいて外部配列として

int board[10][10];

と宣言し、utility.c や board.cでは

extern int board[10][10];

と宣言しておく(つまり、utility.cからみると「別のファイルである main.cにおいて宣

言されている配列である」ことを宣言する)。また、main.c では関数 sandwitched_stones

や clear_board, print_baordのプロトタイプ宣言が必要である:

13

Page 14: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

#include <stdio.h>

int board[10][10];

int first_player;

int sandwitched_stones(int, int, int);

void clear_board(void);

void print_board(void);

void main()

{

clear_board();

board[4][3]=board[5][3]=board[6][3]=HUMAN;

board[4][4]=board[5][5]=HUMAN; 適当に石を置く

board[2][3]=board[7][3]=board[6][6]=COMPUTER;

print_board();

printf(“COMPUTERが(3,3)に石を置くと相手石を%d枚挟む(正解は5)”, sandwitched_stones(COMPUTER,3,3));

printf(“HUMANが(8,3)に石を置くと相手石を%d枚挟む(正解は1)”, sandwitched_stones(HUMAN,8,3));

}

これらをコンパイルするには

% gcc main.c utility.c -o othello

とすると実行ファイルが othello という名前のファイルに作られるので、

% othello

とすると実行できる(このことについての詳細はもっと後の課題で説明する)。

(課題Dでの習得項目)

ccコマンドの -o オプションと -c オプション(分割コンパイルの基礎)

extern宣言

14

Page 15: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

課題E:そこには自分の石が置けるかな?

課題Dで作った関数

int sandwitched_stones(int player, int x, int y)

を使って、以下の関数を作り、それらを一緒にしたものを改めて

~/c/game/utility.c

にセーブせよ。また、それらが正しく動作することを確認するための main関数を作れ。

作るべき関数:

(1) boolean legal_square(int player, int x, int y)

位置 (x,y) にプレーヤ player が次の手を打つことのできるか否かを判定する関

数。関数値は論理値の true(=1)または false(=0)とせよ。

sandwitched_stones を利用せよ。sandwitched_stones(player,x,y)>0 かつ

(x,y)には石がまだ置いてなく(board[x][y]==NOT_USED)、かつ盤外でもないこと

(1<=x && x<=8 && 1<=y && y<=8)を調べればよいだけである。その結果に従って、

関数値を論理値(置ける場合 → true=1、置けない場合 → false=0)とせよ。

このような論理値を値にとるデータ型 booleanを定義するためには

typedef enum { false, true } boolean;

という宣言をファイルの冒頭(関数 sandwitched_stones, legal_square, mainの

定義の前)に置けばよい。あるいは、

#define false 0

#define true 1

としても同じである。

(2) void put_stone(int player, int x, int y)

位置(x,y)には、変数 player で与えられるプレーヤの石を置くことができるとす

る。そのとき、そこに石を置いて、挟んだ相手石をすべて裏返す。

(3) int who_won(int *computer, int *human)

ゲーム終了後、COMPUTER が獲得した石の個数 computer と HUMAN が獲得した石の

個数 human を計算し、call by address でこれらの値を返す。また、どちらが勝

っ た か (computer>human な ら COMPUTER 、 human>computer な ら HUMAN 、

computer=humanなら DRAW)を関数値とする。HUMANと COMPUTERはすでに課題Aで

定義したような定数名(COMPUTER=-1, HUMAN=1)である。新たに、DRAWを

#define DRAW 0

と定義せよ。

以上すべての関数(課題Dで作った sandwitched_stonesも含む)を utility.cにセーブし、

15

Page 16: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

それらの動作を確認する main 関数を main.c にセーブせよ。動作確認のプログラムの実行

ファイルを Eという名前で各自のディレクトリ ~/c/game に作っておくこと。

確認のためには、オセロの局面がある程度進んだ状態を作っておく必要がある。それは結

構面倒なので、前もって作ってある「テスト用盤面」を与えるので利用するとよい

(~moriya/c/task/test.cがそれなので、コピーして使え)。

(課題Eでの習得項目)

call by valueおよび call by addressによるパラメータの受渡し

typedefによる型定義

enum型、論理値

#include "decl.h"

extern int board[10][10];

int sandwitched_stones(int player, int x, int y)

/*******************************************************************

(x,y)はまだ石が置かれていない位置であるとする。ここに playerの石を

置くと、相手石を何個挟むことになるかを計算する

********************************************************************/

{

int dx,dy; /* x方向増分と y方向増分 */

int total,count; /* 挟んだ相手石の数を数える(total=総数,count=行毎) */

int xx,yy,k;

total=0;

for (k=0; k<=8; k++) /* 各方向(8通り)について調べる */

{

if (k==4) continue; /* 除外ケース */

dx = k/3-1; dy = k%3-1; /* x,y方向の増分を計算する */

xx = x+dx; yy = y+dy; /* (xx,yy)が現在位置 */

count=0;

/* 挟んでいる相手石の数をカウントする */

16

Page 17: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

while (board[xx][yy] == -player) /* 相手石がつづく限り反復 */

{

++count;

xx += dx; yy += dy; /* 対象位置を移す */

}

if (board[xx][yy]==player) /* 自分の石にぶつかった */

total += count;

}

return total;

}

boolean legal_square(int player, int x, int y)

/*********************************************************************

位置(x,y)が playerが次の手を打つことのできる位置か否かを判定する

**********************************************************************/

{

/* 既に石が置いてある位置や盤外は不当位置である */

if (board[x][y]!=NOT_USED || x<=0 || x>8 || y<=0 || y>8)

return false;

/* そうでない場合、相手石を1つ以上挟むような位置ならば ok */

if (sandwitched_stones(player,x,y)>0) return true;

else return false;

}

int who_won(int *computer, int *human)

/***************************************************************

どちらが勝ったかを判定し、それを関数値とする

また、それぞれの獲得点(computerと human)も求める

****************************************************************/

{

int x,y;

*human = *computer = 0;

for (x=1; x<=8; x++)

for (y=1; y<=8; y++)

{

switch(board[x][y])

{

case HUMAN: (*human)++; break;

case COMPUTER: (*computer)++; break;

default: break;

}

}

if (*human>*computer) return HUMAN;

if (*computer>*human) return COMPUTER;

17

Page 18: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

return DRAW;

}

void put_stone(int player, int x, int y)

/****************************************************************

位置(x,y)に playerの石を置き、挟んだ石を裏返す

*****************************************************************/

{

int xx,yy,dx,dy,count,k,i;

int temp[10][10]; /* 裏返しの作業用 */

board[x][y]=player; /* まず、(x,y)に自分の石を置く */

/* 作業用に、盤のコピーを作る */

for (xx=1; xx<=8; xx++)

for (yy=1; yy<=8; yy++)

temp[xx][yy]=board[xx][yy];

for (k=0; k<=8; k++) /* 各方向(8方向)それぞれについて */

{

if (k==4) continue;

dx = k/3-1; dy = k%3-1;

xx = x+dx; yy = y+dy;

/* 挟んでいる石の数をカウントする */

count=0;

while (board[xx][yy] == -player)

{

++count;

xx += dx; yy += dy;

}

/* 自分の石にぶつかり、相手の石を挟んでいたら、それらを裏返す */

if (board[xx][yy]==player && count>0)

{

for (i=1; i<=count; i++)

temp[x+i*dx][y+i*dy]=player;

}

}

/* 作業結果を転記する */

for (xx=1; xx<=8; xx++)

for (yy=1; yy<=8; yy++)

board[xx][yy]=temp[xx][yy];

}

18

Page 19: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

課題F:同じものを重複して書く無駄を避ける

各種の定数名(#defineで定義したもの)や上記の boolean型の定義は、今後作る多くの

関数でも使われ、それらの関数はそれぞれ異なるファイルにセーブする。その場合、各フ

ァイルの冒頭でそれらの宣言をいちいち書く必要があるが、それは面倒なので、それらの

宣言だけを書いたものを

decl.h

という名前のファイル(ヘッダファイル)にセーブしておいて、それぞれのソースファイ

ルの冒頭で

#include “decl.h”

としてインクルードするようにするとよい。

プログラムの概形は次のようになるであろう:

ファイル decl.h

#define NOT_USED 0

...

#define OUTSIDE 2

#define DRAW 0

typedef enum { false, true } boolean;

extern int board[10][10];

extern int first_player;

ファイル utility.c

#include “decl.h”

int sandwitched_stones(int player, int x, int y)

{

すでに作ってある

}

boolean legal_square(int player, int x, int y)

{

関数本体の定義(上記の説明参照)

}

int who_won(int *computer, int *human)

{

盤内にある黒石・白石の個数をそれぞれ調べて、どちらが多いかを

比較するだけである。ただし、先手が HUMANか COMPUTERかによって、

黒石が HUMANの石であるか COMPUTERの石であるかが変わるので注意

する。

関数値を returnで返す以外に、求めたそれぞれの石の個数を引数

computer,humanを介して返す。

}

19

Page 20: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

void put_stone(int player, int x, int y)

{

sandwitched_stones()を少し修正するだけで、簡単に作れる。

相手石を実際に裏返しにする部分は、board[10][10]をコピーし、

それを使って行なわないとうまくいかないことに注意する。

裏返しが終わったら、その内容を boardにコピーし直す。

}

ファイル board.c

#include <stdlib.h>

#include “decl.h”

void clear_board()

{

作成済み

}

void initialize()

{

作成済み

}

void print_board()

{

作成済み

}

ファイル test.c

#include “decl.h”

void test_board(int *player, int *step)

{

この関数は完成したものを配布するので、それを利用せよ。

この関数を呼んだ結果、次の手番が誰になるかを引数 playerで、

何手まで打ち終わったかを stepで返す。

}

ファイル main.c

#include <stdio.h>

#include “decl.h” int board[10][10];

int first_player;

void test_board(int *, int *);

int sandwitched_stones(int, int, int);

boolean legal_square(int, int, int);

void put_stone(int, int);

int who_won(int *, int *);

20

Page 21: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

void main()

{

それぞれの関数の動作を確認する

例えば、who_won()の動作を確認するには、

switch (who_won(&computer,&human))

{

case HUMAN: printf(“人間の勝ち”); break; case COMPUTER: printf(“コンピュータの勝ち”); break; case DRAW: printf(“引き分け”); break; }

でチェックできる。また、put_stone()の検証は、例えば、(3,3)に

HUMANの石を置ける場合、

put_stone(HUMAN,3,3);

who_won(&computer,&human);

として、put_stone()をする前と後の石の個数の変化をみればよい。

}

課題Fへの補足:しっかり独り立ちさせよう。

プログラム全体をいくつかの独立したファイルに分割し、それぞれを別々にコンパイル

することを分割コンパイルという。

ファイル A.c, B.cがあったとき、それらは

% gcc -c A.c

% gcc -c B.c

のようにして、別々に分割コンパイルすることができる。オプション -c を付けずに

% gcc A.c

% gcc B.c

とすると、先ず A.c をコンパイルした結果である実行形式が a.out に作られ、次いで B.c

をコンパイルした結果である実行形式(実行ファイル)が a.outに作られるので gcc A.c

した結果は消えてしまう。オプション -c を付けることによってそれぞれのコンパイル結

果(ただし、実行可能形式とするためには A.c のコンパイル結果と B.c のコンパイル結果

をリンクする必要があるが、それはまだ行なわれていない)がそれぞれ A.o, B.o に作ら

れる(これらをオブジェクトファイルという。それに対し、A.c や B.c をソースファイル

という)。gccは、ソースファイルやオブジェクトファイルを混ぜた複数のファイルをコン

パイル・リンクしてくれる。例えば、

% gcc A.o B.o C.c

とすると、すでにコンパイルしてある A.o や B.o は再度コンパイルすることはせず、C.c

だけをコンパイルして、3つのオブジェクトファイルをリンクして実行形式(ロードモジュ

ール)とする。この際、

% gcc A.o B.o C.c -o go (あるいは、% gcc -o go A.o B.o C.c)

のようにオプション -o xxxxxを付けると、実行ファイルが a.outではなく xxxxxという名

21

Page 22: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

前で作られるので、以後、

% xxxxx

とすれば実行できる(-oオプションを付けずに ccコマンドをかけた場合は、% a.outとす

る)。

分割コンパイルの際に注意しなければならないことは、コンパイル対象のファイルの中

に、それとは別のファイルの中で定義されている関数があったら、それがどのようなもの

であるのか(引数の個数とデータ型、関数値のデータ型)をコンパイラに教えてやらなけ

ればならないという点である。そのためには、(同一ファイル内に定義はあるものの、引

用より後に定義が書いてあるような関数に対して、これまで行なってきたように)ファイ

ルの冒頭にそれらの関数(=他のファイルで定義されている関数)のプロトタイプ宣言を

置けばよい。

また、外部変数・外部配列は(たとえ一つ一つが別々のファイルで定義されていようと

も)すべての関数が共有する変数・配列であるから、ファイル毎に重複して宣言すると同

じものが2箇所以上にあることになってしまうので、どこか1箇所だけで宣言する必要が

ある。ただし、そうすると、宣言が置かれていないファイルにおいてそれらを参照したと

きに「宣言がない」というエラーになってしまう。そこで、それが他のファイルの中で宣

言されているということをコンパイラに知らせてやる必要があり、その目的のために

extern 宣言を行なう。例えば、ファイル main.cの中で

int board[10][10];

int first_player;

と、外部配列 board と外部変数 first_player の宣言がしてあったら、他のファイル(例え

ば board.c, utility.c)でこの外部配列・変数を使う場合には

extern int board[10][10];

extern int first_player;

と宣言する。これによって、これらの配列・変数の実体は mian.c だけにあることになり、

board.c や utility.c を分割コンパイルする際にコンパイラはそれらが他のファイルで宣

言されているものであることを知ることができ、board.oや utility.oを main.oとリンク

するときに、それらの名前は対応するオブジェクトの実体と結びつけられる。

ヘッダファイル

上記の decl.h の中に書いてあるような宣言は、多くのファイルのそれぞれが冒頭で書

く必要があるものである。こういった宣言は、この decl.h のように1つのファイルに書

いておいて、その宣言を必要とするファイルでは

#include “decl.h”

によって取り込むようにするとよい。この種の宣言を集めたファイルは、それを必要とす

るファイルの冒頭で取り込まれるので、ヘッダファイルと(header file)とか単にヘッダと

22

Page 23: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

呼ばれる。stdio.hや stdlib.hなどもヘッダファイルである。システムに備わっているヘ

ッダファイルは

#include <stdio.h>

のように < >でくくり、ユーザ定義のヘッダファイルは

#include “decl.h”

のように “ “ でくくる(目的のファイルがどこ(どのディレクトリ)にあるかをコンパイ

ラに知らせるため書き方が異なる)。

上記の decl.h ではそうしていないが、

void test_board(int *, int *);

int sandwitched_stones(int, int, int);

boolean legal_square(int, int, int);

void put_stone(int, int);

int who_won(int *, int *);

などのプロトタイプ宣言もヘッダファイルに書き込んでおくとよい。

(課題Fでの習得項目)

ユーザ定義のヘッダファイルとそのインクルード

23

Page 24: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

#include "decl.h"

extern int board[10][10];

int who_won(int *computer, int *human)

{

int x,y;

*human=0,*computer=0;

for (x=1; x<=8; x++)

for (y=1; y<=8; y++)

{

switch(board[x][y])

{

case HUMAN: (*human)++; break;

case COMPUTER: (*computer)++; break;

default: break;

}

}

if (*human>*computer) return HUMAN;

if (*computer>*human) return COMPUTER;

return DRAW;

}

/* デバッグ時に使うためのテスト用盤面 */

extern int board[10][10];

extern int first_player;

void test_board(int *player, int *step)

{

board[6][3]=board[6][6]=board[7][7]=board[7][5]=first_player;

board[6][2]=board[6][4]=board[3][6]=board[7][6]=first_player;

board[4][7]=board[2][2]=board[2][7]=board[4][6]=first_player;

board[3][5]=board[4][3]=board[7][2]=board[5][3]=-first_player;

board[5][6]=board[8][2]=board[7][1]=board[4][2]=-first_player;

board[5][7]=board[5][8]=board[3][2]=board[3][4]=-first_player;

*player=first_player;

*step=28;

}

24

Page 25: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

課題G:ちょっと待って下さい、5秒だけ。

コンピュータや人間が手を打った後、その盤面を画面に表示させるが、表示内容を確認

するためには、何秒かプログラムの実行を停止させる必要がある。そのために、「指定し

た時間の間、何もせずに待つ」関数

void wait(int sec)

を作れ。secの単位は秒とせよ。

オセロでは、将棋のように「持ち時間」制限はない。しかし、そうしたこともできるよ

うにしておきたい。そこで、player(COMPUTERまたは HUMAN)が、それまでにそれぞれ(何

時間何分)何秒を消費したかを記録する関数

int consumed_time(int player, time_t start_time)

も作れ。 この関数は playerがそれまでに消費した時間(秒)を関数値として返す。

これら2つの関数を合わせたものと、それらの動作を確認する main関数とを一緒にした

ものを

~/c/game/time.c

にセーブせよ。

関数 wait()について

ヘッダファイル <time.h> には、日付や時刻に関するいくつかの関数がある。目的の関

数 wait() を実現するには、その中の time_t time(time_t *) を使えばよい。すなわち、

#include <time.h>

time_t current;

current=time(NULL);

とすれば、そのときのカレンダー時間が currentに得られる。time_tは時間を表わす型で

ある(int型と思ってよい)。2つの時刻 t1,と t2の差は

difftime(t1,t2)

によって得られる(double型で、単位は秒)ので、

difftime(start,current)>sec

となるまで current=time(NULL); を実行しつづける。ただし、start は wait が呼ばれた

直後の時刻である(waitの冒頭で、start=time(NULL); とすればよい)。

関数 consumed_time()について

HUMAN, COMPUTERそれぞれの消費時間(秒)を保持する変数 H_time, C_timeを用意する。

これらの変数はこの関数固有のものであるはずだから、内部(局所)変数とすべきであろ

う。しかし、自動変数は関数が呼ばれるたびに新たに生成され実行が終了すると消滅して

しまうので記録をとっておくための変数としては使えない。そこで、H_time, C_timeは静

25

Page 26: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

的内部変数とし、初期値を 0に設定しておく:

static double H_time=0, C_time=0;

consumed_timeを呼び出すときには、どちらのプレーヤーであるかを第1引数 playerと

し、そのプレーヤーが自分の打つべき手を考え始めた時刻 start_tiemを第2引数とする。

consumed_time(player,start_time) は、呼ばれたら先ず end_time=time(NULL) を求め、

difftime(end_time,start_time) を計算し、それを H_time(player=HUMANのとき)または

C_time(player=COMPUTERのとき)に加えればよい。そうして求めた H_time または C_time

を関数値として返す(ただし、関数値を int型に変換すること)。

動作確認のための main関数の例:

#include <stdio.h>

#include <time.h> /* これらは、decl.hに追加しておいてもよい */

void main()

{

time_t t1,t2;

printf(“10秒間お待ち下さい。\n”); t1=time(NULL); wait(10); t2=time(NULL);

printf(“time difference=%d\n”, (int)difftime(t2,t1));

printf(“HUMANがこれから 5秒使います:\n”); t1=time(NULL); wait(5);

printf(“HUMAN\’s consumed time=%d\n”, consumed_time(HUMAN,t1); printf(“COMPUTERがこれから 1秒使います:\n”); t1=time(NULL); wait(1);

printf(“COMPUTER\’s consumed time=%d\n”, consumed_time(COMPUTER,t1)); printf(“HUMANがこれから 3秒使います:\n”); t1=time(NULL); wait(3);

printf(“HUMAN\’s consumed time=%d\n”, consumed_time(HUMAN,t1); }

(注) time_t 型は実質的には int 型であり、difftime(t2,t1) の代わりに t2-t1 として

もよい。

(課題Gでの習得項目)

オブジェクトの記憶クラス(特に、静的変数、静的配列、静的関数)

<time.h>

26

Page 27: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

#include "decl.h"

#include <stdlib.h>

#include <time.h>

#include <stdio.h> /* 定数名 NULLがあるため */

static int H_time, C_time;

void wait(int sec)

/**********************************************************

遅延生成関数(sec秒だけ何もせず待つ)

***********************************************************/

{

time_t time1,time2;

time1=time(NULL);

do time2 = time(NULL); while ((time2-time1) < sec);

}

27

Page 28: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

課題H:いよいよ、ゲームの全体構成を考えてみよう。

ゲーム全体の流れを考え、それを main関数として書き、

~/c/game/othello.c

にセーブせよ。また、提出期限までに課題が完成した場合は、分割コンパイルしリンクを

とった実行形式のファイル名を Hとし、

~/c/game/H

にセーブしておくこと。

ほぼ次のような流れとなるであろう:

1.準備

ゲーム開始に先立ち、コンピュータが挨拶をする(「こんにちは」「よろしくお

願いします」など)。この部分が大きくなる場合には、関数として独立させよ。

ゲーム開始時には、せめて花文字を書いたり音を出したりして、少しでも華やか

な雰囲気を出したい。花文字は printf を何度も使えば書けるが、banner というコ

マンドを利用してもよい(System V 系 UNIXの場合。BSD系の UNIXではこのコマン

ドが使えない場合もある。この授業の受講者だけには使えるように環境設定してあ

る)。

現在の環境で音楽を奏でることはできないが、少なくともビープ音は出すことが

できる(\a あるいは BEL はブザー音を表わす拡張文字(escape sequence)であり、

printf等で出力する)。

2.先手を決める

人間と対話しながら、どちらを先手とするかを決め、外部変数 first_player に

設定せよ(人間が先手なら first_player=HUMAN とし、コンピュータが先手なら

first_player=COMPUTER とする)。対話の際、yes/no は数値ではなく文字列で入力

するようにせよ。ついでに、<string.h> にある文字列処理関数について調べてみよ

(特に、文字列の比較、連接などの方法)。また、scanf(“%s”, ...); ではなく、gets

を使ってみよ。

先手・後手は、乱数を使った「じゃんけん」で決めてもよい。この場合には、

<stdlib.h> にそのプロトタイプ宣言がある標準関数 srand と rand が必要である

から、調べて使うこと。

3.ゲームを1回行なう

現時点では、この部分は仮に次のような関数としておくだけでよい(次回以降

の課題で具体化してゆく):

int play_game(int *computer_point, int *human_point)

28

Page 29: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

{

*computer_point=34; *human_point=30; /* それぞれの得点 */

return COMPUTER; /* COMPUTERの勝ち */

}

この関数は ~/c/game/play.c にセーブせよ。現時点ではこの関数は小さいもの

であるが、課題I以降で拡張する。

4.勝者を表示して、その健闘を称える

5.終りにするか再戦するか対話して決める

再戦なら2に戻り、終りにするなら後始末(対戦成績発表と「さよなら」)をす

る。対戦成績を表示するためには、戦績(それぞれのプレーヤの勝敗数)を記録し

ておく必要がある(この部分は将来、構造体やファイル入出力について学ぶ際に関

数として独立させファイル utility.cに追加する予定であるが、当面、main関数内

に作る)。

課題A~Fでやってきたのは、ボトムアップ(bottom up)なプログラム開発のミニュチュ

ア版であった。すなわち、部品を組み合わせて次第に大きなものにしていく開発方式であ

る。課題H以降では、逆に大きいものを次第に詳細化していく方式である、トップダウン

(topdown)なプログラム開発方式も併用する。

ある関数が正しく動作することを確かめるために、その関数を呼び出すプログラムを暫定的

に作ったものをテストドライバ(test driver)という。基本的な関数群を先に作り、それを

用いて次第に大きな部分にしていく方法(ボトムアップ開発法)でプログラム開発を行なう場合に、

各関数の動作をチェックするためにテストドライバが用いられる。これまでの課題では、作った関数

が正しく動作することを確認するためにテストドライバとしてmain関数を作ってきた。

逆に、ある関数が正しく動作することを確かめるために、その中で呼び出している関数

を暫定的に作ったものをテストスタブ(test stub)という。段階的詳細化法(トップダウン開発

法)でプログラム開発を行なう場合に用いられる。上記の関数 play_game()はテストスタブで

ある。

(課題Hでの習得項目)

文字列と文字列処理関数ヘッダ<string.>

乱数(srand, rand)とユーティリティ関数ヘッド<stdlib.h>

などの標準ライブラリ

29

Page 30: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

課題I:さあ、1ゲームやろう。

1ゲーム行ない、どちらが勝ったか(HUMAN, COMPUTER, DRAW)を値とする関数

int play_game(int *computer_point, int *human_point)

を作る。この関数は、それぞれのプレーヤー(HUMAN か COMPUTER)が獲得した石の個数も引

数 computer_point, human_pointを介して返す(すなわち、これらの引数は call by address

である)。以後の課題では、この関数をトップダウンに具体化していく。

関数 play_game()の概要

#include "decl.h" /* 各種定数名の宣言等をインクルードする */

#include <stdlib.h>

int next_move(int player); /* playerの次の手を実行する関数(以下で作る) */

int who_won(int *computer_point, int *human_point); /* 作成済み(utility.c) */

void initialize(void); /* 以下の関数も作成済み(board.c) */

void clear_board(void);

int play_game(int *computer_point, int *human_point)

{

int player; /* 次の手を打つ者 */

boolean opposite_passed; /* 直前に相手がパスしたか否か */

int x,y;

/* 盤を初期化する */

initialize();

step=4; /* すでに 4手目までは打ってある */

player = first_player; /* 先手から始める */

opposite_passed = false;

/* 勝負がつくまで交互に打ちつづける */

while (step<64) {

/* パスでない場合、次の手を打ち、ステップ数をカウントする */

if (next_move(player)!=PASSED) {

++step; opposite_passed = false;

}

/* パスの場合、相手が直前にパスしたか否かチェックする */

else {

if (opposite_passed) break; /* ゲーム終了 */

else opposite_passed = true;

}

player = -player; /* プレーヤーを交代する */

}

/* どちらが勝ったかを判定する */

return who_won(computer_point,human_point);

}

30

Page 31: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

makeコマンドの使い方

すでに分割コンパイルについては学んだので、この課題では makeコマンドの使い方につ

いて学びたい。makeコマンドは makefileあるいは Makefileという名前のファイルとセッ

トで使われ、たくさんのソースファイルから成るプログラムを分割コンパイルするときに

再コンパイルの手間を軽減してくれる。

いま、ファイル main.cと sub1.c, sub2.c, sub3.cがあり、main.cと sub1.cは

#include “header.h”

により header.hというファイルを取り込んでおり、sub2.cは sub3.cを呼んでいるとする。

すなわち、これらのファイルの間の依存関係が次のようになっているとする:

main.c

sub1.c sub2.c

header.h sub3.c

このとき、

% gcc -c main.c sub*.c -o maingo

とすればオブジェクトファイル main.o, sub1.o, sub2.o sub3.o とロードモジュール

maingoが作られて、

% maingo

とすれば実行できる。しかし、その後、分割されたファイルのどれかを変更した場合、上

記のように gccを再実行するのは、入力するのが面倒なだけでなく(変更したファイルだけ

を再コンパイルするだけでは不十分であり、すべてのオブジェクトファイルを再度リンク

し直す必要があることに注意)、再コンパイルに無駄な時間がかかることになる。そこで、

次ページに示したようなファイルを作り、makefileあるいは Makefileという名前で(ある

いは、それ以外のどんな名前でもよい)セーブしておくと、

% make (あるいは、 % make -f ファイル名)

とするだけで、再度コンパイルする必要のあるファイルだけが再コンパイルされて再リン

クが行なわれる。

makeコマンドのオプション

-f xxxxx 記述ファイルの指定。-fオプションを省略すると、makefile,

Makefile, s.makefile, s.Makefileの順でひとつが選ばれる。

-s makeの実行時に表示をしない。

-k 既存のエントリを捨て、それに依存しない別のエントリに移る。

-n コマンドの表示のみで実行はしない。

(参考)makeと同様なことは Turbo C++等でもできる。

31

Page 32: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

makefileの例 ターゲットファイル(作りたいファイル)

# コメントは、このように#の後に書く(BSD系 UNIX)

maingo: main.o sub1.o sub2.o sub3.o header.h 依存するファイルのリスト

タブ gcc main.o sub1.o sub2.o sub3.o -o maingo

main.o: main.c header.h ターゲットファイルを生成するためのコマンド

タブ gcc -c main.c

sub1.o: sub1.c header.h

タブ gcc -c sub1.c

sub2.o: sub2.c sub3.c

タブ gcc -c sub2.c sub3.c

sub3.o: sub3.c

タブ gcc -c sub3.c

空白行

# end of makefile(この行もコメント行)

「タブ」と書いた所には、Tabキーを押してタブ文字(画面上では見えない)を入力しておく。

なお、makefileの記述では、次のようなマクロ定義を用いることができる。

# an example of macro use

FILELIST1 = main.o sub1.o sub2.o sub3.o

FILELIST2 = sub2.c sub3.c

maingo: ${FILELIST1}

タブ gcc ${FILELIST1} -o maingo

main.o: main.c header.h

タブ gcc -c main.c

sub1.o: sub1.c header.h

タブ gcc -c sub1.c

sub2.o: ${FILELIST2}

タブ gcc -c ${FILELIST2}

sub3.o: sub3.c

タブ gcc -c sub3.c

# end of makefile

FILELIST1 = ... および FILELIST2 = ... の部分はマクロ定義であり、定義されたマク

ロの値は

${マクロ名}

で示す。このようにしておくと、コンパイルの条件が変わったときにも、マクロ定義の部

分だけを直せばすむ。

課題Iでやるべきことは、makefileを作って分割コンパイルを行なうことである。まず、

ファイル time.c は utility.c に含めてしまうことにする。すると、これまでに作ったフ

ァイルとそれらの依存関係は次図の通り:

othello.c

32

Page 33: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

play.c

board.c

utility.c test.c

decl.h (time.c)

この依存関係に従った makefileを作り、分割コンパイルを行なうこと。実行ファイル名

を goとし、いつものディレクトリ ~/c/game に置いておくこと。関数 play_game()は配

布完成したものをする。play_game()はその中で関数 next_move()を呼んでいるので、下記

の仕様に従った next_move() のテストスタブを作り、play_game()と同じファイル play.c

にセーブすること。

関数 int next_move(int player)

playerの次の手を打つ。playerがパスした場合は PASSED を関数値とし、

パスすることなく次の手が打てた場合は !PASSED を関数値とする。

next_move() は今後トップダウンに作っていくが、さしあたり次のように動作するもの

(playerが打てる位置があれば、そのうちの最も左上隅の位置に打ち、どこにも打てなか

った場合だけパスする)を作ってテストスタブとする:

for (y=1; y<=8; y++) {

for (x=1; x<=8; x++)

if (legal_square(player,x,y)) {

put_stone(player,x,y); return !PASSED;

}

return PASSED;

また、次の1行を othello.cの main関数の前に外部変数の宣言として追加せよ。

int step; /* これまでに打ち終わったステップ数を保持する */

次の定数名と extern宣言をファイル decl.h に追加しておく必要がある:

#define PASSED 0

extern int step;

必要なら盤面がもっと進んだ状態になるように関数 test_board() も修正せよ。

課題Hがまだ完成していない者は、課題Hと課題Iを別々に行なわず、課題Hは課題I

の一部として取り組むこと(従って、実行ファイル名は H ではなく go とすること)。課題

Hがすでに完成しているものはこの限りにあらず。

(課題Iでの習得項目)

分割コンパイルと makefileの活用

33

Page 34: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

#include "decl.h" /* 各種定数名の宣言をインクルードする */

#define DEBUG 1 /* デバッグ中であるか否かを表わす */

int board[10][10]; /* オセロの盤を表わす(盤の周囲には番兵を置く) */

int step; /* これまでに打ち終わったステップ数 */

extern int first_player; /* 先手を表わす外部変数

(ファイル main.cの中で宣言されている) */

void initialize(void); /* 盤の初期化 */

void clear_board(void); /* 盤面のクリア */

int next_move(int player); /* playerの次の手を実行する */

int who_won(int *computer_point, int *human_point);

/* どちらが勝ったかと,各々の得点を計算する */

#if DEBUG

void test_board(int *player, int *step);

#endif /* この関数はデバッグ時にテスト用の盤面を

与えるために使う。そのため、DEBUGがオン

のときのみコンパイルする */

/*

上記関数のうち initialize以外は別のファイルで定義されている

*/

int play_a_game(int *computer_point, int *human_point)

/*******************************************************

1ゲーム行ない、どちらが勝ったかを関数値とする

また、それぞれのプレーヤーの得点も求める

********************************************************/

{

int player; /* 次の手を打つ者 */

boolean opposite_passed; /* 直前に相手がパスしたか否か */

int x,y;

/* 盤を初期化する */

initialize();

step=4; /* すでに 4手目までは打ってある */

player = first_player; /* 先手から始める */

opposite_passed = false;

#if DEBUG /* デバッグ時のみコンパイルする */

test_board(&player,&step);

#endif

34

Page 35: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

/* 勝負がつくまで交互に打ちつづける */

while (step<64)

{

/* パスでない場合、次の手を打ち、ステップ数をカウントする */

if (next_move(player)!=PASSED)

{

++step;

opposite_passed = false;

}

/* パスの場合、相手が直前にパスしたか否かチェックする */

else

{

if (opposite_passed) break; /* ゲーム終了 */

else opposite_passed = true;

}

player = -player; /* プレーヤーを交代する */

}

/* どちらが勝ったかを判定する */

return who_won(computer_point,human_point);

}

void initialize()

/********************************************

盤を初期化する

*********************************************/

{

clear_board();

board[4][5]=board[5][4] = first_player;

board[4][4]=board[5][5] = -first_player;

}

35

Page 36: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

(課題J)次の手を打つ。

次の手を打つ関数 next_move() を作る:

int next_move(int player)

playerがパスした場合は PASSED を関数値とし、パスすることなく次の手が

打てたときは !PASSED を関数値とする。

PASSED は定数名としてヘッダ decl.h において #define により定義しておく。

この関数は前回の課題において、ファイル play.c の中にすでにテストスタブが作って

あるので、それをそのまま拡張することにする。

関数 next_move()の概要

まず、ファイルの冒頭に、以下のマクロとプロトタイプ宣言を追加する必要がある。

#include <stdio.h>

void print_board(void); /* 作成済み(board.c) */

int find_next_move(int player, int *x, int *y);

/* playerの次の手(x,y)を見つける関数(次回作る): */

/* player=HUMANの場合には、次の手がある(パスしなくてもよい) */

/* かどうかだけを調べ、!FAILEDまたは FAILEDを関数値とする。 */

/* player=COMPUTERの場合には、次の手(x,y)も求める。 */

/* #define FAILED 0 を decl.hに追加すること。 */

/* 以上の仕様を満たすテストスタブを作り、ファイル move.c に */

/* にセーブせよ。 */

void wait(int second); /* 作成済み(utility.cまたは time.c) */

int next_move(int player)

/*********************************************************************

playerの次の手を打つ(関数値はパスした(PASSED)か否(!PASSED)か)

**********************************************************************/

{

int x,y,next_mv;

char dummy[10];

/* 次の手を探す */

next_mv = find_next_move(player,&x,&y);

/* 次の打つ手がない場合 */

if (next_mv == FAILED)

{

print_board();

switch (player)

{

case COMPUTER:

36

Page 37: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

printf(" 私はパスしなければなりません。");

break;

case HUMAN:

printf(" あなたはパスしなければなりません。");

break;

}

printf("\n 確認したら、Enterキーを押して下さい。");

gets(dummy); wait(5); getchar();

return PASSED;

}

/* 以下は、パスしない場合 */

switch (player)

{

/****************************************************************/

/* ここを完成せよ: */

/* player=HUMANの場合、次の手(x,y)を入力してもらい、(x,y)が */

/* 確かに HUMANが手を打ってもよい位置だったら HUMANの石を置く。*/

/* 不当位置だったら再入力を求める。入力を求める際には、盤面を */

/* 表示してあげる。 */

/* player=COMPUTERの場合、find_next_move()によって求めた位置 */

/* (x,y)に石を置く。 */

/* いずれの場合も、手を打った後の盤面も表示する。 */

/****************************************************************/

}

return !PASSED;

}

プログラムが完成したら、makefileを適当に修正してから makeする。新たに実行ファイ

ル go ができる。今回の課題により、一応人間と対戦できるオセロゲームのプログラム完

成する(もちろん、勝つための手を何も考えていないので、まったく弱い)。この課題で

はゲームとしてちゃんと動くようになっていることが求められる。ゲームの終了を早める

ために、テスト盤面を与える関数 test_board() (ファイル test.c)を変更して、終盤近

くに設定せよ。

デバッガ について

ここで、デバッグの方法について述べておく。

プログラムのエラーはコンパイル時に見つかるものと実行時に見つかるものに大別され

る。コンパイル時にわかるエラーは構文的な誤りが殆どである(すなわち、syntax errorである)。

誤りも重大さの程度によって

warning(警告) 程度が軽いので、誤りは含んでいるがリンク・実行は行われる。

37

Page 38: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

error (エラー) 程度が重く、実行不可能なもの。コンパイルを途中で中止する

こともある。

に区別される。ある1箇所の小さなエラーが、それ以降の行に次々と影響を及ぼして、雪だる

ま式に増えたエラーメッセージが出力されることが多い。次の点に注意してデバッグするとよい。

(i) 括弧の対応 (と)、{と}、[と] がきちんとしているか。

(ii) , ; * " ' の付け忘れがないか。 'と"、 :と; を間違えていないか。

(iii) 予約語のつづりに誤りがないか。

(iv) 識別子(変数名、配列名など)はきちんと宣言されているか。

(v) 宣言した型との不一致はないか。

(iv) コメントの /*と*/ はきちんと対応しているか。

若い番号の行のエラーをなくせば、大量のエラーメッセージが一挙になくなってしまうこともある。

どうしてもわからないエラーがあるときは、適当な場所にprintfを挿入してどこまで正しく働

いているかを確認するとよい。あるいは、以下に述べるようなデバッガ(debugger)でプログ

ラムの実行をトレースしたり、テストドライバ(test driver)やテストスタブ(test stub)を用意してプログラム

の各部分(関数)毎に動作を確認する。

コンパイル時にエラーがなく、リンク時に出されるエラーは、外部参照名などが解決しないために起こ

ることが多い。関数名のつづりの誤りが原因のことが多い。

実行時のエラーは、0による除算、オーバーフロー、アンダーフロー、配列の添字外への代入、ポインタ

変数に対するメモリが確保されていない、などによって起こることが多い。

良質のソフトウェアを無料で提供することを目的に始まったGNUプロジェクトの一環と

して開発されたCとC++のコンパイラgccとg++は、UNIX上のCコンパイラとしては最も広く用

いられている(ccコマンドで用いていたのはSun OSに標準搭載されているANSI C以前の仕

様に基づくコンパイラである)。このGNUプロジェクトからは、gdbと呼ばれるデバッガ

(symbolic debugger)も配布されている。

デバッガは、プログラムのデバッグをする際に、プログラムソースの必要な部分を表示

させたり、「ブレークポイント」を設定してプログラムの実行を一時停止させたり、1ス

テップずつ実行させながら変数の値を表示させたりすることができ、デバッグ用の強力な

ツールである。UNIX上でよく使われるデバッガとしては gdb, sdb, dbxなどがあるが、こ

こでは gdb について説明する。他もほとんど似たような使い方である(パソコン上の C コ

ンパイラである Turbo C++や Visual C++もそれぞれデバッガを持っている)。

manコマンドで調べて、使ってみるとよい。

38

Page 39: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

gdbについて

次のプログラムを例にして説明する(ファイル debug.cにセーブしてあるものとする):

1 #include <stdio.h>

2

3 int add(int x, int y) {

4 int z;

5 z=x+y;

6 return z;

7 }

8

9 void print(int a, int b) {

10 static int c=0;

11 c=add(a,b);

12 printf("%d+%d=%d\n", a,b,c);

13 }

14

15 void main() {

16 int m,n;

17 m=1200; n=34;

18 print(m,n);

19 }

gdbを使うためには、先ず、コンパイルの際に

% gcc debug.c -g

のように -g オプションを付ける。gdbの実行を開始するには、

% gdb a.out

と入力すると、

GDB is free software and you are welcome to distribute copies of it

under certain conditions; type "show copying" to see the conditions.

There is absolutely no warranty for GDB; type "show warranty" for details.

GDB 4.14 (sparc-sun-sunos4.1.3),

Copyright 1995 Free Software Foundation, Inc...

というメッセージが表示された後、

(gdb)

というプロンプトが表示されるので、

(gdb) break main

と入力して、make 関数にブレークポイントを設定する(break というコマンド名の後ろに

プログラム内の関数名を書く。この例では関数名は main)。以下、アンダーライン部分が

入力、その他は gdbの応答である。

Breakpoint 1 at 0x2324: file test.c, line 17.

(gdb) break add ... 関数 addにもブレークポイントを設定

39

Page 40: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

Breakpoint 2 at 0x229c: file test.c, line 5.

(gdb) run ... プログラムの実行開始

Starting program: /a/aquanaut/usr/aquanaut/user1/moriya/c/temp/a.out

Breakpoint 1, main () at test.c:17

17 m=1200; n=34;

(gdb) print m ... 変数 mの値をプリントしてみる

$1 = 0 ... 未だ値が定まっていない

(gdb) print n ... 変数 nの値をプリントしてみる

$2 = 0 ... 未だ値が定まっていない

(gdb) next ... 次のステップを実行させる

18 print(m,n);

(gdb) print m ... この時点での mの値をプリントさせる

$3 = 1200

(gdb) print n

$4 = 34

(gdb) next

Breakpoint 2, add (x=1200, y=34) at test.c:5

5 z=x+y;

(gdb) print x

$5 = 1200

(gdb) print y

$6 = 34

(gdb) print z

$7 = 0

(gdb) next

6 return z;

(gdb) print z

$8 = 1234

(gdb) step ... 以下、1ステップずつ実行させる

out (a=1200, b=34) at test.c:12

12 printf("%d+%d=%d\n", a,b,c);

(gdb) ... [Enter]のみ(以下、同様)

1200+34=1234 ... line 12の実行結果

13 }

(gdb)

main () at test.c:19

19 }

(gdb)

0x206c in start ()

(gdb)

Single stepping until exit from function start,

which has no line number information.

Program exited with code 01. ... プログラムの全実行終了

(gdb) next

The program is not being run. ... 実行は終了している

(gdb) run ... 再実行してみる

Starting program: /a/aquanaut/usr/aquanaut/user1/moriya/c/temp/a.out

Breakpoint 1, main () at test.c:17

40

Page 41: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

17 m=1200; n=34;

(gdb) break out ... 関数 outにもブレークポイント設定

Breakpoint 3 at 0x22d8: file test.c, line 11.

(gdb) run

Starting program: /a/aquanaut/usr/aquanaut/user1/moriya/c/temp/a.out

...

Breakpoint 3, print (a=1200, b=34) at test.c:11

11 c=add(a,b);

(gdb) c

Continuing.

Breakpoint 2, add (x=1200, y=34) at test.c:5

5 z=x+y;

(gdb) help

コマンドの説明が表示される

(gdb) quit

gdbの終了

どのようなコマンドがあるかは、上記のように help コマンドで調べるか、あるいは、

UNIXの manコマンドで調べよ:

% man gdb

なお、その他のデバッガ sdb や dbx についても、manコマンド(あるいは jmanコマンド)

で調べてみよ。

(課題Jでの習得項目)

デバッグの方法(デバッガの使い方)

manコマンド(もうすでに使いこなしていて欲しい)

41

Page 42: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

#include <stdio.h>

#include "decl.h"

extern int board[10][10]; /* ファイル play_a_game.cで定義されている */

extern int first_player; /* ファイル main.cで定義されている */

extern int step; /* ファイル play_a_game.cで定義されている */

boolean legal_square(int player, int x, int y);

/* playerが点(x,y)に打てるか否か判定する */

void print_board(void); /* 盤面を描画する */

void put_stone(int player, int x, int y);

/* 点(x,y)に playerの石を置く(同時に、挟んだ相手の石を裏返す) */

int find_next_move(int player, int *x, int *y);

/* playerの次の手(x,y)を見つける */

/* void wait(int second); "decl.h"にプロトタイプ宣言がある */

/*

上記の関数はすべて別のファイルで定義されている。この関数以外の複数の

関数から呼ばれるようなものであるか、あるいは、規模が大きいものである

ため独立させている。

*/

int next_move(int player)

/*********************************************************************

playerの次の手を打つ(間数値はパスした(PASSED)か否(!PASSED)か)

**********************************************************************/

{

int x,y,next_mv;

char dummy[10];

/* 次の手を探す */

next_mv = find_next_move(player,&x,&y);

/* 次の打つ手がない場合 */

if (next_mv == FAIL)

{

print_board();

switch (player)

{

case COMPUTER:

printf(" 私はパスしなければなりません。");

break;

case HUMAN:

printf(" あなたはパスしなければなりません。");

break;

}

printf("\n 確認したら、Enterキーを押して下さい。");

42

Page 43: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

gets(dummy); wait(5); getchar();

return PASSED;

}

/* 以下は、パスしない場合 */

switch (player)

{

case HUMAN:

print_board();

printf(" あなたの手を入力して下さい。(x座標と y座標) ");

INPUT:

scanf("%d %d", &x,&y);

if (!legal_square(player,x,y)) /* 入力された位置(x,y)が不当の場合 */

{

print_board();

printf(" (%d,%d)には打てません。\n", x,y);

printf(" 打ち直して下さい。(x座標と y座標) ");

goto INPUT;

}

put_stone(player,x,y); /* 入力された位置が正当→手を打つ */

print_board();

printf(" あなたは(%d,%d)に打ちました。", x,y);

wait(2); /* 少し待つ(適当に時間調整せよ) */

break;

case COMPUTER:

put_stone(player,x,y);

print_board();

printf(" 私は(%d,%d)に打ちました。", x,y);

wait(2);

break;

}

return !PASSED;

}

43

Page 44: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

(最終課題)より強く!

関数 find_next_move() を修正して、コンピュータができるだけ強くなるようにせよ(こ

の関数はファイル move.c に作ること)。例えば、以下のような工夫が考えられる:

(1)序盤、中盤、終盤によって手の打ち方を変える。例えば、次のことが序盤の

基本だと言われているようである:

☆ C A B B A C ☆

C X X C

A A

B 〇 ● B ●が先手

B ● 〇 B 〇が後手

A A

C X X C

☆ C A B B A C ☆

☆ 絶対打った方がいい所

A 打て(安全な根拠地)

B 発展性のある所

C 冒険(危険率 70%)

X 絶対打たない方がいい所

(2)定石があるかどうか調べ、あれば組み込む。

(3)何手か先まで先読みをする。

(4)対戦結果を学習して強くなるような機構を組み込む。

授業最終日を締切日とする。実行ファイルを ~/c/game/othello としておくこと。「強

いこと」「ゲームとして楽しい工夫がされていること」などを基準に採点するので、各自

の独自性を出して欲しい。

可能ならば、授業最終日にコンピュータ同士の対戦をしたいものである。

44

Page 45: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

(課題K)対戦成績を記録しよう

最も最近対戦した Nゲーム(N=10程度)までについてその対戦記録を記録しておき、1

ゲーム終了ごとにその時点までの高得点順位を表示するようにしたい。以下に述べる関数

を作り、それらをファイル

~/c/game/score.c

にセーブせよ(課題KとLの2回に分けて完成させる)。

1ゲームごとに記録すべき項目は次のような構造体で表わす:

struct score {

char name[32]; /* 勝者氏名 */

int point; /* 得点 */

char date[32]; /* 日付 */

};

あるいは

typedef char STRING[32]; /* 文字列型を定義しておく */

typedef struct {

STRING name; /* 勝者氏名 */

int point; /* 得点 */

STRING date; /* 日付 */

} SCORE;

#define Max_gameNO 10 /* 記録しておくゲーム数の最大値 */

static int gameNO; /* これまでに記録されているゲーム数 */

void initialize_score(void)

この関数は main 関数の冒頭で1回だけ呼び出され、対戦記録の初期化を行な

う。記録すべきものは、1ゲームごとの勝者(勝者がコンピュータの場合は

COMPUTER、勝者が HUMANの場合はその氏名)、得点、日付である。データは、構造

体 SCORE型のデータを要素とする配列

static SCORE record[Max_gameNO]; /* 対戦記録保存用配列 */

に記録するものとする (問:なぜ、静的配列としているのか? )。関数

initilaize_score()では、記録の更新に先立ってこの配列の初期化を行なう。今

回は、単に以下のようなテストスタブを作っておくだけでよい:

void initialize_score()

{

gameNO=0;

return;

/* 課題Mで、対戦記録をファイルから読み出すようにする */

}

45

Page 46: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

SCORE update_score(STRING player_name, int point)

/***************************************************************

ゲームの勝者 playerと得点 pointを対戦記録 recordに追加登録する。

recordは、pointが小さい順にソートしておく。pointの高い方から

Max_gameNOゲームだけを記録として残す。

そのうちの最高点を獲得したゲームに関するデータを関数値とする。

****************************************************************/

{

time_t t; char *date;

++gameNO;

time(&t); date=ctime(&t);

/* 記録ゲーム数に余裕がある場合は、recordの末尾に追加する */

if (gameNO<Max_gameNO)

{

strcpy(record[gameNO].name,player_name);

record[gameNO].point=point;

strcpy(record[gameNO].date,date);

}

/* 記録ゲーム数が Max_gameNOを越えている場合は、

今回の得点 pointが、記録されている最小得点 record[1].pointより

大きければそれと入れ替え、そうでなければ今回の結果は記録せずに

捨て去る

*/

else

{

if (point>record[1].point) {

strcpy(record[1].name,player_name);

record[1].point=point;

strcpy(record[1].date,date);

}

}

sort(record,1,gameNO); /* recordをソートし直す */

return record[gameNO];

}

int show_score(int n)

/***************************************************************

対戦記録 recordの中から、pointが大きい順に n個を表示する。

関数値は、記録がない場合は負の値、その他の場合は負でない値

****************************************************************/

{

課題Kでは、この部分を作れ。

}

46

Page 47: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

void sort(SCORE a[], int left, int right)

/********************************************************************

配列 aの添字範囲 leftから rightを a[].pointに関して昇順にソートする

********************************************************************/

{

課題Lでは、すでに作ってある sort.cを修正して、ここを完成させる。

課題Kを行なう際は、この関数は何もしない関数としておけばよい。

}

void main()

/* 確認のためのテストドライバ */

{

SCORE x;

initialize_score();

x=update_score("ABC",40);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("def",42);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("uvw",30);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("PQR",45);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("xyz",45);

printf("%s, %d, %s\n", x.name,x.point,x.date);

show_score(game_NO);

show_score(3);

}

仕様通りに正しく動作することが確認できたらオセロゲームの中に組み込め。

その場合、ゲームの冒頭で HUMANの名前を入力する必要がある。

(課題Kでの習得項目)

構造体

47

Page 48: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

(課題L)普通、ハイスコアって高得点の順に並んでいるものです

課題Kでは、関数 sort() が未完成であったために、ハイスコアを表示する関数

show_score() が表示したものは高得点の順になっていない。配列 record を昇順にソート

するために、次の関数 sort() を完成させよ。

void sort(SCORE a[], int left, int right)

/********************************************************************

配列 aの添字範囲 leftから rightを a[].pointに関して昇順にソートする

********************************************************************/

{

下記の指示に従い、ここを完成させよ。

}

以下のような修正を行なうこと:

(1)課題Kで使ったユーザ定義のデータ型の typedef宣言

typedef char STRING[32]; /* 文字列型を定義しておく */

typedef struct {

STRING name; /* 勝者氏名 */

int point; /* 得点 */

STRING date; /* 日付 */

} SCORE;

をファイル decl.h へ移すこと(従って、ファイル score.cの冒頭には

#include “decl.h”

が必要になる)。

(2)クイックソートを行なう関数を配布するので、それを参考にして、課題Bで作っ

た挿入ソート法を行なう関数を修正して上記の関数 sort()として使うこと。この関数を含

むファイルを sort.c とすること(このファイルは main関数を含んでいないこと)。

ファイル sort.cの冒頭にも

#include “decl.h”

が必要である。

(3)すべての関数が仕様通りに正しく動作することが確認できたらファイル score.c か

らも main 関数を削除し、ハイスコア表示機能をオセロゲームの中に組み込め。すなわち、

すでに作ってあるファイル main.c や othello.c を適当に修正し、さらに makefile を修

正して make し直せ。

ゲームの冒頭で HUMAN の名前を入力してもらうように修正する必要があることに注意す

る。

この(3)の部分は余裕のある者だけがやればよい。

(4)さらに余裕のある者は、ハイスコアの記録を配列ではなく、線形リストを使った

ものに直せ。記録するゲーム数の上限を制限しないものとする。その代わり、不要となっ

48

Page 49: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

た記録を削除するための関数も作れ。

(課題Lでの習得項目)

再帰的関数(クイックソート)

線形リスト(動的データ構造、mallocによるメモリ割り付け)

49

Page 50: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

(課題M)古い記録を調べてみれば

課題KLではハイスコアの記録は配列に記録してあるだけだったので、プログラムの実

行を終了すれば消えてしまった。この課題では、記録をファイルに書き込んでおくことに

より、いつでも古い記録を参照できるようにする。ファイルの入出力について学ぶことが

目的である。ファイル score.c の内容を以下のように修正せよ。

void initialize_score()

{

/*

対戦記録のをファイル record.datから記録してあるデータ読み出し、

関数 update_score() で使えるように配列 recordに書き込む。

この関数を作れ。fscanf では空白を含む文字列を読み込むことが

できないので、代わりに fgets を使うとよい。

*/

}

SCORE update_score(STRING player_name, int point)

{

/* 作成済み */

}

int show_score(int n)

{

/* 作成済み */

}

void save_score()

/***************************************************************

対戦記録 recordをファイル record.datにセーブする

****************************************************************/

{

FILE *fout; int i;

fout=fopen("othello_rec", "w");

if (fout==NULL) {

printf("ファイル record.datがオープンできませんでした。\n");

exit(1);

}

for (i=1; i<=game_NO; i++) {

fprintf(fout,"%s %d ", record[i].name, record[i].point);

fputs(record[i].date, fout);

}

}

void main()

50

Page 51: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

/* 動作確認のためのテストドライバ */

{

SCORE x;

initialize_score();

x=update_score("ABC",40);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("def",42);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("uvw",30);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("PQR",45);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("xyz",45);

printf("%s, %d, %s\n", x.name,x.point,x.date);

printf("適当なキーを押して下さい。"); getchar();

system("clear");

puts("ハイスコア:全記録\n");

show_score(gameNO);

puts("\nハイスコア:上位3人\n");

show_score(3);

save_score();

}

以上の関数群が仕様通りに正しく動作することが確認できたら、オセロゲームの中に組

み込め。すなわち、main 関数を削除したものをあらためてファイル score.c にセーブし、

othello.c を適当に修正し、さらに makefile を修正して make し直せ。各ゲームの冒頭で

HUMANの名前を入力してもらうように修正する必要があることに注意する。

(注)この課題としての採点は score.c だけを見て行なう予定である。

(課題Mでの習得項目)

ファイル入出力

fscanf(“%s”,...) 空白を含まない文字列を読み込む(文字列は空白の直前

で途切れる)

fgets(...) 改行が来るまで読み込む(途中の空白も文字列の一部と

なる。改行文字は文字列に含められ、その後ろに ’\0’ が

付く)

51

Page 52: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

#include "decl.h"

#include <stdio.h>

#include <time.h>

#include <stdlib.h>

#include <string.h>

#define N 10 /* 記録するゲーム数の上限 */

typedef char STRING[32];

typedef struct {

STRING name; /* 勝者氏名 */

int point; /* 得点 */

STRING date; /* 日付 */

} SCORE;

static int game_NO; /* 記録がとってあるゲーム数 */

static SCORE record[N]; /* 対戦記録保存用配列 */

FILE *fin,*fout;

void swap(SCORE *, SCORE *);

void quicksort(SCORE [], int, int);

void initialize_score()

/********************************************************************

対戦記録のをファイル othello.recから読みだし、配列 recordに書き込む

*********************************************************************/

{

STRING name,date; int i,point;

/* 対戦記録をファイルから読み出す */

fin=fopen("othello.rec", "r");

if (fin==NULL) {

printf("othello.recファイルがオープンできませんでした。\n");

exit(1);

}

i=0;

while (fscanf(fin,"%s %d %s", name, &point, date) > 0) {

i++;

strcpy(record[i].name,name); record[i].point=point;

strcpy(record[i].date,date);

}

game_NO=i;

}

SCORE update_score(STRING player_name, int point)

52

Page 53: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

/***************************************************************

ゲームの勝者 playerと得点 pointを対戦記録 recordに追加登録する。

recordは、pointが大きい順にソートしておく。pointの高い方から

Nゲームだけを記録として残す。

そのうちの最高点を獲得したゲームに関するデータを関数値とする。

****************************************************************/

{

time_t t; char *date;

++game_NO;

time(&t); date=ctime(&t);

/* 記録ゲーム数に余裕がある場合は、recordの末尾に追加する */

if (game_NO<N) {

strcpy(record[game_NO].name,player_name);

record[game_NO].point=point;

strcpy(record[game_NO].date,date);

}

/* 記録ゲーム数が Nを越えている場合は、今回の得点 pointが、記録されている

最小得点 record[1].pointより大きければそれと入れ替え、そうでなければ

今回の結果は記録せずに捨て去る */

else {

if (point>record[1].point) {

strcpy(record[1].name,player_name);

record[1].point=point;

strcpy(record[1].date,date);

}

}

quicksort(record,1,game_NO); /* recordをソートし直す */

return record[game_NO];

}

int show_score(int n)

/***************************************************************

対戦記録 recordの中から、pointが大きい順に n個を表示する。

関数値は、記録がない場合は負の値、その他の場合は負でない値

****************************************************************/

{

int i;

if (game_NO<=0) return -1;

printf(" ****************************\n");

printf(" * ハ イ ス コ ア 一 覧 *\n");

printf(" ****************************\n");

printf(" 氏 名 得点 日 時\n");

53

Page 54: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

for (i=game_NO; i>game_NO-n; i--)

printf("%16sさん %5d点 %s",

record[i].name,record[i].point,record[i].date);

}

void save_score()

/***************************************************************

対戦記録 recordをファイル othello.recにセーブする

****************************************************************/

{

FILE *fout;

int i;

fout=fopen("othello_rec", "w");

if (fout==NULL) {

printf("ファイル othello.recがオープンできませんでした。\n");

exit(1);

}

for (i=1; i<=game_NO; i++) {

fprintf(fout,"%s %d %d\n",

record[i].name, record[i].point, record[i].date);

}

}

void swap(SCORE *x, SCORE *y)

/************************************

xと yを入れ替える

*************************************/

{

SCORE z;

z = *x; *x = *y; *y = z;

}

void quicksort(SCORE a[], int left, int right)

/***********************************************************

配列 aの添字範囲 leftから rightを quicksortする

************************************************************/

{

int LEFT,RIGHT,middle;

if (left<right) {

middle=a[((LEFT=left)+(RIGHT=right))/2].point;

do {

while (a[LEFT].point<middle) LEFT++;

while (a[RIGHT].point>middle) RIGHT--;

if (LEFT<=RIGHT)

swap(&a[LEFT++], &a[RIGHT--]);

54

Page 55: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

} while (RIGHT>=LEFT);

if (left<RIGHT) quicksort(a,left,RIGHT);

if (LEFT<right) quicksort(a,LEFT,right);

}

}

void main()

{

SCORE x;

initialize_score();

x=update_score("ABC",40);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("def",42);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("uvw",30);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("PQR",45);

printf("%s, %d, %s\n", x.name,x.point,x.date);

x=update_score("xyz",45);

printf("%s, %d, %s\n", x.name,x.point,x.date);

printf("適当なキーを押して下さい。"); getchar();

system("clear");

puts("ハイスコア:全記録\n");

show_score(game_NO);

puts("\nハイスコア:上位3人\n");

show_score(3);

save_score();

}

55

Page 56: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

(課題N)私の名前は〇〇です

課題Lでは、コンピュータが求める形で対戦相手の人間の名前を入力した。これを、プ

ログラムの実行開始時に引数として入力するように改めよ。すなわち、

% othello 名前

のようにコマンドを投入してゲームを始めるものとし、ゲームの中で名前の入力を求めな

いものとする。

その他に、例えば次のようなことを組み込んでみよ:

1)ゲーム冒頭での挨拶を時間帯によって変える(例えば、5:00~12:00なら「おはよう」、

12:00~18:00なら「こんにちは」、18:00~5:00なら「こんばんは」)。

2)HUMANおよび COMPUTER双方の消費時間を表示する(すでに作ってある time 関係の

関数を利用せよ)。

(課題Nでの習得項目)

main関数の引数

56

Page 57: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

(課題O)先読みしよう

(課題Oでの習得項目)

再帰的関数

OSとのパラメータ受渡し

UNIXシェルプログラミング

57

Page 58: 総合課題「コンピュータゲームを作る」 - Waseda …...次のソーティングアルゴリズムは挿入法(insertion sort)と呼ばれるもの で、ソートしたいデータがソートされている状態に近いと効率がよい:

58

課題P 拡張

・人間同士が対戦することもできるようにする

・割り込み処理(中止のとき)

OSとの対話