Claude Code Plugins

Community-maintained marketplace

Feedback

Virtual Currency System (Prop Coins)

@nategarelik/prop-pigeon
0
0

Rules, patterns, and anti-cheating for virtual currency

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name Virtual Currency System (Prop Coins)
description Rules, patterns, and anti-cheating for virtual currency
when_to_use Working with Prop Coins, wagers, transactions, or balance
version 1.0.0
languages swift

Virtual Currency System

CRITICAL: Virtual Only

THIS IS NOT REAL MONEY

  • Always call it "Prop Coins"
  • Never "cash", "money", or "dollars"
  • No conversion to real currency
  • "For entertainment only" disclaimers
  • No payment processing
  • App Store compliant (no gambling)

Starting Balance

struct CurrencyConstants {
    static let startingBalance = 1000
    static let dailyReward = 100
    static let minimumWager = 10
}

CurrencyManager Implementation

@MainActor
class CurrencyManager: ObservableObject {
    static let shared = CurrencyManager()
    
    @Published private(set) var balance: Int
    @Published private(set) var transactions: [Transaction] = []
    
    private var lockedFunds: [UUID: Int] = [:]
    private let queue = DispatchQueue(label: "currency.serial")
    
    private init() {
        self.balance = UserDefaults.standard.integer(forKey: "balance")
        if balance == 0 {
            balance = CurrencyConstants.startingBalance
            saveBalance()
        }
    }
    
    // MARK: - Wager Management
    
    func canWager(_ amount: Int) -> Bool {
        return amount >= CurrencyConstants.minimumWager && amount <= availableBalance
    }
    
    var availableBalance: Int {
        let locked = lockedFunds.values.reduce(0, +)
        return balance - locked
    }
    
    func lockWager(_ amount: Int, for gameID: UUID) -> Bool {
        guard canWager(amount) else { return false }
        
        queue.sync {
            lockedFunds[gameID] = amount
        }
        
        return true
    }
    
    func unlockWager(for gameID: UUID) {
        queue.sync {
            lockedFunds.removeValue(forKey: gameID)
        }
    }
    
    // MARK: - Transactions
    
    func addCoins(_ amount: Int, reason: TransactionReason) {
        queue.async { [weak self] in
            guard let self = self else { return }
            
            DispatchQueue.main.async {
                self.balance += amount
                self.recordTransaction(amount, reason: reason)
                self.saveBalance()
            }
        }
    }
    
    func removeCoins(_ amount: Int, reason: TransactionReason) {
        queue.async { [weak self] in
            guard let self = self else { return }
            
            DispatchQueue.main.async {
                self.balance -= amount
                self.recordTransaction(-amount, reason: reason)
                self.saveBalance()
            }
        }
    }
    
    func processGameResult(gameID: UUID, winner: UUID, wager: Int) async {
        let isLocalWinner = (winner == UIDevice.current.identifierForVendor)
        
        queue.sync {
            lockedFunds.removeValue(forKey: gameID)
        }
        
        if isLocalWinner {
            addCoins(wager * 2, reason: .gameWin(gameID: gameID))
        } else {
            // Wager already deducted when locked
            recordTransaction(-wager, reason: .gameLoss(gameID: gameID))
        }
    }
    
    // MARK: - Daily Rewards
    
    func claimDailyReward() -> Bool {
        let lastClaim = UserDefaults.standard.double(forKey: "lastDailyReward")
        let now = Date().timeIntervalSince1970
        
        // 24 hours = 86400 seconds
        guard now - lastClaim > 86400 else {
            return false
        }
        
        addCoins(CurrencyConstants.dailyReward, reason: .dailyReward)
        UserDefaults.standard.set(now, forKey: "lastDailyReward")
        
        return true
    }
    
    // MARK: - Persistence
    
    private func saveBalance() {
        UserDefaults.standard.set(balance, forKey: "balance")
    }
    
    private func recordTransaction(_ amount: Int, reason: TransactionReason) {
        let transaction = Transaction(
            id: UUID(),
            date: Date(),
            amount: amount,
            reason: reason
        )
        
        transactions.insert(transaction, at: 0)
        
        // Keep last 100 transactions
        if transactions.count > 100 {
            transactions = Array(transactions.prefix(100))
        }
        
        saveTransactions()
    }
    
    private func saveTransactions() {
        if let data = try? JSONEncoder().encode(transactions) {
            UserDefaults.standard.set(data, forKey: "transactions")
        }
    }
}

Transaction Types

struct Transaction: Identifiable, Codable {
    let id: UUID
    let date: Date
    let amount: Int // Positive = gain, negative = loss
    let reason: TransactionReason
}

enum TransactionReason: Codable {
    case dailyReward
    case gameWin(gameID: UUID)
    case gameLoss(gameID: UUID)
    case achievement(name: String)
    case spectatorWin(gameID: UUID)
    case spectatorLoss(gameID: UUID)
    
