| 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()
}
}
func canWager(_ amount: Int) -> Bool {
return amount >= CurrencyConstants.minimumWager && amount <= availableBalance
}
var availableBalance: {
locked lockedFunds.values.reduce(, )
balance locked
}
( : , : ) -> {
canWager(amount) { }
queue.sync {
lockedFunds[gameID] amount
}
}
( : ) {
queue.sync {
lockedFunds.removeValue(forKey: gameID)
}
}
( : , : ) {
queue.async { [ ]
{ }
.main.async {
.balance amount
.recordTransaction(amount, reason: reason)
.saveBalance()
}
}
}
( : , : ) {
queue.async { [ ]
{ }
.main.async {
.balance amount
.recordTransaction(amount, reason: reason)
.saveBalance()
}
}
}
(: , : , : ) {
isLocalWinner (winner .current.identifierForVendor)
queue.sync {
lockedFunds.removeValue(forKey: gameID)
}
isLocalWinner {
addCoins(wager , reason: .gameWin(gameID: gameID))
} {
recordTransaction(wager, reason: .gameLoss(gameID: gameID))
}
}
() -> {
lastClaim .standard.double(forKey: )
now ().timeIntervalSince1970
now lastClaim {
}
addCoins(.dailyReward, reason: .dailyReward)
.standard.set(now, forKey: )
}
() {
.standard.set(balance, forKey: )
}
( : , : ) {
transaction (
id: (),
date: (),
amount: amount,
reason: reason
)
transactions.insert(transaction, at: )
transactions.count {
transactions (transactions.prefix())
}
saveTransactions()
}
() {
data ().encode(transactions) {
.standard.set(data, forKey: )
}
}
}
Transaction Types
struct Transaction: Identifiable, Codable {
let id: UUID
let date: Date
let amount: Int
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
}
}
}
Wagering Rules
func validateWager(_ amount: Int, opponentBalance: Int) -> Bool {
guard amount >= CurrencyConstants.minimumWager else {
return false
}
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 {
guard state.isValid() else {
throw CurrencyError.tamperedState
}
guard state.player1Balance >= 0 && state.player2Balance >= 0 else {
throw CurrencyError.negativeBalance
}
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 {
let maxBet = currentGameWager / 2
guard bet.amount <= maxBet else {
return false
}
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 {
addCoins(bet.amount * 2, reason: .spectatorWin(gameID: gameID))
} else {
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
manager.balance = 1000
}
func testWagerValidation() {
XCTAssertTrue(manager.canWager(10))
XCTAssertTrue(manager.canWager(100))
XCTAssertFalse(manager.canWager(5))
XCTAssertFalse(manager.canWager(2000))
}
func testWagerLocking() {
let gameID = UUID()
XCTAssertTrue(manager.lockWager(100, for: gameID))
XCTAssertEqual(manager.availableBalance, 900)
XCTAssertFalse(manager.lockWager(950, for: UUID()))
}
func testGameResultProcessing() async {
let gameID = UUID()
let winner = ()
wager
manager.lockWager(wager, for: gameID)
manager.processGameResult(
gameID: gameID,
winner: winner,
wager: wager
)
(manager.balance, )
}
() {
state (
player1Balance: ,
player2Balance: ,
wager: ,
timestamp: ()
)
hash state.computeHash()
(hash.isEmpty)
(hash.count, )
}
}
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)