Semua bahan sudah di meja: model dari bagian 1 menetapkan jadwal dan durasi, struktur data dari bagian 2 menyimpan keadaan antrean. Sekarang kita menyusun keduanya menjadi mesin yang benar-benar berjalan: loop per menit yang memproses kedatangan, memanggil teller, dan mencatat apa pun yang terjadi. Pilihan desain terpenting di bagian ini adalah waktu diskrit — satu iterasi loop mewakili satu menit — karena angka yang dihasilkan bisa kita periksa tangan baris demi baris, sesuatu yang mustahil dilakukan bila waktu mengalir kontinu.
Tick Loop Satu Menit #
Mesin simulasi kita bekerja dengan dua daftar yang perannya berbeda. std::queue<Pelanggan> menyimpan orang-orang yang sedang menunggu — mereka sudah datang tetapi belum dilayani. std::vector<Pelanggan> menyimpan riwayat — orang-orang yang layanannya sudah dimulai, lengkap dengan waktu mulai dan selesai. Loop berjalan sampai seluruh pelanggan pindah ke daftar riwayat, dan pada setiap tick melakukan tiga langkah yang urutannya sudah kita tetapkan di bagian 1: proses kedatangan, beri pekerjaan ke teller yang bebas, catat panjang antrean.
Program berikut adalah simulator lengkap untuk satu skenario (ramai: kedatangan tiap 3 menit, layanan 4 menit, 10 pelanggan). Struct dan logika antreannya persis yang kita bangun di bagian sebelumnya, ditambah pencatatan peristiwa per tick:
// simulasi-per-tick.cpp — Bagian 3: menjalankan simulasi satu skenario.
// Tick loop: satu iterasi = satu menit. Setiap tick kita catat peristiwa
// (kedatangan, mulai layanan) dan panjang antrean, lalu mencetak log
// peristiwa beserta tabel per pelanggan. Hasilnya sepenuhnya deterministik.
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <queue>
#include <string>
#include <vector>
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;
};
int main() {
Skenario skenario{"ramai", 3, 4, 10};
std::queue<Pelanggan> antrean;
Teller teller{1, 0, 0};
std::vector<Pelanggan> riwayat;
std::vector<std::string> peristiwa; // catatan kejadian pada tick ini
int tick = 0;
int sudahDatang = 0;
while (static_cast<int>(riwayat.size()) < skenario.jumlahPelanggan) {
peristiwa.clear();
// 1) Kedatangan: pelanggan ke-n tiba pada n * interval.
if (sudahDatang < skenario.jumlahPelanggan &&
tick == sudahDatang * skenario.intervalKedatangan) {
antrean.push(Pelanggan{sudahDatang + 1, tick, 0, 0});
++sudahDatang;
peristiwa.push_back("p" + std::to_string(sudahDatang) + " datang");
}
// 2) 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 + skenario.durasiLayanan;
teller.bebasPada = p.selesaiPada;
++teller.jumlahDilayani;
riwayat.push_back(p);
peristiwa.push_back("p" + std::to_string(p.id) +
" mulai dilayani (selesai menit " +
std::to_string(p.selesaiPada) + ")");
}
// 3) Catat tick yang ada peristiwanya beserta sisa antrean.
if (!peristiwa.empty()) {
std::cout << std::setw(3) << tick << " | ";
for (std::size_t i = 0; i < peristiwa.size(); ++i) {
if (i > 0) std::cout << "; ";
std::cout << peristiwa[i];
}
std::cout << " | antrean " << antrean.size() << '\n';
}
++tick;
}
// 4) Tabel per pelanggan.
std::cout << "\nid datang mulai selesai tunggu\n";
double totalTunggu = 0.0;
for (const Pelanggan& p : riwayat) {
int tunggu = p.mulaiPada - p.datangPada;
totalTunggu += tunggu;
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) << tunggu << '\n';
}
std::cout << "\nrata-rata tunggu: " << std::fixed << std::setprecision(3)
<< totalTunggu / riwayat.size() << " menit\n";
return 0;
}g++ -std=c++23 -Wall -Wextra simulasi-per-tick.cpp -o simulasi-per-tick
./simulasi-per-tickOutput yang Dihasilkan:
0 | p1 datang; p1 mulai dilayani (selesai menit 4) | antrean 0
3 | p2 datang | antrean 1
4 | p2 mulai dilayani (selesai menit 8) | antrean 0
6 | p3 datang | antrean 1
8 | p3 mulai dilayani (selesai menit 12) | antrean 0
9 | p4 datang | antrean 1
12 | p5 datang; p4 mulai dilayani (selesai menit 16) | antrean 1
15 | p6 datang | antrean 2
16 | p5 mulai dilayani (selesai menit 20) | antrean 1
18 | p7 datang | antrean 2
20 | p6 mulai dilayani (selesai menit 24) | antrean 1
21 | p8 datang | antrean 2
24 | p9 datang; p7 mulai dilayani (selesai menit 28) | antrean 2
27 | p10 datang | antrean 3
28 | p8 mulai dilayani (selesai menit 32) | antrean 2
32 | p9 mulai dilayani (selesai menit 36) | antrean 1
36 | p10 mulai dilayani (selesai menit 40) | antrean 0
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
rata-rata tunggu: 4.500 menitBedah Mesin Simulasi Baris demi Baris #
Bagian yang benar-benar menentukan hidup-mati simulator ada pada loop utama. Mari kita bedah:
while (static_cast<int>(riwayat.size()) < skenario.jumlahPelanggan): kondisi terminasi. Pelanggan pindah ke riwayat saat layanannya dimulai — bukan selesai — sehingga loop berhenti segera setelah orang terakhir kebagian teller.static_cast<int>diperlukan karenasize()bertipe tak bertanda dan dibandingkan denganint.tick == sudahDatang * skenario.intervalKedatangan: jantung determinisme. Karena perbandingannya persis (bukan rentang), tidak ada dua kedatangan yang tumpang tindih dan tidak ada kedatangan yang terlewat. PembandingsudahDatang < jumlahPelanggandi depan mencegah kedatangan fiktif setelah jadwal habis.antrean.push(Pelanggan{sudahDatang + 1, tick, 0, 0}): pelanggan baru masuk paling belakang;datangPadatercatat pada menit kedatangannya.if (tick >= teller.bebasPada && !antrean.empty()): syarat teller mengambil pekerjaan. Operator>=penting — bila layanan selesai menit 8 dan ada antrean, teller mulai lagi pada menit 8 yang sama, bukan menit 9.p.mulaiPada = tick; p.selesaiPada = tick + skenario.durasiLayanan; teller.bebasPada = p.selesaiPada;: tiga baris yang menyambung keadaan. Waktu selesai pelanggan menjadi waktu bebas teller — inilah mekanisme yang membuat layanan tidak pernah tumpang tindih.riwayat.push_back(p): salinan pelanggan yang sudah lengkap waktu-waktunya disimpan; antrean kembali berkurang satu.std::cout << ... << " | antrean " << antrean.size(): ukuran dicatat setelah teller mengambil, sesuai definisi bagian 1 — panjang antrean adalah orang yang masih menunggu pada akhir tick.
Membaca Log dan Tabel Hasil #
Log peristiwa memperlihatkan mekanisme antrean dengan jelas. Pada menit 12 terjadi dua kejadian dalam satu baris: p5 datang dan p4 mulai dilayani — urutan dalam baris mengikuti urutan pemrosesan tick. Baris menit 27 menarik untuk diamati: p10 datang tetapi teller baru bebas pada menit 28, sehingga barisan berisi tiga orang — dan itulah panjang antrean maksimum skenario ini.
Tabel per pelanggan lalu memperlihatkan pola yang rapi: waktu tunggu naik teratur 0, 1, 2, …, 9. Pola ini bukan kebetulan; pada skenario dengan layanan 4 menit dan kedatangan 3 menit, teller bekerja tanpa henti sejak menit 0, sehingga setiap pelanggan baru “mewarisi” seluruh keterlambatan terkumpul plus satu menit selisih interval. Rata-rata tunggu 4.500 menit adalah rata-rata deret 0 sampai 9. Saat kita menambahkan lapisan statistik di bagian 4, angka-angka seperti tungguMaks = 9 dan rata-rata ini akan dijadikan nilai emas pada harness — hasil yang sama harus muncul di setiap eksekusi, dan bila berubah berarti ada yang rusak.
Mesin sudah berjalan dan angkanya bisa dijelaskan. Satu langkah tersisa: mengubah log mentah menjadi statistik yang layak diputuskan — dan membandingkan skenario yang berbeda di samping satu sama lain.