Proyek simulasi paling sering gagal bukan karena kodenya salah, melainkan karena modelnya tidak pernah dipikirkan: programmer langsung menulis loop, lalu bertanya-tanya mengapa angkanya sulit dijelaskan. Pada bagian pertama ini kita tidak menulis mesin simulasi dulu. Kita menetapkan apa yang dimodelkan, asumsi apa yang dipakai, entitas apa yang terlibat, dan bagaimana alur waktu berjalan. Setelah kerangka ini jelas, bagian-bagian berikutnya tinggal menerjemahkannya menjadi struct, antrean, dan loop — bukan menebak-nebak di tengah jalan.
Asumsi Model yang Kita Tetapkan #
Model kita adalah antrean teller tunggal: satu loket, satu barisan, dan aturan dilayani terlebih dulu yang datang lebih dulu (FIFO). Dari dunia nyata yang jauh lebih ramai, kita menyaring empat asumsi kerja:
- Kedatangan tetap: pelanggan ke-n datang tepat pada menit n × interval. Tidak ada kejutan, tidak ada angka acak.
- Layanan tetap: setiap pelanggan memakan waktu persis
durasiLayananmenit, tanpa variasi. - Satu teller: hanya satu pelanggan yang bisa dilayani pada satu menit tertentu.
- Waktu diskrit: jam simulasi berjalan per menit (tick). Semua peristiwa terjadi pada batas menit.
Keempat asumsi ini membuat simulasi kita deterministik: parameter skenario sepenuhnya menentukan hasil. Versi acak dari model yang sama (kedatangan mengikuti proses Poisson, layanan menyebar eksponensial) dikenal sebagai rantai antrean M/M/1 — dan justru karena versi deterministiknya bisa dihitung tangan, ia adalah pijakan terbaik sebelum menyentuh keacakan. Bila suatu saat hasil versi acak terlihat aneh, kita bisa membandingkannya dengan jangkar yang sudah teruji ini.
Entity dan Parameter Skenario #
Dua entitas pertama kita wujudkan sebagai struct — kumpulan data yang dikelompokkan menjadi satu tipe. Struct Skenario menyimpan parameter pengerjaan satu eksperimen, sedangkan struct Pelanggan mencatat jejak satu orang dari datang hingga selesai dilayani:
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
};Mari kita baca keputusan di balik field-field ini:
intervalKedatangandandurasiLayanandalam menit: memakai satuan bulat kecil membuat setiap tick bisa dilacak tangan di atas kertas. Simulasi yang bisa diverifikasi manual jauh lebih mudah diuji.datangPadadihitung dari rumus,mulaiPadadanselesaiPadadiisi simulator: field kosong bukan kelemahan desain, melainkan kontrak — model menyediakan jadwal, mesin simulasi yang melengkapinya.idbertipeintdan mulai dari 1: berguna untuk log yang dapat dibaca manusia (“p7 mulai dilayani”) dan untuk assertion urutan FIFO pada harness di bagian akhir.namapadaSkenario: laporan akhir menampilkan nama skenario (lancar, sibuk, ramai), sehingga tabel perbandingan langsung bercerita.
Waktu tunggu sendiri tidak disimpan sebagai field — ia hasil turunan: tunggu = mulaiPada - datangPada. Menyimpan nilai yang bisa dihitung dari dua field lain mengundang ketidakkonsistenan; prinsip satu sumber kebenaran membuat data kita selalu sah.
Alur Simulasi sebagai Event Loop #
Jantung simulator adalah loop peristiwa (event loop): pada setiap tick kita tanyakan dua hal — apakah ada yang datang, dan apakah teller siap melayani. Diagram berikut merangkum alur satu tick:
Urutan pertanyaan di dalam satu tick penting dan sengaja ditetapkan: kedatangan diproses sebelum teller mengambil antrean, sehingga pelanggan yang datang pada menit yang sama dengan teller menjadi bebas bisa langsung dilayani tanpa menunggu satu tick. Panjang antrean dicatat pada akhir tick agar angkanya punya definisi yang konsisten: banyaknya orang yang masih menunggu setelah teller mengambil satu pelanggan. Keputusan kecil semacam ini terlihat sepele, tetapi tanpa diputuskan lebih dulu, dua orang bisa menulis “simulasi antrean” yang menghasilkan statistik berbeda dari kode yang mirip.
Membaca Jadwal Kedatangan #
Sebelum membangun mesin penuh, kita uji lapisan paling luar dari model: generator jadwal kedatangan. Program kecil berikut berdiri sendiri dan menghasilkan daftar kedatangan untuk dua skenario:
// model-kedatangan.cpp — Bagian 1: membaca jadwal kedatangan deterministik.
// Program kecil untuk memeriksa model sebelum mesin simulasi dibangun:
// siapa datang pada menit berapa untuk setiap skenario.
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
struct Skenario {
std::string nama;
int intervalKedatangan; // menit antar kedatangan
int durasiLayanan; // menit per layanan
int jumlahPelanggan;
};
struct Pelanggan {
int id;
int datangPada; // menit kedatangan
int mulaiPada; // menit mulai dilayani (diisi simulasi)
int selesaiPada; // menit selesai dilayani (diisi simulasi)
};
std::vector<Pelanggan> susunJadwalKedatangan(const Skenario& s) {
std::vector<Pelanggan> jadwal;
jadwal.reserve(static_cast<std::size_t>(s.jumlahPelanggan));
for (int n = 0; n < s.jumlahPelanggan; ++n) {
// Pelanggan ke-n (mulai dari 0) datang tepat pada n * interval.
jadwal.push_back(Pelanggan{n + 1, n * s.intervalKedatangan, 0, 0});
}
return jadwal;
}
int main() {
std::vector<Skenario> skenario = {
{"lancar", 5, 4, 6},
{"ramai", 3, 4, 6},
};
for (const Skenario& s : skenario) {
std::cout << "Skenario " << s.nama << " (interval "
<< s.intervalKedatangan << " menit, layanan "
<< s.durasiLayanan << " menit)\n";
for (const Pelanggan& p : susunJadwalKedatangan(s)) {
std::cout << " p" << p.id << " datang pada menit " << std::setw(2)
<< p.datangPada << '\n';
}
std::cout << '\n';
}
return 0;
}g++ -std=c++23 -Wall -Wextra model-kedatangan.cpp -o model-kedatangan
./model-kedatanganOutput yang Dihasilkan:
Skenario lancar (interval 5 menit, layanan 4 menit)
p1 datang pada menit 0
p2 datang pada menit 5
p3 datang pada menit 10
p4 datang pada menit 15
p5 datang pada menit 20
p6 datang pada menit 25
Skenario ramai (interval 3 menit, layanan 4 menit)
p1 datang pada menit 0
p2 datang pada menit 3
p3 datang pada menit 6
p4 datang pada menit 9
p5 datang pada menit 12
p6 datang pada menit 15Bedah bagian intinya:
susunJadwalKedatangan: mengembalikanstd::vector<Pelanggan>berisi seluruh kedatangan satu skenario. Karena rumusnyan * interval, jadwal hanya bergantung pada parameter — persis definisi deterministik.jadwal.reserve(...): memberi tahu vector kapasitas akhirnya sehingga tidak perlu realokasi berulang saatpush_back.Pelanggan{n + 1, n * s.intervalKedatangan, 0, 0}: aggregate initialization mengisi field berurutan; dua field terakhir sengaja nol karena belum diisi simulasi.std::setw(2): merapikan kolom menit agar daftar mudah dibandingkan mata antar skenario.
Perhatikan pula bahwa program ini belum menyentuh std::queue sama sekali — dan itu memang sengaja. Jadwal adalah input simulasi; antrean adalah keadaan (state) yang hidup saat simulasi berjalan. Memisahkan keduanya sejak awal membuat bagian berikutnya fokus pada satu hal saja: struktur data yang menampung keadaan antrean itu.