Skip to main content
PulseScanner.io

Address

0x29b95c4e1c35e6616badf324ef593b9c8a122001
Current Holdings
$0.00
TXs sent
not counted
First Active
2026-02-24
block 25,868,928
Last Active
203 days ago
block 25,868,928
Funded By
not identified

Net worth historyi

No net-worth snapshots recorded yet
exact matchW1NNRTournamentsolc 0.8.20+commit.a1b79de6runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title W1NNRTournament
 * @notice Single-elimination bracket tournament with on-chain prize escrow.
 * @dev    Designed to never brick. Every failure mode has an escape hatch.
 *
 * ANTI-BRICK SCENARIOS:
 *   1. No-show          → claimNoShow() after noShowWindow
 *   2. Dispute + no admin → forceAdvance() after disputeWindow (first-reporter wins)
 *   3. Admin never locks  → selfLockBracket() after deadline + 6h grace
 *   4. Not enough players → cancelTournament() after deadline, full refunds
 *   5. Treasury fails     → fee stored in pendingTreasuryFee, prizes still distribute
 *   6. Rounding dust      → added to 1st place prize
 */

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

interface IW1NNRPlayerRegistry {
    function registerFor(address wallet) external;
}

struct TournamentConfig {
    address admin;
    address treasury;
    address token;
    address playerRegistry;
    uint256 entryFee;
    uint256 maxPlayers;
    uint256 sportId;
    uint256 registrationDeadline;
    string  name;
    uint256 noShowWindow;
    uint256 disputeWindow;
}

