Game Logic

How Adrift uses onchain check-ins and randomness for a last-player-standing survival game.

Adrift is an onchain survival game where players must regularly check in to avoid disqualification. All game logic and randomness are enforced by smart contracts, with parameters like intervals, grace period, and outcome odds configurable by the admin. This ensures fairness, transparency, and an engaging experience. The last player remaining wins.

Players experience the game as follows:

  1. Register before the game starts.
  2. Check in regularly to stay in the game.
  3. Each check-in (handled by the Adrift and CheckInOutcomes contracts) has a random outcome: buff, debuff, or disqualification.
  4. Miss a check-in or get disqualified, and you’re out.
  5. The last player remaining wins.

Registration

// Key lines from register()
nextCheckIn[player] = CHECKIN_INITIALIZED;
playerCount++;
if (address(this).balance >= REGISTRATION_GAS) {
    Address.sendValue(payable(player), REGISTRATION_GAS);
}
  1. Before the game starts (before gameStartTime).
  2. Players register by calling the register function in the Adrift contract.
  3. The player's next check-in time is initialized.
  4. The player count increases.
  5. If the contract has enough balance, the player receives a registration gas reward (1 token by default), paid in the appchain's custom gas token. Custom gas tokens make subsidies simple and low cost.
  6. An event is emitted with registration details.

Game Start

// Key lines from setGameStartTime()
gameStartTime = startTime;
emit GameStartTimeSet(startTime);
  1. The game starts at a predefined gameStartTime (set by the admin).
  2. No check-ins are allowed before this time.

Check-In

// Key lines from checkIn()
int256 outcome = checkInOutcomes.getOutcome(msg.sender);
if (outcome == checkInOutcomes.DISQUALIFIED_OUTCOME()) {
    return disqualifyFromCheckIn(msg.sender, outcome);
}
nextCheckIn[msg.sender] = nextCheckInTime;
emit PlayerCheckedIn(msg.sender, block.timestamp, buffOrDebuff, nextCheckInTime, false);
  1. During the game, players must call checkIn before their nextCheckInTime lapses.
  2. The outcome of each check-in is determined by the CheckInOutcomes contract and can be a buff, debuff, or disqualification.
  3. If disqualified, the player is removed from the game.
  4. The next check-in time is updated based on the outcome.
  5. An event is emitted with the check-in result.

Disqualification

// Key lines from disqualifyInactivePlayer/disqualifyFromCheckIn
_disqualify(player, nextCheckInTime);
nextCheckIn[player] = CHECKIN_DISQUALIFIED;
isPlayerDQed[player] = true;
  1. If a player misses their check-in window, anyone can call disqualifyInactivePlayer in the Adrift contract to remove them.
  2. Disqualification can also occur randomly during check-in.
  3. The player's status is updated to disqualified and they are excluded from the game.
  4. An event is emitted for disqualification.

We run a simple cron job that periodically calls a heartbeat endpoint to automatically disqualify inactive players and finalize the game when a single active player remains.

Outcomes & Randomness

// Key lines from CheckInOutcomes.sol
uint256 rand = uint256(keccak256(abi.encodePacked(random.random(), playerNonces[player]++, player)));
if (rand % PRECISION < DISQUALIFICATION_CHANCE) {
    return DISQUALIFIED_OUTCOME;
}
uint256 outcome = (rand % OUTCOME_RANGE) + 1;
bool isNegative = (rand >> 128) % 2 == 0;
return isNegative ? -int256(outcome) : int256(outcome);
// Key lines from Random.sol
uint256 public random;
function setRandom(uint256 _random) external onlyRandomnessAdmin {
    random = _random;
}

Check-in outcomes are determined by the CheckInOutcomes contract, which uses fresh sequencer-injected randomness via the Random contract for every block. When a player checks in:

  1. The contract combines the latest random value, the player's address, and a nonce to generate a unique random number.
  2. There is a configurable chance (default: 2%) that the player is immediately disqualified.
  3. Otherwise, a random outcome (buff or debuff) is generated within a set range (default: 1–24), and its sign (positive/negative) is chosen randomly.
  4. The Random contract's value is set by the sequencer, ensuring unpredictability and auditability.

This system ensures that every check-in is unpredictable, fair, and verifiable onchain.

Winning & Game End

// Key lines from endGame()
if (playerCount == 1) {
    winner = player;
}
gameEndTime = block.timestamp;
emit GameEnded(gameEndTime, winner);
  1. The last remaining active player is declared the winner.
  2. If all players are disqualified, there is no winner.
  3. The game end time and winner are recorded and emitted as an event.

On this page