diff --git a/contracts/tic-tac-toe.clar b/contracts/tic-tac-toe.clar
index 4bc01e3..0b50bbe 100644
--- a/contracts/tic-tac-toe.clar
+++ b/contracts/tic-tac-toe.clar
@@ -4,6 +4,11 @@
(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
+(define-constant ERR_UNDO_REQUEST_PENDING u105) ;; Error thrown when an undo is requested but one is already pending
+(define-constant ERR_NO_UNDO_REQUEST_PENDING u106) ;; Error thrown when an undo is approved but none is pending
+(define-constant ERR_CANNOT_REQUEST_UNDO u107) ;; Error thrown when a player who did not just move tries to request an undo
+(define-constant ERR_CANNOT_APPROVE_UNDO u108) ;; Error thrown when a player tries to approve an undo for the wrong turn
+(define-constant ERR_NO_LAST_MOVE u109) ;; Error thrown when approving an undo but there is no last move to revert
;; The Game ID to use for the next game
(define-data-var latest-game-id uint u0)
@@ -18,7 +23,11 @@
bet-amount: uint,
board: (list 9 uint),
- winner: (optional principal)
+ winner: (optional principal),
+
+ ;; Co-op Undo
+ undo-requester: (optional principal),
+ last-move-position: (optional uint)
}
)
@@ -37,7 +46,11 @@
is-player-one-turn: false,
bet-amount: bet-amount,
board: game-board,
- winner: none
+ winner: none,
+
+ ;; Co-op Undo
+ undo-requester: none,
+ last-move-position: (some move-index)
})
)
@@ -74,7 +87,10 @@
(game-data (merge original-game-data {
board: game-board,
player-two: (some contract-caller),
- is-player-one-turn: true
+ is-player-one-turn: true,
+
+ ;; Co-op Undo
+ last-move-position: (some move-index)
}))
)
@@ -120,7 +136,12 @@
(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),
+
+ ;; Co-op Undo
+ ;; A move implicitly denies any pending undo request
+ undo-requester: none,
+ last-move-position: (some move-index)
}))
)
@@ -143,6 +164,67 @@
(ok game-id)
))
+(define-public (request-undo (game-id uint))
+ (let (
+ (game (unwrap! (map-get? games game-id) (err ERR_GAME_NOT_FOUND)))
+ (requester tx-sender)
+ (p1 (get player-one game))
+ (p2 (unwrap! (get player-two game) (err ERR_GAME_CANNOT_BE_JOINED)))
+ (turn-is-p1 (get is-player-one-turn game))
+ )
+ ;; Game must be ongoing
+ (asserts! (is-none (get winner game)) (err ERR_INVALID_MOVE))
+ ;; No undo request can be currently pending
+ (asserts! (is-none (get undo-requester game)) (err ERR_UNDO_REQUEST_PENDING))
+
+ ;; Requester must be the player who just moved.
+ ;; If it's p1's turn now, p2 must be the requester.
+ (asserts! (if turn-is-p1 (is-eq requester p2) (is-eq requester p1)) (err ERR_CANNOT_REQUEST_UNDO))
+
+ (map-set games game-id
+ (merge game { undo-requester: (some requester) }))
+ (ok true)
+))
+
+(define-public (approve-undo (game-id uint))
+ (let ((game (unwrap! (map-get? games game-id) (err ERR_GAME_NOT_FOUND))))
+ (let
+ (
+ (approver tx-sender)
+ (requester (unwrap! (get undo-requester game) (err ERR_NO_UNDO_REQUEST_PENDING)))
+ (pos-to-undo (unwrap! (get last-move-position game) (err ERR_NO_LAST_MOVE)))
+ (current-board (get board game))
+ (turn-is-p1 (get is-player-one-turn game))
+ )
+ ;; Body of the let
+ ;; Game must be ongoing
+ (asserts! (is-none (get winner game)) (err ERR_INVALID_MOVE))
+ ;; The approver must be the current player
+ (asserts! (if turn-is-p1 (is-eq approver (get player-one game)) (is-eq approver (unwrap! (get player-two game) (err ERR_GAME_NOT_FOUND)))) (err ERR_CANNOT_APPROVE_UNDO))
+ ;; The approver cannot be the requester
+ (asserts! (not (is-eq approver requester)) (err ERR_CANNOT_APPROVE_UNDO))
+
+ (let
+ (
+ (new-board (unwrap! (replace-at? current-board pos-to-undo u0) (err ERR_INVALID_MOVE)))
+ )
+ ;; Body of the inner let
+ (map-set games game-id
+ (merge game {
+ board: new-board,
+ ;; Toggle turn back to the requester
+ is-player-one-turn: (not turn-is-p1),
+ ;; Clear undo state
+ undo-requester: none,
+ last-move-position: none
+ })
+ )
+ (ok true)
+ )
+ )
+ )
+)
+
(define-read-only (get-game (game-id uint))
(map-get? games game-id)
)
diff --git a/deployments/default.testnet-plan.yaml b/deployments/default.testnet-plan.yaml
index fba5452..9ceae3c 100644
--- a/deployments/default.testnet-plan.yaml
+++ b/deployments/default.testnet-plan.yaml
@@ -9,8 +9,8 @@ plan:
- id: 0
transactions:
- contract-publish:
- contract-name: tic-tac-toe
- expected-sender: ST3P49R8XXQWG69S66MZASYPTTGNDKK0WW32RRJDN
+ contract-name: tic-tac-toe-v2
+ expected-sender: ST2QK4128H22NH4H8MD2AVP72M0Q72TKS48VB5469
cost: 90730
path: contracts/tic-tac-toe.clar
anchor-block-only: true
diff --git a/frontend/components/play-game.tsx b/frontend/components/play-game.tsx
index 07f17ae..101f95c 100644
--- a/frontend/components/play-game.tsx
+++ b/frontend/components/play-game.tsx
@@ -12,25 +12,34 @@ interface PlayGameProps {
}
export function PlayGame({ game }: PlayGameProps) {
- const { userData, handleJoinGame, handlePlayGame } = useStacks();
+ const {
+ userData,
+ handleJoinGame,
+ handlePlayGame,
+ handleRequestUndo,
+ handleApproveUndo,
+ } = useStacks();
const [board, setBoard] = useState(game.board);
const [playedMoveIndex, setPlayedMoveIndex] = useState(-1);
if (!userData) return null;
- const isPlayerOne =
- userData.profile.stxAddress.testnet === game["player-one"];
- const isPlayerTwo =
- userData.profile.stxAddress.testnet === game["player-two"];
+ const myAddress = userData.profile.stxAddress.testnet;
+ const isPlayerOne = myAddress === game["player-one"];
+ const isPlayerTwo = myAddress === game["player-two"];
const isJoinable = game["player-two"] === null && !isPlayerOne;
const isJoinedAlready = isPlayerOne || isPlayerTwo;
const nextMove = game["is-player-one-turn"] ? Move.X : Move.O;
- const isMyTurn =
- (game["is-player-one-turn"] && isPlayerOne) ||
- (!game["is-player-one-turn"] && isPlayerTwo);
+ const isMyTurn = (game["is-player-one-turn"] && isPlayerOne) || (!game["is-player-one-turn"] && isPlayerTwo);
const isGameOver = game.winner !== null;
+ // Undo feature state
+ const undoRequestedByOpponent = game["undo-requester"] !== null && game["undo-requester"] !== myAddress;
+ const iJustMoved = !isMyTurn && isJoinedAlready && !isGameOver;
+ const undoRequestIsPending = game["undo-requester"] !== null;
+
function onCellClick(index: number) {
+ if (undoRequestedByOpponent) return; // Don't allow moves if opponent is requesting undo
const tempBoard = [...game.board];
tempBoard[index] = nextMove;
setBoard(tempBoard);
@@ -47,50 +56,22 @@ export function PlayGame({ game }: PlayGameProps) {
/>
-
- Bet Amount:
- {formatStx(game["bet-amount"])} STX
-
+ {/* Game Info unchanged */}
+
-
-
Player One:
-
+
Undo Request
+
Your opponent wants to undo their last move.
+
-
-
- Player Two:
- {game["player-two"] ? (
-
- {abbreviateAddress(game["player-two"])}
-
- ) : (
- Nobody
- )}
-
-
- {game["winner"] && (
-
- Winner:
-
- {abbreviateAddress(game["winner"])}
-
-
- )}
-
+ )}
{isJoinable && (