Mesin simulasi dari bagian 3 sudah mencetak tabel per pelanggan, tetapi tabel itu masih mentah: memutuskan “apakah layanan ini perlu teller tambahan” berarti membaca ulang sepuluh baris angka setiap kali pertanyaan muncul. Bagian terakhir proyek ini menambahkan lapisan analisis di atas mesin: struct Statistik yang meringkas satu eksekusi menjadi lima angka, program final yang menjalankan tiga skenario berdampingan, dan harness uji yang mengunci seluruh hasil. Ketiganya bertumpu pada satu prinsip yang kita pegang sejak bagian 1 — karena simulasi ini deterministik, angkanya bisa dijadikan kontrak, bukan sekadar catatan.

Lima Angka yang Meringkas Satu Eksekusi #

Struct Statistik dideklarasikan di samping model pada src/simulasi-antrean.cpp:

cpp src/simulasi-antrean.cpp — struct Statistik
struct Statistik {
    double rataTunggu = 0.0;
    int tungguMaks = 0;
    int antreanMaks = 0;
    double utilisasi = 0.0;
    int selesaiPada = 0; // menit saat seluruh pelanggan selesai dilayani
};

Setelah loop tick selesai dan seluruh riwayat lengkap, jalankanSimulasi mengisi struct ini:

cpp src/simulasi-antrean.cpp — perhitungan statistik di akhir jalankanSimulasi
    long long totalTunggu = 0;
    for (const Pelanggan& p : riwayat) {
        int tunggu = p.mulaiPada - p.datangPada;
        totalTunggu += tunggu;
        stat.tungguMaks = std::max(stat.tungguMaks, tunggu);
    }
    stat.rataTunggu = static_cast<double>(totalTunggu) / riwayat.size();

    int selesaiTerakhir = 0;
    for (const Pelanggan& p : riwayat) {
        selesaiTerakhir = std::max(selesaiTerakhir, p.selesaiPada);
    }
    stat.selesaiPada = selesaiTerakhir;
    stat.utilisasi =
        static_cast<double>(teller.jumlahDilayani * s.durasiLayanan) /
        selesaiTerakhir;
    return riwayat;

Mari kita bedah keputusan di balik setiap field:

  1. rataTunggu: total waktu tunggu diakumulasi pada long long — aman untuk skenario besar — lalu dibagi riwayat.size() setelah static_cast<double>. Tanpa cast, pembagian integer akan membuang bagian pecahan; rata-rata 4.5 menit akan tercetak sebagai 4.
  2. tungguMaks: rata-rata menyembunyikan pengalaman terburuk. Dua skenario bisa punya rata-rata sama tetapi maksimum berbeda jauh — dan bagi orang yang mengantre, yang terasa adalah kasusnya sendiri.
  3. antreanMaks: diukur di akhir setiap tick dengan std::max, sesuai definisi yang kita tetapkan di bagian 1 — banyaknya orang yang masih menunggu setelah teller mengambil satu pelanggan.
  4. selesaiPada: nilai selesaiPada tertinggi, yaitu menit ketika pelanggan terakhir selesai dilayani. Angka ini juga menjadi penyebut formula utilisasi.
  5. utilisasi: menit kerja teller dibagi menit operasional — (jumlahDilayani * durasiLayanan) / selesaiTerakhir. Perkalian int dulu, baru dikonversi ke double untuk pembagian; hasilnya berada di rentang 0 sampai 1 dan dikalikan 100 saat dicetak.

Program Final: Tiga Skenario Berdampingan #

Program final merangkum seluruh perjalanan proyek: model dari bagian 1, antrean dan Teller dari bagian 2, tick loop dari bagian 3, ditambah perhitungan dan pencetakan statistik. Ia menjalankan tiga skenario — lancar, sibuk, dan ramai — lalu mencetak tabel ringkas beserta detail skenario ramai:

cpp
// simulasi-antrean.cpp — Proyek: Simulasi Sistem Antrean (versi final).
// Simulasi antrean teller tunggal yang DETERMINISTIK: jadwal kedatangan
// (interval tetap) dan durasi layanan (tetap) ditentukan penuh oleh
// skenario, tanpa angka acak, sehingga setiap eksekusi menghasilkan
// angka yang persis sama.

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <queue>
#include <string>
#include <vector>

// ---------- Model ----------
struct Skenario {
    std::string nama;
    int intervalKedatangan; // jarak antar kedatangan (menit)
    int durasiLayanan;      // lama satu layanan (menit)
    int jumlahPelanggan;
};

struct Pelanggan {
    int id;
    int datangPada;  // menit kedatangan
    int mulaiPada;   // menit mulai dilayani
    int selesaiPada; // menit selesai dilayani
};

struct Teller {
    int id;
    int bebasPada;      // menit teller bebas melayani lagi
    int jumlahDilayani; // banyak pelanggan yang sudah dilayani
};

struct Statistik {
    double rataTunggu = 0.0;
    int tungguMaks = 0;
    int antreanMaks = 0;
    double utilisasi = 0.0;
    int selesaiPada = 0; // menit saat seluruh pelanggan selesai dilayani
};

// ---------- Mesin simulasi: satu tick = satu menit ----------
std::vector<Pelanggan> jalankanSimulasi(const Skenario& s, Statistik& stat) {
    std::queue<Pelanggan> antrean;
    Teller teller{1, 0, 0};
    std::vector<Pelanggan> riwayat;
    stat = Statistik{};

    int tick = 0;
    int sudahDatang = 0;
    while (static_cast<int>(riwayat.size()) < s.jumlahPelanggan) {
        // Kedatangan: pelanggan ke-n tiba tepat pada n * interval.
        if (sudahDatang < s.jumlahPelanggan &&
            tick == sudahDatang * s.intervalKedatangan) {
            antrean.push(Pelanggan{sudahDatang + 1, tick, 0, 0});
            ++sudahDatang;
        }
        // Teller yang bebas mengambil antrean terdepan (FIFO).
        if (tick >= teller.bebasPada && !antrean.empty()) {
            Pelanggan p = antrean.front();
            antrean.pop();
            p.mulaiPada = tick;
            p.selesaiPada = tick + s.durasiLayanan;
            teller.bebasPada = p.selesaiPada;
            ++teller.jumlahDilayani;
            riwayat.push_back(p);
        }
        // Panjang antrean diukur di akhir tick (setelah teller mengambil).
        stat.antreanMaks =
            std::max(stat.antreanMaks, static_cast<int>(antrean.size()));
        ++tick;
    }

    long long totalTunggu = 0;
    for (const Pelanggan& p : riwayat) {
        int tunggu = p.mulaiPada - p.datangPada;
        totalTunggu += tunggu;
        stat.tungguMaks = std::max(stat.tungguMaks, tunggu);
    }
    stat.rataTunggu = static_cast<double>(totalTunggu) / riwayat.size();

    int selesaiTerakhir = 0;
    for (const Pelanggan& p : riwayat) {
        selesaiTerakhir = std::max(selesaiTerakhir, p.selesaiPada);
    }
    stat.selesaiPada = selesaiTerakhir;
    stat.utilisasi =
        static_cast<double>(teller.jumlahDilayani * s.durasiLayanan) /
        selesaiTerakhir;
    return riwayat;
}

// ---------- Laporan ----------
void cetakBarisStatistik(const Skenario& s, const Statistik& stat) {
    std::cout << std::left << std::setw(10) << s.nama << std::right
              << std::setw(13) << std::fixed << std::setprecision(3)
              << stat.rataTunggu << std::setw(13) << stat.tungguMaks
              << std::setw(14) << stat.antreanMaks << std::setw(11)
              << std::setprecision(1) << stat.utilisasi * 100.0 << '%'
              << std::setw(10) << stat.selesaiPada << '\n';
}

