【C言語プログラミング13】マリオのピクロスを完成させる
前回に引き続きピクロスを作ります。前回までの作業でカーソルが自由に動かせるようになりました。今回で完成させます。
不正解のブロックにペケ印を付ける
数字をヒントにここは削っちゃイカン!というブロックが明らかになった場合、そのブロックにペケ印を付けて、誤って削らないようにします。あるブロックにカーソルが当たっている状態で、ペケ印の意味の’x’の入力があったらブロックにペケ印を表示します。switch-case文のcaseに'x'を追加します。
case 'x':
if (GameBlocks[cursorY][cursorX] == W) {
GameBlocks[cursorY][cursorX] = X;
}
else if (GameBlocks[cursorY][cursorX] == X) {
GameBlocks[cursorY][cursorX] = W;
}
break;
GameBlocksの[cursorY][cursorX]番目がWだったら、そこにXを入れます。逆に[cursorY][cursorX]番目がXだったら、そこにWを入れます。WやXは冒頭で#define W 1、#define B 2、#define X 3と定義します。W(White)は数値の1、B(Black)は数値の2、X(ペケ印)は数値の3です。ここの処理はブロックが白(削られていなければ)ペケ印にする。ペケ印であれば、白に戻すという処理になります。
ブロックを削る
あるブロックにカーソルが当たっている状態でEnterキーを押すとブロックが削れるようにします。但し、この処理は単純に削るわけには行きません。不正解のブロックが削られた場合、アラームを鳴らさないといけません。アラームはWindowsの警告音で賄います。ポロン♬みたいな音です。あるブロックが正解か否かを判断する為には、正解のブロックがどんなものなのかを定義しておく必要があります。それを定義したのが以下です。AnswerBlocksという名称で二次元配列をもう一つ定義しました。Wが白、Bが黒です。Bだけに注目するとメガネの形になっていますね。
/* 解答ブロック */
char AnswerBlocks[NUM_OF_BLOCK_Y + NUM_OF_HINT][NUM_OF_BLOCK_X + NUM_OF_HINT] = {
{ W , W , W , W , W , W , W , W , W , W },
{ W , B , B , W , W , W , W , B , B , W },
{ B , W , W , B , W , W , B , W , W , B },
{ B , W , W , W , W , W , W , W , W , B },
{ B , W , W , W , W , W , W , W , W , B },
{ B , B , B , B , W , W , B , B , B , B },
{ B , W , W , B , W , W , B , W , W , B },
{ B , W , W , B , B , B , B , W , W , B },
{ B , W , W , B , W , W , B , W , W , B },
{ W , B , B , B , W , W , B , B , B , W },
};
正解ブロックが準備できたので、Enterキーが押された時の処理をswitch-caseのcaseに追加しましょう。'\r'はEnterキーが入力された時に入って来る文字です。cursorYとcursorXは現在のカーソルの位置を保持していますので、その位置情報を元にAnswerBlocksの内容をチェックします。
もし、AnswerBlocksの現在のカーソルの位置がB(Black)であれば、そこは削る対象だと言うことになりますので、GameBlocksの同じ位置にBを入れています。正解!とコメントのある行ですね。AnswerBlocksの現在のカーソルの位置がW(White)であれば、そこは削ってはいけない場所なので、printf("\a");としています。これでアラームを鳴らすことができます。
case '\r':
if (AnswerBlocks[cursorY][cursorX] == B) {
GameBlocks[cursorY][cursorX] = B; // 正解!
}
else if (AnswerBlocks[cursorY][cursorX] == W) {
printf("\a"); // 不正解!アラーム!
}
break;
ゲームブロック表示処理を修正する
前回作ったゲームブロック表示処理では、白ブロックとカーソル(*)しか表示できませんので、黒ブロックとペケ印を表示できるようにしましょう。BとXを表示するようのelse if 文を追加しました。これで、白、黒、✕、*、ヒント数字が全て表示できるようになりました。
for (int y = 0; y < 13; y++) {
for (int x = 0; x < 15; x++) {
if (GameBlocks[y][x] == 0) {
break;
}
else if (y == cursorY && x == cursorX) { // カーソルを表示
printf("*");
}
else if (GameBlocks[y][x] == W) { // 白ブロックを表示(削られる前)
printf("■");
}
else if (GameBlocks[y][x] == B) { // 黒グロックを表示(削られた後)
printf("□");
}
else if (GameBlocks[y][x] == X) {
printf("X"); // ペケ印を表示
}
else {
printf("%c%c", GameBlocks[y][x], ' '); // ヒントを表示
}
}
}
ゲームクリア判定
ブロックを削ったら、ゲームクリアの判定を行います。削る必要があるブロックが全て削られたかどうかを判定し、オールクリアなら正解の画像を表示しゲームを終了します。判定の方法はシンプルで、AnserBlocksでBと定義されている箇所とGameBlocksのBが入っている箇所が全部一致すればゲームクリアです。ソースコードは以下の通り。
int GameClear = true;
/* 結果判定 */
for (int y = 0; y < NUM_OF_BLOCK_Y; y++) {
for (int x = 0; x < NUM_OF_BLOCK_X; x++) {
if (AnswerBlocks[y][x] == B && GameBlocks[y][x] != B) {
GameClear = false;
break;
}
}
if (GameClear == false) {
break;
}
}
/* 正解の絵を表示する */
if (GameClear == true) {
system("cls");
printf("Game Clear!!!!\n\n");
for (int y = 0; y < NUM_OF_BLOCK_Y; y++) {
for (int x = 0; x < NUM_OF_BLOCK_X; x++) {
if (AnswerBlocks[y][x] == B) {
printf("□");
}
else {
printf("%c%c", ' ', ' ');
}
}
printf("\n");
}
break;
}
最初にGameClear変数を宣言し、trueを入れています。trueは1のことです。そして、forループを使いAnswerBlocksとGameBlocksの内容を左上から右下に向かって順番に比較します。AnswerBlocksの内容でBつまり削る対象であるブロックがGameBlocksでもBになっていればOKです。Bであるべき箇所が一つでもB以外であればGameClear変数にfalse(0)を入れてbreakします。ループ処理内にbreakを入れるとループ回数に到達していなくても即抜けることができます。
判定処理が終わると、正解表示処理に入ります。GameClear変数がtrueであればゲームクリアとなるので、system("cls");で画面を一旦クリアして、正解の絵を表示します。正解の絵は見やすくするために、■だけで表示し、☐の箇所はスペースで埋めます。
Let's play!!
以下プレイ画面です。ブロックを削れているし、ペケ印も付けられています。大丈夫そうですね。
以下ゲームクリア画面となります。
全ソースコード
ピクロスゲームの全ソースコードは以下の通りです。一旦はこれで動きます。
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define NUM_OF_BLOCK_X 10
#define NUM_OF_BLOCK_Y 10
#define NUM_OF_HINT 8
#define W 1 /* 白ブロック */
#define B 2 /* 黒ブロック */
#define X 3 /* ペケ印 */
#define true 1
#define false 0
/* ゲームブロック */
char GameBlocks[NUM_OF_BLOCK_Y + NUM_OF_HINT][NUM_OF_BLOCK_X + NUM_OF_HINT] = {
{ W , W , W , W , W , W , W , W , W , W , '0',' ',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '2','2',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '4','4',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','4','1',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '3','3',' ',' ','\n' },
{ '7','1','1','1','1','1','1','1','1','7','\n' },
{ ' ','1','1','5',' ',' ','5','1','1',' ','\n' },
{ ' ','1','1',' ',' ',' ',' ','1','1',' ','\n' },
};
/* 解答ブロック */
char AnswerBlocks[NUM_OF_BLOCK_Y + NUM_OF_HINT][NUM_OF_BLOCK_X + NUM_OF_HINT] = {
{ W , W , W , W , W , W , W , W , W , W },
{ W , B , B , W , W , W , W , B , B , W },
{ B , W , W , B , W , W , B , W , W , B },
{ B , W , W , W , W , W , W , W , W , B },
{ B , W , W , W , W , W , W , W , W , B },
{ B , B , B , B , W , W , B , B , B , B },
{ B , W , W , B , W , W , B , W , W , B },
{ B , W , W , B , B , B , B , W , W , B },
{ B , W , W , B , W , W , B , W , W , B },
{ W , B , B , B , W , W , B , B , B , W },
};
void main(void)
{
int cursorX = 0;
int cursorY = 0;
while (1) {
system("cls");
/*--------------------------*/
/* ゲームブロックを表示する */
/*--------------------------*/
printf("%c", ' ');
for (int y = 0; y < 13; y++) {
for (int x = 0; x < 15; x++) {
if (GameBlocks[y][x] == 0) {
break;
}
else if (y == cursorY && x == cursorX) {
printf("*");
}
else if (GameBlocks[y][x] == W) { // 白ブロックを表示(削られる前)
printf("■");
}
else if (GameBlocks[y][x] == B) { // 黒グロックを表示(削られた後)
printf("□");
}
else if (GameBlocks[y][x] == X) {
printf("X"); // ペケ印を表示
}
else {
printf("%c%c", GameBlocks[y][x], ' '); // ヒントを表示
}
}
}
/*----------------------------------*/
/* キーボードからの入力を監視する */
/*----------------------------------*/
char key = _getch();
switch (key) {
case 'w': // ↑
if (cursorY > 0) {
cursorY--;
}
break;
case 's': // ↓
if (cursorY < NUM_OF_BLOCK_Y - 1) {
cursorY++;
}
break;
case 'a': // ←
if (cursorX > 0) {
cursorX--;
}
break;
case 'd': // →
if (cursorX < NUM_OF_BLOCK_X - 1) {
cursorX++;
}
break;
case 'x': // ペケ印
/* 削ってはいけない場所をペケでマーキングする */
if (GameBlocks[cursorY][cursorX] == W) {
GameBlocks[cursorY][cursorX] = X;
}
else if (GameBlocks[cursorY][cursorX] == X) {
GameBlocks[cursorY][cursorX] = W;
}
break;
case '\r': // 削る
/* 削ろうとしているブロックが正解かどうかを判定する */
if (AnswerBlocks[cursorY][cursorX] == B) {
GameBlocks[cursorY][cursorX] = B; // 正解!
}
else if (AnswerBlocks[cursorY][cursorX] == W) {
printf("\a"); // 不正解!アラーム!
}
break;
default:
break;
}
/* 結果判定 */
int GameClear = true;
for (int y = 0; y < NUM_OF_BLOCK_Y; y++) {
for (int x = 0; x < NUM_OF_BLOCK_X; x++) {
if (AnswerBlocks[y][x] == B && GameBlocks[y][x] != B) {
GameClear = false;
break;
}
}
if (GameClear == false) {
break;
}
}
/* 正解の絵を表示する */
if (GameClear == true) {
system("cls");
printf("Game Clear!!!!\n\n");
for (int y = 0; y < NUM_OF_BLOCK_Y; y++) {
for (int x = 0; x < NUM_OF_BLOCK_X; x++) {
if (AnswerBlocks[y][x] == B) {
printf("□");
}
else {
printf("%c%c", ' ', ' ');
}
}
printf("\n");
}
break;
}
}
}
改良版ソースコード
全ソースコードで示したソースコードは一つの関数が縦に長くて読みにくいですよね。こんなソースのことをスパゲッティソースと言いまして、あまり褒められるものではありません。この程度のコード量であれば、何とか全体を把握できますが、もっとコード量が増えると読めたものではなくなります。通常は特定の処理単位で関数化します。以下は改良版のソースコードです。
1)ゲームブロックを表示する処理
2)キーボードの入力をチェックする処理
3)ゲームクリアを判定する処理
4)解答を表示する処理
をそれぞれ関数化し、main関数からそれらをコールする形にしました。こうすることで、例えばキーボードからの入力に新しいキーを追加したい場合、CheckInput関数だけに着目して変更をすることができます。最初のスパゲティに比べれば随分読みやすくなりましたね。ソースコードは読み易いに越したことはありません。以下のコードには関数の頭にブロックコメントで関数の説明を記載しています。このコメントのことを関数ヘッダと言って、関数が何者なのかを簡単に説明するために使います。単にソースコードが書かれているだけよりも、分かり易いですね。
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define NUM_OF_BLOCK_X 10
#define NUM_OF_BLOCK_Y 10
#define NUM_OF_HINT 8
#define W 1 /* 白ブロック */
#define B 2 /* 黒ブロック */
#define X 3 /* ペケ印 */
#define true 1
#define false 0
/* ゲームブロック */
char GameBlocks[NUM_OF_BLOCK_Y + NUM_OF_HINT][NUM_OF_BLOCK_X + NUM_OF_HINT] = {
{ W , W , W , W , W , W , W , W , W , W , '0',' ',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '2','2',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '4','4',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','4','1',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '3','3',' ',' ','\n' },
{ '7','1','1','1','1','1','1','1','1','7','\n' },
{ ' ','1','1','5',' ',' ','5','1','1',' ','\n' },
{ ' ','1','1',' ',' ',' ',' ','1','1',' ','\n' },
};
/* 解答ブロック */
char AnswerBlocks[NUM_OF_BLOCK_Y + NUM_OF_HINT][NUM_OF_BLOCK_X + NUM_OF_HINT] = {
{ W , W , W , W , W , W , W , W , W , W },
{ W , B , B , W , W , W , W , B , B , W },
{ B , W , W , B , W , W , B , W , W , B },
{ B , W , W , W , W , W , W , W , W , B },
{ B , W , W , W , W , W , W , W , W , B },
{ B , B , B , B , W , W , B , B , B , B },
{ B , W , W , B , W , W , B , W , W , B },
{ B , W , W , B , B , B , B , W , W , B },
{ B , W , W , B , W , W , B , W , W , B },
{ W , B , B , B , W , W , B , B , B , W },
};
// グローバル変数
int cursorX = 0;
int cursorY = 0;
// 関数プロトタイプ宣言
void printGameBlocks(void);
void CheckInput(void);
int CheckGameClear(void);
void printAnswer(void);
//-------------------------------------------
// main関数
//-------------------------------------------
void main(void)
{
while (1) {
// ゲームブロックを表示する
printGameBlocks();
// キーボードからの入力をチェックする
CheckInput();
// 結果判定
int GameClear = CheckGameClear();
// 解答表示
if (GameClear == true) {
printAnswer();
break;
}
}
}
//-------------------------------------------
// 関数 : printGameBlocks
// 説明 : ゲームブロックを表示する
// 引数 : なし
// 戻値 : なし
//-------------------------------------------
void printGameBlocks(void)
{
system("cls");
printf("%c", ' ');
for (int y = 0; y < 13; y++) {
for (int x = 0; x < 15; x++) {
if (GameBlocks[y][x] == 0) {
break;
}
else if (y == cursorY && x == cursorX) {
printf("*");
}
else if (GameBlocks[y][x] == W) { // 白ブロックを表示(削られる前)
printf("■");
}
else if (GameBlocks[y][x] == B) { // 黒グロックを表示(削られた後)
printf("□");
}
else if (GameBlocks[y][x] == X) {
printf("X"); // ペケ印を表示
}
else {
printf("%c%c", GameBlocks[y][x], ' '); // ヒントを表示
}
}
}
}
//-------------------------------------------
// 関数 : CheckInput
// 説明 : キーボードからの入力をチェックする
// 引数 : なし
// 戻値 : なし
//-------------------------------------------
void CheckInput(void)
{
char key = _getch();
switch (key) {
case 'w': // ↑
if (cursorY > 0) {
cursorY--;
}
break;
case 's': // ↓
if (cursorY < NUM_OF_BLOCK_Y - 1) {
cursorY++;
}
break;
case 'a': // ←
if (cursorX > 0) {
cursorX--;
}
break;
case 'd': // →
if (cursorX < NUM_OF_BLOCK_X - 1) {
cursorX++;
}
break;
case 'x': // ペケ印
/* 削ってはいけない場所をペケでマーキングする */
if (GameBlocks[cursorY][cursorX] == W) {
GameBlocks[cursorY][cursorX] = X;
}
else if (GameBlocks[cursorY][cursorX] == X) {
GameBlocks[cursorY][cursorX] = W;
}
break;
case '\r': // 削る
/* 削ろうとしているブロックが正解かどうかを判定する */
if (AnswerBlocks[cursorY][cursorX] == B) {
GameBlocks[cursorY][cursorX] = B; // 正解!
}
else if (AnswerBlocks[cursorY][cursorX] == W) {
printf("\a"); // 不正解!アラーム!
}
break;
default:
break;
}
}
//-------------------------------------------
// 関数 : CheckGameClear
// 説明 : ゲームクリアを判定する
// 引数 : なし
// 戻値 : int true = クリア false = 未クリア
//-------------------------------------------
int CheckGameClear(void)
{
int GameClear = true;
for (int y = 0; y < NUM_OF_BLOCK_Y; y++) {
for (int x = 0; x < NUM_OF_BLOCK_X; x++) {
if (AnswerBlocks[y][x] == B && GameBlocks[y][x] != B) {
GameClear = false;
break;
}
}
if (GameClear == false) {
break;
}
}
return GameClear;
}
//-------------------------------------------
// 関数 : printAnswer
// 説明 : 解答の絵を表示する
// 引数 : なし
// 戻値 : なし
//-------------------------------------------
void printAnswer(void)
{
system("cls");
printf("Game Clear!!!!\n\n");
for (int y = 0; y < NUM_OF_BLOCK_Y; y++) {
for (int x = 0; x < NUM_OF_BLOCK_X; x++) {
if (AnswerBlocks[y][x] == B) {
printf("□");
}
else {
printf("%c%c", ' ', ' ');
}
}
printf("\n");
}
}
まとめ
今回でピクロスゲームが完成です。環境がある方は実際に動かしてみて下さい。特にテクニカルなコードは書いていませんので、操作方法など改造も簡単に出来ると思います。今回はメガネだけですが、二次元配列のGameBlocksとAnswerBlocksを定義し直せば、無限に遊ぶことができます。ゲームトップ画面を作って、ステージ選択機能を追加しても良いかも知れませんね。
今回でC言語プログラミングが13まで来ましたが、ここまで登場した文法が使えれば、ほとんどどんなプログラムでも書けます。後はどう組み合わせるかだけの話です。しかし、それが難しい。ひらがな、カタカナ、常用漢字が読み書きできるからと言って、小説が書けるかと言うと、それは別の話でしょう。プログラムも同じです。
私の場合、「こんなんやりたいなぁ~」と思ったことをパッとソースコードにするのに三年はかかりました。1日8時間が3年です。そして、それを人が見ても分かり易く、修正や変更が簡単にできるようなソースコードが書けるようになるには更に3年かかっています。5、6年ってのは自分の周りを見ても大体平均的な話です。これ、ホント。まぁ、仕事にするとしたらの話ですけどね。
今回でコンソールを使った何かを作るのはお終いにして、次からは組み込みの世界に入ろうかと思います。C言語でどうやってLEDが光るんじゃい!ってな話です。それでは、今回はこの辺で。お疲れ様でした。