Back to list
nategarelik

game-implementation-for-prop-pigeon

by nategarelik

0🍴 0📅 Nov 16, 2025

SKILL.md


name: Game Implementation for Prop Pigeon description: Patterns for implementing the 6 games (8 Ball, Blackjack, War, Texas Hold'em, Archery, Pong) when_to_use: Implementing game logic, rules, or rendering version: 1.0.0 languages: swift

Game Implementation Patterns

Game Protocol (MANDATORY)

All games MUST conform to this protocol:

protocol GameProtocol {
    associatedtype State: Codable, Equatable
    
    var state: State { get set }
    
    /// Validate move without applying it
    func validateMove(_ move: GameMove) -> Bool
    
    /// Apply move and update state (throws if invalid)
    mutating func applyMove(_ move: GameMove) throws
    
    /// Check if game has ended and who won
    func checkWinCondition() -> GameResult?
    
    /// Serialize state for message passing
    func serialize() -> Data
    
    /// Deserialize state from message
    static func deserialize(_ data: Data) -> Self?
}

enum GameResult {
    case player1Win
    case player2Win
    case tie
}

Implementation Order (START SIMPLE)

  1. War - Simplest, fully automated
  2. Pong - Simple physics, real-time
  3. Blackjack - Simple rules, player decisions
  4. Archery - 3D, physics-based
  5. 8 Ball - Complex physics
  6. Texas Hold'em - Most complex logic

Game Structure Template

For each game, create:

Games/<GameName>/
├── <GameName>Game.swift          # Core logic + GameProtocol
├── <GameName>State.swift         # Codable game state
├── <GameName>ViewModel.swift     # MVVM state management
├── <GameName>View.swift          # SwiftUI interface
├── <GameName>Scene.swift         # SpriteKit/SceneKit (if needed)
└── Tests/
    ├── <GameName>GameTests.swift
    └── <GameName>ViewModelTests.swift

Example: War Card Game

WarGame.swift

struct WarGame: GameProtocol {
    typealias State = WarState
    
    var state: WarState
    
    init(player1ID: UUID, player2ID: UUID, wager: Int) {
        self.state = WarState(
            player1ID: player1ID,
            player2ID: player2ID,
            wager: wager
        )
        dealCards()
    }
    
    func validateMove(_ move: GameMove) -> Bool {
        // War is automated - no player moves
        return false
    }
    
    mutating func applyMove(_ move: GameMove) throws {
        // Not used - automated game
    }
    
    mutating func playRound() {
        guard !state.isGameOver else { return }
        
        // Both players reveal top card
        guard let card1 = state.player1Deck.first,
              let card2 = state.player2Deck.first else {
            return
        }
        
        state.player1Deck.removeFirst()
        state.player2Deck.removeFirst()
        
        if card1.rank > card2.rank {
            // Player 1 wins round
            state.player1Deck.append(contentsOf: [card1, card2])
        } else if card2.rank > card1.rank {
            // Player 2 wins round
            state.player2Deck.append(contentsOf: [card1, card2])
        } else {
            // War!
            handleWar(card1: card1, card2: card2)
        }
    }
    
    func checkWinCondition() -> GameResult? {
        if state.player1Deck.isEmpty {
            return .player2Win
        } else if state.player2Deck.isEmpty {
            return .player1Win
        }
        return nil
    }
    
    func serialize() -> Data {
        try! JSONEncoder().encode(state)
    }
    
    static func deserialize(_ data: Data) -> WarGame? {
        guard let state = try? JSONDecoder().decode(WarState.self, from: data) else {
            return nil
        }
        var game = WarGame(
            player1ID: state.player1ID,
            player2ID: state.player2ID,
            wager: state.wager
        )
        game.state = state
        return game
    }
    
    private mutating func dealCards() {
        var deck = Card.standardDeck.shuffled()
        state.player1Deck = Array(deck.prefix(26))
        state.player2Deck = Array(deck.suffix(26))
    }
    
    private mutating func handleWar(card1: Card, card2: Card) {
        // Each player puts down 3 cards face down
        var warPile = [card1, card2]
        
        for _ in 0..<3 {
            if let c1 = state.player1Deck.first {
                state.player1Deck.removeFirst()
                warPile.append(c1)
            }
            if let c2 = state.player2Deck.first {
                state.player2Deck.removeFirst()
                warPile.append(c2)
            }
        }
        
        // Compare next cards
        guard let warCard1 = state.player1Deck.first,
              let warCard2 = state.player2Deck.first else {
            return
        }
        
        state.player1Deck.removeFirst()
        state.player2Deck.removeFirst()
        warPile.append(contentsOf: [warCard1, warCard2])
        
        if warCard1.rank > warCard2.rank {
            state.player1Deck.append(contentsOf: warPile)
        } else {
            state.player2Deck.append(contentsOf: warPile)
        }
    }
}

WarState.swift

struct WarState: Codable, Equatable {
    let player1ID: UUID
    let player2ID: UUID
    let wager: Int
    
    var player1Deck: [Card]
    var player2Deck: [Card]
    
    var isGameOver: Bool {
        player1Deck.isEmpty || player2Deck.isEmpty
    }
}

struct Card: Codable, Equatable {
    enum Rank: Int, Codable {
        case two = 2, three, four, five, six, seven
        case eight, nine, ten, jack, queen, king, ace
    }
    
    enum Suit: String, Codable {
        case hearts, diamonds, clubs, spades
    }
    
    let rank: Rank
    let suit: Suit
    
    static var standardDeck: [Card] {
        var deck: [Card] = []
        for suit in [Suit.hearts, .diamonds, .clubs, .spades] {
            for rank in [Rank.two, .three, .four, .five, .six, .seven,
                        .eight, .nine, .ten, .jack, .queen, .king, .ace] {
                deck.append(Card(rank: rank, suit: suit))
            }
        }
        return deck
    }
}

Testing Games (TDD REQUIRED)

class WarGameTests: XCTestCase {
    func testInitialDealSplitsEvenly() {
        let game = WarGame(
            player1ID: UUID(),
            player2ID: UUID(),
            wager: 100
        )
        
        XCTAssertEqual(game.state.player1Deck.count, 26)
        XCTAssertEqual(game.state.player2Deck.count, 26)
    }
    
    func testHigherCardWinsRound() {
        var game = WarGame(
            player1ID: UUID(),
            player2ID: UUID(),
            wager: 100
        )
        
        // Set up known cards
        game.state.player1Deck = [Card(rank: .ace, suit: .hearts)]
        game.state.player2Deck = [Card(rank: .two, suit: .clubs)]
        
        game.playRound()
        
        XCTAssertEqual(game.state.player1Deck.count, 2)
        XCTAssertEqual(game.state.player2Deck.count, 0)
    }
    
    func testStateSerialization() {
        let game = WarGame(
            player1ID: UUID(),
            player2ID: UUID(),
            wager: 100
        )
        
        let data = game.serialize()
        let deserializedGame = WarGame.deserialize(data)
        
        XCTAssertNotNil(deserializedGame)
        XCTAssertEqual(game.state, deserializedGame?.state)
    }
}

Currency Integration

// In ViewModel
class WarViewModel: ObservableObject {
    @Published var game: WarGame
    
    private let currencyManager = CurrencyManager.shared
    
    func startGame() async {
        // Lock wagers
        guard currencyManager.lockWager(game.state.wager, for: game.id) else {
            showError("Insufficient funds")
            return
        }
        
        // Auto-play until completion
        while game.checkWinCondition() == nil {
            game.playRound()
            try? await Task.sleep(nanoseconds: 500_000_000) // 0.5s delay
        }
        
        // Handle result
        if let result = game.checkWinCondition() {
            await handleGameEnd(result)
        }
    }
    
    func handleGameEnd(_ result: GameResult) async {
        let winner = result == .player1Win ? game.state.player1ID : game.state.player2ID
        
        await currencyManager.processGameResult(
            gameID: game.id,
            winner: winner,
            wager: game.state.wager
        )
    }
}

Performance Guidelines

Memory Budget

  • Per game: <10MB
  • Profile with Instruments
  • Release assets when done
  • Use texture atlases

Frame Rate

  • Target: 60fps
  • Physics: 60Hz update
  • Rendering: Metal when possible
  • Async work off main thread

State Size

  • Target: <50KB
  • Minimize state data
  • Don't store derived values
  • Compress if needed

When This Skill Activates

  • Implementing any of the 6 games
  • Working with game logic
  • Creating SpriteKit/SceneKit scenes
  • Writing game tests
  • Integrating with currency system

References

  • See docs/GAMES.md for detailed game rules
  • GameProtocol in Models/Game.swift
  • Example games in Games/ directory

Score

Total Score

55/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

0/5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon