Skip to main content

第3回 問題を作る処理を作る問題

🎲 第3回 重複しない4桁の問題を作ろう

今回は、コンピュータが出題する4桁の問題を作ります。

条件は、次のとおりです。

  • 使用する数字は0~9
  • 同じ数字を2回使わない
  • 実行するたびに違う並びになる

いきなりC言語のコードを書くのではなく、最初に疑似言語で処理の流れを整理してから、C言語へ置き換えていきます。


1.重複しない乱数をどう作るか

rand() % 10を4回実行するだけでは、同じ数字が出る可能性があります。

1 5 5 8

このままでは、「同じ数字を2回使わない」という条件を満たせません。

そこで、最初に0~9を1個ずつ並べ、その順番をシャッフルします。

最初:0 1 2 3 4 5 6 7 8 9
                 ↓ シャッフル
結果:8 2 5 0 7 1 9 4 6 3
                 ↓ 先頭4つ
問題:8 2 5 0

最初から重複していない数字を並べているので、順番を変えても重複は発生しません。

処理の流れは、次の3段階です。

0~9を順番に並べる
        ↓
ランダムに何度も入れ替える
        ↓
先頭4文字を問題として取り出す

2.問題を作る処理を疑似言語で確認する

まずは、問題を作る処理全体を疑似言語で確認します。

今回は、昔のIPAの疑似言語を参考にした記号式で表します。

このページでは、処理の流れを読みやすくするために、授業用の簡略表記を使用しています。

○ MakeProblem(文字型配列:answer)

○ 文字型配列:temp
○ 文字型:work
○ 整数型:i,index1,index2

・temp ← "0123456789"

■ iを0から99まで1ずつ増やす
│
│ ・index1 ← 0以上9以下の乱数
│ ・index2 ← 0以上9以下の乱数
│
│ ・work ← temp[index1]
│ ・temp[index1] ← temp[index2]
│ ・temp[index2] ← work
│
■

■ iを0から3まで1ずつ増やす
│
│ ・answer[i] ← temp[i]
│
■

・answer[4] ← '\0'

記号の読み方

記号 読み方
手続き、配列、変数などを用意する
その処理を実行する
右側の値を左側へ代入する
から 囲まれた処理を繰り返す
[i] 配列のi番目の要素を表す

例えば、

・temp ← "0123456789"

は、次のように読みます。

tempに、文字列"0123456789"を代入する。

次の部分は、

■ iを0から99まで1ずつ増やす

次のように読みます。

iを0、1、2、……、99と変化させながら、中の処理を100回繰り返す。

次の3行は、配列の2か所を交換する処理です。

・work ← temp[index1]
・temp[index1] ← temp[index2]
・temp[index2] ← work

次のように読みます。

temp[index1]の中身を一時変数workへ避難させてから、
temp[index1]temp[index2]の中身を入れ替える。

最後の繰り返しでは、シャッフルした配列の先頭4文字をanswerへコピーします。

■ iを0から3まで1ずつ増やす
│
│ ・answer[i] ← temp[i]
│
■

最後に、文字列の終わりを表す終端文字を入れます。

・answer[4] ← '\0'

これから、この疑似言語を上から順番にC言語へ置き換えていきます。


3.0~9を文字列として用意する

疑似言語の次の部分をC言語へ置き換えます。

・temp ← "0123456789"

C言語では、次のように書きます。

char temp[] = "0123456789";

配列の中には、次のように保存されます。

添え字       0    1    2    3    4    5    6    7    8    9    10
中身        '0'  '1'  '2'  '3'  '4'  '5'  '6'  '7'  '8'  '9'  '\0'

0~9の数字を整数として保存しているのではありません。

今回は、HIT&BLOWの問題を文字列として扱うため、'0'から'9'までの文字を配列に保存しています。


4.ここでC言語:乱数の準備

疑似言語では、次のように書きました。

・index1 ← 0以上9以下の乱数
・index2 ← 0以上9以下の乱数

C言語で乱数を使うには、次のヘッダーファイルを読み込みます。

#include <stdlib.h>
#include <time.h>

プログラムの最初で、乱数の種を設定します。

srand((unsigned int)time(NULL));

同じ種からは同じ順番の乱数が作られます。

そこで、実行するたびに変わる現在時刻を種として使い、毎回違う並びになりやすくします。

乱数を1個作るには、rand関数を使います。

int value = rand();

rand関数は、0からRAND_MAXまでの間の整数を1個返します。

RAND_MAXは、処理系が定めているかなり大きな整数です。

0~9へ収めるには、10で割った余りを使います。

int index = rand() % 10;

整数を10で割った余りは、必ず0~9になります。

そのため、rand() % 10の結果も0~9です。

疑似言語と対応させると、次のようになります。

疑似言語
・index1 ← 0以上9以下の乱数
index1 = rand() % 10;

5.配列の2要素を交換する

疑似言語では、次の処理で配列の2要素を交換しました。

・work ← temp[index1]
・temp[index1] ← temp[index2]
・temp[index2] ← work

例えば、temp[2]temp[7]を交換するとします。

直接上書きすると、片方の値が消えてしまいます。

temp[2] = temp[7];
temp[7] = temp[2];

1行目を実行した時点で、もともとのtemp[2]の値が失われます。

そこで、一時的に保存する変数を用意します。

char work;

work = temp[2];
temp[2] = temp[7];
temp[7] = work;

処理の流れは、次のとおりです。

temp[2]の値をworkへ避難
        ↓
temp[7]の値をtemp[2]へ移動
        ↓
