前回の「すごろくゲーム」の続きです。
前回までのプログラムを実行してみると分かりますが、ゴールするというプログラムがありませんので、ゴールを超えても延々と続いていきます。
これは非常に危険な状態につながります。
配列は、基本的に準備された添え字を超えて読み書きしてはいけません。
ゴールを飛び越えて進むということは、用意した添え字0から29(MAX_MASU - 1)を超えています。
今は不具合は出ていませんが、ここからプログラムを続けていくと様々な不具合が出てきますので今のうちに対処します。
まずはゴールするプログラムを作ってみましょう。
ゴールするということは、マス目がGOALの所に止まったということです。
駒のpositionは添え字を表していますから、
masu[position] |
と書けば、マス目の中で駒が止まっている所になります。
そこがGOALであれば、「上がり!」と表示し、無限ループを抜けましょう。
考えてみてください。
解答例です。
<sample program 075-01>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX_MASU 30
#define START 0
#define NORMAL 1
#define GOAL 2
int main(void)
{
int masu[MAX_MASU];
int i;
int position;
int dice;
/* 乱数の種をセット */
srand((unsigned int)time(NULL));
/* マス目の初期化 */
for (i = 0; i < MAX_MASU; i++) {
masu[i] = NORMAL;
}
masu[0] = START;
masu[MAX_MASU - 1] = GOAL;
/* 駒の初期化 */
position = 0;
for (;;) {
system("cls");
/* マス目の表示 */
for (i = 0; i < MAX_MASU; i++) {
switch (masu[i]) {
case START:
printf("S");
break;
case NORMAL:
printf(".");
break;
case GOAL:
printf("G");
break;
}
}
printf("\n");
/* 駒の表示 */
for (i = 0; i < position; i++) {
printf(" ");
}
printf("P");
printf("\n");
/* サイコロを振る */
printf("サイコロを振ります\n");
getchar();
dice = rand() % 6 + 1;
/* 駒を進める */
printf("%d進みます\n", dice);
getchar();
position += dice;
/* ゴールチェック */
if (masu[position] == GOAL) {
printf("上がり!\n");
getchar();
break;
}
}
return 0;
}
|
<実行結果>
S............................G P サイコロを振ります 5進みます 上がり! 続行するには何かキーを押してください・・・
上の実行結果は「たまたま」ゴールした時の結果です。
やってみたら分かるように、丁度ゴールに止まらなければゴールを超えて進んでいきます。
危険ですが、ゴールを超えてもしつこくサイコロを振っているとゴールする時があります。
これは「たまたま」配列を超えた先にGOALと同じデータがあったということです。
上にも書きましたが、これは危険なプログラムです。
このまま放置は出来ませんので、さらなる対処が必要です。
ゴールを超えさせないために、通常のすごろくでも大きく2つのルールがあるように思います。
1.ゴールにたどり着いたら、サイコロの目が余っていてもゴールとみなす 2.ゴール時にサイコロの目が余っていたら、その分後戻りする
今回は1番を採用します。
とは言え、1歩1歩チェックしてゴールしたかどうかを判断している訳ではありません。
サイコロの目だけ一気に位置が変わるので、ゴールを超えたかどうかマス目の中身を見たのでは判断がつきません。
そこで、MAX_MASUを使ってpositionがMAX_MASU - 1を超えないよう調整します。
<sample program 075-02>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX_MASU 30
#define START 0
#define NORMAL 1
#define GOAL 2
int main(void)
{
int masu[MAX_MASU];
int i;
int position;
int dice;
/* 乱数の種をセット */
srand((unsigned int)time(NULL));
/* マス目の初期化 */
for (i = 0; i < MAX_MASU; i++) {
masu[i] = NORMAL;
}
masu[0] = START;
masu[MAX_MASU - 1] = GOAL;
/* 駒の初期化 */
position = 0;
for (;;) {
system("cls");
/* マス目の表示 */
for (i = 0; i < MAX_MASU; i++) {
switch (masu[i]) {
case START:
printf("S");
break;
case NORMAL:
printf(".");
break;
case GOAL:
printf("G");
break;
}
}
printf("\n");
/* 駒の表示 */
for (i = 0; i < position; i++) {
printf(" ");
}
printf("P");
printf("\n");
/* サイコロを振る */
printf("サイコロを振ります\n");
getchar();
dice = rand() % 6 + 1;
/* 駒を進める */
printf("%d進みます\n", dice);
getchar();
position += dice;
if (position >= MAX_MASU) {
position = MAX_MASU - 1;
}
/* ゴールチェック */
if (masu[position] == GOAL) {
printf("上がり!\n");
getchar();
break;
}
}
return 0;
}
|
<実行結果>
S............................G P サイコロを振ります 5進みます 上がり! 続行するには何かキーを押してください・・・
上の実行結果でも分かる通り、残り3でゴールですが、サイコロの目が5でもゴールしています。
これで完成!
ではありません。
実行結果を見ると、駒を表す"P"はゴールまで進んでいない状態でプログラムが終わっています。
なぜこうなっているかというと、実行する順番に原因があります。
プログラム中のコメントを抜き出して「部品ごとに」実行順を追ってみます。
<実行順> for (;;) { /* マス目の表示 */ /* 駒の表示 */ /* サイコロを振る */ /* 駒を進める */ /* ゴールチェック */ break; } |
ゴールチェック後、マス目や駒を表示することなくbreakしているため、ゴール後の様子が表示されないのです。
これを解消するには、駒を進めた後にマス目や駒を表示してから、ゴールのチェックをするよう順番を変える必要があります。
<新しい実行順> for (;;) { /* マス目の表示 */ /* 駒の表示 */ /* ゴールチェック */ break; /* サイコロを振る */ /* 駒を進める */ } |
<sample program 075-03>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX_MASU 30
#define START 0
#define NORMAL 1
#define GOAL 2
int main(void)
{
int masu[MAX_MASU];
int i;
int position;
int dice;
/* 乱数の種をセット */
srand((unsigned int)time(NULL));
/* マス目の初期化 */
for (i = 0; i < MAX_MASU; i++) {
masu[i] = NORMAL;
}
masu[0] = START;
masu[MAX_MASU - 1] = GOAL;
/* 駒の初期化 */
position = 0;
for (;;) {
system("cls");
/* マス目の表示 */
for (i = 0; i < MAX_MASU; i++) {
switch (masu[i]) {
case START:
printf("S");
break;
case NORMAL:
printf(".");
break;
case GOAL:
printf("G");
break;
}
}
printf("\n");
/* 駒の表示 */
for (i = 0; i < position; i++) {
printf(" ");
}
printf("P");
printf("\n");
/* ゴールチェック */
if (masu[position] == GOAL) {
printf("上がり!\n");
getchar();
break;
}
/* サイコロを振る */
printf("サイコロを振ります\n");
getchar();
dice = rand() % 6 + 1;
/* 駒を進める */
printf("%d進みます\n", dice);
getchar();
position += dice;
if (position >= MAX_MASU) {
position = MAX_MASU - 1;
}
}
return 0;
}
|
<実行結果>
S............................G P 上がり! 続行するには何かキーを押してください・・・
これできちんとゴールした様子が見れるようになりました。
このようにプログラムは実行手順が非常に重要です。
長いプログラムになると、どのような手順で実行されているか分かりづらくなりますので、コメントをきちんと入れて分かりやすくする工夫をしてください。
ずいぶん先になりますが、プログラムを分割して記述する方法も説明します。
さて、このままでは何も面白くありませんので、1つイベントを入れてみます。
よくあるパターンのイベントですが、「スタート地点に戻る」というイベントを入れます。
マス目に配置するため、「スタート地点に戻る」イベントは3番ということにします。
#defineを追加してください。
名前は TRAP_START にしましょう。
<sample program 075-043> 抜粋
#define START 0
#define NORMAL 1
#define GOAL 2
#define TRAP_START 3
|
ここで新しい命令の説明をします。
上の#defineですが、イベントを増やす度にどんどん増えていくことが予想されます。
4番、5番、6番と追加していくのですが、0から始まる連番を定義するときに#defineよりも簡単に定義する方法があります。
それがenum(列挙体)です。
まずは、上の#defineと同じ事を列挙体を使って書くとどうなるか書いてみます。
enum { START, NORMAL, GOAL, TRAP_START, }; |
※括弧閉じるの後のセミコロンを忘れないよう注意してください。
こうやって書くだけで、STARTには0をNORMALには1をGOALには2を割り当ててくれます。
追加したい場合は、どんどん名前を書くだけで追加出来ます。
この数値は整数として使えますので、#defineと同じく計算などにも使用できます。
連番の開始番号を変更することも可能で、
enum { AAA = 5, BBB, CCC, DDD, }; |
とすれば、AAAは5、BBBは6、CCCは7となります。
とりあえず、#defineの箇所を列挙体で書き換えてみましょう。
次は、どこにこのトラップを仕掛けるか、ですがランダムで配置することにします。
マス目の中にランダムで配置しますが、総数は決めたいと思います。
今回は3か所に設置することにします。
そこで#defineを使ってTRAP_START_NUMを定義し、3に設定しましょう。
TRAP_START_NUM回(3回)繰り返すfor文を作成し、マス目にトラップを仕掛けるプログラムを作ってください。
気を付けるべき所は、スタート地点やゴール地点にはトラップを仕掛けてはいけない、という所です。
TRAP_START の表示は "*" にしましょう。
解答例です。
<sample program 075-05>
#include <stdio.h> #include <stdlib.h> #include <time.h> #define MAX_MASU 30 enum { START, NORMAL, GOAL, TRAP_START, }; #define TRAP_START_NUM 3 int main(void) { int masu[MAX_MASU]; int i; int position; int dice; int index; /* 乱数の種をセット */ srand((unsigned int)time(NULL)); /* マス目の初期化 */ for (i = 0; i < MAX_MASU; i++) { masu[i] = NORMAL; } masu[0] = START; masu[MAX_MASU - 1] = GOAL; /* トラップの設置 */ for (i = 0; i < TRAP_START_NUM; i++) { index = rand() % (MAX_MASU - 2) + 1; masu[index] = TRAP_START; } /* 駒の初期化 */ position = 0; for (;;) { system("cls"); /* マス目の表示 */ for (i = 0; i < MAX_MASU; i++) { switch (masu[i]) { case START: printf("S"); break; case NORMAL: printf("."); break; case GOAL: printf("G"); break; case TRAP_START: printf("*"); break; } } printf("\n"); /* 駒の表示 */ for (i = 0; i < position; i++) { printf(" "); } printf("P"); printf("\n"); /* ゴールチェック */ if (masu[position] == GOAL) { printf("上がり!\n"); getchar(); break; } /* サイコロを振る */ printf("サイコロを振ります\n"); getchar(); dice = rand() % 6 + 1; /* 駒を進める */ printf("%d進みます\n", dice); getchar(); position += dice; if (position >= MAX_MASU) { position = MAX_MASU - 1; } } return 0; } |
<実行結果>
S.......*.....*....*.........G P 上がり! 続行するには何かキーを押してください・・・
まだトラップの発動は作っていませんので、スタート地点に戻ることはありません。
ただ、何度か実行すると "*" が2つしかないケースを見かけることがあると思います。
<実行結果>
S.......*..........*.........G P サイコロを振ります
このバグの原因は何でしょうか?
考えてみてください。
解答です。
答えは、同じ箇所に2つトラップを設置しているからです。
乱数で適当に位置を決定しているため、同じ乱数が出た場合、同じ個所に設置してしまいます。
どうすれば対処出来るでしょうか?
考えて修正してみましょう。
解答例です。
<sample program 075-06>
#include <stdio.h> #include <stdlib.h> #include <time.h> #define MAX_MASU 30 enum { START, NORMAL, GOAL, TRAP_START, }; #define TRAP_START_NUM 3 int main(void) { int masu[MAX_MASU]; int i; int position; int dice; int index; /* 乱数の種をセット */ srand((unsigned int)time(NULL)); /* マス目の初期化 */ for (i = 0; i < MAX_MASU; i++) { masu[i] = NORMAL; } masu[0] = START; masu[MAX_MASU - 1] = GOAL; /* トラップの設置 */ for (i = 0; i < TRAP_START_NUM; i++) { do { index = rand() % (MAX_MASU - 2) + 1; } while (masu[index] == TRAP_START); masu[index] = TRAP_START; } /* 駒の初期化 */ position = 0; for (;;) { system("cls"); /* マス目の表示 */ for (i = 0; i < MAX_MASU; i++) { switch (masu[i]) { case START: printf("S"); break; case NORMAL: printf("."); break; case GOAL: printf("G"); break; case TRAP_START: printf("*"); break; } } printf("\n"); /* 駒の表示 */ for (i = 0; i < position; i++) { printf(" "); } printf("P"); printf("\n"); /* ゴールチェック */ if (masu[position] == GOAL) { printf("上がり!\n"); getchar(); break; } /* サイコロを振る */ printf("サイコロを振ります\n"); getchar(); dice = rand() % 6 + 1; /* 駒を進める */ printf("%d進みます\n", dice); getchar(); position += dice; if (position >= MAX_MASU) { position = MAX_MASU - 1; } } return 0; } |
<実行結果>
S.*........*..........*......G P サイコロを振ります
do〜whileを使って、すでにトラップが設置されている場合はもう一度乱数を取得し直します。
これで重複することは無くなりました。
最後にトラップの発動です。
スタート地点に戻るトラップですから、positionを0にするだけです。
どこでチェックすれば良いか、考えて作ってみてください。
解答例です。
<sample program 075-07>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX_MASU 30
enum {
START,
NORMAL,
GOAL,
TRAP_START,
};
#define TRAP_START_NUM 3
int main(void)
{
int masu[MAX_MASU];
int i;
int position;
int dice;
int index;
/* 乱数の種をセット */
srand((unsigned int)time(NULL));
/* マス目の初期化 */
for (i = 0; i < MAX_MASU; i++) {
masu[i] = NORMAL;
}
masu[0] = START;
masu[MAX_MASU - 1] = GOAL;
/* トラップの設置 */
for (i = 0; i < TRAP_START_NUM; i++) {
do {
index = rand() % (MAX_MASU - 2) + 1;
} while (masu[index] == TRAP_START);
masu[index] = TRAP_START;
}
/* 駒の初期化 */
position = 0;
for (;;) {
system("cls");
/* マス目の表示 */
for (i = 0; i < MAX_MASU; i++) {
switch (masu[i]) {
case START:
printf("S");
break;
case NORMAL:
printf(".");
break;
case GOAL:
printf("G");
break;
case TRAP_START:
printf("*");
break;
}
}
printf("\n");
/* 駒の表示 */
for (i = 0; i < position; i++) {
printf(" ");
}
printf("P");
printf("\n");
/* ゴールチェック */
if (masu[position] == GOAL) {
printf("上がり!\n");
getchar();
break;
}
/* サイコロを振る */
printf("サイコロを振ります\n");
getchar();
dice = rand() % 6 + 1;
/* 駒を進める */
printf("%d進みます\n", dice);
getchar();
position += dice;
if (position >= MAX_MASU) {
position = MAX_MASU - 1;
}
/* イベント処理 */
if (masu[position] == TRAP_START) {
printf("スタート地点へ戻る・・・\n");
getchar();
position = 0;
}
}
return 0;
}
|
<実行結果>
S.*........*..........*......G P サイコロを振ります 2進みます スタート地点へ戻る・・・
これでトラップが発動するようになりました。
ただ、トラップの箇所にプレイヤーの"P"が移動する場面は表示されません。
表示する前にpositionを0に戻しているため、スタート地点に戻ってしまっています。
これを解消するように直すと、スタート地点に戻るメッセージを出す前に画面を再度表示し直す必要があります。
これ以上プログラムを書くと非常に長くなりますので、ここから先は皆さんにお任せします。
色々とイベントを増やしていくと様々な不具合が出てくると思います。
皆さんで工夫しながら作ってみてください。