C言語における擬似乱数の初期化と応用:制限付き数字当てゲームの開発手順

仕様の整理

本プロジェクトでは、以下のルールに基づいた対話型推測ゲームをC言語で構築する。

  • システムは1〜100の整数範囲から目標値を内部的に生成する。
  • プレイヤーの入力値に対して「更高い」「更低い」の判定を表示し、正解するまで繰り返す。
  • 挑戦上限は5回とし、回数耗尽後に敗北処理を行う。
  • 1ラウンド完了後、継続してプレイ可能な状態に移行する。

擬似乱数生成の基本原理

C言語の標準ライブラリ〈stdlib.h〉には rand() 関数が用意されており、0から RAND_MAX(環境依存だが概ね32767)の整数を返す。ただしこれは暗号学的安全性を持たない決定論的アルゴリズムによる擬似乱数であり、初期シード値が固定されている場合は同一プロセス起動内で連番が再現されてしまう。

毎回異なる数列を得るためには、srand() 関数でシード(種)を変更する必要がある。シード値自体が変動すれば、アルゴリズムの計算結果も必然的に変化するため、外部的に変数を与える方式が標準的な実装パターンとなる。

時刻を基にしたシード初期化

プログラム実行直前の状態として最も安定して変動するのはシステムクロックである。<time.h> を読み込み、time() 関数を呼び出すことでUnixエポック(1970年1月1日 00:00:00 UTC)からの経過秒数が取得できる。この値をキャストして srand() に渡せば、起動のたびに異なった初期状態が準備される。

#include <time.h>
// シードを現在時刻に変更
srand((unsigned int)time(NULL));

指定区間への収束処理

乱数生成器から得た値を任意の閉区間 [a, b] に制限する際は剰余演算を活用する。数学的には以下の変換式が成立する。

int value = rand() % (b - a + 1) + a;

例えば100〜150の範囲 desired な場合:rand() % 51 + 100 となる。この手法により、ランダム性を保ちつつ境界条件を厳密に制御可能である。

実装コード例

上記の概念を統合し、入力バリデーションと制御フローを整理した完全なサンプルを実装する。変数名は意味付けを明確化し、マジックナンバーを定数化している。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#ifdef _WIN32
    #include <windows.h>
#else
    #include <unistd.h>
#endif

#define MAX_ATTEMPTS 5
#define RANGE_START  1
#define RANGE_END    100

void render_menu(void) {
    printf("================================================\n");
    printf("[ 1 ] ゲームを開始する\n");
    printf("[ 0 ] アプリケーションを終了する\n");
    printf("================================================\n");
}

void execute_guessing_session(void) {
    int target_value = (rand() % (RANGE_END - RANGE_START + 1)) + RANGE_START;
    int attempts_left = MAX_ATTEMPTS;
    int user_prediction;

    printf("\n[ %d 〜 %d の数字を予想せよ ]\n", RANGE_START, RANGE_END);
    printf("許容誤差: %d 回\n\n", MAX_ATTEMPTS);

    while (attempts_left > 0) {
        printf("残り %d 回 / 推測値を入力: ", attempts_left);
        
        // 入力エラー時のバッファ破綻防止対策
        if (scanf("%d", &user_prediction) != 1) {
            printf(">> 無効なデータです。数字のみ入力してください。\n");
            while (getchar() != '\n');
            continue;
        }

        if (user_prediction == target_value) {
            printf("🎉 正解! 答えは %d でした。\n\n", target_value);
            break;
        } else if (user_prediction < target_value) {
            printf("⬆️ 推測値が高すぎます。\n");
        } else {
            printf("⬇️ 推測値が低すぎます。\n");
        }
        attempts_left--;
    }

    if (attempts_left == 0 && user_prediction != target_value) {
        printf("💀 チャレンジ失敗... 正解は %d でした。\n\n", target_value);
    }
}

int main(void) {
    srand((unsigned int)time(NULL));

    int menu_option;
    do {
        render_menu();
        printf("選択してください: ");
        scanf("%d", &menu_option);

        switch (menu_option) {
            case 1:
                execute_guessing_session();
#ifdef _WIN32
                Sleep(2500);          // Windows用一時停止
                system("cls");        // ウィンドウ表示クリア
#else
                sleep(3);             // POSIX系一時停止
                system("clear");
#endif
                break;
            case 0:
                printf("実行を终止しました。お疲れ様です。\n");
                break;
            default:
                printf(">> エラー: 対応していないコマンドです。\n\n");
                break;
        }
    } while (menu_option != 0);

    return EXIT_SUCCESS;
}

設計上の最適化ポイント

・switch-case 構文を採用し、メニュー分岐の可読性と拡張性を向上させた。
・scanf の戻り値を検知し、異常入力時に入力ストリームを強制的にクリーンアップする対策を実装。
・OS固有のスクリーンクリア関数とスリープ関数をプリプロセッサディレクティブで分岐させ、コンパイルターゲットの互換性を確保。
・境界値や最大試行回数を #define マクロへ分離し、ロジック修正時の保守コストを削減した。

タグ: C言語 擬似乱数 srand time 剰余演算

10月6日 02:30 投稿