// SPDX-License-Identifier: MIT pragma solidity 0.8.30; /// @notice No-stakes, two-wallet tic-tac-toe. Each move is publicly verifiable. contract TactyraBoard { struct Game { address x; address o; uint8[9] cells; uint8 turn; uint8 status; uint8 winner; uint8 moves; } // status: 1 invited, 2 active, 3 finished, 4 cancelled; winner: 0 draw/none, 1 X, 2 O. uint256 public gameCount; mapping(uint256 => Game) private games; event Invited(uint256 indexed id, address indexed x, address indexed o); event Accepted(uint256 indexed id); event Moved(uint256 indexed id, address indexed account, uint8 cell, uint8 mark, uint8 winner, uint8 status); event Cancelled(uint256 indexed id); event Resigned(uint256 indexed id, address indexed account, uint8 winner); error InvalidOpponent(); error WrongState(); error NotParticipant(); error NotYourTurn(); error InvalidCell(); error Occupied(); function getGame(uint256 id) external view returns (Game memory) { return games[id]; } function create(address opponent) external returns (uint256 id) { if (opponent == address(0) || opponent == msg.sender) revert InvalidOpponent(); id = ++gameCount; Game storage g = games[id]; g.x = msg.sender; g.o = opponent; g.turn = 1; g.status = 1; emit Invited(id, msg.sender, opponent); } function accept(uint256 id) external { Game storage g = games[id]; if (g.status != 1) revert WrongState(); if (msg.sender != g.o) revert NotParticipant(); g.status = 2; emit Accepted(id); } function cancel(uint256 id) external { Game storage g = games[id]; if (g.status != 1) revert WrongState(); if (msg.sender != g.x) revert NotParticipant(); g.status = 4; emit Cancelled(id); } function move(uint256 id, uint8 cell) external { Game storage g = games[id]; if (g.status != 2) revert WrongState(); uint8 mark = msg.sender == g.x ? 1 : msg.sender == g.o ? 2 : 0; if (mark == 0) revert NotParticipant(); if (g.turn != mark) revert NotYourTurn(); if (cell > 8) revert InvalidCell(); if (g.cells[cell] != 0) revert Occupied(); g.cells[cell] = mark; ++g.moves; if (_won(g.cells, mark)) { g.winner = mark; g.status = 3; } else if (g.moves == 9) { g.status = 3; } else { g.turn = mark == 1 ? 2 : 1; } emit Moved(id, msg.sender, cell, mark, g.winner, g.status); } function resign(uint256 id) external { Game storage g = games[id]; if (g.status != 2) revert WrongState(); if (msg.sender != g.x && msg.sender != g.o) revert NotParticipant(); g.winner = msg.sender == g.x ? 2 : 1; g.status = 3; emit Resigned(id, msg.sender, g.winner); } function _won(uint8[9] storage b, uint8 m) private view returns (bool) { for (uint8 i; i < 3; ++i) { if (b[i*3] == m && b[i*3+1] == m && b[i*3+2] == m) return true; if (b[i] == m && b[i+3] == m && b[i+6] == m) return true; } return (b[0] == m && b[4] == m && b[8] == m) || (b[2] == m && b[4] == m && b[6] == m); } }