Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Clarinet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ authors = []
telemetry = false
cache_dir = './.cache'
requirements = []
[contracts.tic-tac-toe]
path = 'contracts/tic-tac-toe.clar'
[contracts.tic-tac-toev3]
path = 'contracts/tic-tac-toev3.clar'
clarity_version = 3
epoch = 3.0
[repl.analysis]
Expand Down
203 changes: 146 additions & 57 deletions contracts/tic-tac-toe.clar → contracts/tic-tac-toev3.clar
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
(define-constant THIS_CONTRACT (as-contract tx-sender)) ;; The address of this contract itself
(define-constant ERR_MIN_BET_AMOUNT u100) ;; Error thrown when a player tries to create a game with a bet amount less than the minimum (0.0001 STX)
(define-constant ERR_INVALID_MOVE u101) ;; Error thrown when a move is invalid, i.e. not within range of the board or not an X or an O
(define-constant ERR_GAME_NOT_FOUND u102) ;; Error thrown when a game cannot be found given a Game ID, i.e. invalid Game ID
(define-constant ERR_GAME_CANNOT_BE_JOINED u103) ;; Error thrown when a game cannot be joined, usually because it already has two players
(define-constant ERR_NOT_YOUR_TURN u104) ;; Error thrown when a player tries to make a move when it is not their turn
;; Tic Tac Toe Contract with Timeout Mechanism

;; Constants
(define-constant THIS_CONTRACT (as-contract tx-sender))
(define-constant ERR_MIN_BET_AMOUNT u100)
(define-constant ERR_INVALID_MOVE u101)
(define-constant ERR_GAME_NOT_FOUND u102)
(define-constant ERR_GAME_CANNOT_BE_JOINED u103)
(define-constant ERR_NOT_YOUR_TURN u104)
(define-constant ERR_TIMEOUT_NOT_REACHED u105) ;; NEW
(define-constant ERR_NOT_A_PLAYER u106) ;; NEW
(define-constant ERR_GAME_ALREADY_OVER u107) ;; NEW

;; NEW: 24 hours in blocks (assuming ~10 second block time after Nakamoto)
;; 24 hours = 86400 seconds / 10 seconds per block = 8640 blocks
;; For testing purposes, we can set this lower, e.g., 10 blocks
(define-constant TIMEOUT_BLOCKS u10)

;; The Game ID to use for the next game
(define-data-var latest-game-id uint u0)
Expand All @@ -14,14 +25,64 @@
player-one: principal,
player-two: (optional principal),
is-player-one-turn: bool,

bet-amount: uint,
board: (list 9 uint),

winner: (optional principal)
winner: (optional principal),
last-move-block: uint ;; NEW: Block height of last move
}
)

;; Private Functions

(define-private (validate-move (board (list 9 uint)) (move-index uint) (move uint))
(let (
;; Validate that the move is being played within range of the board
(index-in-range (and (>= move-index u0) (< move-index u9)))

;; Validate that the move is either an X or an O
(x-or-o (or (is-eq move u1) (is-eq move u2)))

;; Validate that the cell the move is being played on is currently empty
(empty-spot (is-eq (unwrap! (element-at? board move-index) false) u0))
)

;; All three conditions must be true for the move to be valid
(and (is-eq index-in-range true) (is-eq x-or-o true) empty-spot)
))

;; Given a board and three cells to look at on the board
;; Return true if all three are not empty and are the same value (all X or all O)
;; Return false if any of the three is empty or a different value
(define-private (is-line (board (list 9 uint)) (a uint) (b uint) (c uint))
(let (
;; Value of cell at index a
(a-val (unwrap! (element-at? board a) false))
;; Value of cell at index b
(b-val (unwrap! (element-at? board b) false))
;; Value of cell at index c
(c-val (unwrap! (element-at? board c) false))
)

;; a-val must equal b-val and must also equal c-val while not being empty (non-zero)
(and (is-eq a-val b-val) (is-eq a-val c-val) (not (is-eq a-val u0)))
))

