Truth or Dare API Documentation

A comprehensive guide to implementing the Truth or Dare game in your application

Game Engine Logic

Overview

The Truth or Dare game allows partners to engage in a fun, interactive experience that can help deepen their connection through answering personal questions or performing playful dares. The game supports both online mode (between connected partners) and offline mode (for local play).

Game Flow

The Truth or Dare game connects partners through a turn-based experience where they answer personal questions or perform dares.

  1. Connection: Users connect with their unique IDs to join the game.
  2. Invitation: One partner sends a game invitation to the other.
  3. Acceptance: The invited partner accepts the invitation to start the game.
  4. Taking Turns: Players alternate turns, with each turn following this sequence:
    • Active player chooses "Truth" or "Dare"
    • System presents a random question or challenge
    • Player answers and submits their response
    • Turn passes to their partner
  5. Viewing & Reacting: When it's your turn, you first see your partner's previous answer and can react with an emoji before proceeding to your challenge.
  6. Waiting State: After completing your turn, you'll see a "Waiting for your partner..." screen that displays any reaction they gave to your answer.
  7. Game Completion: The game ends after a set number of turns (default: 10), or when players choose to end it.

API Reference

Follow these steps to implement the Truth or Dare game in your Swift application. Each step corresponds to a specific API endpoint and game state.

Step 1: Check Partner Status

Before starting a game, verify that the user has a connected partner.

GET /api/games/truth-or-dare/partner-status

Query Parameters:
  • userId - The unique ID of the user
Success Response:
{
  "hasPartner": true,
  "partnerId": "user_partner123",
  "partnerName": "Partner Name",
  "connectionStatus": "connected"
}
Error Response:
{
  "error": "Failed to check partner status",
  "details": "Error message details"
}

Swift Implementation:

