Source Code

;; ---------------------------------------------------------
;; RockPaperStacks Game Contract
;; ---------------------------------------------------------

;; Constants
(define-constant err-game-not-found (err u300))
(define-constant err-not-participant (err u301))

(define-constant err-already-committed (err u303))
(define-constant err-not-committed (err u304))
(define-constant err-already-revealed (err u305))
(define-constant err-hash-mismatch (err u306))
(define-constant err-invalid-move (err u307))
(define-constant err-game-not-open (err u308))
(define-constant err-timeout-not-reached (err u309))
(define-constant err-wrong-opponent (err u310))
(define-constant err-game-complete (err u311))
(define-constant err-not-creator (err u312))
(define-constant err-reveal-before-both-commit (err u313))

(define-constant timeout-join u500)
(define-constant timeout-commit u150)
(define-constant timeout-reveal u100)
(define-constant fee-percent u2)

;; Data Vars
(define-constant fee-address tx-sender)
(define-data-var game-counter uint u0)

;; Game maps
(define-map games uint {
    player1: principal,
    player2: (optional principal),
    wager: uint,
    status: (string-ascii 20),
    winner: (optional principal),
    created-at: uint,
    updated-at: uint,
    round: uint,
    series-wins-p1: uint,
    series-wins-p2: uint,
    mode: (string-ascii 8)
})

(define-map commitments { game-id: uint, round: uint, player: principal } {
    move-hash: (buff 32),
    revealed: bool,
    move: (optional uint),
    salt: (optional (buff 32))
})

(define-map player-stats principal {
    wins: uint, losses: uint, draws: uint, total-games: uint,
    stx-won: uint, stx-lost: uint,
    current-streak: uint, best-streak: uint
})

(define-data-var open-games-var (list 50 uint) (list))

;; ---------------------------------------------------------
;; Utility Testing Functions (Counter Logic)
;; ---------------------------------------------------------
(define-data-var test-counter uint u0)

(define-public (increment)
    (begin
        (var-set test-counter (+ (var-get test-counter) u1))
        (ok (var-get test-counter))
    )
)

(define-public (decrement)
    (begin
        (var-set test-counter (- (var-get test-counter) u1))
        (ok (var-get test-counter))
    )
)

(define-read-only (get-counter)
    (var-get test-counter)
)

;; ---------------------------------------------------------
;; Core Game Logic
;; ---------------------------------------------------------
(define-public (create-game (wager uint) (opponent (optional principal)) (mode (string-ascii 8)))
    (let (
        (new-id (+ (var-get game-counter) u1))
    )
        (let
            (
                (wager-to-check wager)
                (opponent-to-check opponent)
            )
            (asserts! (or (is-eq mode "single") (is-eq mode "best-of-3") (is-eq mode "best-of-5")) (err u400))
            (try! (stx-transfer? wager tx-sender (as-contract tx-sender)))
            (map-set games new-id {
                player1: tx-sender,
                player2: opponent-to-check,
                wager: wager-to-check,
                status: "open",
                winner: none,
                created-at: block-height,
                updated-at: block-height,
                round: u1,
                series-wins-p1: u0,
                series-wins-p2: u0,
                mode: mode
            })
            (var-set game-counter new-id)
            (if (is-none opponent-to-check)
                (match (as-max-len? (append (var-get open-games-var) new-id) u50)
                    success (var-set open-games-var success)
                    false
                )
                false
            )
            (ok new-id)
        )
    )
)

(define-public (join-game (game-id uint))
    (let (
        (game (unwrap! (map-get? games game-id) err-game-not-found))
    )
        (begin
            (asserts! (is-eq (get status game) "open") err-game-not-open)
            (asserts! (or (is-none (get player2 game)) (is-eq (some tx-sender) (get player2 game))) err-wrong-opponent)
            (try! (stx-transfer? (get wager game) tx-sender (as-contract tx-sender)))
            (map-set games game-id (merge game {
                player2: (some tx-sender),
                status: "joined",
                updated-at: block-height
            }))
            (ok true)
        )
    )
)

