🍱 学食ガチャ

GAKUSHOKU GACHA SYSTEM v1.0

▶ STEP 1 — 予算設定
予算: (50円刻みでご入力ください)
▶ STEP 2 — 結果
ガチャを回してください

▶ 過去1週間のログ(選出履歴)
ログなし
C言語ソースコード(参考実装)
// ============================================================
// gakushoku_gacha.c  —  学食ガチャ  C言語 参考実装
// コンパイル: gcc -o gacha gakushoku_gacha.c
// ============================================================

#include <stdio.h>   // printf / scanf / fopen などの標準入出力
#include <stdlib.h>  // rand / srand / atoi など
#include <string.h>  // memset / memcpy など文字列・メモリ操作
#include <time.h>    // time() でログの日時管理に使う

// ── 定数定義 ────────────────────────────────────────────────
#define MENU_COUNT  11          // メニューの総数
#define PICK_COUNT   3          // 1回のガチャで出す候補数
#define LOG_DAYS     7          // 過去何日分のログを参照するか
#define LOG_FILE  "gacha_log.txt" // ログの保存先ファイル名

// ── 構造体定義 ──────────────────────────────────────────────
// メニュー1件分の情報をまとめた構造体
// ポインタ経由でも参照できるよう typedef で型名を Menu にする
typedef struct {
    char name[64]; // メニュー名(文字列)
    int  money;    // 価格(円)
    int  weight;   // 抽選の重み。大きいほど当たりやすい
} Menu;

// ── メニューデータ(全11件) ─────────────────────────────────
// グローバル配列として宣言し、どの関数からもポインタで参照できる
Menu menuList[MENU_COUNT] = {
    { "SETMENU1",             530, 100 },
    { "SETMENU2",             580, 100 },
    { "DON(550)",             550, 100 },
    { "DON(500)",             500, 100 },
    { "NOODLE(かけうどん/そば)", 250, 100 },
    { "ラーメン",              550, 100 },
    { "NOODLE1",              550, 100 },
    { "NOODLE2",              450, 100 },
    { "IRONKITCHEN(600)",     600, 100 },
    { "IRONKITCHEN(コスパ丼)", 400, 100 },
    { "IRONKITCHEN(500)",     500, 100 },
};

// ============================================================
// 関数: loadLogWeights
// 作業: ログファイルを読み込み、過去7日間の選出回数を集計する。
//       選出が少ないメニューほど weight を大きくして
//       次のガチャで出やすくする(レア度補正)
// 引数: なし(グローバルの menuList を直接書き換える)
// ============================================================
void loadLogWeights(void) {

    // ログファイルを読み取りモードで開く
    FILE *fp = fopen(LOG_FILE, "r");
    if (!fp) {
        // ファイルがなければログなしとみなし、weight はデフォルトのまま
        printf("ログファイルなし。全メニューを均等確率で抽選します。\n");
        return;
    }

    // 各メニューの選出カウントを 0 で初期化
    int counts[MENU_COUNT];
    memset(counts, 0, sizeof(counts)); // 配列を全部 0 にする

    // 7日前のタイムスタンプ(カットオフ)を計算
    time_t now    = time(NULL);
    time_t cutoff = now - (time_t)(LOG_DAYS * 86400); // 86400秒 = 1日

    // ログを1行ずつ読み込む(while で EOF まで繰り返す)
    time_t ts;
    int    idx;
    while (fscanf(fp, "%ld %d", &ts, &idx) == 2) {

        // カットオフより新しく、かつ有効なインデックスのみカウント
        if (ts >= cutoff && idx >= 0 && idx < MENU_COUNT) {
            counts[idx]++; // そのメニューの出現回数を +1
        }
    }
    fclose(fp); // ファイルを閉じる(開いたら必ず閉じる)

    // for で全メニューをループし、出現回数に応じて weight を設定する
    int i; // ループカウンタ
    for (i = 0; i < MENU_COUNT; i++) {

        // ポインタ演算: &menuList[i] はi番目のMenuのアドレス
        // ここでは直接 menuList[i].weight でもアクセスできるが
        // ポインタを使う練習として Menu* p を使う
        Menu *p = &menuList[i]; // p は i番目のMenuを指すポインタ

        if (counts[i] == 0) {
            p->weight = 200; // 一度も出ていない → 最も出やすくする
        } else if (counts[i] == 1) {
            p->weight = 150; // 1回だけ出た → 少し出やすくする
        } else {
            p->weight = 100; // 2回以上出た → 標準の確率
        }
    }
}

