Cara Membuat Bagan Turnamen Sistem Gugur Otomatis dengan Google Apps Script (Seed, BYE, dan Pemenang Maju)
Setiap panitia lomba pernah mengalaminya: tim yang mendaftar ada 12, padahal bagan yang biasa digambar di kertas karton cuma untuk 8 atau 16. Akhirnya ada tim yang "dilewatkan" tanpa aturan jelas, unggulan bertemu di babak awal, dan papan skor di dinding sering terlambat diperbarui.
Di tutorial ini kita membuat inti bagan turnamen sistem gugur dengan Google Apps Script: menghitung jumlah slot, menempatkan unggulan dengan adil, membagikan BYE, dan meloloskan pemenang ke babak berikutnya secara otomatis. Contohnya diambil dari aplikasi Turnamen & Bagan Pertandingan yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Bulatkan jumlah slot ke pangkat dua
Bagan gugur selalu berbentuk 2, 4, 8, 16, 32 slot karena setiap babak membagi dua jumlah peserta. Kalau tim ada 12, kita butuh 16 slot dan 4 di antaranya menjadi BYE (lolos tanpa bertanding).
var P = 2;
while (P < tim.length) P *= 2; // 12 tim -> 16 slot
var totalBabak = Math.round(Math.log(P) / Math.LN2); // 16 slot -> 4 babak
var jumlahBye = P - tim.length; // 4 BYE
Karena P adalah pangkat dua terkecil yang memuat semua tim, jumlah tim selalu lebih dari setengah slot. Artinya tidak akan pernah ada laga "BYE melawan BYE", asalkan BYE ditempatkan dengan benar. Itu tugas langkah berikutnya.
2. Urutan seed standar
Aturan yang dipakai hampir semua turnamen resmi: unggulan 1 dan 2 berada di sisi bagan yang berbeda sehingga baru bisa bertemu di final, unggulan 1-4 baru bertemu di semifinal, dan seterusnya. Posisi seperti ini bisa dibangun dengan cara rekursif yang sangat pendek:
function urutSeed_(n) {
var a = [1];
while (a.length < n) {
var m = a.length * 2 + 1, b = [];
a.forEach(function (x) { b.push(x, m - x); });
a = b;
}
return a;
}
urutSeed_(8); // [1, 8, 4, 5, 2, 7, 3, 6]
Setiap seed x dipasangkan dengan m - x, yaitu lawan "terlemah" yang tersisa pada ukuran bagan itu. Untuk 8 slot, laga pertama adalah seed 1 lawan seed 8, laga kedua seed 4 lawan seed 5, dan seterusnya. Perhatikan bahwa seed 1 ada di paruh atas, sedangkan seed 2 di paruh bawah.
Triknya: seed yang nomornya melebihi jumlah tim kita anggap BYE. Karena seed terbesar selalu dipasangkan dengan seed terkecil, BYE otomatis jatuh ke unggulan teratas. Adil, dan tidak perlu aturan tambahan.
3. Susun daftar tim: unggulan dulu, sisanya diacak
Panitia biasanya hanya mengunggulkan beberapa tim, misalnya juara tahun lalu. Tim lain diundi. Kita gabungkan keduanya menjadi satu daftar urut:
var unggul = tim.filter(function (t) { return t.seed > 0; })
.sort(function (a, b) { return a.seed - b.seed; });
var lain = tim.filter(function (t) { return !(t.seed > 0); });
for (var i = lain.length - 1; i > 0; i--) { // acak Fisher-Yates
var j = Math.floor(Math.random() * (i + 1)), x = lain[i];
lain[i] = lain[j]; lain[j] = x;
}
var urut = unggul.concat(lain); // indeks 0 = seed 1
Gunakan Fisher-Yates, bukan sort(() => Math.random() - 0.5). Cara kedua terlihat praktis tetapi hasil acaknya berat sebelah, dan dalam undian lomba hal kecil seperti ini bisa jadi bahan protes.
4. Buat semua laga sekaligus
Alih-alih membuat babak dua setelah babak satu selesai, kita buat seluruh laga dari awal dengan ID yang mudah ditebak: KODE-babak-nomor. Babak 1 diisi tim, babak berikutnya masih kosong.
var pos = urutSeed_(P), rows = [];
for (var r = 1; r <= totalBabak; r++) {
for (var no = 1; no <= P / Math.pow(2, r); no++) {
var a = '', b = '';
if (r === 1) {
var sa = urut[pos[(no - 1) * 2] - 1], sb = urut[pos[(no - 1) * 2 + 1] - 1];
a = sa ? sa.id : ''; // undefined = BYE
b = sb ? sb.id : '';
}
rows.push([kode + '-' + r + '-' + no, kode, r, no, a, b, '', '', '', '', '', 'Belum']);
}
}
Dengan pola ini, letak laga lanjutan cukup dihitung: pemenang laga nomor no di babak r masuk ke laga nomor Math.ceil(no / 2) di babak r + 1. Nomor ganjil mengisi slot A, nomor genap mengisi slot B. Tidak perlu tabel relasi rumit.
5. BYE dan pemenang maju otomatis
Laga babak 1 yang salah satu timnya kosong langsung ditandai BYE dan timnya diteruskan:
if (!(m.a && m.b)) {
m.status = 'BYE';
m.menang = m.a || m.b;
var nx = peta['2-' + Math.ceil(m.no / 2)];
nx[m.no % 2 ? 'a' : 'b'] = m.menang;
}
Saat panitia menyimpan skor, logikanya sama, ditambah dua pengaman. Pertama, sistem gugur tidak mengenal seri, jadi skor sama ditolak dan panitia diminta memasukkan hasil akhir termasuk adu penalti. Kedua, kalau panitia mengoreksi skor dan pemenangnya berubah, koreksi hanya diizinkan selama laga berikutnya belum dimainkan:
if (sa === sb) throw new Error('Sistem gugur tidak boleh seri.');
var menang = sa > sb ? m.a : m.b;
if (nx && m.menang && m.menang !== menang && nx.status === 'Selesai')
throw new Error('Babak berikutnya sudah dimainkan, pemenang tidak bisa diganti.');
sh.getRange(nx.baris, m.no % 2 ? 5 : 6).setValue(menang); // timpa slot di laga berikutnya
Seluruh fungsi ini dibungkus LockService.getScriptLock() supaya dua panitia yang mengisi skor bersamaan dari HP masing-masing tidak saling menimpa.
6. Menggambar bagan dengan CSS tanpa library
Di browser, setiap babak adalah satu kolom flex. Laga dikelompokkan berpasangan dalam .pair, dan setiap laga berada di .slot dengan flex: 1. Karena semua kolom setinggi kolom pertama, titik tengah laga di babak berikutnya otomatis sejajar dengan tengah pasangannya. Garis penghubung cukup memakai pseudo-element:
.round { display: flex; flex-direction: column; width: 230px; }
.pair { flex: 1; display: flex; flex-direction: column; position: relative; }
.slot { flex: 1; display: flex; align-items: center; position: relative; }
.round:not(:last-child) .pair::after { /* garis vertikal penghubung pasangan */
content: ''; position: absolute; right: -15px; top: 25%; bottom: 25%;
border-right: 1px solid #2c5639;
}
.round:not(:last-child) .slot::after { /* garis keluar dari tiap laga */
content: ''; position: absolute; right: -15px; width: 15px; top: 50%;
border-top: 1px solid ;
}
Nilai top: 25% dan bottom: 25% bekerja karena dua slot dalam satu pasangan membagi tinggi sama rata, sehingga titik tengahnya berada di 25% dan 75%. Bagan 4, 8, maupun 32 tim tergambar rapi dengan kode yang sama.
Ringkasan
- Bulatkan jumlah tim ke pangkat dua; selisihnya menjadi BYE.
- Urutan seed standar membuat unggulan terpisah dan BYE otomatis jatuh ke unggulan teratas.
- Acak tim non-unggulan dengan Fisher-Yates.
- Buat semua laga di awal; laga lanjutan dihitung dengan
Math.ceil(no / 2)dan ganjil-genap. - Tolak skor seri dan kunci koreksi pemenang setelah laga berikutnya dimainkan.
- Bagan cukup digambar dengan flexbox dan pseudo-element, tanpa library.
Download source code lengkapnya
Versi lengkapnya sudah mencakup pendaftaran tim dengan slot dan anti dobel, kode akses untuk cek status, verifikasi pembayaran, seed unggulan, jadwal per hari, podium juara, panel panitia, dan ekspor CSV. Semua berjalan di Google Sheets tanpa hosting.
Download Source Code Turnamen & Bagan Pertandingan di Papicode
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.
Tutorial lainnya
Cara Membuat Sistem Tiket Travel dengan Pilih Kursi di Google Apps Script: Layout Armada, Anti Kursi Dobel, dan Kursi Kedaluwarsa
20 Oktober 2026
Cara Membuat Website Donasi Transparan dengan Google Apps Script: Kode Unik Transfer dan Laporan Penyaluran
20 Oktober 2026