// SPDX-License-Identifier: MIT pragma solidity 0.8.30; interface IEscrowState { function engine() external view returns (address); function joined(uint256, address) external view returns (bool); function exited(uint256, address) external view returns (bool); function game(uint256) external view returns (address, uint64, uint8, uint256, uint256, address[] memory); } /// @notice Transitional rules engine: an explicit human scorekeeper reports final /// NYSA cash-outs. It does not generate dice or prove game results. contract NysaScorekeeperEngine { address public immutable scorekeeper; address public immutable deployer; address public escrow; mapping(uint256 => bool) public started; mapping(uint256 => uint256) public reserved; mapping(uint256 => uint256) public pendingReports; mapping(uint256 => mapping(address => bool)) public reported; mapping(uint256 => mapping(address => uint256)) public payout; event Bound(address indexed escrow); event CashOutReported(uint256 indexed gameId, address indexed player, uint256 amount); constructor(address scorekeeper_) { require(scorekeeper_ != address(0), "scorekeeper required"); scorekeeper = scorekeeper_; deployer = msg.sender; } function bind(address escrow_) external { require(msg.sender == deployer && escrow == address(0), "only deployer, once"); require(escrow_.code.length > 0 && IEscrowState(escrow_).engine() == address(this), "engine mismatch"); escrow = escrow_; emit Bound(escrow_); } function startGame(uint256 id, address[] calldata, uint256) external { require(msg.sender == escrow && !started[id], "escrow only, once"); started[id] = true; } /// @notice Reports a FINAL exit amount in atomic units (1 = 0.001 NYSA). /// Once reported it cannot be edited, including before withdrawal. function reportCashOut(uint256 id, address player, uint256 amount) external { require(msg.sender == scorekeeper, "scorekeeper only"); IEscrowState vault = IEscrowState(escrow); (,, uint8 phase, uint256 remaining, uint256 active,) = vault.game(id); require(started[id] && phase == 2 && vault.joined(id, player) && !vault.exited(id, player), "not active player"); require(!reported[id][player], "already reported"); uint256 nextReserved = reserved[id] + amount; require(nextReserved <= remaining, "exceeds available pot"); // With every active player's result reported, none of the pot may strand. if (pendingReports[id] + 1 == active) require(nextReserved == remaining, "final report must balance pot"); reserved[id] = nextReserved; pendingReports[id]++; reported[id][player] = true; payout[id][player] = amount; emit CashOutReported(id, player, amount); } function exitGame(uint256 id, address player) external returns (uint256 amount) { require(msg.sender == escrow && reported[id][player], "escrow only, score required"); amount = payout[id][player]; reserved[id] -= amount; pendingReports[id]--; } }