tabletopkit

1.2Kinstalls670stars

Install to Claude Code

npx -y skills add https://github.com/dpearson2699/swift-ios-skills --skill tabletopkit

Description

tabletopkit

SKILL.md

---
name: tabletopkit
description: "Builds multiplayer spatial board games using TabletopKit on visionOS. Use when creating tabletop game experiences with boards, pieces, cards, or dice; managing seats, turns, equipment state, TabletopAction flows, or TabletopInteraction delegates; synchronizing gameplay through FaceTime Group Activities; rendering with RealityKit; or implementing snapping, tosses, and physics on a virtual table surface."
---

# TabletopKit

Create multiplayer spatial board games on a virtual table surface using
TabletopKit. Handles game layout, equipment interaction, player seating, turn
management, state synchronization, and RealityKit rendering. TabletopKit is
visionOS-only. Core APIs are visionOS 2.0+; availability-sensitive APIs are
called out below. Targets Swift 6.3.

## Contents

- [Setup](#setup)
- [Game Configuration](#game-configuration)
- [Table and Board](#table-and-board)
- [Equipment (Pieces, Cards, Dice)](#equipment-pieces-cards-dice)
- [Player Seats](#player-seats)
- [Game Actions and Turns](#game-actions-and-turns)
- [Interactions](#interactions)
- [RealityKit Rendering](#realitykit-rendering)
- [Group Activities Integration](#group-activities-integration)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Setup

### Platform Requirement

TabletopKit is exclusive to visionOS. Core gameplay, equipment, seating,
actions, interactions, rendering, and Group Activities coordination are
visionOS 2.0+. `TabletopInteraction.Configuration` is visionOS 2.2+.
`CustomAction`, `CustomEquipmentState`, `TableSetup.register(action:)`, and
the direct `TabletopGame.addAction(_ action: some CustomAction)` overload are
visionOS 26.0+. The Simulator supports single-player layout testing but not
Group Activities multiplayer.

### Project Configuration

1. `import TabletopKit` in source files that define game logic.
2. `import RealityKit` for entity-based rendering.
3. For multiplayer, add the **Group Activities** capability in Signing &
   Capabilities.
4. Provide 3D assets (USDZ) in a RealityKit content bundle for tables, pieces,
   cards, and dice.

### Key Types Overview

| Type | Role |
|---|---|
| `TabletopGame` | Central game manager; owns setup, actions, observers, rendering |
| `TableSetup` | Configuration object passed to `TabletopGame` init |
| `Tabletop` / `EntityTabletop` | Protocol for the table surface |
| `Equipment` / `EntityEquipment` | Protocol for interactive game pieces |
| `TableSeat` / `EntityTableSeat` | Protocol for player seat positions |
| `TabletopAction` | Commands that modify game state |
| `TabletopInteraction` | Gesture-driven player interactions with equipment |
| `TabletopGame.Observer` | Callback protocol for reacting to confirmed actions |
| `TabletopGame.RenderDelegate` | Callback protocol for visual updates |
| `EntityRenderDelegate` | RealityKit-specific render delegate |

## Game Configuration

Build a game in three steps: define the table, configure the setup, create the
`TabletopGame` instance.

```swift
import TabletopKit
import RealityKit

let table = GameTable()
var setup = TableSetup(tabletop: table)
setup.add(seat: PlayerSeat(index: 0, pose: seatPose0))
setup.add(seat: PlayerSeat(index: 1, pose: seatPose1))
setup.add(equipment: GamePawn(id: .init(1)))
setup.add(equipment: GameDie(id: .init(2)))

let game = TabletopGame(tableSetup: setup)
game.claimAnySeat()
```

Call `update(deltaTime:)` each frame if automatic updates are not enabled via
the `.tabletopGame(_:parent:automaticUpdate:)` modifier. Read state safely with
`withCurrentSnapshot(_:)`.

## Table and Board

### Tabletop Protocol

Conform to `EntityTabletop` to define the playing surface. Provide a `shape`
(round or rectangular) and a RealityKit `Entity` for visual representation.

```swift
struct GameTable: EntityTabletop {
    var shape: TabletopShape
    var entity: Entity
    var id: EquipmentIdentifier

    init() {
        entity = try! Entity.load(named: "table/game_table", in: contentBundle)
        shape = .round(entity: entity)
        id = .init(0)
    }
}
```

### Table Shapes

Use factory methods on `TabletopShape`:

```swift
// Round table from dimensions
let round = TabletopShape.round(
    center: .init(x: 0, y: 0, z: 0),
    radius: 0.5,
    thickness: 0.05,
    in: .meters
)

// Rectangular table from entity
let rect = TabletopShape.rectangular(entity: tableEntity)
```

## Equipment (Pieces, Cards, Dice)

### Equipment Protocol

All interactive game objects conform to `Equipment` (or `EntityEquipment` for
RealityKit-rendered pieces). Each piece has an `id` (`EquipmentIdentifier`) and
an `initialState` property.

Choose the state type based on the equipment:

| State Type | Use Case |
|---|---|
| `BaseEquipmentState` | Generic pieces, pawns, tokens |
| `CardState` | Playing cards (tracks `faceUp` / face-down) |
| `DieState` | Dice with an integer `value` |
| `RawValueState` | Custom data encoded as `UInt64` |
| `CustomEquipmentState` | visionOS 26.0+ custom state with a `BaseEquipmentState` plus game data |

### Defining Equipment

```swift
// Pawn -- uses BaseEquipmentState
struct GamePawn: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: BaseEquipmentState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "pieces/pawn", in: contentBundle)
        self.initialState = BaseEquipmentState(
            parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}

// Card -- uses CardState (tracks faceUp)
struct PlayingCard: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: CardState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "cards/card", in: contentBundle)
        self.initialState = .faceDown(
            parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}

// Die -- uses DieState (tracks integer value)
struct GameDie: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: DieState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "dice/d6", in: contentBundle)
        self.initialState = DieState(
            value: 1, parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}
```

### ControllingSeats

Restrict which players can interact with a piece via `seatControl`:
- `.any` -- any player
- `.restricted([seatID1, seatID2])` -- specific seats only
- `.restrictedCurrent([seatID1, seatID2])` -- specific seats only while they are in turn
- `.current` -- only the seat whose turn it is
- `.inherited` -- inherits from parent equipment

### Equipment Hierarchy and Layout

Equipment can be parented to other equipment. Override `layoutChildren(for:visualState:)`
to position children. Return one of:
- `.planarStacked(layout:animationDuration:)` -- cards/tiles stacked vertically
- `.planarOverlapping(layout:animationDuration:)` -- cards fanned or overlapping
- `.volumetric(layout:animationDuration:)` -- full 3D layout

See [references/tabletopkit-patterns.md](references/tabletopkit-patterns.md) for card fan, grid, and overlap layout examples.

## Player Seats

Conform to `EntityTableSeat` and provide a pose around the table:

```swift
struct PlayerSeat: EntityTableSeat {
    var id: TableSeatIdentifier
    var initialState: TableSeatState
    var entity: Entity

    init(index: Int, pose: TableVisualState.Pose2D) {
        self.id = TableSeatIdentifier(index)
        self.entity = Entity()
        self.initialState = TableSeatState(pose: pose, context: 0)
    }
}
```

Claim a seat before interacting: `game.claimAnySeat()`, `game.claimSeat(matching:)`,
or `game.releaseSeat()`. Observe changes via `TabletopGame.Observer.playerChangedSeats`.

## Game Actions and Turns

### Built-in Actions

Use `TabletopAction` factory methods to modify game state:

```swift
// Move equipment to a new parent
game.addAction(.moveEquipment(matching: pieceID, childOf: targetID, pose: newPose))

// Flip a card face-up
game.addAction(.updateEquipment(card, faceUp: true))

// Update die value
game.addAction(.updateEquipment(die, value: 6))

// Set whose turn it is
game.addAction(.setTurn(matching: TableSeatIdentifier(1)))

// Update a score counter
game.addAction(.updateCounter(matching: counterID, value: 100))

// Create a state bookmark (for undo/reset)
game.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))
```

### Custom Actions

For game-specific logic on visionOS 26.0+, conform to `CustomAction`.
Custom action application and validation must depend only on the action data and
the supplied `TableState` / `TableSnapshot` so every peer resolves the same
result. Register custom action types during setup before dispatching them:

```swift
setup.register(action: CollectCoin.self)
game.addAction(CollectCoin(coinID: coinID, playerID: playerID))
```

`CustomAction`, `CustomEquipmentState`, `setup.register(action:)`, and
`.customAction(_:context:)` are visionOS 26.0+. Register each custom action type
before dispatching it. See [references/tabletopkit-patterns.md](references/tabletopkit-patterns.md)
for full custom action and custom state examples.

### Score Counters

```swift
setup.add(counter: ScoreCounter(id: .init(0), value: 0))
// Update: game.addAction(.updateCounter(matching: .init(0), value: 42))
// Read:   snapshot.counter(matching: .init(0))?.value
```

### State Bookmarks

Save and restore game state for undo/reset:

```swift
game.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))
game.jumpToBookmark(matching: StateBookmarkIdentifier(1))
```

## Interactions

### TabletopInteraction.Delegate

Return an interaction delegate from the `.tabletopGame` modifier to handle
player gestures on equipment:

```swift
.tabletopGame(game.tabletopGame, parent: game.renderer.root) { value in
    if game.tabletopGame.equipment(of: GameDie.self, matching: value.startingEquipmentID) != nil {
        return DieInteraction(game: game)
    }
    return DefaultInteraction(game: game)
}
```

Use `interaction.value.gesture` for gesture-specific state. Avoid deprecated
`gesturePhase`. For destination control, prefer
`interaction.setConfiguration(.init(allowedDestinations: ...))` on visionOS
2.2+ rather than deprecated `setAllowedDestinations(_:)` or
`value.allowedDestinations`.

### Handling Gestures and Tossing Dice

Basic `toss(equipmentID:as:)` is core TabletopKit. `onTossStart`, `TabletopInteraction.TossOutcome`, and `TossableRepresentation.face(for:)` are visionOS 26.0+.

```swift
class DieInteraction: TabletopInteraction.Delegate {
    let game: Game

    func update(interaction: TabletopInteraction) {
        switch interaction.value.phase {
        case .started:
            interaction.setConfiguration(.init(allowedDestinations: .any))
        case .update:
            if interaction.value.gesture?.phase == .ended {
                interaction.toss(
                    equipmentID: interaction.value.controlledEquipmentID,
                    as: .cube(height: 0.02, in: .meters)
                )
            }
        case .ended, .cancelled:
            break
        }
    }

    func onTossStart(interaction: TabletopInteraction,
                     outcomes: [TabletopInteraction.TossOutcome]) {
        for outcome in outcomes {
            let face = outcome.tossableRepresentation.face(for: outcome.restingOrientation)
            interaction.addAction(.updateEquipment(
                die, rawValue: face.rawValue, pose: outcome.pose
            ))
        }
    }
}
```

### Tossable Representations

Dice physics shapes: `.cube` (d6), `.tetrahedron` (d4), `.octahedron` (d8),
`.decahedron` (d10), `.dodecahedron` (d12), `.icosahedron` (d20), `.sphere`.
All take `height:in:` (or `radius:in:` for sphere) and optional `restitution:`.

### Programmatic Interactions

Start interactions from code: `game.startInteraction(onEquipmentID: pieceID)`.

See [references/tabletopkit-patterns.md](references/tabletopkit-patterns.md) for group
toss, predetermined outcomes, interaction acceptance/rejection, and destination
restriction patterns.

## RealityKit Rendering

Conform to `EntityRenderDelegate` to bridge state to RealityKit. Provide a
`root` entity. TabletopKit automatically positions `EntityEquipment` entities.

```swift
class GameRenderer: EntityRenderDelegate {
    let root = Entity()

    func onUpdate(timeInterval: Double, snapshot: TableSnapshot,
                  visualState: TableVisualState) {
        // Custom visual updates beyond automatic positioning
    }
}
```

Connect to SwiftUI with `.tabletopGame(_:parent:automaticUpdate:)` on a
`RealityView`:

```swift
struct GameView: View {
    let game: Game

    var body: some View {
        RealityView { content in
            content.entities.append(game.renderer.root)
        }
        .tabletopGame(game.tabletopGame, parent: game.renderer.root) { value in
            GameInteraction(game: game)
        }
    }
}
```

Debug outlines: `game.tabletopGame.debugDraw(options: [.drawTable, .drawSeats, .drawEquipment])`

## Group Activities Integration

TabletopKit integrates directly with GroupActivities for FaceTime-based
multiplayer. Define a `GroupActivity`, then call `coordinateWithSession(_:)`.
TabletopKit automatically synchronizes all equipment state, seat assignments,
actions, and interactions. No manual message passing required.

```swift
import GroupActivities

struct BoardGameActivity: GroupActivity {
    var metadata: GroupActivityMetadata {
        var meta = GroupActivityMetadata()
        meta.type = .generic
        meta.title = "Board Game"
        return meta
    }
}

@Observable
class GroupActivityManager {
    let tabletopGame: TabletopGame
    private var sessionTask: Task<Void, Never>?

    init(tabletopGame: TabletopGame) {
        self.tabletopGame = tabletopGame
        sessionTask = Task { @MainActor in
            for await session in BoardGameActivity.sessions() {
                tabletopGame.coordinateWithSession(session)
            }
        }
    }

    deinit { tabletopGame.detachNetworkCoordinator() }
}
```

Implement `TabletopGame.MultiplayerDelegate` for `joinAccepted()`,
`playerJoined(_:)`, `didRejectPlayer(_:reason:)`, and
`multiplayerSessionFailed(reason:)`. See
[references/tabletopkit-patterns.md](references/tabletopkit-patterns.md) for custom
network coordinators and arbiter role management.

## Common Mistakes

- **Forgetting platform restriction.** TabletopKit is visionOS-only. Do not
  conditionally compile for iOS/macOS; the framework does not exist there.
- **Skipping seat claim.** Players must call `claimAnySeat()` or `claimSeat(_:)`
  before interacting with equipment. Without a seat, actions are rejected.
- **Mutating state outside actions.** All state changes must go through
  `TabletopAction` or `CustomAction`. Directly modifying equipment properties
  bypasses synchronization.
- **Missing custom action registration.** Custom actions must be registered with
  `setup.register(action:)` before use. Custom actions are visionOS 26.0+.
- **Not handling action rollback.** Actions are optimistically applied and can be
  rolled back if validation fails on the arbiter. Implement
  `actionWasRolledBack(_:snapshot:)` to revert UI state.
- **Ignoring discarded actions on visionOS 26.** Implement
  `actionWasDiscarded(_:)` when local action queue pressure matters; it is
  called for local actions that cannot be enqueued.
- **Using wrong parent ID.** Equipment `parentID` in state must reference a
  valid equipment ID (typically the table or a container). An invalid parent
  causes the piece to disappear.
- **Ignoring TossOutcome faces.** After a toss, read the face from
  `outcome.tossableRepresentation.face(for: outcome.restingOrientation)` rather
  than generating a random value. The physics simulation determines the result.
- **Testing multiplayer in Simulator.** Group Activities do not work in Simulator.
  Multiplayer requires physical Apple Vision Pro devices on a FaceTime call.

## Review Checklist

- [ ] `import TabletopKit` present; answer explicitly states TabletopKit is visionOS-only and target is visionOS 2.0+
- [ ] Availability checked for visionOS 2.2+ `TabletopInteraction.Configuration` and visionOS 26.0+ custom action/custom state APIs
- [ ] `TableSetup` created with a `Tabletop`/`EntityTabletop` conforming type
- [ ] All equipment conforms to `Equipment` or `EntityEquipment` with correct state type
- [ ] Seats added and `claimAnySeat()` / `claimSeat(_:)` called at game start
- [ ] All custom actions registered with `setup.register(action:)`
- [ ] `TabletopGame.Observer` implemented for reacting to confirmed actions
      and, on visionOS 26.0+, discarded local actions when relevant
- [ ] `EntityRenderDelegate` or `RenderDelegate` connected
- [ ] `.tabletopGame(_:parent:automaticUpdate:)` modifier on `RealityView`
- [ ] `GroupActivity` defined and `coordinateWithSession(_:)` called; multiplayer described as Group Activities/SharePlay synchronization
- [ ] Group Activities capability added in Xcode for multiplayer builds
- [ ] Debug visualization (`debugDraw`) disabled before release
- [ ] Device notes state Simulator is single-player only; multiplayer requires 2+ Apple Vision Pro units on FaceTime

## References

- [references/tabletopkit-patterns.md](references/tabletopkit-patterns.md) -- extended patterns for observer implementation, custom actions, dice simulation, card overlap, and network coordination
- [Apple Documentation: TabletopKit](https://sosumi.ai/documentation/tabletopkit)
- [Creating tabletop games (sample code; visionOS 2.2+, Xcode 16.2+)](https://sosumi.ai/documentation/tabletopkit/creating-tabletop-games)
- [Synchronizing group gameplay with TabletopKit (sample code; visionOS 26.0+, Xcode 26.0+)](https://sosumi.ai/documentation/tabletopkit/synchronizing-group-gameplay-with-tabletopkit)
- [Simulating dice rolls as a component for your game (sample code; visionOS 26.0+, Xcode 26.0+)](https://sosumi.ai/documentation/tabletopkit/simulating-dice-rolls-as-a-component-for-your-game)
- [Implementing playing card overlap and physical characteristics (sample code; visionOS 26.0+, Xcode 26.0+)](https://sosumi.ai/documentation/tabletopkit/implementing-playing-card-overlap-and-physical-characteristics)
- [WWDC24 session 10091: Build a spatial board game](https://sosumi.ai/videos/play/wwdc2024/10091/)
tabletopkit — Claude Code Skill — SearchAITools