// SPDX-License-Identifier: MIT pragma solidity 0.8.30; /// @notice Experimental Sepolia-only competition. Shared-market coalition resistance is unproven. contract NightMarket { uint256 public constant VERSION=2; uint256 public constant ENTRY=0.005 ether; uint256 public constant RECOVERY_DELAY=2 days; uint256 public constant RECOVERY_GAS=3000000; uint256 public constant WINDOW=120; uint256 public constant PHASES=16; address public immutable house; bool public retired; uint256 public count; uint256 public houseFees; uint256 private entered; mapping(address=>uint256) public claims; struct Player {address wallet;uint32 cash;uint16[4] cargo;uint8 location;uint8 target;uint8 arrival;bool disqualified;} struct Market {uint16 stock;uint32 budget;uint16 ask;uint16 bid;} struct Order {uint8 buy;uint8 buyQty;uint8 sell;uint8 sellQty;uint8 minimum;uint8 dest;} struct Game {uint64 entryEnd;uint64 commitEnd;uint64 revealEnd;uint32 seed;uint8 seats;uint8 phase;bool settled;bool canceled;Player[4] players;Market[16] markets;bytes32[4] commitments;bool[4] revealed;Order[4] orders;} mapping(uint256=>Game) private games; event RecoveryAttempt(uint256 indexed id,bool refunded); event Created(uint256 indexed id,uint256 entryEnd);event Joined(uint256 indexed id,address indexed wallet,uint256 seat);event Committed(uint256 indexed id,uint256 phase,uint256 seat);event Revealed(uint256 indexed id,uint256 phase,uint256 seat,Order order);event Advanced(uint256 indexed id,uint256 phase);event Settled(uint256 indexed id);event Canceled(uint256 indexed id);event Paid(address indexed wallet,uint256 amount); constructor(){require(block.chainid==11155111||block.chainid==1337,"Test networks only");house=msg.sender;} modifier guarded(){require(entered==0,"Reentry");entered=1;_;entered=0;} function snapshot(uint256 id) external view returns(Game memory){require(id>0&&id<=count,"Unknown game");return games[id];} function create() external returns(uint256 id){require(msg.sender==house&&!retired,"House only / retired");id=++count;Game storage g=games[id];g.entryEnd=uint64(block.timestamp+15 minutes);g.seed=uint32(id);emit Created(id,g.entryEnd);} function retire() external{require(msg.sender==house,"House only");retired=true;} function join(uint256 id) external payable {Game storage g=games[id];require(id>0&&id<=count&&!retired&&block.timestamp0&&id<=count&&g.phase==0&&!g.canceled&&block.timestamp>=g.entryEnd,"Not refundable");g.canceled=true;for(uint256 i;i0&&!g.settled&&block.timestamp0&&!g.settled&&block.timestamp>=g.commitEnd&&block.timestamp0&&!g.settled,"Not active");bool ready=true;for(uint256 i;i<4;i++)if(!g.revealed[i]&&!g.players[i].disqualified)ready=false;require(block.timestamp>=g.revealEnd||(block.timestamp>=g.commitEnd&&ready),"Too early");for(uint256 i;i<4;i++)if(!g.revealed[i]){g.players[i].disqualified=true;delete g.orders[i];g.orders[i].dest=g.players[i].location;} clearSells(g);clearBuys(g);for(uint256 i;i<4;i++){Player storage p=g.players[i];Order storage o=g.orders[i];if(p.arrival==0&&o.dest!=p.location){(uint8 time,uint32 fee)=route(p.location,o.dest);if(p.cash>=fee&&o.minimum==0){p.cash-=fee;p.target=o.dest;p.arrival=g.phase+time;}}delete g.orders[i];delete g.commitments[i];g.revealed[i]=false;} if(g.phase==16){settle(g);emit Settled(id);}else{g.phase++;for(uint256 i;i<4;i++){Player storage p=g.players[i];if(p.arrival>0&&p.arrival<=g.phase){p.location=p.target;p.arrival=0;}}update(g);g.commitEnd=uint64(block.timestamp+WINDOW);g.revealEnd=g.commitEnd+uint64(WINDOW);}emit Advanced(id,g.phase);} /// @notice A healthy game advances; only an objectively failing resolution may refund. /// @dev Fixed forwarded gas and an EIP-150 headroom check prevent caller gas starvation. function recover(uint256 id) external guarded { Game storage g=games[id]; require(id>0&&id<=count&&g.phase>0&&!g.settled&&!g.canceled,"Not active"); require(block.timestamp>=uint256(g.revealEnd)+RECOVERY_DELAY,"Recovery delay"); require(gasleft()>=RECOVERY_GAS+RECOVERY_GAS/63+250000,"Insufficient recovery gas"); try this.resolve{gas:RECOVERY_GAS}(id) {emit RecoveryAttempt(id,false);} catch { // The reverted inner call leaves no partial trades or prize allocations behind. g.canceled=true;g.settled=true;retired=true; for(uint256 i;i<4;i++)claims[g.players[i].wallet]+=ENTRY; emit Canceled(id);emit RecoveryAttempt(id,true); } } function route(uint8 a,uint8 b) public pure returns(uint8,uint32){if(a==b)return(0,0);uint8 diff=a>b?a-b:b-a;return diff==1||diff==3?(uint8(1),uint32(18)):(uint8(2),uint32(10));} function base(uint256 j) private pure returns(uint32){return [uint32(45),75,110,150][j];} function salvage(uint256 j) private pure returns(uint32){return [uint32(12),20,30,40][j];} function update(Game storage g) private{for(uint256 d;d<4;d++)for(uint256 j;j<4;j++){Market storage m=g.markets[d*4+j];int256 stock=int256(uint256(m.stock))+(d==j?int256(7):int256(2))-((g.seed+g.phase*7+d*3+j*5)%9<3?int256(7):int256(3));m.stock=uint16(uint256(stock<0?int256(0):stock>40?int256(40):stock));uint32 mid=(base(j)*(272-uint32(m.stock)*5)+80)/160;m.ask=uint16(mid+6>salvage(j)+5?mid+6:salvage(j)+5);m.bid=uint16(mid>salvage(j)+6?mid-6:salvage(j));m.budget=base(j)*12;}} function min(uint256 a,uint256 b) private pure returns(uint256){return a0){uint16 q=uint16(total>cap?qty[i]*cap/total:qty[i]);g.players[i].cargo[j]-=q;uint32 value=uint32(q)*m.bid;g.players[i].cash+=value;m.stock+=q;m.budget-=value;}}} function clearBuys(Game storage g) private{uint256[4] memory requests;for(uint256 i;i<4;i++){Player storage p=g.players[i];Order storage o=g.orders[i];if(p.arrival>0)continue;uint256 cargo;for(uint256 j;j<4;j++)cargo+=p.cargo[j];(,uint32 fee)=route(p.location,o.dest);uint256 money=p.cash>=fee?p.cash-fee:0;requests[i]=min(o.buyQty,min(12-cargo,money/g.markets[p.location*4+o.buy].ask));} for(uint256 d;d<4;d++)for(uint256 j;j<4;j++){Market storage m=g.markets[d*4+j];uint256 total;for(uint256 i;i<4;i++)if(g.players[i].arrival==0&&g.players[i].location==d&&g.orders[i].buy==j)total+=requests[i];uint256 available=m.stock;for(uint256 i;i<4;i++){Player storage p=g.players[i];Order storage o=g.orders[i];if(p.arrival>0||p.location!=d||o.buy!=j)continue;uint16 q=uint16(total>available?requests[i]*available/total:requests[i]);if(q=o.minimum)o.minimum=0;else o.minimum=1;}}} function score(Player memory p) public pure returns(uint256 n){n=p.cash;for(uint256 j;j<4;j++)n+=uint256(p.cargo[j])*salvage(j);} function settle(Game storage g) private{g.settled=true;uint256 pool=4*ENTRY;uint256 eligible;for(uint256 i;i<4;i++)if(!g.players[i].disqualified)eligible++;if(eligible==0){for(uint256 i;i<4;i++)claims[g.players[i].wallet]+=ENTRY;return;}uint256 fee=pool/100;houseFees+=fee;uint256 net=pool-fee;uint256 allocated;for(uint256 i;i<4;i++){Player storage p=g.players[i];if(p.disqualified)continue;uint256 above;uint256 equal;uint256 value=score(p);for(uint256 j;j<4;j++)if(!g.players[j].disqualified){uint256 other=score(g.players[j]);if(other>value)above++;if(other==value)equal++;}uint256 share;for(uint256 r=above;r0,"Nothing to claim");claims[msg.sender]=0;(bool ok,)=recipient.call{value:amount}("");require(ok,"Transfer failed");emit Paid(msg.sender,amount);} function withdrawFees(address payable recipient) external guarded{require(msg.sender==house&&recipient!=address(0),"House only");uint256 amount=houseFees;houseFees=0;(bool ok,)=recipient.call{value:amount}("");require(ok,"Transfer failed");} }