int main() {
    std::vector<Skenario> skenario = {
        {"lancar", 5, 4, 10},
        {"sibuk", 4, 4, 10},
        {"ramai", 3, 4, 10},
    };

    std::cout << "Simulasi Antrean Teller — deterministik (tanpa angka acak)\n";
    std::cout << '\n';
    std::cout << "skenario  interval  layanan  pelanggan\n";
    for (const Skenario& s : skenario) {
        std::cout << std::left << std::setw(10) << s.nama << std::right
                  << std::setw(8) << s.intervalKedatangan << std::setw(9)
                  << s.durasiLayanan << std::setw(10) << s.jumlahPelanggan
                  << '\n';
    }

    std::cout << '\n';
    std::cout << "Hasil ringkas:\n";
    std::cout << "skenario   rata tunggu  tunggu maks  antrean maks  utilisasi"
                 "  selesai (mnt)\n";

    std::vector<Pelanggan> riwayatRamai;
    for (const Skenario& s : skenario) {
        Statistik stat;
        std::vector<Pelanggan> riwayat = jalankanSimulasi(s, stat);
        if (s.nama == "ramai") {
            riwayatRamai = riwayat;
        }
        cetakBarisStatistik(s, stat);
    }

    std::cout << '\n';
    std::cout << "Detail skenario ramai (waktu dalam menit):\n";
    std::cout << "id  datang  mulai  selesai  tunggu\n";
    for (const Pelanggan& p : riwayatRamai) {
        std::cout << std::right << std::setw(2) << p.id << std::setw(8)
                  << p.datangPada << std::setw(7) << p.mulaiPada << std::setw(9)
                  << p.selesaiPada << std::setw(8) << p.mulaiPada - p.datangPada
                  << '\n';
    }
    return 0;
}

Output yang Dihasilkan:

text
Simulasi Antrean Teller — deterministik (tanpa angka acak)

skenario  interval  layanan  pelanggan
lancar           5        4        10
sibuk            4        4        10
ramai            3        4        10

Hasil ringkas:
skenario   rata tunggu  tunggu maks  antrean maks  utilisasi  selesai (mnt)
lancar            0.000            0             0       81.6%        49
sibuk             0.000            0             0      100.0%        40
ramai             4.500            9             3      100.0%        40

Detail skenario ramai (waktu dalam menit):
id  datang  mulai  selesai  tunggu
 1       0      0        4       0
 2       3      4        8       1
 3       6      8       12       2
 4       9     12       16       3
 5      12     16       20       4
 6      15     20       24       5
 7      18     24       28       6
 8      21     28       32       7
 9      24     32       36       8
10      27     36       40       9

Baca tabel ringkasnya baris demi baris. Skenario lancar (interval 5, layanan 4) mencatat utilisasi 81.6%: selama 49 menit operasional, teller bekerja 10 x 4 = 40 menit, dan sisanya menganggur karena kedatangan berikutnya selalu datang satu menit setelah layanan selesai — 40 / 49 kira-kira 0.816. Skenario sibuk (interval 4 sama dengan layanan 4) berada pada titik jenuh yang rapi: setiap pelanggan berikutnya datang tepat saat teller selesai, sehingga tidak pernah ada yang menunggu, tetapi teller juga tidak pernah kosong — utilisasi 100.0% dengan antrean maksimum 0. Skenario ramai (interval 3 lebih pendek dari layanan 4) mengumpulkan keterlambatan yang tidak pernah sempat habis: antrean maksimum 3 orang, tunggu maksimum 9 menit, rata-rata 4.500 menit.

Utilisasi Tinggi Saja Tidak Menjawab Semuanya #

Tabel berikut menyusun ulang hasil ketiga skenario agar pertukarannya terlihat:

SkenarioInterval (mnt)UtilisasiAntrean maksRata tunggu (mnt)
lancar581.6%00.000
sibuk4100.0%00.000
ramai3100.0%34.500

Dua skenario terakhir sama-sama mencapai utilisasi 100%, tetapi cerita operasionalnya bertolak belakang: sibuk sehat tanpa satu pun penunggu, sedangkan ramai menumpuk orang dengan tunggu maksimum 9 menit. Angka utilisasi sendirian tidak membedakan keduanya. Kesimpulan analitiknya: utilisasi mendekati 100% masih wajar selama interval kedatangan tidak lebih pendek dari durasi layanan; begitu terbalik, antrean tumbuh terus selama sistem berjalan. Keputusan seperti “perlu teller kedua?” karenanya tidak bisa diambil dari satu kolom — kita membutuhkan rata tunggu, tunggu maksimum, dan panjang antrean maksimum bersama-sama. Model acak dari sistem yang sama (kedatangan Poisson, layanan eksponensial, rantai antrean M/M/1) memperdalam analisis ini; versi deterministik kita menjadi jangkar angka yang bisa dihitung tangan sebelum menyentuh keacakan.

