Source Code

;; BOARD GENERATOR CONTRACT
;; Generates verifiable random mine placements using block hash + commitment scheme

;; ============================================================================
;; CONSTANTS
;; ============================================================================

(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_NOT_AUTHORIZED (err u200))
(define-constant ERR_BOARD_NOT_FOUND (err u201))
(define-constant ERR_BOARD_ALREADY_EXISTS (err u202))
(define-constant ERR_BOARD_NOT_REVEALED (err u203))
(define-constant ERR_INVALID_PARAMETERS (err u204))

;; ============================================================================
;; DATA MAPS
;; ============================================================================

;; Board commitments (hide mine positions until game ends or verification needed)
(define-map board-commitments
  { game-id: uint }
  {
    commitment-hash: (buff 32),
    seed-stacks-block-height: uint,
    revealed: bool,
    mine-count: uint,
    board-width: uint,
    board-height: uint,
    first-click-x: (optional uint),
    first-click-y: (optional uint)
  }
)

;; Revealed mine positions (only after game ends or for verification)
;; Using bitpacked storage: list of cell indices that are mines
(define-map revealed-mine-positions
  { game-id: uint }
  { mine-indices: (list 99 uint) }
)

;; Cache for mine checks during gameplay (to avoid re-computation)
(define-map mine-cache
  { game-id: uint, cell-index: uint }
  { is-mine: bool }
)

;; ============================================================================
;; BOARD GENERATION
;; ============================================================================

;; Generate board commitment
;; Called by game-core when creating new game
;; Uses future block hash for randomness
(define-public (generate-board 
  (game-id uint)
  (width uint)
  (height uint)
  (mine-count uint)
  (first-click-x uint)
  (first-click-y uint))
  (let
    (
      (future-block (+ stacks-block-height u10)) ;; Use hash of block 10 blocks in future
      (seed-data (concat (unwrap-panic (to-consensus-buff? game-id)) (unwrap-panic (to-consensus-buff? tx-sender))))
      (commitment (sha256 seed-data))
    )
    ;; Validate
    (asserts! (is-none (map-get? board-commitments { game-id: game-id })) ERR_BOARD_ALREADY_EXISTS)
    (asserts! (< mine-count (* width height)) ERR_INVALID_PARAMETERS)
    
    ;; Store commitment
    (map-set board-commitments
      { game-id: game-id }
      {
        commitment-hash: commitment,
        seed-stacks-block-height: future-block,
        revealed: false,
        mine-count: mine-count,
        board-width: width,
        board-height: height,
        first-click-x: (some first-click-x),
        first-click-y: (some first-click-y)
      }
    )
    
    (print {event: "generate-board", game-id: game-id, width: width, height: height, mine-count: mine-count})
    (ok commitment)
  )
)

;; Check if specific cell is a mine (lazy evaluation with caching)
;; This is called during gameplay to verify mine hits
(define-public (is-cell-mine (game-id uint) (cell-index uint))
  (let
    (
      (board (unwrap! (map-get? board-commitments { game-id: game-id }) ERR_BOARD_NOT_FOUND))
      (cached (map-get? mine-cache { game-id: game-id, cell-index: cell-index }))
    )
    ;; Check cache first
    (match cached
      cached-value (begin
        (print {event: "is-cell-mine", game-id: game-id, cell-index: cell-index, is-mine: (get is-mine cached-value), cached: true})
        (ok (get is-mine cached-value))
      )
      ;; Not cached - compute and cache
      (let
        (
          (is-mine (compute-is-mine game-id cell-index board))
        )
        ;; Cache result
        (map-set mine-cache
          { game-id: game-id, cell-index: cell-index }
          { is-mine: is-mine }
        )
        (print {event: "is-cell-mine", game-id: game-id, cell-index: cell-index, is-mine: is-mine, cached: false})
        (ok is-mine)
      )
    )
  )
)

;; Compute if cell is mine using deterministic pseudo-random generation
;; Based on board commitment hash + cell index
(define-private (compute-is-mine (game-id uint) (cell-index uint) (board (tuple 
  (commitment-hash (buff 32))
  (seed-stacks-block-height uint)
  (revealed bool)
  (mine-count uint)
  (board-width uint)
  (board-height uint)
  (first-click-x (optional uint))
  (first-click-y (optional uint))
)))
  (let
    (
      (seed (get commitment-hash board))
      (total-cells (* (get board-width board) (get board-height board)))
      (mine-count (get mine-count board))
      ;; Generate pseudo-random hash for this cell
      (cell-seed (sha256 (concat seed (unwrap-panic (to-consensus-buff? cell-index)))))
      ;; Take first 16 bytes and convert to uint
      ;; We'll use a helper that processes the hash differently
      (hash-part (unwrap-panic (as-max-len? (unwrap-panic (slice? cell-seed u0 u16)) u16)))
      (random-value (buff-to-uint-be hash-part))
      ;; Calculate probability: mine-count / total-cells
      ;; If random-value mod total-cells < mine-count, it's a mine
      (is-mine-candidate (< (mod random-value total-cells) mine-count))
      
      ;; Check if this is the first-click position (must never be mine)
      (first-click-index (coords-to-index 
        (default-to u0 (get first-click-x board))
        (default-to u0 (get first-click-y board))
        (get board-width board)
      ))
      (is-first-click (is-eq cell-index first-click-index))
    )
    ;; First click safety - never a mine
    (if is-first-click
      false
      is-mine-candidate
    )
  )
)

