Pada proyek kedua ini kita membangun game tebak angka klasik: komputer menyimpan angka acak antara 1 sampai 100, dan pengguna punya 10 ronde untuk menebak. Setelah setiap tebakan, program memberi petunjuk “terlalu kecil” atau “terlalu besar” sehingga pengguna dapat mempersempit rentang dengan strategi binary search. Proyek ini melatih tiga keterampilan penting: memakai pustaka <stdlib.h> untuk angka acak, mengelola loop dengan batas ronde, dan menghitung skor berdasarkan ronde kemenangan.

Program yang akan kita bangun terdiri dari dua berkas logika: game.h/game.c berisi aturan game (rentang tebakan, periksa tebakan, hitung skor), dan main.c berisi I/O interaktif. Pemisahan ini memungkinkan kita menulis test runner yang menguji aturan tanpa menyimulasikan keyboard.

Tampilan Program Akhir #

Sesi pemakaian berikut menunjukkan hasil akhir proyek. Karena angka rahasia acak, tebakan yang benar-benar menang bergantung pada saat program dijalankan:

text
=== Game Tebak Angka ===
Saya menyimpan angka antara 1 sampai 100.
Anda punya 10 ronde untuk menebak.

Tebakan Anda: Ronde 1: Tebakan terlalu besar.
Tebakan Anda: Ronde 2: Tebakan terlalu besar.
Tebakan Anda: Ronde 3: Tebakan terlalu besar.
Tebakan Anda: Ronde 4: Tebakan terlalu kecil.
Tebakan Anda: Ronde 5: Tebakan terlalu kecil.
Tebakan Anda: Ronde 6: Tebakan terlalu kecil.
Tebakan Anda: Ronde 7: Tebakan terlalu kecil.
Tebakan Anda: Ronde 8: Tebakan terlalu kecil.
Tebakan Anda: Ronde 9: Tebakan terlalu kecil.
Tebakan Anda: Ronde 10: Tebakan benar!

Selamat! Angka rahasia: 7
Anda menang di ronde 10 dengan skor 10.

Skor penuh 100 diberikan bila pengguna menebak dengan benar di ronde pertama. Setiap ronde tambahan mengurangi skor 10 poin, sehingga menang di ronde ke-10 menghasilkan skor 10. Strategi binary search optimal menjamin kemenangan dalam paling banyak tujuh tebakan.

Tujuan Pembelajaran #

Setelah menyelesaikan proyek ini, kita mampu:

  • memakai srand() dan rand() dari <stdlib.h> untuk menghasilkan angka acak,
  • memetakan rentang rand() % N ke rentang custom [min, maks],
  • mendesain enum status (TEBAK_BENAR, TEBAK_KECIL, TEBAK_BESAR, dst.) untuk dispatch yang jelas,
  • mengelola loop for dengan batas ronde dan keluar lebih awal saat menang,
  • menghitung skor berbasis ronde dengan validasi input aman,
  • menulis test untuk fungsi yang melibatkan nilai acak.

Prasyarat #

Proyek ini memakai materi bab sebelumnya:

  • Library Standar C — <stdlib.h>, <time.h>, <stdio.h>,
  • Tipe Data dan Operator — int, modulo %, perbandingan,
  • Kontrol Alur — for, switch, break, continue,
  • Fungsi dan Header Files — modularisasi game.h/game.c,
  • Struct dan Enum — TebakStatus,
  • Validasi Input — pola fgets + strtol.

Tahapan Pengerjaan #

Proyek dikerjakan dalam lima bagian berurutan:

  1. Bagian 1: Random Number — memakai srand(time(NULL)) dan rand() % N untuk menghasilkan angka rahasia dalam rentang 1-100.
  2. Bagian 2: Loop dan Kondisi — loop for dengan 10 ronde dan dispatch switch pada TebakStatus.
  3. Bagian 3: Validasi Tebakan — menolak tebakan di luar rentang dan input non-numerik tanpa memutus loop.
  4. Bagian 4: Sistem Skor — menghitung skor berbasis ronde kemenangan dengan validasi aman.
  5. Bagian 5: Refactoring dan Testing — memisahkan logika ke game.c dan menulis tests.c dengan 10 test case.