Mengunci Hasil dengan Harness Uji #

Statistik yang menarik belum tentu statistik yang bisa dipercaya — dan angka yang bergantung pada detail kecil (ukur panjang antrean sebelum atau sesudah teller mengambil?) mudah bergeser diam-diam saat kode disentuh. Karena itu langkah terakhir proyek adalah harness uji yang menyalin inti simulator, mengujinya pada skenario kecil yang bisa diperiksa manual, lalu mengunci nilai emas skenario ramai:

cpp
// uji-simulasi.cpp — harness uji untuk proyek Simulasi Sistem Antrean.
// Memeriksa perilaku inti simulasi: urutan FIFO, layanan teller tunggal yang
// tidak tumpang tindih, nilai emas skenario "ramai", dan determinisme
// (dua eksekusi menghasilkan keadaan yang identik). Exit code 0 = semua lulus.

#include <algorithm>
#include <iostream>
#include <queue>
#include <string>
#include <vector>

// ---------- Kode yang diuji (salinan inti simulasi-antrean.cpp) ----------
struct Skenario {
    std::string nama;
    int intervalKedatangan;
    int durasiLayanan;
    int jumlahPelanggan;
};

struct Pelanggan {
    int id;
    int datangPada;
    int mulaiPada;
    int selesaiPada;
};

struct Teller {
    int id;
    int bebasPada;
    int jumlahDilayani;
};

struct Statistik {
    double rataTunggu = 0.0;
    int tungguMaks = 0;
    int antreanMaks = 0;
    double utilisasi = 0.0;
    int selesaiPada = 0;
};

std::vector<Pelanggan> jalankanSimulasi(const Skenario& s, Statistik& stat) {
    std::queue<Pelanggan> antrean;
    Teller teller{1, 0, 0};
    std::vector<Pelanggan> riwayat;
    stat = Statistik{};

    int tick = 0;
    int sudahDatang = 0;
    while (static_cast<int>(riwayat.size()) < s.jumlahPelanggan) {
        if (sudahDatang < s.jumlahPelanggan &&
            tick == sudahDatang * s.intervalKedatangan) {
            antrean.push(Pelanggan{sudahDatang + 1, tick, 0, 0});
            ++sudahDatang;
        }
        if (tick >= teller.bebasPada && !antrean.empty()) {
            Pelanggan p = antrean.front();
            antrean.pop();
            p.mulaiPada = tick;
            p.selesaiPada = tick + s.durasiLayanan;
            teller.bebasPada = p.selesaiPada;
            ++teller.jumlahDilayani;
            riwayat.push_back(p);
        }
        stat.antreanMaks =
            std::max(stat.antreanMaks, static_cast<int>(antrean.size()));
        ++tick;
    }

    long long totalTunggu = 0;
    for (const Pelanggan& p : riwayat) {
        int tunggu = p.mulaiPada - p.datangPada;
        totalTunggu += tunggu;
        stat.tungguMaks = std::max(stat.tungguMaks, tunggu);
    }
    stat.rataTunggu = static_cast<double>(totalTunggu) / riwayat.size();

    int selesaiTerakhir = 0;
    for (const Pelanggan& p : riwayat) {
        selesaiTerakhir = std::max(selesaiTerakhir, p.selesaiPada);
    }
    stat.selesaiPada = selesaiTerakhir;
    stat.utilisasi =
        static_cast<double>(teller.jumlahDilayani * s.durasiLayanan) /
        selesaiTerakhir;
    return riwayat;
}

// ---------- Mini harness ----------
static int jumlahLulus = 0;
static int jumlahGagal = 0;
static std::string namaUjiAktif;

#define MULAI_UJI(nama) namaUjiAktif = (nama)
#define CHECK_TRUE(kondisi, deskripsi) cekBoolean((kondisi), (deskripsi))
#define CHECK_EQ(aktual, diharapkan) cekSama((aktual), (diharapkan), #aktual)