// ============================================================
// 関数: calcTotalWeight
// 作業: プール内の全メニューの weight を合計して返す
//       weightedRandom から呼ばれる内部ヘルパー関数
// 引数: pool[]  抽選対象のインデックス配列(ポインタで受け取る)
//       size    pool の要素数
// 戻り値: 重みの合計値(int)
// ============================================================
int calcTotalWeight(int *pool, int size) {
    int total = 0;
    int i;
    for (i = 0; i < size; i++) {
        total += menuList[pool[i]].weight; // pool[i]番目のMenuのweightを加算
    }
    return total;
}

// ============================================================
// 関数: weightedRandom
// 作業: プールの中から weight に比例した確率で1件を選んで返す
//       weight が大きいメニューほど選ばれやすい
// 引数: pool[]  抽選対象インデックスの配列(ポインタで受け取る)
//       size    pool の要素数
// 戻り値: 選ばれたメニューの menuList インデックス(int)
// ============================================================
int weightedRandom(int *pool, int size) {

    // 合計重みを計算(別関数に切り出してポインタ渡し)
    int total = calcTotalWeight(pool, size);

    // 0 以上 total 未満のランダム値を生成
    int r = rand() % total;

    // for でプールを先頭から走査し、r を weight ずつ削っていく
    // r が 0 未満になった時点でそのメニューが「当たり」
    int i;
    for (i = 0; i < size; i++) {
        r -= menuList[pool[i]].weight;
        if (r < 0) {
            return pool[i]; // 当たり!このインデックスを返す
        }
    }
    return pool[size - 1]; // 浮動小数点誤差対策で末尾を返す(通常ここには来ない)
}

// ============================================================
// 関数: removeFromPool
// 作業: pool 配列から特定のインデックスを削除する
//       「末尾と入れ替えてサイズを 1 減らす」方式で O(1) 削除
// 引数: pool[]   対象の配列(ポインタ)
//       size     現在の要素数へのポインタ(削除後に -1 する)
//       target   削除したいメニューインデックス
// ============================================================
void removeFromPool(int *pool, int *size, int target) {
    int i;
    for (i = 0; i < *size; i++) {
        if (pool[i] == target) {
            // 見つかったら末尾の要素と入れ替えてサイズを減らす
            pool[i] = pool[--(*size)]; // *size を先に -1 してから末尾を代入
            return; // 削除完了なので即リターン
        }
    }
}

// ============================================================
// 関数: gacha
// 作業: 予算以下のメニューをプールに集め、重み付き抽選で
//       重複なし 3件を選んで results[] に格納する
// 引数: budget     予算(円)
//       excludeIdx リセマラで除外するインデックス(-1なら除外なし)
//       results[]  結果を書き込む配列(ポインタで受け取る)
// 戻り値: 実際に選べた件数(budget が低すぎると 3 未満になる)
// ============================================================
int gacha(int budget, int excludeIdx, int *results) {

    // ── Step1: 抽選プールを作る ──────────────────────────────
    int pool[MENU_COUNT]; // プール(選択候補のインデックスを入れる)
    int poolSize = 0;      // プールの現在の要素数
    int i;

    // for で全メニューをチェックし、条件を満たすものだけ pool に追加
    for (i = 0; i < MENU_COUNT; i++) {

        // ポインタでメニューを参照(menuList[i] と同じだが明示的に使う)
        Menu *m = &menuList[i];

        // 条件1: 価格が予算以下 条件2: リセマラ除外対象ではない
        if (m->money <= budget && i != excludeIdx) {
            pool[poolSize] = i; // インデックスをプールに追加
            poolSize++;         // プールサイズを増やす
        }
    }

    // ── Step2: プールから重複なしで PICK_COUNT 件を選ぶ ────────
    int picked   = 0;         // 選んだ件数のカウンタ
    int workPool[MENU_COUNT]; // pool のコピー(元 pool を壊さないため)
    int workSize = poolSize;

    // pool を workPool にコピー(ポインタ経由でメモリをまとめてコピー)
    memcpy(workPool, pool, sizeof(int) * poolSize);

    // while で「必要数に達するか、プールが空になるまで」繰り返す
    while (picked < PICK_COUNT && workSize > 0) {

        // 重み付きランダムで1件選ぶ(workPool のポインタを渡す)
        int chosen = weightedRandom(workPool, workSize);

        // 選ばれたインデックスを結果配列(ポインタ)に書き込む
        results[picked] = chosen;
        picked++;

        // 同じメニューが2回選ばれないよう workPool から chosen を削除
        removeFromPool(workPool, &workSize, chosen);
    }

    return picked; // 選べた件数を返す
}

// ============================================================
// 関数: printResults
// 作業: ガチャ結果(results[])を画面に見やすく表示する
// 引数: results[]  選ばれたメニューのインデックス配列(ポインタ)
//       n          結果の件数
// ============================================================
void printResults(int *results, int n) {
    printf("\n====== 今日の候補 ======\n");
    int i;
    for (i = 0; i < n; i++) {

        // ポインタを使ってメニューを参照
        Menu *m = &menuList[results[i]];

        printf("%d: %-24s ¥%d", i + 1, m->name, m->money);

        // weight が 100 より大きければ「出現率UP」表示
        if (m->weight > 100) {
            printf("  ★出現率UP!");
        }
        printf("\n");
    }
    printf("========================\n");
}