;; Given a board, return true if any possible three-in-a-row line has been completed
(define-private (has-won (board (list 9 uint)))
(or
(is-line board u0 u1 u2) ;; Row 1
(is-line board u3 u4 u5) ;; Row 2
(is-line board u6 u7 u8) ;; Row 3
(is-line board u0 u3 u6) ;; Column 1
(is-line board u1 u4 u7) ;; Column 2
(is-line board u2 u5 u8) ;; Column 3
(is-line board u0 u4 u8) ;; Left to Right Diagonal
(is-line board u2 u4 u6) ;; Right to Left Diagonal
)
)

;; Public Functions

(define-public (create-game (bet-amount uint) (move-index uint) (move uint))
(let (
;; Get the Game ID to use for creation of this new game
Expand All @@ -37,7 +98,8 @@
is-player-one-turn: false,
bet-amount: bet-amount,
board: game-board,
winner: none
winner: none,
last-move-block: stacks-block-height ;; NEW: Record creation block
})
)

Expand Down Expand Up @@ -74,7 +136,8 @@
(game-data (merge original-game-data {
board: game-board,
player-two: (some contract-caller),
is-player-one-turn: true
is-player-one-turn: true,
last-move-block: stacks-block-height ;; NEW: Update timestamp
}))
)

Expand Down Expand Up @@ -120,7 +183,8 @@
(game-data (merge original-game-data {
board: game-board,
is-player-one-turn: (not is-player-one-turn),
winner: (if is-now-winner (some player-turn) none)
winner: (if is-now-winner (some player-turn) none),
last-move-block: stacks-block-height ;; NEW: Update timestamp
}))
)

Expand All @@ -143,57 +207,82 @@
(ok game-id)
))

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

(define-read-only (get-latest-game-id)
(var-get latest-game-id)
)

(define-private (validate-move (board (list 9 uint)) (move-index uint) (move uint))
;; NEW: Cancel game due to timeout
(define-public (cancel-game-timeout (game-id uint))
(let (
;; Validate that the move is being played within range of the board
(index-in-range (and (>= move-index u0) (< move-index u9)))
(game-data (unwrap! (map-get? games game-id) (err ERR_GAME_NOT_FOUND)))
(player-one (get player-one game-data))
(player-two (unwrap! (get player-two game-data) (err ERR_GAME_CANNOT_BE_JOINED)))
(is-player-one-turn (get is-player-one-turn game-data))
(last-move-block (get last-move-block game-data))
(blocks-passed (- stacks-block-height last-move-block))
(bet-amount (get bet-amount game-data))

;; The player who can cancel is the one waiting for opponent's move
(can-cancel-player (if is-player-one-turn player-two player-one))
;; The inactive player who timed out
(inactive-player (if is-player-one-turn player-one player-two))
)

;; Validate that the move is either an X or an O
(x-or-o (or (is-eq move u1) (is-eq move u2)))
;; Ensure game is not already over
(asserts! (is-none (get winner game-data)) (err ERR_GAME_ALREADY_OVER))

;; Ensure caller is a player in this game
(asserts! (or (is-eq contract-caller player-one) (is-eq contract-caller player-two)) (err ERR_NOT_A_PLAYER))

;; Ensure caller is the player waiting for their opponent
(asserts! (is-eq contract-caller can-cancel-player) (err ERR_NOT_YOUR_TURN))

;; Ensure timeout has been reached
(asserts! (>= blocks-passed TIMEOUT_BLOCKS) (err ERR_TIMEOUT_NOT_REACHED))

;; Validate that the cell the move is being played on is currently empty
(empty-spot (is-eq (unwrap! (element-at? board move-index) false) u0))
)
;; Transfer both bets to the active player (winner by timeout)
(try! (as-contract (stx-transfer? (* u2 bet-amount) tx-sender can-cancel-player)))

;; All three conditions must be true for the move to be valid
(and (is-eq index-in-range true) (is-eq x-or-o true) empty-spot)
;; Mark game as finished with the active player as winner
(map-set games game-id (merge game-data { winner: (some can-cancel-player) }))

(print {
action: "cancel-game-timeout",
winner: can-cancel-player,
inactive-player: inactive-player,
game-id: game-id
})
(ok true)
))