(define-public (cancel-game (game-id uint))
    (let (
        (game (unwrap! (map-get? games game-id) err-game-not-found))
    )
        (begin
            (asserts! (is-eq (get status game) "open") err-game-complete)
            (asserts! (is-eq tx-sender (get player1 game)) err-not-creator)
            (try! (as-contract (stx-transfer? (get wager game) tx-sender (get player1 game))))
            (map-set games game-id (merge game {
                status: "cancelled",
                updated-at: block-height
            }))
            (ok true)
        )
    )
)

(define-public (commit-move (game-id uint) (move-hash (buff 32)))
    (let (
        (game (unwrap! (map-get? games game-id) err-game-not-found))
        (round (get round game))
    )
        (let
            (
                (hash-to-check move-hash)
            )
            (asserts! (or (is-eq (get status game) "joined") (is-eq (get status game) "committed")) err-game-complete)
            (asserts! (or (is-eq tx-sender (get player1 game)) (is-eq (some tx-sender) (get player2 game))) err-not-participant)
            (asserts! (is-none (map-get? commitments { game-id: game-id, round: round, player: tx-sender })) err-already-committed)
            
            (map-set commitments { game-id: game-id, round: round, player: tx-sender } {
                move-hash: hash-to-check,
                revealed: false,
                move: none,
                salt: none
            })
            
            (let (
                (opponent (if (is-eq tx-sender (get player1 game)) (unwrap-panic (get player2 game)) (get player1 game)))
                (opponent-commit (map-get? commitments { game-id: game-id, round: round, player: opponent }))
            )
                (if (is-some opponent-commit)
                    (map-set games game-id (merge game { status: "committed", updated-at: block-height }))
                    (map-set games game-id (merge game { status: "joined", updated-at: block-height }))
                )
            )
            (ok true)
        )
    )
)

(define-private (calculate-fee (wager uint))
    (/ (* wager fee-percent) u100)
)

(define-private (update-stats (player principal) (is-win bool) (is-draw bool) (s-won uint) (s-lost uint))
    (let (
        (stats (default-to { wins: u0, losses: u0, draws: u0, total-games: u0, stx-won: u0, stx-lost: u0, current-streak: u0, best-streak: u0 } (map-get? player-stats player)))
        (new-streak (if is-win (+ (get current-streak stats) u1) u0))
        (best-streak (if (> new-streak (get best-streak stats)) new-streak (get best-streak stats)))
    )
        (map-set player-stats player {
            wins: (+ (get wins stats) (if is-win u1 u0)),
            losses: (+ (get losses stats) (if (and (not is-win) (not is-draw)) u1 u0)),
            draws: (+ (get draws stats) (if is-draw u1 u0)),
            total-games: (+ (get total-games stats) u1),
            stx-won: (+ (get stx-won stats) s-won),
            stx-lost: (+ (get stx-lost stats) s-lost),
            current-streak: new-streak,
            best-streak: best-streak
        })
    )
)

(define-private (resolve-round (game-id uint) (game { player1: principal, player2: (optional principal), wager: uint, status: (string-ascii 20), winner: (optional principal), created-at: uint, updated-at: uint, round: uint, series-wins-p1: uint, series-wins-p2: uint, mode: (string-ascii 8) }))
    (let (
        (round (get round game))
        (p1 (get player1 game))
        (p2 (unwrap-panic (get player2 game)))
        (m1 (unwrap-panic (get move (unwrap-panic (map-get? commitments { game-id: game-id, round: round, player: p1 })))))
        (m2 (unwrap-panic (get move (unwrap-panic (map-get? commitments { game-id: game-id, round: round, player: p2 })))))
        (p1-wins (or (and (is-eq m1 u1) (is-eq m2 u3)) (and (is-eq m1 u2) (is-eq m2 u1)) (and (is-eq m1 u3) (is-eq m2 u2))))
        (draw (is-eq m1 m2))
        (p2-wins (and (not p1-wins) (not draw)))
        (wins-req (if (is-eq (get mode game) "single") u1 (if (is-eq (get mode game) "best-of-3") u2 u3)))
        (new-p1-wins (+ (get series-wins-p1 game) (if p1-wins u1 u0)))
        (new-p2-wins (+ (get series-wins-p2 game) (if p2-wins u1 u0)))
        (series-over-by-wins (or (>= new-p1-wins wins-req) (>= new-p2-wins wins-req)))
        (is-single (is-eq (get mode game) "single"))
        (series-over (or series-over-by-wins (and is-single draw)))
    )
        (if series-over
            (let (
                (wager (get wager game))
                (fee (calculate-fee wager))
                (payout (- (* wager u2) fee))
            )
                (if (and is-single draw)
                    (begin
                        (try! (as-contract (stx-transfer? wager tx-sender p1)))
                        (try! (as-contract (stx-transfer? wager tx-sender p2)))
                        (update-stats p1 false true u0 u0)
                        (update-stats p2 false true u0 u0)
                        (map-set games game-id (merge game { status: "finished", round: round, updated-at: block-height }))
                        (ok true)
                    )
                    (let (
                        (winner (if (>= new-p1-wins wins-req) p1 p2))
                        (loser (if (>= new-p1-wins wins-req) p2 p1))
                    )
                        (begin
                            (try! (as-contract (stx-transfer? payout tx-sender winner)))
                            (if (> fee u0) (try! (as-contract (stx-transfer? fee tx-sender fee-address))) true)
                            (update-stats winner true false payout wager)
                            (update-stats loser false false u0 wager)
                            (try! (as-contract (contract-call? .rps-token mint u10000000 winner)))
                            (map-set games game-id (merge game { status: "finished", winner: (some winner), series-wins-p1: new-p1-wins, series-wins-p2: new-p2-wins, updated-at: block-height }))
                            (ok true)
                        )
                    )
                )
            )
            (begin
                (map-set games game-id (merge game {
                    status: "joined",
                    round: (+ round u1),
                    series-wins-p1: new-p1-wins,
                    series-wins-p2: new-p2-wins,
                    updated-at: block-height
                }))
                (ok true)
            )
        )
    )
)

