Source Code

;; ClarityXO - Blockchain Tic-Tac-Toe Game
;; A strategic game of X's and O's on the Stacks blockchain

;; Constants
(define-constant contract-owner tx-sender)
(define-constant err-not-authorized (err u100))
(define-constant err-game-finished (err u101))
(define-constant err-invalid-move (err u102))
(define-constant err-not-your-turn (err u103))
(define-constant err-cell-occupied (err u104))

;; Cell values
(define-constant EMPTY u0)
(define-constant PLAYER_X u1)
(define-constant PLAYER_O u2)

;; Game status
(define-constant STATUS_ACTIVE u0)
(define-constant STATUS_X_WON u1)
(define-constant STATUS_O_WON u2)
(define-constant STATUS_DRAW u3)

;; Data Variables
(define-data-var game-board (list 9 uint) (list u0 u0 u0 u0 u0 u0 u0 u0 u0))
(define-data-var game-status uint STATUS_ACTIVE)
(define-data-var current-turn uint PLAYER_X)
(define-data-var player-address (optional principal) none)
(define-data-var moves-count uint u0)

;; Private Functions

;; Convert row and column to index (0-8)
(define-private (get-index (row uint) (col uint))
  (+ (* row u3) col)
)

;; Get cell value at position
(define-private (get-cell (row uint) (col uint))
  (let ((idx (get-index row col)))
    (default-to EMPTY (element-at (var-get game-board) idx))
  )
)

;; Set cell value at position
(define-private (set-cell (row uint) (col uint) (value uint))
  (let ((idx (get-index row col)))
    (var-set game-board 
      (map set-cell-helper 
        (var-get game-board)
        (list u0 u1 u2 u3 u4 u5 u6 u7 u8)
        (list (if (is-eq idx u0) value (default-to EMPTY (element-at (var-get game-board) u0)))
              (if (is-eq idx u1) value (default-to EMPTY (element-at (var-get game-board) u1)))
              (if (is-eq idx u2) value (default-to EMPTY (element-at (var-get game-board) u2)))
              (if (is-eq idx u3) value (default-to EMPTY (element-at (var-get game-board) u3)))
              (if (is-eq idx u4) value (default-to EMPTY (element-at (var-get game-board) u4)))
              (if (is-eq idx u5) value (default-to EMPTY (element-at (var-get game-board) u5)))
              (if (is-eq idx u6) value (default-to EMPTY (element-at (var-get game-board) u6)))
              (if (is-eq idx u7) value (default-to EMPTY (element-at (var-get game-board) u7)))
              (if (is-eq idx u8) value (default-to EMPTY (element-at (var-get game-board) u8))))
      )
    )
  )
)

(define-private (set-cell-helper (current uint) (index uint) (new-val uint))
  new-val
)

;; Check if three cells have the same non-empty value
(define-private (check-line (a uint) (b uint) (c uint))
  (and (is-eq a b) (is-eq b c) (not (is-eq a EMPTY)))
)

;; Check all win conditions
(define-private (check-winner-internal)
  (let (
    (board (var-get game-board))
    (c0 (default-to EMPTY (element-at board u0)))
    (c1 (default-to EMPTY (element-at board u1)))
    (c2 (default-to EMPTY (element-at board u2)))
    (c3 (default-to EMPTY (element-at board u3)))
    (c4 (default-to EMPTY (element-at board u4)))
    (c5 (default-to EMPTY (element-at board u5)))
    (c6 (default-to EMPTY (element-at board u6)))
    (c7 (default-to EMPTY (element-at board u7)))
    (c8 (default-to EMPTY (element-at board u8)))
  )
    ;; Check rows
    (if (check-line c0 c1 c2) c0
    (if (check-line c3 c4 c5) c3
    (if (check-line c6 c7 c8) c6
    ;; Check columns
    (if (check-line c0 c3 c6) c0
    (if (check-line c1 c4 c7) c1
    (if (check-line c2 c5 c8) c2
    ;; Check diagonals
    (if (check-line c0 c4 c8) c0
    (if (check-line c2 c4 c6) c2
    EMPTY))))))))
  )
)

;; Check if board is full
(define-private (is-board-full)
  (is-eq (var-get moves-count) u9)
)

;; Update game status based on winner or draw
(define-private (update-game-status)
  (let ((winner (check-winner-internal)))
    (if (is-eq winner PLAYER_X)
      (var-set game-status STATUS_X_WON)
      (if (is-eq winner PLAYER_O)
        (var-set game-status STATUS_O_WON)
        (if (is-board-full)
          (var-set game-status STATUS_DRAW)
          (var-set game-status STATUS_ACTIVE)
        )
      )
    )
  )
)

