Source Code

;; -------------------------------------------------------
;; Constants
;; -------------------------------------------------------

(define-constant ERR-GAME-NOT-FOUND     (err u100))
(define-constant ERR-GAME-FULL          (err u101))
(define-constant ERR-NOT-YOUR-GAME      (err u102))
(define-constant ERR-ALREADY-REVEALED   (err u103))
(define-constant ERR-BAD-COMMIT         (err u104))
(define-constant ERR-INVALID-MOVE       (err u105))
(define-constant ERR-NOT-READY          (err u106))
(define-constant ERR-SAME-PLAYER        (err u107))
(define-constant ERR-GAME-OVER          (err u108))
(define-constant ERR-NOT-EXPIRED        (err u109))

;; Moves encoded as uint
;; 0 = None (not yet revealed)
;; 1 = Rock
;; 2 = Paper
;; 3 = Scissors
(define-constant MOVE-NONE      u0)
(define-constant MOVE-ROCK      u1)
(define-constant MOVE-PAPER     u2)
(define-constant MOVE-SCISSORS  u3)

;; Game status
(define-constant STATUS-WAITING  u0)  ;; waiting for p2 to join
(define-constant STATUS-ACTIVE   u1)  ;; both joined, waiting for reveals
(define-constant STATUS-DONE     u2)  ;; game finished

;; Reveal timeout -- p2 has 144 blocks (~24hrs on Stacks) to reveal
(define-constant REVEAL-TIMEOUT u144)

;; -------------------------------------------------------
;; Data Maps
;; -------------------------------------------------------

(define-map games
  { game-id: uint }
  {
    p1:         principal,
    p2:         (optional principal),
    p1-commit:  (buff 32),
    p2-commit:  (optional (buff 32)),
    p1-move:    uint,
    p2-move:    uint,
    status:     uint,
    winner:     (optional principal),
    created-at: uint    ;; block height
  }
)

;; Track wins and draws per player
(define-map player-stats
  { player: principal }
  { wins: uint, losses: uint, draws: uint, games-played: uint }
)

;; -------------------------------------------------------
;; Data Variables
;; -------------------------------------------------------

(define-data-var game-count uint u0)

;; -------------------------------------------------------
;; Private Helpers
;; -------------------------------------------------------

;; Determine winner: returns 0=draw, 1=p1 wins, 2=p2 wins
(define-private (get-winner (p1-move uint) (p2-move uint))
  (if (is-eq p1-move p2-move)
    u0  ;; draw
    (if (or
          (and (is-eq p1-move MOVE-ROCK)     (is-eq p2-move MOVE-SCISSORS))
          (and (is-eq p1-move MOVE-PAPER)    (is-eq p2-move MOVE-ROCK))
          (and (is-eq p1-move MOVE-SCISSORS) (is-eq p2-move MOVE-PAPER))
        )
      u1  ;; p1 wins
      u2  ;; p2 wins
    )
  )
)

;; Validate move is 1, 2, or 3
(define-private (valid-move (move uint))
  (and (>= move u1) (<= move u3))
)

;; Get or default player stats
(define-private (get-stats (player principal))
  (default-to
    { wins: u0, losses: u0, draws: u0, games-played: u0 }
    (map-get? player-stats { player: player })
  )
)

;; Update stats for both players after a game
(define-private (update-stats (p1 principal) (p2 principal) (result uint))
  (let (
    (s1 (get-stats p1))
    (s2 (get-stats p2))
  )
    (if (is-eq result u0)
      ;; draw
      (begin
        (map-set player-stats { player: p1 }
          (merge s1 { draws: (+ (get draws s1) u1), games-played: (+ (get games-played s1) u1) }))
        (map-set player-stats { player: p2 }
          (merge s2 { draws: (+ (get draws s2) u1), games-played: (+ (get games-played s2) u1) }))
      )
      (if (is-eq result u1)
        ;; p1 wins
        (begin
          (map-set player-stats { player: p1 }
            (merge s1 { wins: (+ (get wins s1) u1), games-played: (+ (get games-played s1) u1) }))
          (map-set player-stats { player: p2 }
            (merge s2 { losses: (+ (get losses s2) u1), games-played: (+ (get games-played s2) u1) }))
        )
        ;; p2 wins
        (begin
          (map-set player-stats { player: p1 }
            (merge s1 { losses: (+ (get losses s1) u1), games-played: (+ (get games-played s1) u1) }))
          (map-set player-stats { player: p2 }
            (merge s2 { wins: (+ (get wins s2) u1), games-played: (+ (get games-played s2) u1) }))
        )
      )
    )
  )
)

;; -------------------------------------------------------
;; Public Functions
;; -------------------------------------------------------