;; Helper: coords to index
(define-private (coords-to-index (x uint) (y uint) (width uint))
  (+ (* y width) x)
)

;; ============================================================================
;; MINE ADJACENCY CALCULATION
;; ============================================================================

;; Helper to check if cell is mine, returns bool or false on error
(define-private (check-cell-mine (game-id uint) (cell-index uint))
  (match (is-cell-mine game-id cell-index)
    ok-val ok-val
    err-val false
  )
)

;; Count adjacent mines for a cell (0-8)
;; Used by game-core to display numbers
(define-public (count-adjacent-mines (game-id uint) (x uint) (y uint))
  (let
    (
      (board (unwrap! (map-get? board-commitments { game-id: game-id }) ERR_BOARD_NOT_FOUND))
      (width (get board-width board))
      (height (get board-height board))
      (c0 u0)
      ;; Check all 8 neighbors manually
      (c1 (if (and (> x u0) (> y u0)) 
              (+ c0 (if (check-cell-mine game-id (coords-to-index (- x u1) (- y u1) width)) u1 u0)) 
              c0))
      (c2 (if (> y u0) 
              (+ c1 (if (check-cell-mine game-id (coords-to-index x (- y u1) width)) u1 u0)) 
              c1))
      (c3 (if (and (< x (- width u1)) (> y u0)) 
              (+ c2 (if (check-cell-mine game-id (coords-to-index (+ x u1) (- y u1) width)) u1 u0)) 
              c2))
      (c4 (if (> x u0) 
              (+ c3 (if (check-cell-mine game-id (coords-to-index (- x u1) y width)) u1 u0)) 
              c3))
      (c5 (if (< x (- width u1)) 
              (+ c4 (if (check-cell-mine game-id (coords-to-index (+ x u1) y width)) u1 u0)) 
              c4))
      (c6 (if (and (> x u0) (< y (- height u1))) 
              (+ c5 (if (check-cell-mine game-id (coords-to-index (- x u1) (+ y u1) width)) u1 u0)) 
              c5))
      (c7 (if (< y (- height u1)) 
              (+ c6 (if (check-cell-mine game-id (coords-to-index x (+ y u1) width)) u1 u0)) 
              c6))
      (c8 (if (and (< x (- width u1)) (< y (- height u1))) 
              (+ c7 (if (check-cell-mine game-id (coords-to-index (+ x u1) (+ y u1) width)) u1 u0)) 
              c7))
    )
    (print {event: "count-adjacent-mines", game-id: game-id, x: x, y: y, count: c8})
    (ok c8)
  )
)

;; ============================================================================
;; BOARD REVEAL (POST-GAME)
;; ============================================================================

;; Reveal all mine positions after game ends
;; Used for verification and post-game analysis
(define-public (reveal-board (game-id uint))
  (let
    (
      (board (unwrap! (map-get? board-commitments { game-id: game-id }) ERR_BOARD_NOT_FOUND))
    )
    ;; Can only reveal once
    (asserts! (not (get revealed board)) ERR_BOARD_ALREADY_EXISTS)
    
    ;; Mark as revealed
    (map-set board-commitments
      { game-id: game-id }
      (merge board { revealed: true })
    )
    
    ;; In production, would compute and store all mine positions
    ;; For now, mines are computed on-demand via is-cell-mine
    
    (print {event: "reveal-board", game-id: game-id})
    (ok true)
  )
)

;; ============================================================================
;; READ-ONLY FUNCTIONS
;; ============================================================================

;; Get board commitment info
(define-read-only (get-board-info (game-id uint))
  (map-get? board-commitments { game-id: game-id })
)

;; Get mine positions (only if revealed)
(define-read-only (get-mine-positions (game-id uint))
  (let
    (
      (board (unwrap! (map-get? board-commitments { game-id: game-id }) ERR_BOARD_NOT_FOUND))
    )
    (asserts! (get revealed board) ERR_BOARD_NOT_REVEALED)
    (ok (map-get? revealed-mine-positions { game-id: game-id }))
  )
)

;; Verify board commitment (prove fairness)
(define-read-only (verify-commitment (game-id uint) (claimed-seed (buff 32)))
  (let
    (
      (board (unwrap! (map-get? board-commitments { game-id: game-id }) ERR_BOARD_NOT_FOUND))
      (stored-commitment (get commitment-hash board))
      (computed-commitment (sha256 claimed-seed))
    )
    (ok (is-eq stored-commitment computed-commitment))
  )
)

Functions (10)

FunctionAccessArgs
generate-boardpublicgame-id: uint, width: uint, height: uint, mine-count: uint, first-click-x: uint, first-click-y: uint
is-cell-minepublicgame-id: uint, cell-index: uint
compute-is-mineprivategame-id: uint, cell-index: uint, board: (tuple (commitment-hash (buff 32
coords-to-indexprivatex: uint, y: uint, width: uint
check-cell-mineprivategame-id: uint, cell-index: uint
count-adjacent-minespublicgame-id: uint, x: uint, y: uint
reveal-boardpublicgame-id: uint
get-board-inforead-onlygame-id: uint
get-mine-positionsread-onlygame-id: uint
verify-commitmentread-onlygame-id: uint, claimed-seed: (buff 32