(define-public (reveal-move (game-id uint) (move uint) (salt (buff 32)))
    (let (
        (game (unwrap! (map-get? games game-id) err-game-not-found))
        (round (get round game))
        (commit (unwrap! (map-get? commitments { game-id: game-id, round: round, player: tx-sender }) err-not-committed))
    )
        (begin
            (asserts! (is-eq (get status game) "committed") err-reveal-before-both-commit)
            (asserts! (not (get revealed commit)) err-already-revealed)
            (asserts! (or (is-eq move u1) (is-eq move u2) (is-eq move u3)) err-invalid-move)
            
            ;; verify hash
            (asserts! (is-eq (get move-hash commit) (sha256 (unwrap-panic (to-consensus-buff? { move: move, salt: salt })))) err-hash-mismatch)
            
            (map-set commitments { game-id: game-id, round: round, player: tx-sender } (merge commit {
                revealed: true,
                move: (some move),
                salt: (some salt)
            }))
            
            (let (
                (opponent (if (is-eq tx-sender (get player1 game)) (unwrap-panic (get player2 game)) (get player1 game)))
                (opponent-commit (unwrap-panic (map-get? commitments { game-id: game-id, round: round, player: opponent })))
            )
                (if (get revealed opponent-commit)
                    (resolve-round game-id (merge game { status: "revealed", updated-at: block-height }))
                    (begin
                        (map-set games game-id (merge game { updated-at: block-height }))
                        (ok true)
                    )
                )
            )
        )
    )
)

(define-public (claim-timeout (game-id uint))
    (let (
        (game (unwrap! (map-get? games game-id) err-game-not-found))
        (status (get status game))
        (updated (get updated-at game))
        (round (get round game))
    )
        (if (is-eq status "open")
            (begin
                (asserts! (is-eq tx-sender (get player1 game)) err-not-participant)
                (asserts! (>= block-height (+ updated timeout-join)) err-timeout-not-reached)
                (try! (as-contract (stx-transfer? (get wager game) tx-sender (get player1 game))))
                (map-set games game-id (merge game { status: "cancelled", updated-at: block-height }))
                (ok true)
            )
            (if (is-eq status "joined")
                (let (
                    (player-commit-opt (map-get? commitments { game-id: game-id, round: round, player: tx-sender }))
                )
                    (begin
                        (asserts! (is-some player-commit-opt) err-not-committed)
                        (asserts! (>= block-height (+ updated timeout-commit)) err-timeout-not-reached)
                        (let (
                            (wager (get wager game))
                            (payout (* wager u2))
                        )
                            (begin
                                (try! (as-contract (stx-transfer? payout tx-sender tx-sender)))
                                (update-stats tx-sender true false payout wager)
                                (map-set games game-id (merge game { status: "finished", winner: (some tx-sender), updated-at: block-height }))
                                (ok true)
                            )
                        )
                    )
                )
                (if (is-eq status "committed")
                    (let (
                        (player-commit-opt (map-get? commitments { game-id: game-id, round: round, player: tx-sender }))
                    )
                        (begin
                            (asserts! (is-some player-commit-opt) err-not-committed)
                            (let (
                                (player-commit (unwrap-panic player-commit-opt))
                            )
                                (begin
                                    (asserts! (get revealed player-commit) err-not-committed)
                                    (asserts! (>= block-height (+ updated timeout-reveal)) err-timeout-not-reached)
                                    (let (
                                        (wager (get wager game))
                                        (payout (* wager u2))
                                    )
                                        (begin
                                            (try! (as-contract (stx-transfer? payout tx-sender tx-sender)))
                                            (update-stats tx-sender true false payout wager)
                                            (map-set games game-id (merge game { status: "finished", winner: (some tx-sender), updated-at: block-height }))
                                            (ok true)
                                        )
                                    )
                                )
                            )
                        )
                    )
                    err-game-complete
                )
            )
        )
    )
)