Mulai dari Bagian 1 dan ikuti urutannya. Berkas sumber lengkap tersedia pada bagian bawah halaman masing-masing part.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

game.c

Unduh berkas
c
/* game.c -- implementasi logika game tebak angka. */

#include "game.h"

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

void game_init(void) {
    /* Seed generator dengan waktu saat ini. */
    srand((unsigned int)time(NULL));
}

int game_angka_rahasia(void) {
    /* rand() % N menghasilkan nilai [0, N-1]; kita geser ke rentang yang diinginkan. */
    int rentang = GAME_TEBAK_MAKS - GAME_TEBAK_MIN + 1;
    return GAME_TEBAK_MIN + (rand() % rentang);
}

TebakStatus game_periksa(int tebak, int rahasia, int ronde_kini) {
    if (ronde_kini > GAME_MAKS_RONDE) {
        return TEBAK_HABIS;
    }
    if (tebak < GAME_TEBAK_MIN || tebak > GAME_TEBAK_MAKS) {
        return TEBAK_INVALID;
    }
    if (tebak == rahasia) {
        return TEBAK_BENAR;
    }
    if (tebak < rahasia) {
        return TEBAK_KECIL;
    }
    return TEBAK_BESAR;
}

int game_hitung_skor(int ronde_menang) {
    /* Setiap ronde setelah ronde 1 mengurangi 10 poin. */
    int pengurangan = (ronde_menang - 1) * 10;
    int skor = GAME_SKOR_AWAL - pengurangan;
    if (skor < 0) skor = 0;
    return skor;
}

int game_hitung_skor_aman(int ronde_menang) {
    /* Versi yang menolak input tidak valid. */
    if (ronde_menang < 1 || ronde_menang > GAME_MAKS_RONDE) {
        return 0;
    }
    return game_hitung_skor(ronde_menang);
}

const char *game_rentang_teks(void) {
    static char buffer[32];
    snprintf(buffer, sizeof(buffer), "%d sampai %d",
             GAME_TEBAK_MIN, GAME_TEBAK_MAKS);
    return buffer;
}

main.c

Unduh berkas
c
/* main.c -- titik masuk game tebak angka.
 *
 * Memisahkan logika (game.c) dari I/O (main.c) supaya game.c dapat diuji
 * tanpa menyimulasikan keyboard.
 */

#include "game.h"

#include <stdio.h>
#include <stdlib.h>

/* Membaca satu integer dari stdin.
 * Mengembalikan 1 bila berhasil, 0 bila input tidak terurai, -1 bila EOF.
 */
static int baca_int(const char *prompt, int *out) {
    char buffer[64];
    printf("%s", prompt);
    if (fgets(buffer, sizeof(buffer), stdin) == 0) return -1;
    char *sisa = 0;
    long nilai = strtol(buffer, &sisa, 10);
    if (sisa == buffer) return 0;
    *out = (int)nilai;
    return 1;
}

static const char *status_pesan(TebakStatus s) {
    switch (s) {
        case TEBAK_BENAR:  return "Tebakan benar!";
        case TEBAK_KECIL:  return "Tebakan terlalu kecil.";
        case TEBAK_BESAR:  return "Tebakan terlalu besar.";
        case TEBAK_INVALID:return "Tebakan di luar rentang yang sah.";
        case TEBAK_HABIS:  return "Ronde sudah habis.";
        default:           return "Status tidak dikenal.";
    }
}