void cekBoolean(bool kondisi, const std::string& deskripsi) {
    if (kondisi) {
        ++jumlahLulus;
        std::cout << "[ LULUS ] " << namaUjiAktif << " - " << deskripsi << "\n";
    } else {
        ++jumlahGagal;
        std::cout << "[ GAGAL ] " << namaUjiAktif << " - " << deskripsi << "\n";
    }
}

template <typename T, typename U>
void cekSama(const T& aktual, const U& diharapkan, const char* ekspresi) {
    if (aktual == diharapkan) {
        ++jumlahLulus;
        std::cout << "[ LULUS ] " << namaUjiAktif << " - " << ekspresi << "\n";
    } else {
        ++jumlahGagal;
        std::cout << "[ GAGAL ] " << namaUjiAktif << " - " << ekspresi
                  << " (aktual " << aktual << ", diharapkan " << diharapkan
                  << ")\n";
    }
}

// ---------- Test case ----------
void ujiKasusKecil() {
    MULAI_UJI("kasus kecil");
    Statistik stat;
    std::vector<Pelanggan> riwayat =
        jalankanSimulasi(Skenario{"kecil", 5, 4, 3}, stat);

    CHECK_EQ(static_cast<int>(riwayat.size()), 3);
    CHECK_EQ(riwayat[0].datangPada, 0);
    CHECK_EQ(riwayat[0].mulaiPada, 0);
    CHECK_EQ(riwayat[0].selesaiPada, 4);
    CHECK_EQ(riwayat[1].datangPada, 5);
    CHECK_EQ(riwayat[1].mulaiPada, 5);
    CHECK_EQ(riwayat[1].selesaiPada, 9);
    CHECK_EQ(riwayat[2].selesaiPada, 14);
    CHECK_EQ(stat.tungguMaks, 0);
}

void ujiFifoDanTidakTumpangTindih() {
    MULAI_UJI("fifo dan layanan tunggal");
    Statistik stat;
    std::vector<Pelanggan> riwayat =
        jalankanSimulasi(Skenario{"ramai", 3, 4, 10}, stat);

    bool urutId = true;
    bool tanpaTumpangTindih = true;
    for (std::size_t i = 0; i < riwayat.size(); ++i) {
        if (riwayat[i].id != static_cast<int>(i) + 1) {
            urutId = false;
        }
        if (i > 0 && riwayat[i - 1].selesaiPada > riwayat[i].mulaiPada) {
            tanpaTumpangTindih = false;
        }
    }
    CHECK_TRUE(urutId, "urutan layanan sesuai urutan kedatangan (FIFO)");
    CHECK_TRUE(tanpaTumpangTindih, "teller tunggal tidak melayani dua orang sekaligus");
}

void ujiNilaiEmasRamai() {
    MULAI_UJI("nilai emas skenario ramai");
    Statistik stat;
    std::vector<Pelanggan> riwayat =
        jalankanSimulasi(Skenario{"ramai", 3, 4, 10}, stat);

    bool rataSesuai = (stat.rataTunggu - 4.5) * (stat.rataTunggu - 4.5) < 1e-9;
    CHECK_TRUE(rataSesuai, "rata-rata tunggu = 4.500 menit");
    CHECK_EQ(stat.tungguMaks, 9);
    CHECK_EQ(stat.antreanMaks, 3);
    CHECK_EQ(stat.selesaiPada, 40);
    bool utilisasiWajar = stat.utilisasi > 0.0 && stat.utilisasi <= 1.0;
    CHECK_TRUE(utilisasiWajar, "utilisasi berada pada rentang 0..1");
}

void ujiDeterminisme() {
    MULAI_UJI("determinisme");
    Statistik a, b;
    std::vector<Pelanggan> riwayatA = jalankanSimulasi(Skenario{"ramai", 3, 4, 10}, a);
    std::vector<Pelanggan> riwayatB = jalankanSimulasi(Skenario{"ramai", 3, 4, 10}, b);

    bool sama = (a.rataTunggu == b.rataTunggu) && (a.tungguMaks == b.tungguMaks) &&
                (a.antreanMaks == b.antreanMaks) && (a.utilisasi == b.utilisasi) &&
                (a.selesaiPada == b.selesaiPada) &&
                (riwayatA.size() == riwayatB.size());
    for (std::size_t i = 0; sama && i < riwayatA.size(); ++i) {
        sama = riwayatA[i].id == riwayatB[i].id &&
               riwayatA[i].datangPada == riwayatB[i].datangPada &&
               riwayatA[i].mulaiPada == riwayatB[i].mulaiPada &&
               riwayatA[i].selesaiPada == riwayatB[i].selesaiPada;
    }
    CHECK_TRUE(sama, "dua eksekusi skenario sama menghasilkan keadaan identik");
}