// ============================================================
// 関数: saveLog
// 作業: ガチャ結果をファイルに追記保存する
//       次回起動時に loadLogWeights() がこのデータを読む
// 引数: results[]  選ばれたメニューのインデックス配列(ポインタ)
//       n          結果の件数
// ============================================================
void saveLog(int *results, int n) {

    // 追記モード "a" で開く(ファイルがなければ自動作成される)
    FILE *fp = fopen(LOG_FILE, "a");
    if (!fp) {
        printf("警告: ログファイルを開けませんでした。\n");
        return;
    }

    time_t now = time(NULL); // 現在時刻を取得
    int i;

    // for で選ばれた件数分だけ「タイムスタンプ インデックス」を書き込む
    for (i = 0; i < n; i++) {
        fprintf(fp, "%ld %d\n", (long)now, results[i]);
    }
    fclose(fp); // 書き込んだら必ずファイルを閉じる
}

// ============================================================
// 関数: inputBudget
// 作業: ユーザーから予算を入力してもらい、その値を返す
//       Enter だけ押された場合はデフォルト値を返す
// 引数: defaultBudget  入力省略時に使うデフォルト予算
// 戻り値: 確定した予算(int)
// ============================================================
int inputBudget(int defaultBudget) {
    printf("予算を入力してください(50円刻み, デフォルト%d円): ", defaultBudget);
    char buf[32];
    fgets(buf, sizeof(buf), stdin); // 1行まるごと読み込む

    // Enter のみ(改行だけ)なら入力なしとみなしてデフォルトを返す
    if (buf[0] == '\n') {
        return defaultBudget;
    }

    // 文字列を整数に変換して返す
    return atoi(buf);
}

// ============================================================
// main 関数
// 作業: プログラム全体の流れを制御するエントリーポイント
//       1. 予算入力 → 2. ガチャ → 3. リセマラ or 確定 を繰り返す
// ============================================================
int main(void) {

    // 乱数の種をセット(time を使うことで毎回結果が変わる)
    srand((unsigned)time(NULL));

    // ── Step1: 予算の入力 ────────────────────────────────────
    int budget = inputBudget(1000); // デフォルト 1000 円

    // ── Step2: ログを読んでメニューの出現重みを補正する ─────────
    loadLogWeights();

    // ── Step3: ガチャのメインループ ──────────────────────────
    int results[PICK_COUNT]; // ガチャ結果を入れる配列
    int excludeIdx    = -1;  // -1 = 除外なし(リセマラ時に設定される)
    int reSimaraCount =  0;  // リセマラの回数カウンタ

    // while(1) で「確定 or 終了」するまで無限にループ
    while (1) {

        // ガチャを実行し、結果を results[] に書き込む(ポインタ渡し)
        int n = gacha(budget, excludeIdx, results);

        // 結果を画面に表示する
        printResults(results, n);

        // ユーザーの入力を受け付ける
        printf("[1〜3]これにする  [r]リセマラ  [q]終了 > ");
        char ans[8];
        fgets(ans, sizeof(ans), stdin);

        // 「q」が入力されたらプログラムを終了する
        if (ans[0] == 'q') {
            printf("終了します。\n");
            break; // while を抜ける
        }

        // 「r」が入力されたらリセマラ処理
        if (ans[0] == 'r') {

            // 初回リセマラのときだけ「最初の1位」を除外対象に設定する
            if (reSimaraCount == 0) {
                excludeIdx = results[0]; // 1位のインデックスを記憶
            }
            reSimaraCount++;

            // ポインタで除外中メニューの名前を表示
            Menu *excluded = &menuList[excludeIdx];
            printf("リセマラ %d回目(「%s」を除外中)\n",
                   reSimaraCount, excluded->name);

            continue; // while の先頭に戻り、もう一度ガチャを回す
        }

        // 「1〜3」が入力されたら選択確定
        int choice = ans[0] - '1'; // 文字 '1' を整数 0 に変換
        if (choice >= 0 && choice < n) {

            // ポインタで選ばれたメニューを参照して表示
            Menu *decided = &menuList[results[choice]];
            printf("「%s」に決定!よい食事を。\n", decided->name);

            // 今回の結果をログに保存する(次回の出現確率補正に使う)
            saveLog(results, n);
            break; // 確定したので while を抜ける
        }

        // 上記以外の入力は無効なので再度ループ
        printf("無効な入力です。もう一度入力してください。\n");
    }

    return 0; // main が 0 を返すと「正常終了」を意味する
}