workへ避難した値をtemp[7]へ移動

これは、2つのコップの中身を入れ替えるときに、空のコップを1個使うのと同じです。


6.100回入れ替えてシャッフルする

疑似言語の次の部分を、C言語へ置き換えます。

■ iを0から99まで1ずつ増やす
│
│ ・index1 ← 0以上9以下の乱数
│ ・index2 ← 0以上9以下の乱数
│
│ ・work ← temp[index1]
│ ・temp[index1] ← temp[index2]
│ ・temp[index2] ← work
│
■

C言語では、次のようになります。

int i;

for (i = 0; i < 100; i++)
{
    int index1 = rand() % 10;
    int index2 = rand() % 10;
    char work;

    work = temp[index1];
    temp[index1] = temp[index2];
    temp[index2] = work;
}

for文の中で、毎回次の処理を行います。

  1. 0~9の場所をランダムに2か所選ぶ
  2. その2か所の文字を入れ替える
  3. これを100回繰り返す

💡 今回は、シャッフルの考え方を分かりやすくするため、ランダムな2か所の交換を100回繰り返します。
0~9の10文字しかないので、100回も交換すれば見た目には十分混ざります。
ただし、すべての並びを厳密に同じ確率で作るシャッフル方法ではありません。


7.先頭4文字を問題へコピーする

疑似言語では、次のように書きました。

■ iを0から3まで1ずつ増やす
│
│ ・answer[i] ← temp[i]
│
■

・answer[4] ← '\0'

問題を保存する配列を用意します。

char answer[5];

4桁なのに5要素必要なのは、最後の1要素へ終端文字'\0'を入れるためです。

シャッフルした配列の先頭4文字をコピーします。

for (i = 0; i < 4; i++)
{
    answer[i] = temp[i];
}

実際には、次の4回の代入が行われます。

answer[0] = temp[0];
answer[1] = temp[1];
answer[2] = temp[2];
answer[3] = temp[3];

最後に、終端文字を入れます。

answer[4] = '\0';

これを忘れると、answerを正しい文字列として表示できません。

添え字       0    1    2    3    4
answer      '8'  '2'  '5'  '0'  '\0'

8.問題作成を関数にする

ここまで作った処理を、MakeProblem関数にまとめます。

void MakeProblem(char answer[5])
{
    char temp[] = "0123456789";
    int i;

    for (i = 0; i < 100; i++)
    {
        int index1 = rand() % 10;
        int index2 = rand() % 10;
        char work;

        work = temp[index1];
        temp[index1] = temp[index2];
        temp[index2] = work;
    }

    for (i = 0; i < 4; i++)
    {
        answer[i] = temp[i];
    }

    answer[4] = '\0';
}

疑似言語の、

○ MakeProblem(文字型配列:answer)

が、C言語では次の部分に対応します。

void MakeProblem(char answer[5])

なぜ戻り値ではなく引数へ入れるのか

この関数は、呼び出し側で用意した配列answerの中身を書き換えます。

char answer[5];
MakeProblem(answer);

関数を呼び出した後、answerの中に問題が入っています。

呼び出す前

answer
[ ? ][ ? ][ ? ][ ? ][ ? ]

        ↓ MakeProblem(answer)

呼び出した後

answer
[ 8 ][ 2 ][ 5 ][ 0 ][\0]

💡 配列を関数へ渡すと、関数の中から呼び出し側の配列を書き換えられます。

MakeProblem関数そのものは、呼び出し元へ値を1個返しているわけではありません。

そのため、戻り値の型はvoidです。
(配列書き換えたいときはこうだよ。って覚えちゃったほうがすっきりするよ)


9.動作確認用プログラム

ここまでの処理を1つのプログラムにまとめます。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void MakeProblem(char answer[5])
{
    char temp[] = "0123456789";
    int i;

    for (i = 0; i < 100; i++)
    {
        int index1 = rand() % 10;
        int index2 = rand() % 10;
        char work;

        work = temp[index1];
        temp[index1] = temp[index2];
        temp[index2] = work;
    }

    for (i = 0; i < 4; i++)
    {
        answer[i] = temp[i];
    }

    answer[4] = '\0';
}

int main(void)
{
    char answer[5];

    srand((unsigned int)time(NULL));
    MakeProblem(answer);

    printf("問題:%s\n", answer);

    return 0;
}

#define _CRT_SECURE_NO_WARNINGSは、この回のコードだけではまだ必要ありません。
後の回でscanfを使用するため、シリーズ全体で同じ形にそろえて入れています。

何度か実行し、毎回違う4桁が表示されることを確認してください。

問題:8250

もう一度実行すると、違う問題が表示されます。

問題:4071

ゲーム完成時には、答えを表示する次のprintfをコメントアウトします。

printf("問題:%s\n", answer);

問題を画面に表示したままでは、答えを当てるゲームにならないからです。


🏁 今回のまとめ

  • 0~9を最初に1個ずつ並べる
  • 疑似言語で問題作成の流れを整理した
  • 配列の順番をシャッフルする
  • rand()は0からRAND_MAXまでの整数を返す
  • rand() % 10で0~9の添え字を作る
  • 配列の要素を交換するときは、一時変数を使う
  • ランダムな2か所の交換を100回繰り返す
  • 先頭4文字を問題としてコピーする
  • 文字列の最後には'\0'を入れる
  • 配列は関数へ渡して中身を書き換えられる
  • MakeProblem関数は戻り値を返さないため、戻り値の型はvoid

次回は、プレイヤーが何度も4桁を入力できるループを作ります。