A card game with RPCs
Card games are the clean case for server authority: the deck is secret, the rules are strict, and no client should be trusted to enforce either. State is server-owned; every move is an RPC.
Schema
export const schema = defineSchema({
match: entity({ turn: string(), phase: enumOf(['deal', 'play', 'over']) }),
hands: map(list(string())), // per-player, only sent to its owner
pile: list(string()),
}, { serverOwned: true }); Hands use per-owner visibility — a client only receives its own hand, so the secret never reaches other browsers.
Moves as RPCs
export const rpc = defineRpc({
play: { args: { card: string() }, returns: { ok: boolean() } },
draw: { args: {} },
}); export default defineRoom(schema, {
rpc: {
play(state, { card }, ctx) {
if (state.match.turn !== ctx.clientId) return { ok: false };
const hand = state.hands[ctx.clientId];
if (!hand.includes(card)) return { ok: false };
hand.splice(hand.indexOf(card), 1);
state.pile.push(card);
state.match.turn = nextPlayer(state, ctx.clientId);
return { ok: true };
},
},
}); Client
async function onPlay(card: string) {
const { ok } = await room.rpc.play({ card });
if (!ok) nudge();
}
room.subscribe((s) => renderTable(s.pile, s.hands[room.me], s.match.turn)); Turn order, legality, and secrecy are all enforced in one place the client can’t bypass.
Next steps
- Server authority & validation
- Simulated players — test a full hand headlessly.
Placeholder guide.