GAKUSHOKU GACHA SYSTEM v1.0
// ============================================================ // 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 を返すと「正常終了」を意味する }