;; Simple AI: Find first empty cell using fold
(define-private (find-empty-cell-helper (index uint) (acc uint))
  (let ((board (var-get game-board)))
    (if (and
          (is-eq acc u999)
          (is-eq (default-to PLAYER_X (element-at board index)) EMPTY))
      index
      acc
    )
  )
)

(define-private (find-first-empty)
  (let ((result (fold find-empty-cell-helper (list u0 u1 u2 u3 u4 u5 u6 u7 u8) u999)))
    (if (is-eq result u999) u0 result)
  )
)

;; Public Functions

;; Start a new game
(define-public (start-new-game)
  (begin
    (var-set game-board (list u0 u0 u0 u0 u0 u0 u0 u0 u0))
    (var-set game-status STATUS_ACTIVE)
    (var-set current-turn PLAYER_X)
    (var-set player-address (some tx-sender))
    (var-set moves-count u0)
    (ok true)
  )
)

;; Player makes a move
(define-public (make-move (row uint) (col uint))
  (let (
    (current-cell (get-cell row col))
    (status (var-get game-status))
  )
    (begin
      ;; Validate move
      (asserts! (is-eq status STATUS_ACTIVE) err-game-finished)
      (asserts! (is-eq (var-get current-turn) PLAYER_X) err-not-your-turn)
      (asserts! (and (< row u3) (< col u3)) err-invalid-move)
      (asserts! (is-eq current-cell EMPTY) err-cell-occupied)
      
      ;; Make player move
      (set-cell row col PLAYER_X)
      (var-set moves-count (+ (var-get moves-count) u1))
      (var-set current-turn PLAYER_O)
      (update-game-status)
      
      ;; If game still active, computer makes move inline
      (if (is-eq (var-get game-status) STATUS_ACTIVE)
        (let (
          (empty-idx (find-first-empty))
          (comp-row (/ empty-idx u3))
          (comp-col (mod empty-idx u3))
        )
          (begin
            (set-cell comp-row comp-col PLAYER_O)
            (var-set moves-count (+ (var-get moves-count) u1))
            (var-set current-turn PLAYER_X)
            (update-game-status)
            (ok true)
          )
        )
        (ok true)
      )
    )
  )
)

;; Player resigns
(define-public (resign-game)
  (begin
    (asserts! (is-eq (some tx-sender) (var-get player-address)) err-not-authorized)
    (var-set game-status STATUS_O_WON)
    (ok true)
  )
)

;; Read-Only Functions

;; Get current board state
(define-read-only (get-board-state)
  (ok (var-get game-board))
)

;; Get game status
(define-read-only (get-game-status)
  (ok (var-get game-status))
)

;; Get winner
(define-read-only (get-winner)
  (let ((status (var-get game-status)))
    (ok (if (is-eq status STATUS_X_WON)
          (some PLAYER_X)
          (if (is-eq status STATUS_O_WON)
            (some PLAYER_O)
            none
          )
        ))
  )
)

;; Get current turn
(define-read-only (get-current-turn)
  (ok (var-get current-turn))
)

;; Check if move is valid
(define-read-only (is-valid-move (row uint) (col uint))
  (let (
    (current-cell (get-cell row col))
    (status (var-get game-status))
  )
    (ok (and 
      (is-eq status STATUS_ACTIVE)
      (is-eq (var-get current-turn) PLAYER_X)
      (< row u3)
      (< col u3)
      (is-eq current-cell EMPTY)
    ))
  )
)

;; Get player address
(define-read-only (get-player-address)
  (ok (var-get player-address))
)

;; Get moves count
(define-read-only (get-moves-count)
  (ok (var-get moves-count))
)

Functions (20)

FunctionAccessArgs
get-indexprivaterow: uint, col: uint
get-cellprivaterow: uint, col: uint
set-cellprivaterow: uint, col: uint, value: uint
set-cell-helperprivatecurrent: uint, index: uint, new-val: uint
check-lineprivatea: uint, b: uint, c: uint
check-winner-internalprivate
is-board-fullprivate
update-game-statusprivate
find-empty-cell-helperprivateindex: uint, acc: uint
find-first-emptyprivate
start-new-gamepublic
make-movepublicrow: uint, col: uint
resign-gamepublic
get-board-stateread-only
get-game-statusread-only
get-winnerread-only
get-current-turnread-only
is-valid-moveread-onlyrow: uint, col: uint
get-player-addressread-only
get-moves-countread-only