int main() {
    ujiKasusKecil();
    ujiFifoDanTidakTumpangTindih();
    ujiNilaiEmasRamai();
    ujiDeterminisme();

    std::cout << "\nringkasan: " << jumlahLulus << " lulus, " << jumlahGagal
              << " gagal\n";
    return jumlahGagal == 0 ? 0 : 1;
}

Output yang Dihasilkan:

text
[ LULUS ] kasus kecil - static_cast<int>(riwayat.size())
[ LULUS ] kasus kecil - riwayat[0].datangPada
[ LULUS ] kasus kecil - riwayat[0].mulaiPada
[ LULUS ] kasus kecil - riwayat[0].selesaiPada
[ LULUS ] kasus kecil - riwayat[1].datangPada
[ LULUS ] kasus kecil - riwayat[1].mulaiPada
[ LULUS ] kasus kecil - riwayat[1].selesaiPada
[ LULUS ] kasus kecil - riwayat[2].selesaiPada
[ LULUS ] kasus kecil - stat.tungguMaks
[ LULUS ] fifo dan layanan tunggal - urutan layanan sesuai urutan kedatangan (FIFO)
[ LULUS ] fifo dan layanan tunggal - teller tunggal tidak melayani dua orang sekaligus
[ LULUS ] nilai emas skenario ramai - rata-rata tunggu = 4.500 menit
[ LULUS ] nilai emas skenario ramai - stat.tungguMaks
[ LULUS ] nilai emas skenario ramai - stat.antreanMaks
[ LULUS ] nilai emas skenario ramai - stat.selesaiPada
[ LULUS ] nilai emas skenario ramai - utilisasi berada pada rentang 0..1
[ LULUS ] determinisme - dua eksekusi skenario sama menghasilkan keadaan identik

ringkasan: 17 lulus, 0 gagal

Empat kelompok uji membagi tugas menjaga kebenaran:

  1. Kasus kecil (interval 5, layanan 4, 3 pelanggan): jejak waktu setiap pelanggan dicek satu per satu — siapa datang kapan, kapan mulai dan selesai dilayani — karena tiga pelanggan masih bisa dilacak di atas kertas.
  2. FIFO dan layanan tunggal: id pada riwayat harus 1 sampai 10 berurutan (bukti antrean FIFO tidak mendahului siapa pun), dan selesaiPada satu pelanggan tidak boleh melewati mulaiPada pelanggan berikutnya (bukti teller tunggal tidak pernah melayani dua orang sekaligus).
  3. Nilai emas skenario ramai: rata-rata tunggu 4.500 (dibandingkan dengan toleransi 1e-9 karena perbandingan double), tunggu maksimum 9, antrean maksimum 3, selesai pada menit 40.
  4. Determinisme: dua eksekusi skenario identik harus menghasilkan keadaan yang persis sama, dari seluruh isi Statistik sampai urutan pelanggan di riwayat.

Harness keluar dengan exit code 0 — jumlah gagal nol — dan itulah kontrak yang menghubungkan proyek ini dengan otomasi: script CI tidak membaca teks [ LULUS ], ia hanya menilai exit code, sama seperti pola pada Chapter Unit Testing.

Dengan itu proyek simulasi antrean tuntas: model, struktur data, mesin, dan analisis berkumpul dalam satu program yang angkanya bisa dijelaskan baris demi baris. Pola deterministik yang kita bangun di sini langsung terpakai pada proyek berikutnya — kali ini untuk menumbuhkan dataset “acak” yang tetap bisa diuji, dengan generator berseed tetap.

Rujukan resmi: std::max (cppreference) , std::setw (cppreference) .