Lógica del juego

Cómo Adrift utiliza registros en cadena y aleatoriedad para un juego de supervivencia donde gana el último jugador en pie.

Adrift es un juego de supervivencia en cadena donde los jugadores deben registrarse regularmente para evitar la descalificación. Toda la lógica del juego y la aleatoriedad se aplican mediante contratos inteligentes, con parámetros como intervalos, período de gracia y probabilidades de resultados configurables por el administrador. Esto garantiza equidad, transparencia y una experiencia atractiva. El último jugador que quede gana.

Los jugadores experimentan el juego de la siguiente manera:

  1. Registrarse antes de que comience el juego.
  2. Registrarse regularmente para permanecer en el juego.
  3. Cada registro (gestionado por los contratos Adrift y CheckInOutcomes) tiene un resultado aleatorio: beneficio, penalización o descalificación.
  4. Si te pierdes un registro o eres descalificado, quedas fuera.
  5. El último jugador que quede gana.

Registro

// Key lines from register()
nextCheckIn[player] = CHECKIN_INITIALIZED;
playerCount++;
if (address(this).balance >= REGISTRATION_GAS) {
    Address.sendValue(payable(player), REGISTRATION_GAS);
}
  1. Antes de que comience el juego (antes de gameStartTime).
  2. Los jugadores se registran llamando a la función register en el contrato Adrift.
  3. Se inicializa el próximo tiempo de registro del jugador.
  4. El contador de jugadores aumenta.
  5. Si el contrato tiene saldo suficiente, el jugador recibe una recompensa de gas por registro (1 token por defecto), pagada en el token de gas personalizado de la appchain. Los tokens de gas personalizados hacen que los subsidios sean simples y de bajo costo.
  6. Se emite un evento con los detalles del registro.

Inicio del juego

// Key lines from setGameStartTime()
gameStartTime = startTime;
emit GameStartTimeSet(startTime);
  1. El juego comienza en un gameStartTime predefinido (establecido por el administrador).
  2. No se permiten registros antes de este tiempo.

Registro

// 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. Durante el juego, los jugadores deben llamar a checkIn antes de que su nextCheckInTime caduque.
  2. El resultado de cada registro está determinado por el contrato CheckInOutcomes y puede ser un beneficio, una penalización o una descalificación.
  3. Si es descalificado, el jugador es eliminado del juego.
  4. El próximo tiempo de registro se actualiza según el resultado.
  5. Se emite un evento con el resultado del registro.

Descalificación

// Key lines from disqualifyInactivePlayer/disqualifyFromCheckIn
_disqualify(player, nextCheckInTime);
nextCheckIn[player] = CHECKIN_DISQUALIFIED;
isPlayerDQed[player] = true;
  1. Si un jugador pierde su ventana de registro, cualquiera puede llamar a disqualifyInactivePlayer en el contrato Adrift para eliminarlo.
  2. La descalificación también puede ocurrir aleatoriamente durante el registro.
  3. El estado del jugador se actualiza a descalificado y queda excluido del juego.
  4. Se emite un evento para la descalificación.

Ejecutamos un simple trabajo cron que periódicamente llama a un endpoint de latido para descalificar automáticamente a los jugadores inactivos y finalizar el juego cuando queda un solo jugador activo.

Resultados y aleatoriedad

// 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;
}

Los resultados de los registros están determinados por el contrato CheckInOutcomes, que utiliza aleatoriedad fresca inyectada por el secuenciador a través del contrato Random para cada bloque. Cuando un jugador se registra:

  1. El contrato combina el último valor aleatorio, la dirección del jugador y un nonce para generar un número aleatorio único.
  2. Existe una probabilidad configurable (por defecto: 2%) de que el jugador sea descalificado inmediatamente.
  3. De lo contrario, se genera un resultado aleatorio (mejora o penalización) dentro de un rango establecido (por defecto: 1-24), y su signo (positivo/negativo) se elige aleatoriamente.
  4. El valor del contrato Random es establecido por el secuenciador, asegurando imprevisibilidad y auditabilidad.

Este sistema asegura que cada registro sea impredecible, justo y verificable en la cadena.

Victoria y fin del juego

// Key lines from endGame()
if (playerCount == 1) {
    winner = player;
}
gameEndTime = block.timestamp;
emit GameEnded(gameEndTime, winner);
  1. El último jugador activo restante es declarado ganador.
  2. Si todos los jugadores son descalificados, no hay ganador.
  3. La hora de finalización del juego y el ganador se registran y se emiten como un evento.