Turn-Based Games
Host controls game state. Players send move requests. Host validates and broadcasts.
When to Use
- Chess, checkers, Go
- Card games (Uno, poker, etc.)
- Board games
- Strategy games
- Any game with discrete turns
The Pattern
import { connect } from '@watchtower-sdk/core'
const room = await connect('chess-game')
// Game state (host is source of truth)
let gameState = {
board: createInitialBoard(),
turn: null,
winner: null
}
// Initialize game when both players join
room.on('join', (player) => {
if (room.isHost && room.playerCount === 2) {
// Assign colors and start
gameState.turn = room.playerId // Host goes first
broadcastState()
}
})
// Host broadcasts authoritative state
function broadcastState() {
if (room.isHost) {
room.broadcast({ type: 'state', ...gameState })
}
}
// Everyone receives state updates
room.on('message', (from, data) => {
if (data.type === 'state') {
gameState = data
render()
}
// Host processes move requests
if (data.type === 'move' && room.isHost) {
handleMoveRequest(from, data)
}
})
// Host validates and applies moves
function handleMoveRequest(playerId, move) {
// Only process if it's their turn
if (gameState.turn !== playerId) return
// Validate the move
if (!isValidMove(gameState.board, move.from, move.to)) return
// Apply the move
gameState.board = applyMove(gameState.board, move.from, move.to)
// Check for winner
if (checkWinner(gameState.board)) {
gameState.winner = playerId
} else {
// Next player's turn
gameState.turn = getOtherPlayer(playerId)
}
broadcastState()
}
// Players request moves (not apply directly)
function makeMove(from, to) {
room.broadcast({ type: 'move', from, to })
}Key Points
- Host authority — only host modifies game state
- Request/response — players send requests, host validates
- Single source of truth — everyone syncs from host's state
- Turn enforcement — host checks if it's your turn
Handling Host Disconnect
When the host leaves, a new host is assigned automatically. Transfer game state:
// Store state locally so new host can continue
let localGameState = null
room.on('message', (from, data) => {
if (data.type === 'state') {
localGameState = data
}
})
// When you become host, broadcast current state
room.on('join', () => {
if (room.isHost && localGameState) {
gameState = localGameState
broadcastState()
}
})Private Information (Card Games)
For games with hidden information, use direct messages:
// Host deals cards privately
function dealCards() {
if (!room.isHost) return
const deck = shuffleDeck()
for (const player of room.players) {
const hand = deck.splice(0, 5)
room.send(player.id, { type: 'deal', hand })
}
// Broadcast public info only
room.broadcast({
type: 'state',
playerCardCounts: getCardCounts(),
deckRemaining: deck.length
})
}
// Players receive their private hand
room.on('message', (from, data) => {
if (data.type === 'deal') {
myHand = data.hand
}
})Full Chess Example
import { connect } from '@watchtower-sdk/core'
const room = await connect()
let gameState = { board: initialBoard(), turn: null, winner: null }
let myColor = null
// Determine colors on join
room.on('join', (player) => {
if (room.isHost) {
if (room.playerCount === 1) {
myColor = 'white'
} else if (room.playerCount === 2) {
gameState.turn = room.playerId
room.broadcast({ type: 'state', ...gameState })
room.send(player.id, { type: 'color', color: 'black' })
}
}
})
room.on('message', (from, data) => {
switch (data.type) {
case 'state':
gameState = data
renderBoard()
break
case 'color':
myColor = data.color
break
case 'move':
if (room.isHost) {
if (gameState.turn === from && isValid(data.from, data.to)) {
gameState.board = applyMove(gameState.board, data.from, data.to)
gameState.turn = gameState.turn === room.playerId
? room.players.find(p => p.id !== room.playerId).id
: room.playerId
room.broadcast({ type: 'state', ...gameState })
}
}
break
}
})
function onSquareClick(square) {
if (gameState.turn !== room.playerId) return // Not my turn
if (selectedSquare) {
room.broadcast({ type: 'move', from: selectedSquare, to: square })
selectedSquare = null
} else {
selectedSquare = square
}
}Next Steps
- Add move timers
- Add undo/redo functionality
- Add spectator support