;; ---------------------------------------------------------
;; Read-Only Functions
;; ---------------------------------------------------------

(define-read-only (get-game (game-id uint))
    (map-get? games game-id)
)

(define-read-only (get-commitment (game-id uint) (player principal))
    (map-get? commitments { game-id: game-id, round: (unwrap-panic (get round (map-get? games game-id))), player: player })
)

(define-read-only (get-player-stats (player principal))
    (map-get? player-stats player)
)

(define-read-only (get-open-games)
    (var-get open-games-var)
)

(define-read-only (get-game-count)
    (var-get game-counter)
)

(define-read-only (both-committed (game-id uint))
    (let (
        (game (unwrap! (map-get? games game-id) false))
        (round (get round game))
        (p1 (get player1 game))
        (p2-opt (get player2 game))
    )
        (if (is-none p2-opt)
            false
            (let (
                (p2 (unwrap-panic p2-opt))
                (c1 (is-some (map-get? commitments { game-id: game-id, round: round, player: p1 })))
                (c2 (is-some (map-get? commitments { game-id: game-id, round: round, player: p2 })))
            )
                (and c1 c2)
            )
        )
    )
)

(define-read-only (both-revealed (game-id uint))
    (let (
        (game (unwrap! (map-get? games game-id) false))
        (round (get round game))
        (p1 (get player1 game))
        (p2-opt (get player2 game))
    )
        (if (is-none p2-opt)
            false
            (let (
                (p2 (unwrap-panic p2-opt))
                (c1 (map-get? commitments { game-id: game-id, round: round, player: p1 }))
                (c2 (map-get? commitments { game-id: game-id, round: round, player: p2 }))
            )
                (and 
                    (is-some c1) (get revealed (unwrap-panic c1))
                    (is-some c2) (get revealed (unwrap-panic c2))
                )
            )
        )
    )
)

(define-read-only (get-fee-rate)
    fee-percent
)

(define-read-only (hash-move-wrapper (move uint) (salt (buff 32)))
    ;; Utility function explicitly for testing or easy generation of the exact commit hash format
    (sha256 (unwrap-panic (to-consensus-buff? { move: move, salt: salt })))
)

Functions (20)

FunctionAccessArgs
incrementpublic
decrementpublic
get-counterread-only
create-gamepublicwager: uint, opponent: (optional principal
join-gamepublicgame-id: uint
cancel-gamepublicgame-id: uint
commit-movepublicgame-id: uint, move-hash: (buff 32
calculate-feeprivatewager: uint
update-statsprivateplayer: principal, is-win: bool, is-draw: bool, s-won: uint, s-lost: uint
reveal-movepublicgame-id: uint, move: uint, salt: (buff 32
claim-timeoutpublicgame-id: uint
get-gameread-onlygame-id: uint
get-commitmentread-onlygame-id: uint, player: principal
get-player-statsread-onlyplayer: principal
get-open-gamesread-only
get-game-countread-only
both-committedread-onlygame-id: uint
both-revealedread-onlygame-id: uint
get-fee-rateread-only
hash-move-wrapperread-onlymove: uint, salt: (buff 32