int main(void) {
    game_init();
    int rahasia = game_angka_rahasia();

    printf("=== Game Tebak Angka ===\n");
    printf("Saya menyimpan angka antara %s.\n", game_rentang_teks());
    printf("Anda punya %d ronde untuk menebak.\n\n", GAME_MAKS_RONDE);

    int menang = 0;
    int ronde_menang = 0;

    for (int ronde = 1; ronde <= GAME_MAKS_RONDE; ronde++) {
        int tebak = 0;
        int hasil_baca = baca_int("Tebakan Anda: ", &tebak);
        if (hasil_baca == -1) {
            printf("\nInput berakhir (EOF). Berhenti.\n");
            break;
        }
        if (hasil_baca == 0) {
            printf("Input tidak valid. Coba lagi.\n");
            ronde--;   /* jangan pakai ronde untuk input invalid */
            continue;
        }

        TebakStatus s = game_periksa(tebak, rahasia, ronde);
        printf("Ronde %d: %s\n", ronde, status_pesan(s));

        if (s == TEBAK_INVALID) {
            ronde--;   /* input invalid tidak mengurangi ronde */
            continue;
        }
        if (s == TEBAK_BENAR) {
            menang = 1;
            ronde_menang = ronde;
            break;
        }
    }

    if (menang) {
        int skor = game_hitung_skor_aman(ronde_menang);
        printf("\nSelamat! Angka rahasia: %d\n", rahasia);
        printf("Anda menang di ronde %d dengan skor %d.\n", ronde_menang, skor);
    } else {
        printf("\nMaaf, ronde habis. Angka rahasia: %d\n", rahasia);
    }
    return 0;
}

tests.c

Unduh berkas
c
/* tests.c -- rangkaian test untuk modul game. */

#include "game.h"

#include <stdio.h>

static int lulus = 0;
static int gagal = 0;

#define PERIKSA(kondisi, nama)                              \
    do {                                                    \
        if (kondisi) { lulus++; printf("[LULUS] %s\n", nama); } \
        else { gagal++; printf("[GAGAL] %s\n", nama); }     \
    } while (0)

static void test_rentang_rahasia(void) {
    /* Karena angka acak, kita tes sebanyak 1000 kali untuk memastikan
     * seluruh hasil berada dalam rentang sah.
     */
    game_init();
    int semua_sah = 1;
    for (int i = 0; i < 1000; i++) {
        int r = game_angka_rahasia();
        if (r < GAME_TEBAK_MIN || r > GAME_TEBAK_MAKS) {
            semua_sah = 0;
            break;
        }
    }
    PERIKSA(semua_sah, "Angka rahasia selalu di rentang sah");
}

static void test_periksa_benar(void) {
    TebakStatus s = game_periksa(50, 50, 1);
    PERIKSA(s == TEBAK_BENAR, "Tebakan 50 dengan rahasia 50 -> BENAR");
}

static void test_periksa_kecil(void) {
    TebakStatus s = game_periksa(30, 50, 1);
    PERIKSA(s == TEBAK_KECIL, "Tebakan 30 dengan rahasia 50 -> KECIL");
}

static void test_periksa_besar(void) {
    TebakStatus s = game_periksa(70, 50, 1);
    PERIKSA(s == TEBAK_BESAR, "Tebakan 70 dengan rahasia 50 -> BESAR");
}

static void test_periksa_invalid(void) {
    TebakStatus s = game_periksa(0, 50, 1);
    PERIKSA(s == TEBAK_INVALID, "Tebakan 0 di luar rentang -> INVALID");
}

static void test_periksa_habis(void) {
    TebakStatus s = game_periksa(50, 50, GAME_MAKS_RONDE + 1);
    PERIKSA(s == TEBAK_HABIS, "Ronde > MAKs -> HABIS");
}

static void test_skor_ronde1(void) {
    int skor = game_hitung_skor_aman(1);
    PERIKSA(skor == GAME_SKOR_AWAL, "Menang ronde 1 -> skor penuh");
}

static void test_skor_ronde5(void) {
    int skor = game_hitung_skor_aman(5);
    PERIKSA(skor == 60, "Menang ronde 5 -> skor 60");
}

static void test_skor_invalid(void) {
    int skor = game_hitung_skor_aman(0);
    PERIKSA(skor == 0, "Ronde 0 -> skor 0");
    skor = game_hitung_skor_aman(GAME_MAKS_RONDE + 1);
    PERIKSA(skor == 0, "Ronde > MAKs -> skor 0");
}

int main(void) {
    test_rentang_rahasia();
    test_periksa_benar();
    test_periksa_kecil();
    test_periksa_besar();
    test_periksa_invalid();
    test_periksa_habis();
    test_skor_ronde1();
    test_skor_ronde5();
    test_skor_invalid();
    printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
    return gagal == 0 ? 0 : 1;
}