;; Step 1: Player 1 creates a game with a commit hash
;; Commit = sha256(move + salt)
;; e.g. sha256(concat(0x01, your-random-32-bytes))
(define-public (create-game (commit (buff 32)))
  (let (
    (id (var-get game-count))
  )
    (map-set games { game-id: id }
      {
        p1:         tx-sender,
        p2:         none,
        p1-commit:  commit,
        p2-commit:  none,
        p1-move:    MOVE-NONE,
        p2-move:    MOVE-NONE,
        status:     STATUS-WAITING,
        winner:     none,
        created-at: block-height
      }
    )
    (var-set game-count (+ id u1))
    (ok id)
  )
)

;; Step 2: Player 2 joins and also submits a commit
(define-public (join-game (game-id uint) (commit (buff 32)))
  (let (
    (game (unwrap! (map-get? games { game-id: game-id }) ERR-GAME-NOT-FOUND))
  )
    (asserts! (is-eq (get status game) STATUS-WAITING)  ERR-GAME-FULL)
    (asserts! (not (is-eq tx-sender (get p1 game)))     ERR-SAME-PLAYER)

    (map-set games { game-id: game-id }
      (merge game {
        p2:        (some tx-sender),
        p2-commit: (some commit),
        status:    STATUS-ACTIVE
      })
    )
    (ok true)
  )
)

;; Step 3: Each player reveals their move and salt
;; Move: u1=Rock, u2=Paper, u3=Scissors
(define-public (reveal (game-id uint) (move uint) (salt (buff 32)))
  (let (
    (game  (unwrap! (map-get? games { game-id: game-id }) ERR-GAME-NOT-FOUND))
    (p1    (get p1 game))
    (p2    (unwrap! (get p2 game) ERR-NOT-READY))
    (is-p1 (is-eq tx-sender p1))
    (is-p2 (is-eq tx-sender p2))
    (commit (sha256 (concat (if (is-eq move u1) 0x01 (if (is-eq move u2) 0x02 0x03)) salt)))
  )
    (asserts! (is-eq (get status game) STATUS-ACTIVE) ERR-GAME-OVER)
    (asserts! (valid-move move)                        ERR-INVALID-MOVE)
    (asserts! (or is-p1 is-p2)                        ERR-NOT-YOUR-GAME)

    ;; Verify commit matches
    (if is-p1
      (begin
        (asserts! (is-eq (get p1-move game) MOVE-NONE) ERR-ALREADY-REVEALED)
        (asserts! (is-eq commit (get p1-commit game))  ERR-BAD-COMMIT)
        (map-set games { game-id: game-id } (merge game { p1-move: move }))
      )
      (begin
        (asserts! (is-eq (get p2-move game) MOVE-NONE) ERR-ALREADY-REVEALED)
        (asserts! (is-eq commit (unwrap! (get p2-commit game) ERR-BAD-COMMIT)) ERR-BAD-COMMIT)
        (map-set games { game-id: game-id } (merge game { p2-move: move }))
      )
    )

    ;; Re-fetch updated game and check if both revealed
    (let (
      (updated (unwrap! (map-get? games { game-id: game-id }) ERR-GAME-NOT-FOUND))
      (p1m (get p1-move updated))
      (p2m (get p2-move updated))
    )
      (if (and (not (is-eq p1m MOVE-NONE)) (not (is-eq p2m MOVE-NONE)))
        ;; Both revealed -- resolve
        (let (
          (result (get-winner p1m p2m))
          (winner-opt (if (is-eq result u0)
                       none
                       (some (if (is-eq result u1) p1 p2))))
        )
          (update-stats p1 p2 result)
          (map-set games { game-id: game-id }
            (merge updated { status: STATUS-DONE, winner: winner-opt })
          )
          (ok result)
        )
        (ok u99) ;; u99 = waiting for other player to reveal
      )
    )
  )
)

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

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

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

(define-read-only (get-total-games)
  (var-get game-count)
)

;; Returns the move name as a string for UI display
(define-read-only (move-name (move uint))
  (if (is-eq move MOVE-ROCK)     "Rock"
  (if (is-eq move MOVE-PAPER)    "Paper"
  (if (is-eq move MOVE-SCISSORS) "Scissors"
  "None")))
)

Functions (11)

FunctionAccessArgs
get-winnerprivatep1-move: uint, p2-move: uint
valid-moveprivatemove: uint
get-statsprivateplayer: principal
update-statsprivatep1: principal, p2: principal, result: uint
create-gamepubliccommit: (buff 32
join-gamepublicgame-id: uint, commit: (buff 32
revealpublicgame-id: uint, move: uint, salt: (buff 32
get-gameread-onlygame-id: uint
get-player-statsread-onlyplayer: principal
get-total-gamesread-only
move-nameread-onlymove: uint