TicTacToe

Description:

Smart contract deployed on Ethereum.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

// SPDX-License-Identifier: MIT

// Accessible at: https://tictactoe-eth.pages.dev/

pragma solidity ^0.8.30;

enum Cell {
    EMPTY,
    X,
    O
}

enum GameStatus {
    ONGOING,
    X_WINS,
    O_WINS,
    DRAW,
    STARTED
}

struct BoardState {
    uint8 A1;
    uint8 A2;
    uint8 A3;
    uint8 B1;
    uint8 B2;
    uint8 B3;
    uint8 C1;
    uint8 C2;
    uint8 C3;
}

struct GameState {
    address playerX;
    address playerO;
    uint256 lastMoveTimestamp;
    BoardState board;
}

contract TicTacToe {
    mapping(uint256 => GameState) private games;
    uint256 private currentGameId;
    bool private active;
    address private immutable owner;
    uint256 private constant PRICE = 0.001 ether;
    uint256 private constant TURN_TIMEOUT = 60 * 3; // 3 minutes

    error SendFailed();

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner");
        _;
    }

    constructor() {
        owner = msg.sender;
        active = true;
    }

    function isActive() public view returns (bool) {
        return active;
    }

    function getCurrentGameState() public view returns (GameState memory) {
        return games[currentGameId];
    }

    function getCurrentGameId() public view returns (uint256) {
        return currentGameId;
    }

    function getCurrentGameStatus() public view returns (GameStatus) {
        return getGameStatus(currentGameId);
    }

    function getGameStatus(uint256 gameId) public view returns (GameStatus) {
        BoardState memory board = games[gameId].board;

        if (
            board.A1 != uint8(Cell.EMPTY) &&
            board.A1 == board.A2 &&
            board.A2 == board.A3
        ) {
            return
                board.A1 == uint8(Cell.X)
                    ? GameStatus.X_WINS
                    : GameStatus.O_WINS;
        }
        if (
            board.B1 != uint8(Cell.EMPTY) &&
            board.B1 == board.B2 &&
            board.B2 == board.B3
        ) {
            return
                board.B1 == uint8(Cell.X)
                    ? GameStatus.X_WINS
                    : GameStatus.O_WINS;
        }
        if (
            board.C1 != uint8(Cell.EMPTY) &&
            board.C1 == board.C2 &&
            board.C2 == board.C3
        ) {
            return
                board.C1 == uint8(Cell.X)
                    ? GameStatus.X_WINS
                    : GameStatus.O_WINS;
        }
        if (
            board.A1 != uint8(Cell.EMPTY) &&
            board.A1 == board.B1 &&
            board.B1 == board.C1
        ) {
            return
                board.A1 == uint8(Cell.X)
                    ? GameStatus.X_WINS
                    : GameStatus.O_WINS;
        }
        if (
            board.A2 != uint8(Cell.EMPTY) &&
            board.A2 == board.B2 &&
            board.B2 == board.C2
        ) {
            return
                board.A2 == uint8(Cell.X)
                    ? GameStatus.X_WINS
                    : GameStatus.O_WINS;
        }
        if (
            board.A3 != uint8(Cell.EMPTY) &&
            board.A3 == board.B3 &&
            board.B3 == board.C3
        ) {
            return
                board.A3 == uint8(Cell.X)
                    ? GameStatus.X_WINS
                    : GameStatus.O_WINS;
        }
        if (
            board.A1 != uint8(Cell.EMPTY) &&
            board.A1 == board.B2 &&
            board.B2 == board.C3
        ) {
            return
                board.A1 == uint8(Cell.X)
                    ? GameStatus.X_WINS
                    : GameStatus.O_WINS;
        }
        if (
            board.A3 != uint8(Cell.EMPTY) &&
            board.A3 == board.B2 &&
            board.B2 == board.C1
        ) {
            return
                board.A3 == uint8(Cell.X)
                    ? GameStatus.X_WINS
                    : GameStatus.O_WINS;
        }

        if (
            board.A1 != uint8(Cell.EMPTY) &&
            board.A2 != uint8(Cell.EMPTY) &&
            board.A3 != uint8(Cell.EMPTY) &&
            board.B1 != uint8(Cell.EMPTY) &&
            board.B2 != uint8(Cell.EMPTY) &&
            board.B3 != uint8(Cell.EMPTY) &&
            board.C1 != uint8(Cell.EMPTY) &&
            board.C2 != uint8(Cell.EMPTY) &&
            board.C3 != uint8(Cell.EMPTY)
        ) {
            return GameStatus.DRAW;
        }

        if (
            board.A1 == uint8(Cell.EMPTY) &&
            board.A2 == uint8(Cell.EMPTY) &&
            board.A3 == uint8(Cell.EMPTY) &&
            board.B1 == uint8(Cell.EMPTY) &&
            board.B2 == uint8(Cell.EMPTY) &&
            board.B3 == uint8(Cell.EMPTY) &&
            board.C1 == uint8(Cell.EMPTY) &&
            board.C2 == uint8(Cell.EMPTY) &&
            board.C3 == uint8(Cell.EMPTY)
        ) {
            return GameStatus.STARTED;
        }

        return GameStatus.ONGOING;
    }

    function getNumberOfPlacedCells(
        uint256 gameId
    ) private view returns (uint8) {
        BoardState memory board = games[gameId].board;
        uint8 moves = 0;

        if (board.A1 != uint8(Cell.EMPTY)) moves++;
        if (board.A2 != uint8(Cell.EMPTY)) moves++;
        if (board.A3 != uint8(Cell.EMPTY)) moves++;
        if (board.B1 != uint8(Cell.EMPTY)) moves++;
        if (board.B2 != uint8(Cell.EMPTY)) moves++;
        if (board.B3 != uint8(Cell.EMPTY)) moves++;
        if (board.C1 != uint8(Cell.EMPTY)) moves++;
        if (board.C2 != uint8(Cell.EMPTY)) moves++;
        if (board.C3 != uint8(Cell.EMPTY)) moves++;

        return moves;
    }

    function getNumberOfEspecificPlacedCells(
        uint256 gameId,
        Cell cellType
    ) private view returns (uint8) {
        BoardState memory board = games[gameId].board;
        uint8 moves = 0;

        if (board.A1 == uint8(cellType)) moves++;
        if (board.A2 == uint8(cellType)) moves++;
        if (board.A3 == uint8(cellType)) moves++;
        if (board.B1 == uint8(cellType)) moves++;
        if (board.B2 == uint8(cellType)) moves++;
        if (board.B3 == uint8(cellType)) moves++;
        if (board.C1 == uint8(cellType)) moves++;
        if (board.C2 == uint8(cellType)) moves++;
        if (board.C3 == uint8(cellType)) moves++;

        return moves;
    }

    function getTurn(uint256 gameId) public view returns (Cell) {
        return getNumberOfPlacedCells(gameId) % 2 == 0 ? Cell.X : Cell.O;
    }

    function isPlayerCellAvailable(Cell witch) public view returns (bool) {
        bool timeout = (block.timestamp -
            games[currentGameId].lastMoveTimestamp) > TURN_TIMEOUT;

        if (
            (witch == Cell.X && games[currentGameId].playerX == address(0)) ||
            timeout
        ) {
            return true;
        }
        if (
            (witch == Cell.O && games[currentGameId].playerO == address(0)) ||
            timeout
        ) {
            return true;
        }
        return false;
    }

    function getSenderCell(uint256 gameId) public view returns (Cell) {
        if (msg.sender == games[gameId].playerX) {
            return Cell.X;
        } else if (msg.sender == games[gameId].playerO) {
            return Cell.O;
        } else {
            return Cell.EMPTY;
        }
    }

    receive() external payable {
        revert();
    }

    function finishGame() public {
        GameStatus status = getGameStatus(currentGameId);
        require(status != GameStatus.STARTED, "Game just started");
        bool timeout = (block.timestamp -
            games[currentGameId].lastMoveTimestamp) > TURN_TIMEOUT;
        require(
            timeout || status != GameStatus.ONGOING,
            "Game is still ongoing"
        );
        currentGameId++;
    }

    function payPrize(uint256 gameId) public {
        require(gameId < currentGameId, "Game is not finished yet");
        require(
            msg.sender == games[gameId].playerX ||
                msg.sender == games[gameId].playerO,
            "Not a player or already paid out"
        );
        GameStatus status = getGameStatus(gameId);
        require(status != GameStatus.STARTED, "Game is in invalid state");

        if (status == GameStatus.ONGOING) {
            // Timeout situation
            status = GameStatus.X_WINS;
            if (getTurn(gameId) == Cell.X) {
                status = GameStatus.O_WINS;
            }
        }

        uint8 placedCellsToPay = getNumberOfPlacedCells(gameId) - 1;
        if (placedCellsToPay < 1) {
            placedCellsToPay = 1;
        }

        if (status == GameStatus.DRAW) {
            if (msg.sender == games[gameId].playerX) {
                status = GameStatus.X_WINS;
                placedCellsToPay =
                    getNumberOfEspecificPlacedCells(gameId, Cell.X) -
                    1;
                if (placedCellsToPay < 1) {
                    placedCellsToPay = 1;
                }
            } else if (msg.sender == games[gameId].playerO) {
                status = GameStatus.O_WINS;
                placedCellsToPay =
                    getNumberOfEspecificPlacedCells(gameId, Cell.O) -
                    1;
                if (placedCellsToPay < 1) {
                    placedCellsToPay = 1;
                }
            }
        }

        if (status == GameStatus.X_WINS) {
            require(msg.sender == games[gameId].playerX, "Not the winner");
            delete games[gameId].playerX;
            (bool sent, ) = payable(msg.sender).call{
                value: placedCellsToPay * PRICE
            }("");
            if (!sent) revert SendFailed();
        }

        if (status == GameStatus.O_WINS) {
            require(msg.sender == games[gameId].playerO, "Not the winner");
            delete games[gameId].playerO;
            (bool sent, ) = payable(msg.sender).call{
                value: placedCellsToPay * PRICE
            }("");
            if (!sent) revert SendFailed();
        }
    }

    function play(uint8 place) public payable {
        require(active, "Contract is not active");
        require(msg.value == PRICE, "Incorrect ETH amount");
        Cell senderCell = getSenderCell(currentGameId);
        Cell turn = getTurn(currentGameId);
        if (senderCell == Cell.EMPTY) {
            if (turn == Cell.X && isPlayerCellAvailable(Cell.X)) {
                games[currentGameId].playerX = msg.sender;
                senderCell = Cell.X;
            } else if (turn == Cell.O && isPlayerCellAvailable(Cell.O)) {
                games[currentGameId].playerO = msg.sender;
                senderCell = Cell.O;
            } else {
                revert("You are not part of this game");
            }
        } else {
            require(senderCell == turn, "Not your turn");
        }
        GameStatus status = getGameStatus(currentGameId);
        require(
            status == GameStatus.ONGOING || status == GameStatus.STARTED,
            "Game is over"
        );

        BoardState storage board = games[currentGameId].board;
        if (place == 1) {
            require(board.A1 == uint8(Cell.EMPTY), "Cell is occupied");
            board.A1 = uint8(senderCell);
        } else if (place == 2) {
            require(board.A2 == uint8(Cell.EMPTY), "Cell is occupied");
            board.A2 = uint8(senderCell);
        } else if (place == 3) {
            require(board.A3 == uint8(Cell.EMPTY), "Cell is occupied");
            board.A3 = uint8(senderCell);
        } else if (place == 4) {
            require(board.B1 == uint8(Cell.EMPTY), "Cell is occupied");
            board.B1 = uint8(senderCell);
        } else if (place == 5) {
            require(board.B2 == uint8(Cell.EMPTY), "Cell is occupied");
            board.B2 = uint8(senderCell);
        } else if (place == 6) {
            require(board.B3 == uint8(Cell.EMPTY), "Cell is occupied");
            board.B3 = uint8(senderCell);
        } else if (place == 7) {
            require(board.C1 == uint8(Cell.EMPTY), "Cell is occupied");
            board.C1 = uint8(senderCell);
        } else if (place == 8) {
            require(board.C2 == uint8(Cell.EMPTY), "Cell is occupied");
            board.C2 = uint8(senderCell);
        } else if (place == 9) {
            require(board.C3 == uint8(Cell.EMPTY), "Cell is occupied");
            board.C3 = uint8(senderCell);
        } else {
            revert("Invalid place");
        }
        games[currentGameId].lastMoveTimestamp = block.timestamp;
    }

    function adminWithdraw() public onlyOwner {
        require(!active, "Contract must be deactivated");

        (bool sent, ) = payable(owner).call{value: address(this).balance}("");
        if (!sent) revert SendFailed();
    }

    function activate() public onlyOwner {
        active = true;
    }

    function deactivate() public onlyOwner {
        active = false;
    }

    function fundContract() external payable onlyOwner {}
}

Tags:
addr:0xa092d0177684c02154b5ef2f87533e0982bcdca9|verified:true|block:23614298|tx:0xe06cce744da4b43edba7aca42b808d9a51985eed7dd22b738c0e570c77f08e4f|first_check:1760956319

Submitted on: 2025-10-20 12:31:59

Comments

Log in to comment.

No comments yet.