;; Given a board, return true if any possible three-in-a-row line has been completed
(define-private (has-won (board (list 9 uint)))
(or
(is-line board u0 u1 u2) ;; Row 1
(is-line board u3 u4 u5) ;; Row 2
(is-line board u6 u7 u8) ;; Row 3
(is-line board u0 u3 u6) ;; Column 1
(is-line board u1 u4 u7) ;; Column 2
(is-line board u2 u5 u8) ;; Column 3
(is-line board u0 u4 u8) ;; Left to Right Diagonal
(is-line board u2 u4 u6) ;; Right to Left Diagonal
)
;; Read-only Functions

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

;; Given a board and three cells to look at on the board
;; Return true if all three are not empty and are the same value (all X or all O)
;; Return false if any of the three is empty or a different value
(define-private (is-line (board (list 9 uint)) (a uint) (b uint) (c uint))
(let (
;; Value of cell at index a
(a-val (unwrap! (element-at? board a) false))
;; Value of cell at index b
(b-val (unwrap! (element-at? board b) false))
;; Value of cell at index c
(c-val (unwrap! (element-at? board c) false))
)
(define-read-only (get-latest-game-id)
(var-get latest-game-id)
)

;; a-val must equal b-val and must also equal c-val while not being empty (non-zero)
(and (is-eq a-val b-val) (is-eq a-val c-val) (not (is-eq a-val u0)))
))
;; NEW: Check if a game can be cancelled due to timeout
(define-read-only (can-cancel-game (game-id uint))
(match (map-get? games game-id)
game-data
(let (
(last-move-block (get last-move-block game-data))
(blocks-passed (- stacks-block-height last-move-block))
(has-winner (is-some (get winner game-data)))
(has-both-players (is-some (get player-two game-data)))
)
(ok {
can-cancel: (and
(not has-winner)
has-both-players
(>= blocks-passed TIMEOUT_BLOCKS)
),
blocks-until-timeout: (if (>= blocks-passed TIMEOUT_BLOCKS)
u0
(- TIMEOUT_BLOCKS blocks-passed)
),
blocks-passed: blocks-passed
}))
(err ERR_GAME_NOT_FOUND)
)
)
27 changes: 20 additions & 7 deletions deployments/default.simnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,64 @@ genesis:
- name: deployer
address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: faucet
address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_1
address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_2
address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_3
address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_4
address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_5
address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_6
address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_7
address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_8
address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP
balance: "100000000000000"
sbtc-balance: "1000000000"
contracts:
- genesis
- lockup
- bns
- cost-voting
- costs
- pox
- costs-2
- pox-2
- costs-3
- pox-3
- pox-4
- lockup
- costs-2
- costs-3
- cost-voting
- bns
- signers
- signers-voting
plan:
batches:
- id: 0
transactions:
- emulated-contract-publish:
contract-name: tic-tac-toe
contract-name: tic-tac-toev2
emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
path: contracts/tic-tac-toe.clar
path: contracts/tic-tac-toev2.clar
clarity-version: 3
epoch: "3.0"
8 changes: 4 additions & 4 deletions deployments/default.testnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ plan:
- id: 0
transactions:
- contract-publish:
contract-name: tic-tac-toe
expected-sender: ST3P49R8XXQWG69S66MZASYPTTGNDKK0WW32RRJDN
cost: 90730
path: contracts/tic-tac-toe.clar
contract-name: tic-tac-toev3
expected-sender: ST1ASQH73Y0HWBRB4NM5RTE148YSYG8WYN2EPGCRN
cost: 121340
path: contracts/tic-tac-toev3.clar
anchor-block-only: true
clarity-version: 3
epoch: "3.0"
Loading