    var displayName: String {
        switch self {
        case .dailyReward: return "Daily Reward"
        case .gameWin: return "Game Win"
        case .gameLoss: return "Game Loss"
        case .achievement(let name): return "Achievement: \(name)"
        case .spectatorWin: return "Spectator Win"
        case .spectatorLoss: return "Spectator Loss"
        }
    }
}

Wagering Rules

func validateWager(_ amount: Int, opponentBalance: Int) -> Bool {
    // Minimum
    guard amount >= CurrencyConstants.minimumWager else {
        return false
    }
    
    // Maximum = 10% of smaller balance
    let maxWager = min(availableBalance, opponentBalance) / 10
    guard amount <= maxWager else {
        return false
    }
    
    return true
}

Anti-Cheating

State Hashing

extension GameState {
    func computeHash() -> String {
        let data = "\(player1Balance)\(player2Balance)\(wager)\(timestamp)"
        return SHA256.hash(data: data.data(using: .utf8)!)
            .compactMap { String(format: "%02x", $0) }
            .joined()
    }
    
    func isValid() -> Bool {
        return stateHash == computeHash()
    }
}

State Validation

func validateGameState(_ state: GameState) throws {
    // Check hash
    guard state.isValid() else {
        throw CurrencyError.tamperedState
    }
    
    // Check balances
    guard state.player1Balance >= 0 && state.player2Balance >= 0 else {
        throw CurrencyError.negativeBalance
    }
    
    // Check wager
    guard state.wager >= CurrencyConstants.minimumWager else {
        throw CurrencyError.invalidWager
    }
}

Spectator Betting

struct SpectatorBet {
    let spectatorID: UUID
    let gameID: UUID
    let predictedWinner: UUID
    let amount: Int
}

func placeSpectatorBet(_ bet: SpectatorBet) -> Bool {
    // Max 50% of player wager
    let maxBet = currentGameWager / 2
    guard bet.amount <= maxBet else {
        return false
    }
    
    // Lock funds
    return lockWager(bet.amount, for: bet.gameID)
}

func processSpectatorBets(gameID: UUID, winner: UUID) {
    let bets = spectatorBets[gameID] ?? []
    
    for bet in bets {
        if bet.predictedWinner == winner {
            // Correct prediction: 2x payout
            addCoins(bet.amount * 2, reason: .spectatorWin(gameID: gameID))
        } else {
            // Wrong prediction: lose bet
            recordTransaction(-bet.amount, reason: .spectatorLoss(gameID: gameID))
        }
        
        unlockWager(for: gameID)
    }
}

Testing Currency System

class CurrencyManagerTests: XCTestCase {
    var manager: CurrencyManager!
    
    override func setUp() {
        super.setUp()
        manager = CurrencyManager.shared
        // Reset to known state
        manager.balance = 1000
    }
    
    func testWagerValidation() {
        XCTAssertTrue(manager.canWager(10))
        XCTAssertTrue(manager.canWager(100))
        XCTAssertFalse(manager.canWager(5)) // Below minimum
        XCTAssertFalse(manager.canWager(2000)) // Above balance
    }
    
    func testWagerLocking() {
        let gameID = UUID()
        
        XCTAssertTrue(manager.lockWager(100, for: gameID))
        XCTAssertEqual(manager.availableBalance, 900)
        
        // Can't lock more than available
        XCTAssertFalse(manager.lockWager(950, for: UUID()))
    }
    
    func testGameResultProcessing() async {
        let gameID = UUID()
        let winner = UUID()
        let wager = 100
        
        manager.lockWager(wager, for: gameID)
        
        await manager.processGameResult(
            gameID: gameID,
            winner: winner,
            wager: wager
        )
        
        XCTAssertEqual(manager.balance, 1100) // Won 100
    }
    
    func testStateHashing() {
        let state = GameState(
            player1Balance: 1000,
            player2Balance: 1000,
            wager: 100,
            timestamp: Date()
        )
        
        let hash = state.computeHash()
        XCTAssertFalse(hash.isEmpty)
        XCTAssertEqual(hash.count, 64) // SHA256
    }
}

App Store Compliance

In Info.plist

<key>NSHumanReadableCopyright</key>
<string>Virtual currency for entertainment only. No real money wagering.</string>

In UI

struct DisclaimerView: View {
    var body: some View {
        VStack {
            Text("Virtual Currency Only")
                .font(.headline)
            Text("Prop Coins have no real-world value and cannot be exchanged for money.")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
    }
}

When This Skill Activates

  • Working with CurrencyManager
  • Implementing wagers
  • Processing game results
  • Adding transactions
  • Testing currency logic
  • Validating balances

References

  • CurrencyManager.swift
  • docs/CURRENCY.md
  • Apple App Store Review Guidelines (3.1.1)