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 {}
}
Submitted on: 2025-10-20 12:31:59
Comments
Log in to comment.
No comments yet.