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.