Room multiplayer
Room mendudukkan beberapa pemain dalam satu pertandingan dan meneruskan apa yang mereka katakan satu sama lain. Dia dibuat untuk game berbasis giliran (kartu, board game, kuis, artileri), bukan untuk dunia fisika bersama.
Yang dikerjakan arcade, dan yang tidak
| Arcade | Gamemu |
|---|---|
| Mendudukkan pemain, menjaga daftar kehadiran tetap jujur | Aturannya |
| Membagikan kode dan mempertemukan orang asing | Giliran siapa sekarang |
| Meneruskan pesan, berurutan, ke semua orang di room | Arti sebuah pesan |
| Menyadari pemain yang pergi | Apa yang terjadi pada gilirannya |
Room meneruskan; dia hanya menjadi wasit untuk hal yang bisa dia periksa tanpa tahu aturan gamemu. State game tinggal di klien para pemain, jadi arcade tidak bisa membedakan gerakan yang sah dari yang tidak. Yang bisa dia lakukan: menolak gerakan dari kursi yang salah, memegang dek supaya tidak ada yang melihat kartu pemain lain, dan mencatat hasil hanya kalau semua pemain melaporkan hasil yang sama. Ketiganya bersifat opt-in dan dibahas di bawah.
Yang tetap berlaku bahkan dengan ketiganya menyala: klien bisa berbohong tentang apa pun yang tidak dipegang server. Jangan mempertaruhkan koin pada hasil sebuah pertandingan.
Tidak ada server socket. Halaman host melakukan polling sekali per detik dan mendorong apa yang dia baca ke dalam frame-mu, jadi sebuah pesan sampai ke pemain lain dalam kira-kira 0.4-1.2 detik. Itu tidak terasa di game berbasis giliran, dan tidak berguna untuk platformer.
Masuk ke sebuah room
// Main dengan orang asing: masuk ke room terlama yang punya kursi kosong, atau buka yang baru.
const room = await taploka.room.quick({ max: 4 });
// Main dengan teman: satu orang membuat, yang lain mengetik kodenya.
const mine = await taploka.room.create({ max: 2, private: true });
showCode(mine.code); // "KX7Q"
const theirs = await taploka.room.join('KX7Q');
Semuanya menghasilkan state room:
{
"id": "01M2…",
"code": "KX7Q",
"status": "open",
"seq": 12,
"host": "01M1…",
"max_players": 4,
"members": [
{ "id": "01M1…", "name": "Dika", "seat": 0, "is_host": true },
{ "id": "01M2…", "name": "Salsa", "seat": 1, "is_host": false }
]
}
seat stabil selama pemain ada di room dan itulah yang harus dipakai game untuk urutan giliran. Urutan array bukan
janji. id adalah id publik pemain, sama dengan yang diberikan ready() untuk pemain lokal.
Satu pemain hanya ada di satu room per game: masuk ke room kedua berarti keluar dari yang pertama.
Masuk dari halaman game
Setiap room publik yang terbuka muncul di halaman game di arcade, dan menyentuhnya membuka stage dengan room itu
sudah terpilih. Kodenya datang lewat ready(), dan yang memanggil join tetap gamemu sendiri:
const { room } = await taploka.ready();
await (room ? taploka.room.join(room) : taploka.room.quick({ max: 4 }));
room bernilai null pada peluncuran biasa. Sebuah room baru masuk daftar selama dia publik, masih terbuka, masih ada
orangnya dan kursinya belum penuh, jadi kodenya bisa saja sudah basi saat pemain menyentuhnya. Mundur ke quick()
kalau dapat ROOM_NOT_FOUND, ROOM_FULL atau ROOM_STARTED. Room privat tidak pernah masuk daftar: dia cuma bisa
dicapai lewat kode.
Bermain
taploka.room.on('members', (state) => renderLobby(state));
taploka.room.on('started', (event) => beginMatch(event.payload.seats));
taploka.room.on('message', (event) => {
if (event.mine) return; // gerakanmu sendiri, dipantulkan balik
applyMove(event.from, event.payload);
});
taploka.room.on('closed', () => showLobbyAgain());
await taploka.room.start(); // host saja, butuh dua pemain
await taploka.room.send({ card: 'red-7' });
on() mengembalikan fungsi untuk melepas listener-nya lagi.
Chat
chat() adalah relay yang sama, tapi isinya satu baris teks, untuk chatroom di samping papan:
taploka.room.on('chat', (event) => addChatLine(event.payload.name, event.payload.text, event.mine));
await taploka.room.chat('gg wp'); // 200 karakter, khusus anggota room
Nama pemain ikut terbawa di baris itu, jadi barisnya tetap bisa ditampilkan setelah dia keluar. Teksnya apa pun yang
diketik pemain: escape saat dirender, dan jangan taruh state game di situ, itu tugas send().
| Event | Kapan | Payload |
|---|---|---|
message |
Pemain lain memanggil send() |
{ from, mine, payload, seq, at } |
chat |
Pemain lain memanggil chat() |
{ from, mine, payload: { text, name }, seq, at } |
move |
Gerakan berwasit di bawah aturan giliran | { seat, …payload-mu } |
turn |
Giliran berpindah | { seat, reason, deadline, seconds } |
turn.timeout |
Sebuah kursi kehabisan waktu | { seat } |
deck.created / deck.dealt |
Dealer menyiapkan atau membagi | { deck, remaining }, { deck, hands: [{ seat, count }] } |
deck.drew / deck.played / deck.revealed |
Sebuah kartu berpindah | jumlah untuk yang pertama, itemnya untuk dua sisanya |
result / result.disputed |
Pertandingan berakhir, sepakat atau tidak | { outcome, agreed } |
members |
Daftar kursi berubah | State room |
member.joined / member.left |
Ada yang datang atau pergi | { user: { id, name }, seat } |
host.changed |
Host pergi dan room menunjuk yang baru | { user: { id, name } } |
started |
Host memulai pertandingan | { seats: [{ id, seat }] } |
closed |
Pemain terakhir keluar, atau room menganggur | tidak ada |
Pesan tiba dalam urutan yang diterima server, dan masing-masing membawa nomor urut yang diberikan padanya. Pemain yang koneksinya tersendat menyusul dari nomor urut yang dia pegang, jadi tidak ada yang terlewat.
Kehadiran, putus koneksi, dan kembali
Kehadiran itu polling: halaman host mempertahankan kursi dengan membaca room tiap detik. Pemain yang diam 20 detik
(tab ditutup, baterai habis, masuk terowongan) ditandai pergi, yang lain menerima member.left, dan kursinya kosong.
Kalau yang pergi itu host, room menunjuk host baru dan memberi tahu semua orang.
Kembali dalam rentang itu terjadi diam-diam: kursi yang sama, tanpa member.joined kedua. Gamemu harus siap pemain
muncul lagi, jangan perlakukan setiap member.left sebagai final.
Room kosong tetap terbuka dan kodenya, termasuk host-nya, tidak berubah: pemain yang me-refresh halaman cuma hilang sedetik dua detik, dan kalau room ditutup saat dia pergi, tiap refresh dia dapat room baru. Yang mengakhiri room adalah sepi. Sepuluh menit tanpa pesan, arcade menutupnya, kosong maupun tidak, dan kodenya berhenti berlaku.
Batas
| Batas | Nilai |
|---|---|
| Pemain per room | 2-8, sesuai max-mu |
| Ukuran pesan | 2 KB JSON |
| Permintaan | 300 per menit per pemain (polling-nya 60 di antaranya) |
| Log disimpan | 200 event terakhir dalam satu room |
| Timeout kehadiran | 20 detik |
| Room menganggur | Ditutup setelah 10 menit sepi |
Kirim gerakan, bukan state: { card: 'red-7' }, bukan seluruh isi meja. Kalau payload-mu mulai mentok 2 KB, room-nya
sedang dipakai sebagai basis data.
Giliran, kalau gamemu punya
Mulai pertandingan dengan jam dan server jadi wasit urutan giliran:
await taploka.room.start({ turnSeconds: 30 });
await taploka.room.move({ tile: 4 }); // ditolak dengan NOT_YOUR_TURN kalau bukan giliranmu
await taploka.room.pass(); // serahkan giliran lebih awal
move() itu send() versi beraturan: ditolak kecuali giliran kursimu, diteruskan sebagai event move, dan giliran
berpindah setelahnya. send() sengaja dibiarkan tanpa wasit, untuk obrolan, "siap", posisi kursor, apa pun yang bukan
gerakan.
Kursi yang kehabisan waktu dilewati: polling berikutnya yang menyadari tenggatnya memancarkan turn.timeout lalu
turn. Tidak ada proses yang berdetak, jadi pelewatannya terjadi saat ada yang melihat, dalam satu detik dari
tenggatnya. State room membawa jamnya:
{ "turn": { "seat": 1, "deadline": "2026-09-11T04:20:31+00:00", "seconds": 30 } }
Pemain yang pergi di tengah gilirannya tidak menggantung permainan. Kursinya keluar dari urutan dan gilirannya jatuh ke
siapa pun yang kini duduk setelahnya. Mulai tanpa turnSeconds dan semua ini tidak berlaku; semua orang boleh
bergerak bersamaan.
Dealer, untuk informasi tersembunyi
Dek adalah daftar string yang dipegang, dikocok, dan dibagikan server. Tidak ada klien yang pernah melihatnya, host sekalipun. Itulah yang bikin game kartu mungkin di room yang tidak ada wasitnya.
await taploka.room.deck.create('deck', ['red-1', 'red-2', /* … */]); // host saja, sekali
await taploka.room.deck.deal('deck', 7); // host: 7 kartu ke tiap kursi
await taploka.room.deck.deal('deck', 1, { seat: 2 }); // atau ke satu kursi
const { cards, counts } = await taploka.room.deck.hand('deck');
// cards: ['red-1', 'blue-3', …] kartumu, dan cuma pernah jadi milikmu
// counts: [{ seat: 0, count: 7 }, { seat: 1, count: 7 }]
await taploka.room.deck.draw('deck', 1); // giliranmu: ambil dari atas
await taploka.room.deck.play('deck', ['red-7']); // ditolak kecuali kartunya memang kamu pegang
await taploka.room.deck.reveal('deck', 1); // terbuka untuk semua orang
Room diberi tahu jumlahnya, tidak pernah kartunya: deck.dealt membawa berapa kartu ke kursi mana, deck.drew
membawa sebuah angka, dan hanya deck.played serta deck.revealed yang menyebut nama kartunya, karena saat itu
kartunya memang sudah terbuka.
| Aturan | Kenapa |
|---|---|
| Host yang membuat dek, sekali per key | Create kedua berarti mengocok ulang di tengah ronde |
| Hanya host yang membagi | Membagi kartu bukan gerakan yang diambil pemain |
play() memeriksa kartunya memang kamu pegang |
Beda antara memainkan kartu dan mengaku memainkannya |
| Memainkan dua kartu sejenis butuh dua kartunya | Kartu dikeluarkan satu per satu, bukan berdasarkan nilai |
draw dan play butuh giliranmu saat giliran menyala |
Kalau tidak, klien cepat bisa mengosongkan deknya |
Batasnya: 300 item per dek, 48 karakter per item, 30 kartu per pembagian atau pengambilan.
Menutup pertandingan yang disepakati semua orang
const verdict = await taploka.room.result({ winner: 1, score: 42 });
// { status: 'pending' | 'agreed' | 'disputed', agreed, voters }
Setiap pemain yang masih ada di room mengirim versinya. Selama masih ada yang belum, statusnya pending. Saat yang
terakhir masuk:
- Semuanya cocok: arcade mencatat hasilnya, memancarkan
result, dan menutup room. - Tidak cocok: tidak ada yang dicatat. Arcade memancarkan
result.disputed, menutup room, dan tidak menyimpan hasil sama sekali. Hasil yang dikarang satu klien lebih buruk daripada tidak ada hasil.
Urutan key tidak berpengaruh; { winner: 1, score: 42 } dan { score: 42, winner: 1 } adalah laporan yang sama.
Tidak ada pembayaran apa pun, cocok maupun tidak. Catatannya ada supaya game bisa menampilkan riwayat dan supaya arcade punya bahan untuk dikembangkan nanti. Ini bukan penyelesaian taruhan.
Tamu juga ikut main
Pemain yang belum masuk mendapat kursi seperti yang lain. Arcade ini guest-first, dan sebuah room butuh kursi, bukan
alamat email. name adalah nama tampilannya, yang untuk tamu berbentuk "Player 4F2A". Jangan kunci lobi di balik
login.
Yang masih belum ada
- Wasit yang tahu aturan gamemu. Server menegakkan giliran siapa dan kartu apa yang kamu pegang. Dia tidak bisa menilai apakah kartu itu sah dimainkan, karena dia tidak tahu gamenya. Klien yang dimodifikasi masih bisa menaruh red 7 di atas blue 3.
- Taruhan koin. Tidak ada escrow dan tidak ada pembayaran. Hasil yang disepakati adalah catatan, bukan penyelesaian taruhan, dan sebuah pertandingan bukan bukti yang layak jadi dasar pembayaran.
- Suara dan video. Frame-nya tidak punya akses mikrofon maupun kamera.
Jarak antara "server memegang kartunya" dan "server tahu aturannya" adalah tempat kecurangan masih hidup. Untuk main bareng teman jarak itu tidak masalah; untuk liga berhadiah, justru itu seluruh masalahnya.