// Function to check partner status
func checkPartnerStatus(userId: String) async throws -> PartnerStatus {
    guard let url = URL(string: "https://your-api-domain.com/api/games/truth-or-dare/partner-status?userId=\(userId)") else {
        throw NetworkError.invalidURL
    }
    
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, 
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }
    
    do {
        let partnerStatus = try JSONDecoder().decode(PartnerStatus.self, from: data)
        return partnerStatus
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Partner Status model
struct PartnerStatus: Codable {
    let hasPartner: Bool
    let partnerId: String?
    let partnerName: String?
    let connectionStatus: String
}

Step 2: Update and Check Game Presence

Update the user's online status and check if their partner is online to play the game.

POST /api/games/truth-or-dare/game-presence

Request Body:
{
  "userId": "user123",
  "isInGame": true
}
Success Response:
{
  "success": true
}

GET /api/games/truth-or-dare/game-presence

Query Parameters:
  • userId - The unique ID of the user
  • partnerId - The unique ID of the partner
Success Response:
{
  "userOnline": true,
  "partnerOnline": true,
  "lastActive": {
    "userId": 1618047632000,
    "partnerId": 1618047690000
  }
}

Swift Implementation:

// Function to update presence
func updateGamePresence(userId: String) async throws -> Bool {
    guard let url = URL(string: "https://your-api-domain.com/api/games/truth-or-dare/game-presence") else {
        throw NetworkError.invalidURL
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let body = ["userId": userId, "isInGame": true]
    request.httpBody = try JSONSerialization.data(withJSONObject: body)
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, 
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }
    
    do {
        if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
           let success = json["success"] as? Bool {
            return success
        } else {
            throw NetworkError.invalidData
        }
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Function to check game presence
func checkGamePresence(userId: String, partnerId: String) async throws -> GamePresence {
    guard let url = URL(string: "https://your-api-domain.com/api/games/truth-or-dare/game-presence?userId=\(userId)&partnerId=\(partnerId)") else {
        throw NetworkError.invalidURL
    }
    
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, 
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }
    
    do {
        let gamePresence = try JSONDecoder().decode(GamePresence.self, from: data)
        return gamePresence
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Game Presence model
struct GamePresence: Codable {
    let userOnline: Bool
    let partnerOnline: Bool
    let lastActive: [String: TimeInterval]
}

Step 3: Check Current Game State

Check if there's an existing game in progress between the user and their partner.

GET /api/games/truth-or-dare/game-state

Query Parameters:
  • userId - The unique ID of the user
  • partnerId - The unique ID of the partner
Success Response (Game in Progress):
{
  "_id": "game123",
  "userId": "user123",
  "partnerId": "partner456",
  "gameType": "truth_or_dare",
  "currentState": {
    "isGameOver": false,
    "gameStatus": "active",
    "currentTurn": "user123",
    "turnCount": 3,
    "rounds": [...],
    "currentChallenge": {
      "type": "truth",
      "questionId": "question123",
      "question": "What's your biggest fear?"
    },
    "settings": {
      "turnLimit": 10,
      "intimacyLevel": 3,
      "categories": ["fun", "romantic"]
    }
  },
  "lastUpdated": 1618047690000
}
Success Response (No Game):
null

Swift Implementation:

// Function to check game state
func checkGameState(userId: String, partnerId: String) async throws -> GameState? {
    guard let url = URL(string: "https://your-api-domain.com/api/games/truth-or-dare/game-state?userId=\(userId)&partnerId=\(partnerId)") else {
        throw NetworkError.invalidURL
    }
    
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, 
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }
    
    // Check if response is null (no game)
    if data.count == 4, let dataString = String(data: data, encoding: .utf8), dataString == "null" {
        return nil
    }
    
    do {
        let gameState = try JSONDecoder().decode(GameState.self, from: data)
        return gameState
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Game State model (partial example)
struct GameState: Codable {
    let _id: String
    let userId: String
    let partnerId: String
    let gameType: String
    let currentState: CurrentGameState
    let lastUpdated: TimeInterval
}

struct CurrentGameState: Codable {
    let isGameOver: Bool
    let gameStatus: String
    let currentTurn: String
    let turnCount: Int
    let rounds: [GameRound]
    let currentChallenge: Challenge?
    let settings: GameSettings
}

// Add other necessary model structs...

Step 4: Check Pending Invitations

Check if the user has any pending game invitations from their partner.

GET /api/games/truth-or-dare/invitations

Query Parameters:
  • userId - The unique ID of the user
Success Response:
{
  "success": true,
  "invitations": [
    {
      "_id": "game123",
      "userId": "partner456",
      "partnerId": "user123",
      "gameType": "truth_or_dare",
      "currentState": {
        "gameStatus": "invitation_pending",
        "invitedBy": "partner456",
        "invitedAt": 1618047690000,
        "expiresAt": 1618047990000,
        "settings": {
          "turnLimit": 10,
          "intimacyLevel": 3,
          "categories": ["fun", "romantic"]
        }
      }
    }
  ]
}

Swift Implementation:

// Function to check pending invitations
func checkPendingInvitations(userId: String) async throws -> [GameInvitation] {
    guard let url = URL(string: "https://your-api-domain.com/api/games/truth-or-dare/invitations?userId=\(userId)") else {
        throw NetworkError.invalidURL
    }
    
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, 
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }
    
    do {
        let result = try JSONDecoder().decode(InvitationsResponse.self, from: data)
        return result.success ? result.invitations : []
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Invitations Response model
struct InvitationsResponse: Codable {
    let success: Bool
    let invitations: [GameInvitation]
    let error: String?
}

struct GameInvitation: Codable {
    let _id: String
    let userId: String
    let partnerId: String
    let gameType: String
    let currentState: InvitationState
}

struct InvitationState: Codable {
    let gameStatus: String
    let invitedBy: String
    let invitedAt: TimeInterval
    let expiresAt: TimeInterval
    let settings: GameSettings
}

Step 5: Send Game Invitation

If no game exists and there are no pending invitations, send a game invitation to the partner.

POST /api/games/truth-or-dare/invitations

Request Body:
{
  "userId": "user123",
  "partnerId": "partner456",
  "settings": {
    "turnLimit": 10,
    "intimacyLevel": 3,
    "categories": ["fun", "romantic"]
  }
}
Success Response:
{
  "gameId": "game123",
  "success": true
}
Error Response:
{
  "error": "Your partner is not online right now"
}

Swift Implementation:

// Function to send game invitation
func sendGameInvitation(userId: String, partnerId: String) async throws -> String {
    guard let url = URL(string: "https://your-api-domain.com/api/games/truth-or-dare/invitations") else {
        throw NetworkError.invalidURL
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let settings: [String: Any] = [
        "turnLimit": 10,
        "intimacyLevel": 3,
        "categories": ["fun", "romantic"]
    ]
    
    let body: [String: Any] = [
        "userId": userId,
        "partnerId": partnerId,
        "settings": settings
    ]
    
    request.httpBody = try JSONSerialization.data(withJSONObject: body)
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse else {
        throw NetworkError.invalidResponse
    }
    
    if httpResponse.statusCode != 200 {
        // Try to decode error message
        if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
            throw NetworkError.apiError(errorResponse.error)
        } else {
            throw NetworkError.invalidResponse
        }
    }
    
    do {
        let invitationResponse = try JSONDecoder().decode(InvitationResponse.self, from: data)
        if invitationResponse.success {
            return invitationResponse.gameId
        } else {
            throw NetworkError.apiError("Unknown error")
        }
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Response models
struct InvitationResponse: Codable {
    let gameId: String
    let success: Bool
}

struct ErrorResponse: Codable {
    let error: String
    let details: String?
}

Step 6: Respond to Game Invitation

Accept or decline a pending game invitation.

POST /api/games/truth-or-dare/invitations/{gameId}

URL Parameters:
  • gameId - The ID of the game invitation
Request Body:
{
  "userId": "user123",
  "action": "accept"  // or "decline"
}
Success Response:
{
  "success": true,
  "gameState": { /* Game state object */ }
}

Swift Implementation:

// Function to respond to game invitation
func respondToInvitation(gameId: String, userId: String, accept: Bool) async throws -> GameState {
    guard let url = URL(string: "https://your-api-domain.com/api/games/truth-or-dare/invitations/\(gameId)") else {
        throw NetworkError.invalidURL
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let body: [String: Any] = [
        "userId": userId,
        "action": accept ? "accept" : "decline"
    ]
    
    request.httpBody = try JSONSerialization.data(withJSONObject: body)
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, 
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }
    
    do {
        let responseObj = try JSONDecoder().decode(InvitationResponseWithState.self, from: data)
        return responseObj.gameState
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Response model
struct InvitationResponseWithState: Codable {
    let success: Bool
    let gameState: GameState
}

Step 7: Game Actions (Core Gameplay)

Once a game is active, players use this endpoint to perform all gameplay actions including making choices, completing challenges, adding reactions, and ending the game.

POST /api/games/truth-or-dare/games/{gameId}/actions

URL Parameters:
  • gameId - The ID of the active game
Action: Make Choice (Truth or Dare)
{
  "userId": "user123",
  "action": "makeChoice",
  "choice": "truth"  // or "dare"
}
Action: Complete Challenge (Answer/Submit)
{
  "userId": "user123",
  "action": "completeChallenge",
  "answer": "My biggest fear is heights",
  "completed": true,
  "markSeen": false  // Set to true when viewing partner's answer
}
Action: Add Reaction to Partner's Answer
{
  "userId": "user123",
  "action": "addReaction",
  "roundIndex": 2,
  "reaction": "😂"
}
Action: End Game
{
  "userId": "user123",
  "action": "endGame",
  "reason": "ended_by_player"
}
Success Response:
{
  "success": true,
  "challenge": {  // Only for makeChoice action
    "_id": "question123",
    "question": "What's your biggest fear?",
    "category": "deep",
    "intimacyLevel": 3
  }
}

Swift Implementation:

// Function to perform game actions
func performGameAction(gameId: String, userId: String, action: GameAction) async throws -> GameActionResponse {
    guard let url = URL(string: "https://your-api-domain.com/api/games/truth-or-dare/games/\(gameId)/actions") else {
        throw NetworkError.invalidURL
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let body = try action.toRequestBody(userId: userId)
    request.httpBody = try JSONSerialization.data(withJSONObject: body)
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, 
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }
    
    do {
        let actionResponse = try JSONDecoder().decode(GameActionResponse.self, from: data)
        return actionResponse
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Game Action enum
enum GameAction {
    case makeChoice(choice: String)
    case completeChallenge(answer: String, completed: Bool, markSeen: Bool)
    case addReaction(roundIndex: Int, reaction: String)
    case endGame(reason: String?)
    
    func toRequestBody(userId: String) throws -> [String: Any] {
        var body: [String: Any] = ["userId": userId]
        
        switch self {
        case .makeChoice(let choice):
            body["action"] = "makeChoice"
            body["choice"] = choice
        case .completeChallenge(let answer, let completed, let markSeen):
            body["action"] = "completeChallenge"
            body["answer"] = answer
            body["completed"] = completed
            body["markSeen"] = markSeen
        case .addReaction(let roundIndex, let reaction):
            body["action"] = "addReaction"
            body["roundIndex"] = roundIndex
            body["reaction"] = reaction
        case .endGame(let reason):
            body["action"] = "endGame"
            if let reason = reason {
                body["reason"] = reason
            }
        }
        
        return body
    }
}

// Response models
struct GameActionResponse: Codable {
    let success: Bool
    let challenge: Challenge?
    let isGameOver: Bool?
}

struct Challenge: Codable {
    let _id: String
    let question: String?
    let dare: String?
    let category: String
    let intimacyLevel: Int
}

Step 7.5: Get Truth or Dare Game Data (with Rounds)

This API endpoint allows you to retrieve complete game data including all rounds, making it easy to display game history, current state, and detailed round information. This is particularly useful for showing game summaries, displaying previous answers, and implementing game replay features.

POST /api/truth-or-dare

Use Cases:
  • Retrieve current game state with all completed rounds
  • Get specific game by ID for detailed viewing
  • Load game between two specific users
  • Display game history and summaries
  • Show previous answers and reactions
Request Body - Get Most Recent Game:
{
  "userId": "user123"
}
Request Body - Get Game Between Specific Users:
{
  "userId": "user123",
  "partnerId": "partner456"
}
Request Body - Get Specific Game by ID:
{
  "userId": "user123",
  "gameId": "game_abc123def456"
}
Success Response (Game Found):
{
  "success": true,
  "game": {
    "_id": "game_abc123def456",
    "userId": "user123",
    "partnerId": "partner456",
    "gameType": "truth_or_dare",
    "currentState": {
      "isGameOver": false,
      "gameStatus": "active",
      "currentTurn": "partner456",
      "turnCount": 4,
      "settings": {
        "turnLimit": 10,
        "intimacyLevel": 3,
        "categories": ["fun", "romantic"]
      },
      "rounds": [
        {
          "player": "user123",
          "choice": "truth",
          "questionId": "question_123",
          "question": "What's your biggest fear?",
          "answer": "Spiders and heights",
          "completed": true,
          "timestamp": 1618047690000,
          "reaction": "😱",
          "reactionTimestamp": 1618047720000,
          "seen": true,
          "seenAt": 1618047715000
        },
        {
          "player": "partner456",
          "choice": "dare",
          "dareId": "dare_456",
          "dare": "Send a silly selfie to your crush",
          "answer": "Done! Sent a funny photo",
          "completed": true,
          "timestamp": 1618047750000,
          "reaction": "😂",
          "reactionTimestamp": 1618047780000,
          "seen": true,
          "seenAt": 1618047775000
        }
      ],
      "currentChallenge": null,
      "lastAction": {
        "type": "challenge_completed",
        "userId": "partner456",
        "timestamp": 1618047750000
      }
    },
    "lastUpdated": 1618047750000,
    "playersArray": ["user123", "partner456"],
    "rounds": [
      // Same rounds array duplicated here for easy access
    ]
  }
}
Success Response (No Game Found):
{
  "success": true,
  "game": null
}
Error Response:
{
  "success": false,
  "error": "User not found",
  "game": null
}

Swift Implementation:

// Function to get Truth or Dare game data
func getTruthOrDareGame(userId: String, partnerId: String? = nil, gameId: String? = nil) async throws -> TruthOrDareGameResponse {
    guard let url = URL(string: "https://your-api-domain.com/api/truth-or-dare") else {
        throw NetworkError.invalidURL
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    var body: [String: Any] = ["userId": userId]
    if let partnerId = partnerId {
        body["partnerId"] = partnerId
    }
    if let gameId = gameId {
        body["gameId"] = gameId
    }
    
    request.httpBody = try JSONSerialization.data(withJSONObject: body)
    
    let (data, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, 
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }
    
    do {
        let gameResponse = try JSONDecoder().decode(TruthOrDareGameResponse.self, from: data)
        return gameResponse
    } catch {
        throw NetworkError.decodingError(error)
    }
}

// Response models
struct TruthOrDareGameResponse: Codable {
    let success: Bool
    let game: TruthOrDareGame?
    let error: String?
}

struct TruthOrDareGame: Codable {
    let _id: String
    let userId: String
    let partnerId: String
    let gameType: String
    let currentState: GameCurrentState
    let lastUpdated: TimeInterval
    let playersArray: [String]
    let rounds: [GameRound] // Duplicate rounds for easy access
}

struct GameCurrentState: Codable {
    let isGameOver: Bool
    let gameStatus: String
    let currentTurn: String
    let turnCount: Int
    let settings: GameSettings
    let rounds: [GameRound]
    let currentChallenge: CurrentChallenge?
    let lastAction: LastAction
}

struct GameRound: Codable {
    let player: String
    let choice: String
    let questionId: String?
    let dareId: String?
    let question: String?
    let dare: String?
    let answer: String
    let completed: Bool
    let timestamp: TimeInterval
    let reaction: String?
    let reactionTimestamp: TimeInterval?
    let seen: Bool?
    let seenAt: TimeInterval?
}

struct CurrentChallenge: Codable {
    let type: String
    let questionId: String?
    let dareId: String?
    let question: String?
    let dare: String?
}

struct LastAction: Codable {
    let type: String
    let userId: String
    let timestamp: TimeInterval
    let choice: String?
    let questionId: String?
    let dareId: String?
}

struct GameSettings: Codable {
    let turnLimit: Int
    let intimacyLevel: Int
    let categories: [String]
}

// Usage examples:
// Get current game for user
let currentGame = try await getTruthOrDareGame(userId: "user123")

// Get game between specific users  
let gameWithPartner = try await getTruthOrDareGame(userId: "user123", partnerId: "partner456")

// Get specific game by ID
let specificGame = try await getTruthOrDareGame(userId: "user123", gameId: "game_abc123def456")

💡 Pro Tips

  • Use this API to display game history and summaries
  • The rounds array contains complete information about each turn including reactions
  • Check the `seen` property to know if a round answer has been viewed
  • Use `gameStatus` to determine the current state: "active", "paused", "invitation_pending"
  • The `currentChallenge` field shows if a player is in the middle of answering
  • Timestamp values are in milliseconds since epoch

Step 8: Complete Game Flow Implementation

Here's how to implement the complete game flow in your Swift application, combining all the APIs to create a seamless Truth or Dare experience.

Complete Swift Implementation:

class TruthOrDareGameManager: ObservableObject {
    @Published var gameState: GameState?
    @Published var currentScreen: GameScreen = .idle
    @Published var errorMessage: String?
    
    private let userId: String
    private var partnerId: String?
    private var gameId: String?
    private var presenceTimer: Timer?
    
    enum GameScreen {
        case idle
        case connecting
        case inviting
        case waitingForPartner
        case playing
        case viewingPartnerAnswer
        case waitingForReaction
        case gameOver
    }
    
    init(userId: String) {
        self.userId = userId
    }
    
    // MARK: - Main Game Flow
    
    func startGame() async {
        do {
            // Step 1: Check partner status
            let partnerStatus = try await checkPartnerStatus(userId: userId)
            guard partnerStatus.hasPartner else {
                errorMessage = "No partner connected"
                return
            }
            
            partnerId = partnerStatus.partnerId
            
            // Step 2: Update presence
            _ = try await updateGamePresence(userId: userId)
            startPresenceHeartbeat()
            
            // Step 3: Check existing game
            if let existingGame = try await checkGameState(userId: userId, partnerId: partnerId!) {
                gameState = existingGame
                determineCurrentScreen()
                return
            }
            
            // Step 4: Check pending invitations
            let invitations = try await checkPendingInvitations(userId: userId)
            if !invitations.isEmpty {
                // Handle pending invitation
                currentScreen = .waitingForPartner
                return
            }
            
            // Step 5: Send invitation
            currentScreen = .inviting
            let invitation = try await sendGameInvitation(userId: userId, partnerId: partnerId!)
            gameId = invitation.gameId
            currentScreen = .waitingForPartner
            
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    func acceptInvitation(_ invitation: GameInvitation) async {
        do {
            let gameState = try await respondToInvitation(
                gameId: invitation._id,
                userId: userId,
                accept: true
            )
            self.gameState = gameState
            self.gameId = invitation._id
            currentScreen = .playing
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    func makeChoice(_ choice: String) async {
        guard let gameId = gameId else { return }
        
        do {
            let response = try await performGameAction(
                gameId: gameId,
                userId: userId,
                action: .makeChoice(choice: choice)
            )
            
            // Refresh game state to get the challenge
            await refreshGameState()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    func submitAnswer(_ answer: String, completed: Bool = true) async {
        guard let gameId = gameId else { return }
        
        do {
            let response = try await performGameAction(
                gameId: gameId,
                userId: userId,
                action: .completeChallenge(answer: answer, completed: completed, markSeen: false)
            )
            
            await refreshGameState()
            
            if response.isGameOver == true {
                currentScreen = .gameOver
            } else {
                currentScreen = .waitingForPartner
            }
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    func viewPartnerAnswerAndContinue(markSeen: Bool = true) async {
        guard let gameId = gameId else { return }
        
        do {
            _ = try await performGameAction(
                gameId: gameId,
                userId: userId,
                action: .completeChallenge(answer: "", completed: true, markSeen: markSeen)
            )
            
            await refreshGameState()
            currentScreen = .playing
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    func addReaction(_ reaction: String, to roundIndex: Int) async {
        guard let gameId = gameId else { return }
        
        do {
            _ = try await performGameAction(
                gameId: gameId,
                userId: userId,
                action: .addReaction(roundIndex: roundIndex, reaction: reaction)
            )
            
            await refreshGameState()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    func endGame() async {
        guard let gameId = gameId else { return }
        
        do {
            _ = try await performGameAction(
                gameId: gameId,
                userId: userId,
                action: .endGame(reason: "ended_by_player")
            )
            
            currentScreen = .gameOver
            stopPresenceHeartbeat()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    // MARK: - Helper Methods
    
    private func refreshGameState() async {
        guard let partnerId = partnerId else { return }
        
        do {
            gameState = try await checkGameState(userId: userId, partnerId: partnerId)
            determineCurrentScreen()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    private func determineCurrentScreen() {
        guard let gameState = gameState else {
            currentScreen = .idle
            return
        }
        
        if gameState.currentState.isGameOver {
            currentScreen = .gameOver
            return
        }
        
        // Check if partner just completed a challenge and current user needs to see it
        let lastRound = gameState.currentState.rounds.last
        let needsToSeePartnerAnswer = gameState.currentState.currentTurn == userId &&
                                    lastRound?.player != userId &&
                                    lastRound?.seen != true
        
        if needsToSeePartnerAnswer {
            currentScreen = .viewingPartnerAnswer
        } else if gameState.currentState.currentTurn == userId {
            currentScreen = .playing
        } else {
            currentScreen = .waitingForPartner
        }
    }
    
    private func startPresenceHeartbeat() {
        presenceTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { _ in
            Task {
                try? await self.updateGamePresence(userId: self.userId)
            }
        }
    }
    
    private func stopPresenceHeartbeat() {
        presenceTimer?.invalidate()
        presenceTimer = nil
    }
    
    deinit {
        stopPresenceHeartbeat()
    }
}

Step 9: UI Implementation Guidelines

Based on the game flow, here are the different UI screens you'll need to implement in your Swift app:

1. Connection Screen (.idle)

  • User ID input field
  • Connect button
  • Display partner status once connected
  • Show "Invite to Play" button when both users are online

2. Game Choice Screen (.playing - choice phase)

  • Two buttons: "Truth" and "Dare"
  • Game progress indicator (current turn / total turns)
  • Option to end game early

3. Challenge Screen (.playing - answer phase)

  • Display the truth question or dare challenge
  • Text input for answer
  • Submit button
  • For dares: "Can't Do It" button as alternative

4. Partner Answer Review (.viewingPartnerAnswer)

  • Display partner's challenge and answer
  • Emoji reaction buttons (😂, 😮, ❤️, 👍, etc.)
  • "Continue to My Turn" button

5. Waiting Screen (.waitingForPartner)

  • Loading indicator
  • "Waiting for partner..." message
  • Show partner's reaction to your answer (if available)
  • Option to end game

6. Game Over Screen (.gameOver)

  • Game summary with all rounds played
  • Each round showing question/dare, answer, and reactions
  • "Play Again" button
  • "Back to Menu" button

Step 10: Error Handling and Edge Cases

Important considerations for a robust implementation:

Connection Issues

  • Handle partner going offline during game
  • Implement reconnection logic
  • Show appropriate waiting/paused states
  • Auto-resume when both players are back online

Game State Synchronization

  • Poll game state regularly during active gameplay
  • Handle out-of-turn actions gracefully
  • Validate current player's turn before allowing actions
  • Show loading states during API calls

Invitation Management

  • Handle invitation expiration (5 minutes timeout)
  • Check for existing games before sending new invitations
  • Allow declining invitations gracefully
  • Show pending invitation notifications

Data Validation

  • Validate user IDs before making API calls
  • Handle empty or invalid responses
  • Implement proper error messaging for users
  • Add input validation for answers and reactions

Need help?

If you have any questions about implementing the Truth or Dare game in your application, please contact the developer support team.

Return to Documentation Home