contract W1NNRTournament {

    uint256 public constant FEE_BPS           = 800;
    uint256 public constant BPS_DENOMINATOR   = 10000;
    uint256 public constant LOCK_GRACE_PERIOD = 6 hours;

    enum TournamentState { Registration, Locked, Complete, Cancelled }
    enum MatchState      { Pending, Active, Disputed, Complete }

    // ── Immutable config ──────────────────────────────────
    address public admin;
    address public treasury;
    address public token;
    address public playerRegistry;
    uint256 public entryFee;
    uint256 public maxPlayers;
    uint256 public sportId;
    uint256 public registrationDeadline;
    uint256 public noShowWindow;
    uint256 public disputeWindow;

    string public tournamentName;

    // ── Mutable state ─────────────────────────────────────
    TournamentState public state;

    address[]                   public entrants;
    mapping(address => bool)    public isEntrant;
    mapping(address => uint256) public entrantRating;
    mapping(address => bool)    public refundClaimed;

    address[]  public seeds;
    uint256    public roundCount;
    uint256    public currentRound;
    uint256    public roundStartTime;
    bool       public bracketLocked;
    bool       private _initialized;

    mapping(uint256 => mapping(uint256 => Match)) public matches;

    struct Match {
        address playerA;
        address playerB;
        address reportedByA;
        address reportedByB;
        uint256 reportedByA_time;
        uint256 reportedByB_time;
        address winner;
        MatchState state;
        uint256 disputeTime;
    }

    mapping(address => uint256) public prizeOwed;
    mapping(address => bool)    public prizeClaimed;
    uint256 public pendingTreasuryFee;

    // ── Events ────────────────────────────────────────────
    event PlayerRegistered(address indexed player, uint256 rating, uint256 count);
    event BracketLocked(address[] seeds, uint256 rounds, address lockedBy);
    event RoundStarted(uint256 indexed round, uint256 startTime);
    event WinnerReported(uint256 indexed round, uint256 indexed matchIndex, address indexed reporter, address reportedWinner);
    event MatchComplete(uint256 indexed round, uint256 indexed matchIndex, address indexed winner, string method);
    event MatchDisputed(uint256 indexed round, uint256 indexed matchIndex);
    event PrizesAllocated(address indexed first, uint256 firstAmt, address indexed second, uint256 secondAmt);
    event PrizeClaimed(address indexed player, uint256 amount);
    event RefundClaimed(address indexed player, uint256 amount);
    event TournamentCancelled(address indexed by, string reason);
    event TreasuryFeeFailed(uint256 amount);
    event TreasuryFeeSwept(uint256 amount);

    // ── View structs (avoids stack-too-deep in ABI encoder) ──
    struct MatchView {
        address playerA;
        address playerB;
        address reportedByA;
        address reportedByB;
        address winner;
        MatchState matchState;
        uint256 disputeTime;
    }

    struct TournamentInfoView {
        string  name;
        uint256 sport;
        uint256 fee;
        uint256 maxP;
        uint256 currentP;
        uint256 deadline;
        TournamentState tState;
    }

    struct PrizeView {
        uint256 totalPool;
        uint256 platformFee;
        uint256 netPool;
        uint256 firstPrize;
        uint256 secondPrize;
        uint256 thirdFourthPrize;
    }

    struct ProgressView {
        uint256 curRound;
        uint256 rounds;
        uint256 totalPool;
        uint256 noShow;
        uint256 dispute;
    }

    struct RoundMatchesView {
        address[]    playerAs;
        address[]    playerBs;
        address[]    winners;
        MatchState[] states;
    }

    struct PlayerStatusView {
        bool    registered;
        uint256 rating;
        uint256 seedPos;
        uint256 prize;
        bool    claimed;
    }

    // ── Constructor ───────────────────────────────────────
    constructor(TournamentConfig memory cfg) {
        require(cfg.admin    != address(0), "Invalid admin");
        require(cfg.treasury != address(0), "Invalid treasury");
        require(cfg.token    != address(0), "Invalid token");
        require(cfg.entryFee > 0,           "Entry fee required");
        require(cfg.maxPlayers == 4 || cfg.maxPlayers == 8 || cfg.maxPlayers == 16, "Players must be 4, 8, or 16");
        require(cfg.registrationDeadline > block.timestamp, "Deadline must be future");
        require(bytes(cfg.name).length > 0,  "Name required");
        require(cfg.noShowWindow  >= 30 minutes, "noShowWindow min 30m");
        require(cfg.disputeWindow >= 30 minutes, "disputeWindow min 30m");

        _initConfig(cfg);
        roundCount = cfg.maxPlayers == 4 ? 2 : cfg.maxPlayers == 8 ? 3 : 4;
        state      = TournamentState.Registration;
    }

    function _initConfig(TournamentConfig memory cfg) private {
        admin                = cfg.admin;
        treasury             = cfg.treasury;
        token                = cfg.token;
        playerRegistry       = cfg.playerRegistry;
        entryFee             = cfg.entryFee;
        maxPlayers           = cfg.maxPlayers;
        sportId              = cfg.sportId;
        registrationDeadline = cfg.registrationDeadline;
        tournamentName       = cfg.name;
        noShowWindow         = cfg.noShowWindow;
        disputeWindow        = cfg.disputeWindow;
    }

    /**
     * @notice Initializer for EIP-1167 clone deployments.
     * @dev    Called by the factory immediately after cloning.
     *         Can only be called once. Constructor is used for the master.
     */
    function initialize(TournamentConfig calldata cfg) external {
        require(!_initialized, "Already initialized");
        _initialized = true;

        require(cfg.admin    != address(0), "Invalid admin");
        require(cfg.treasury != address(0), "Invalid treasury");
        require(cfg.token    != address(0), "Invalid token");
        require(cfg.entryFee > 0,           "Entry fee required");
        require(cfg.maxPlayers == 4 || cfg.maxPlayers == 8 || cfg.maxPlayers == 16, "Players must be 4, 8, or 16");
        require(cfg.registrationDeadline > block.timestamp, "Deadline must be future");
        require(bytes(cfg.name).length > 0,  "Name required");
        require(cfg.noShowWindow  >= 30 minutes, "noShowWindow min 30m");
        require(cfg.disputeWindow >= 30 minutes, "disputeWindow min 30m");

        _initConfig(cfg);
        roundCount = cfg.maxPlayers == 4 ? 2 : cfg.maxPlayers == 8 ? 3 : 4;
        state      = TournamentState.Registration;
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "Not admin");
        _;
    }

    modifier inState(TournamentState s) {
        require(state == s, "Wrong tournament state");
        _;
    }

    // ══════════════════════════════════════════
    // REGISTRATION
    // ══════════════════════════════════════════

    function register(uint256 rating) external inState(TournamentState.Registration) {
        require(block.timestamp < registrationDeadline, "Registration closed");
        require(entrants.length < maxPlayers,           "Tournament full");
        require(!isEntrant[msg.sender],                 "Already registered");
        require(IERC20(token).transferFrom(msg.sender, address(this), entryFee), "Transfer failed");

        isEntrant[msg.sender]     = true;
        entrantRating[msg.sender] = rating > 0 ? rating : 1000;
        entrants.push(msg.sender);

        if (playerRegistry != address(0)) {
            try IW1NNRPlayerRegistry(playerRegistry).registerFor(msg.sender) {} catch {}
        }

        emit PlayerRegistered(msg.sender, entrantRating[msg.sender], entrants.length);

        if (entrants.length == maxPlayers) {
            _lockWithSeeds(_sortByRating(entrants));
        }
    }

    // ══════════════════════════════════════════
    // BRACKET LOCKING
    // ══════════════════════════════════════════

    function lockBracket(address[] calldata _seeds)
        external
        onlyAdmin
        inState(TournamentState.Registration)
    {
        require(!bracketLocked,                          "Already locked");
        require(block.timestamp >= registrationDeadline, "Deadline not passed");
        require(_seeds.length == entrants.length,        "Must include all entrants");
        require(_seeds.length >= 4,                      "Minimum 4 players");
        require(
            _seeds.length == 4 || _seeds.length == 8 || _seeds.length == 16,
            "Must be 4, 8, or 16"
        );

        for (uint256 i = 0; i < _seeds.length; i++) {
            require(isEntrant[_seeds[i]], "Not registered");
            for (uint256 j = i + 1; j < _seeds.length; j++) {
                require(_seeds[i] != _seeds[j], "Duplicate");
            }
        }

        address[] memory s = new address[](_seeds.length);
        for (uint256 i = 0; i < _seeds.length; i++) s[i] = _seeds[i];
        _lockWithSeeds(s);
        emit BracketLocked(s, roundCount, msg.sender);
    }

    function selfLockBracket() external inState(TournamentState.Registration) {
        require(!bracketLocked,  "Already locked");
        require(isEntrant[msg.sender], "Not an entrant");
        require(block.timestamp >= registrationDeadline + LOCK_GRACE_PERIOD, "Grace period not passed");
        require(entrants.length >= 4, "Not enough players");

        address[] memory sorted = _sortByRating(entrants);
        uint256 bracketSize = _nearestBracketSize(entrants.length);

        address[] memory bracketSeeds = new address[](bracketSize);
        for (uint256 i = 0; i < bracketSize; i++) bracketSeeds[i] = sorted[i];

        for (uint256 i = bracketSize; i < sorted.length; i++) {
            refundClaimed[sorted[i]] = true;
            require(IERC20(token).transfer(sorted[i], entryFee), "Refund failed");
        }

        _lockWithSeeds(bracketSeeds);
        emit BracketLocked(bracketSeeds, roundCount, msg.sender);
    }

    // ══════════════════════════════════════════
    // MATCH PLAY
    // ══════════════════════════════════════════

    function reportWinner(uint256 round, uint256 matchIndex, address winner)
        external
        inState(TournamentState.Locked)
    {
        require(round == currentRound, "Not current round");

        Match storage m = matches[round][matchIndex];
        require(m.state == MatchState.Active,                       "Match not active");
        require(msg.sender == m.playerA || msg.sender == m.playerB, "Not a participant");
        require(winner == m.playerA || winner == m.playerB,         "Invalid winner");

        if (msg.sender == m.playerA) {
            require(m.reportedByA == address(0), "Already reported");
            m.reportedByA      = winner;
            m.reportedByA_time = block.timestamp;
        } else {
            require(m.reportedByB == address(0), "Already reported");
            m.reportedByB      = winner;
            m.reportedByB_time = block.timestamp;
        }

        emit WinnerReported(round, matchIndex, msg.sender, winner);

        if (m.reportedByA != address(0) && m.reportedByB != address(0)) {
            if (m.reportedByA == m.reportedByB) {
                _confirmWinner(round, matchIndex, winner, "agreement");
            } else {
                m.state       = MatchState.Disputed;
                m.disputeTime = block.timestamp;
                emit MatchDisputed(round, matchIndex);
            }
        }
    }

    function claimNoShow(uint256 round, uint256 matchIndex)
        external
        inState(TournamentState.Locked)
    {
        require(round == currentRound, "Not current round");
        require(block.timestamp >= roundStartTime + noShowWindow, "Window not passed");

        Match storage m = matches[round][matchIndex];
        require(m.state == MatchState.Active,                       "Match not active");
        require(msg.sender == m.playerA || msg.sender == m.playerB, "Not a participant");

        if (msg.sender == m.playerA) {
            require(m.reportedByB == address(0), "Opponent reported - use forceAdvance");
        } else {
            require(m.reportedByA == address(0), "Opponent reported - use forceAdvance");
        }

        _confirmWinner(round, matchIndex, msg.sender, "no-show");
    }

    function forceAdvance(uint256 round, uint256 matchIndex)
        external
        inState(TournamentState.Locked)
    {
        Match storage m = matches[round][matchIndex];
        require(m.state == MatchState.Disputed, "Not disputed");
        require(
            msg.sender == m.playerA || msg.sender == m.playerB || msg.sender == admin,
            "Not authorized"
        );
        require(block.timestamp >= m.disputeTime + disputeWindow, "Window not passed");

        address winner = _firstReporter(m);
        require(winner == m.playerA || winner == m.playerB, "Invalid winner");

        _confirmWinner(round, matchIndex, winner, "force-advance");
    }

    function adminResolveDispute(uint256 round, uint256 matchIndex, address winner)
        external
        onlyAdmin
        inState(TournamentState.Locked)
    {
        Match storage m = matches[round][matchIndex];
        require(m.state == MatchState.Disputed,             "Not disputed");
        require(winner == m.playerA || winner == m.playerB, "Invalid winner");
        _confirmWinner(round, matchIndex, winner, "admin-resolved");
    }

    // ══════════════════════════════════════════
    // CANCELLATION
    // ══════════════════════════════════════════

    function cancelTournament(string calldata reason)
        external
        inState(TournamentState.Registration)
    {
        require(
            msg.sender == admin ||
            (block.timestamp >= registrationDeadline && entrants.length < 4),
            "Cannot cancel yet"
        );
        state = TournamentState.Cancelled;
        emit TournamentCancelled(msg.sender, reason);
    }

    // ══════════════════════════════════════════
    // CLAIMS
    // ══════════════════════════════════════════

    function claimPrize() external inState(TournamentState.Complete) {
        uint256 amount = prizeOwed[msg.sender];
        require(amount > 0,                "No prize owed");
        require(!prizeClaimed[msg.sender], "Already claimed");
        prizeClaimed[msg.sender] = true;
        prizeOwed[msg.sender]    = 0;
        require(IERC20(token).transfer(msg.sender, amount), "Transfer failed");
        emit PrizeClaimed(msg.sender, amount);
    }

    function claimRefund() external inState(TournamentState.Cancelled) {
        require(isEntrant[msg.sender],      "Not registered");
        require(!refundClaimed[msg.sender], "Already refunded");
        refundClaimed[msg.sender] = true;
        require(IERC20(token).transfer(msg.sender, entryFee), "Refund failed");
        emit RefundClaimed(msg.sender, entryFee);
    }

    function sweepTreasuryFee() external onlyAdmin {
        uint256 amount = pendingTreasuryFee;
        require(amount > 0, "Nothing to sweep");
        pendingTreasuryFee = 0;
        require(IERC20(token).transfer(treasury, amount), "Sweep failed");
        emit TreasuryFeeSwept(amount);
    }

    // ══════════════════════════════════════════
    // INTERNAL: BRACKET
    // ══════════════════════════════════════════

    function _lockWithSeeds(address[] memory _seeds) internal {
        require(!bracketLocked, "Already locked");
        bracketLocked = true;

        uint256 n  = _seeds.length;
        roundCount = n == 4 ? 2 : n == 8 ? 3 : 4;

        delete seeds;
        for (uint256 i = 0; i < n; i++) seeds.push(_seeds[i]);

        for (uint256 i = 0; i < n / 2; i++) {
            matches[0][i] = Match({
                playerA:          _seeds[i],
                playerB:          _seeds[n - 1 - i],
                reportedByA:      address(0),
                reportedByB:      address(0),
                reportedByA_time: 0,
                reportedByB_time: 0,
                winner:           address(0),
                state:            MatchState.Active,
                disputeTime:      0
            });
        }

        state          = TournamentState.Locked;
        currentRound   = 0;
        roundStartTime = block.timestamp;
        emit RoundStarted(0, block.timestamp);
    }

    /// @dev Confirms winner, marks match complete, checks if round is done.
    function _confirmWinner(
        uint256 round,
        uint256 matchIndex,
        address winner,
        string memory method
    ) internal {
        matches[round][matchIndex].winner = winner;
        matches[round][matchIndex].state  = MatchState.Complete;
        emit MatchComplete(round, matchIndex, winner, method);
        _tryAdvanceRound(round);
    }

    /// @dev Checks if all matches in `round` are complete. If so, builds next round or distributes prizes.
    function _tryAdvanceRound(uint256 round) internal {
        uint256 count = _matchCount(round);
        for (uint256 i = 0; i < count; i++) {
            if (matches[round][i].state != MatchState.Complete) return;
        }
        // All done
        if (round + 1 >= roundCount) {
            _distributePrizes(round);
        } else {
            _buildNextRound(round);
        }
    }

    /// @dev Pairs winners from `round` into `round+1` matches.
    function _buildNextRound(uint256 round) internal {
        uint256 count     = _matchCount(round);
        uint256 nextRound = round + 1;
        for (uint256 i = 0; i < count / 2; i++) {
            matches[nextRound][i] = Match({
                playerA:          matches[round][i * 2].winner,
                playerB:          matches[round][i * 2 + 1].winner,
                reportedByA:      address(0),
                reportedByB:      address(0),
                reportedByA_time: 0,
                reportedByB_time: 0,
                winner:           address(0),
                state:            MatchState.Active,
                disputeTime:      0
            });
        }
        currentRound   = nextRound;
        roundStartTime = block.timestamp;
        emit RoundStarted(nextRound, block.timestamp);
    }

    // ══════════════════════════════════════════
    // INTERNAL: PRIZES
    // ══════════════════════════════════════════

    function _distributePrizes(uint256 finalRound) internal {
        uint256 prizePool = _sendFee();
        address first     = matches[finalRound][0].winner;
        address second    = matches[finalRound][0].playerA == first
            ? matches[finalRound][0].playerB
            : matches[finalRound][0].playerA;
        _allocatePrizes(finalRound, prizePool, first, second);
        state = TournamentState.Complete;
    }

    function _sendFee() internal returns (uint256 prizePool) {
        uint256 totalPool = entryFee * seeds.length;
        uint256 fee       = (totalPool * FEE_BPS) / BPS_DENOMINATOR;
        prizePool         = totalPool - fee;
        bool ok;
        try IERC20(token).transfer(treasury, fee) returns (bool result) { ok = result; } catch {}
        if (!ok) {
            pendingTreasuryFee += fee;
            emit TreasuryFeeFailed(fee);
        }
    }

    function _allocatePrizes(uint256 finalRound, uint256 prizePool, address first, address second) internal {
        uint256 n = seeds.length;
        uint256 firstPrize;
        uint256 secondPrize;
        uint256 allocated;

        if (n == 4) {
            firstPrize  = (prizePool * 7000) / BPS_DENOMINATOR;
            secondPrize = prizePool - firstPrize;
            allocated   = prizePool;
        } else {
            uint256 semiBps   = n == 8 ? 500 : 1000;
            uint256 firstBps  = n == 8 ? 6500 : 5500;
            firstPrize        = (prizePool * firstBps) / BPS_DENOMINATOR;
            secondPrize       = (prizePool * 2500) / BPS_DENOMINATOR;
            uint256 semiEach  = (prizePool * semiBps) / BPS_DENOMINATOR;
            allocated         = firstPrize + secondPrize + (semiEach * 2);
            _awardSemifinalists(finalRound - 1, semiEach);
        }

        firstPrize       += prizePool - allocated; // dust → 1st
        prizeOwed[first]  += firstPrize;
        prizeOwed[second] += secondPrize;
        emit PrizesAllocated(first, firstPrize, second, secondPrize);
    }

    function _awardSemifinalists(uint256 semiRound, uint256 prize) internal {
        uint256 count = _matchCount(semiRound);
        for (uint256 i = 0; i < count; i++) {
            address w = matches[semiRound][i].winner;
            address loser = matches[semiRound][i].playerA == w
                ? matches[semiRound][i].playerB
                : matches[semiRound][i].playerA;
            prizeOwed[loser] += prize;
        }
    }

    // ══════════════════════════════════════════
    // INTERNAL: HELPERS
    // ══════════════════════════════════════════

    function _matchCount(uint256 round) internal view returns (uint256) {
        return (seeds.length / 2) >> round;
    }

    /// @dev First-reporter wins a dispute. Tie goes to playerA.
    function _firstReporter(Match storage m) internal view returns (address) {
        if (m.reportedByA != address(0) && m.reportedByB != address(0)) {
            return m.reportedByA_time <= m.reportedByB_time ? m.reportedByA : m.reportedByB;
        }
        return m.reportedByA != address(0) ? m.reportedByA : m.reportedByB;
    }

    function _sortByRating(address[] storage arr) internal view returns (address[] memory) {
        uint256 n = arr.length;
        address[] memory sorted = new address[](n);
        for (uint256 i = 0; i < n; i++) sorted[i] = arr[i];
        for (uint256 i = 1; i < n; i++) {
            address key       = sorted[i];
            uint256 keyRating = entrantRating[key];
            int256  j         = int256(i) - 1;
            while (j >= 0 && entrantRating[sorted[uint256(j)]] < keyRating) {
                sorted[uint256(j + 1)] = sorted[uint256(j)];
                j--;
            }
            sorted[uint256(j + 1)] = key;
        }
        return sorted;
    }

    function _nearestBracketSize(uint256 n) internal pure returns (uint256) {
        if (n >= 16) return 16;
        if (n >= 8)  return 8;
        return 4;
    }

    // ══════════════════════════════════════════
    // VIEWS
    // ══════════════════════════════════════════

    function getEntrants() external view returns (address[] memory) { return entrants; }
    function getSeeds()    external view returns (address[] memory) { return seeds; }

    function getMatch(uint256 round, uint256 matchIndex)
        external view
        returns (MatchView memory v)
    {
        Match storage m = matches[round][matchIndex];
        v.playerA    = m.playerA;
        v.playerB    = m.playerB;
        v.reportedByA = m.reportedByA;
        v.reportedByB = m.reportedByB;
        v.winner     = m.winner;
        v.matchState = m.state;
        v.disputeTime = m.disputeTime;
    }

    function getRoundMatches(uint256 round)
        external view
        returns (RoundMatchesView memory v)
    {
        uint256 count = _matchCount(round);
        v.playerAs = new address[](count);
        v.playerBs = new address[](count);
        v.winners  = new address[](count);
        v.states   = new MatchState[](count);
        for (uint256 i = 0; i < count; i++) {
            v.playerAs[i] = matches[round][i].playerA;
            v.playerBs[i] = matches[round][i].playerB;
            v.winners[i]  = matches[round][i].winner;
            v.states[i]   = matches[round][i].state;
        }
    }

    function getTournamentInfo()
        external view
        returns (TournamentInfoView memory v)
    {
        v.name     = tournamentName;
        v.sport    = sportId;
        v.fee      = entryFee;
        v.maxP     = maxPlayers;
        v.currentP = entrants.length;
        v.deadline = registrationDeadline;
        v.tState   = state;
    }

    function getTournamentProgress()
        external view
        returns (ProgressView memory v)
    {
        v.curRound  = currentRound;
        v.rounds    = roundCount;
        v.totalPool = entryFee * entrants.length;
        v.noShow    = noShowWindow;
        v.dispute   = disputeWindow;
    }

    function getPlayerStatus(address player)
        external view
        returns (PlayerStatusView memory v)
    {
        v.registered = isEntrant[player];
        v.rating     = entrantRating[player];
        v.prize      = prizeOwed[player];
        v.claimed    = prizeClaimed[player];
        for (uint256 i = 0; i < seeds.length; i++) {
            if (seeds[i] == player) { v.seedPos = i + 1; break; }
        }
    }

    function getPrizeBreakdown()
        external view
        returns (PrizeView memory v)
    {
        uint256 n    = seeds.length > 0 ? seeds.length : maxPlayers;
        v.totalPool  = entryFee * n;
        v.platformFee = (v.totalPool * FEE_BPS) / BPS_DENOMINATOR;
        v.netPool    = v.totalPool - v.platformFee;
        if (n <= 4) {
            v.firstPrize       = (v.netPool * 7000) / BPS_DENOMINATOR;
            v.secondPrize      = v.netPool - v.firstPrize;
            v.thirdFourthPrize = 0;
        } else if (n <= 8) {
            v.firstPrize       = (v.netPool * 6500) / BPS_DENOMINATOR;
            v.secondPrize      = (v.netPool * 2500) / BPS_DENOMINATOR;
            v.thirdFourthPrize = (v.netPool * 500)  / BPS_DENOMINATOR;
        } else {
            v.firstPrize       = (v.netPool * 5500) / BPS_DENOMINATOR;
            v.secondPrize      = (v.netPool * 2500) / BPS_DENOMINATOR;
            v.thirdFourthPrize = (v.netPool * 1000) / BPS_DENOMINATOR;
        }
    }

    function canClaimNoShow(uint256 round, uint256 matchIndex, address caller)
        external view
        returns (bool eligible, string memory reason)
    {
        if (state != TournamentState.Locked)                          return (false, "Not locked");
        if (round != currentRound)                                    return (false, "Not current round");
        if (block.timestamp < roundStartTime + noShowWindow)          return (false, "Window not passed");
        Match storage m = matches[round][matchIndex];
        if (m.state != MatchState.Active)                             return (false, "Match not active");
        if (caller != m.playerA && caller != m.playerB)               return (false, "Not a participant");
        if (caller == m.playerA && m.reportedByB != address(0))       return (false, "Opponent did report");
        if (caller == m.playerB && m.reportedByA != address(0))       return (false, "Opponent did report");
        return (true, "");
    }

    function canForceAdvance(uint256 round, uint256 matchIndex)
        external view
        returns (bool eligible, string memory reason)
    {
        if (state != TournamentState.Locked)                          return (false, "Not locked");
        Match storage m = matches[round][matchIndex];
        if (m.state != MatchState.Disputed)                           return (false, "Not disputed");
        if (block.timestamp < m.disputeTime + disputeWindow)          return (false, "Window not passed");
        return (true, "");
    }
}