Provably Fair
Para garantir transparência e equidade comprovável, geramos uma cadeia de 10 milhões de hashes SHA256 para cada jogo multijogador, começando com um segredo do servidor que foi alimentado repetidamente com a saída SHA256 novamente em si mesmo 10 milhões de vezes
O SHA256 do hash final para Platform é: eeb4d340bfaed702601b9a08faba2f8088c4810c3e3a883293accaf25053b883
O SHA256 do hash final para Crash é: c46f0705f6ba4df891ce44accda8f89f5a6fa8b987eb7c7b445280cf6cabfbc6
O SHA256 do hash final para Battle é: 63cb1eb64ec4eb4de02f6bad85e42365d08f37497a1b7e907bd0b9183f27e1b3
Ao publicar aqui, estamos evitando a capacidade de escolher uma cadeia SHA256 alternativa. Agora o servidor de jogo está reproduzindo esta cadeia de hashes em ordem reversa, usando esses valores para calcular os resultados do jogo de forma comprovadamente justa.
1. Hash Inicial Secreto: Cada tipo de jogo multijogador tem seu próprio hash inicial privado (conhecido apenas pelo servidor), formando a raiz de uma cadeia hash única.
2. Geração de Cadeia Hash: A partir do hash secreto inicial, geramos uma cadeia de 10.000.000 de hashes aplicando SHA256 repetidamente.
func HashSeed(seed string) string {
hash := sha256.Sum256([]byte(seed))
return hex.EncodeToString(hash[:])
}
func CalculateHashChain(firstHash string) []string {
hashes := make([]string, 10_000_000)
for i := int64(0); i <= 10_000_000; i++ {
firstHash = HashSeed(firstHash)
hashes[i] = firstHash
}
return hashes
}
3. Consumo Reverso: Revelamos o hash final na cadeia ao público. Cada rodada de jogo consome os hashes em ordem reversa: Rodada 1 usa hash[10_000_000], Rodada 2 usa hash[9_999_999], e assim por diante...
4. Este design torna impossível falsificar futuros hashes, enquanto permite que qualquer pessoa verifique todos os hashes anteriores.
Imprevisibilidade: Os resultados futuros do jogo não podem ser conhecidos ou influenciados, nem mesmo pelo servidor.
Verificabilidade: Os jogadores podem verificar a integridade de qualquer rodada anterior usando apenas o hash final revelado publicamente.
Isolamento de Jogo: Cada tipo de jogo tem sua própria cadeia, evitando manipulação entre jogos.
Para garantir equidade comprovável em jogos single-player, nossa plataforma usa um sistema baseado em três componentes principais para gerar a semente do resultado:
1. Semente do Cliente: Esta é uma semente controlada pelo usuário. É gerada automaticamente quando um usuário se registra, mas o usuário pode alterá-la a qualquer momento. Alterar a semente do cliente inicia a geração de uma nova semente de servidor.
2. Semente do Servidor: É gerada pelo servidor usando parâmetros internos que não são divulgados ao usuário. Está vinculada exclusivamente à semente do cliente atual. Para cada semente do cliente, há uma semente de servidor correspondente. Quando o usuário altera a semente do cliente, a semente de servidor anterior é revelada para fins de verificação.
3. Nonce: Um número único baseado no carimbo de tempo da aposta (em milissegundos). Garante que até mesmo as ações repetidas produzem resultados diferentes.
Esses três valores são combinados e codificados usando HMAC-SHA256 para gerar a semente de jogo final, que é então usada para determinar o resultado do jogo.
func GenerateUserGameSeed(userSeed string, userServerSeed string, nonce int64) (string, error) {
data := fmt.Sprintf("%s:%s:%d", userServerSeed, userSeed, nonce)
mac := hmac.New(sha256.New, []byte(data))
gameSeed := hex.EncodeToString(mac.Sum(nil))
return gameSeed, nil
}
O servidor fornece a versão codificada da semente de servidor atual antes de qualquer aposta ser feita.
Depois que o usuário altera sua semente de cliente, a semente de servidor usada anteriormente é revelada.
Isso permite que os usuários verifiquem que todos os resultados gerados com a semente de servidor anterior foram consistentes e justos.
No jogo de Dados, um usuário seleciona de 1 a 5 números (de 6 lados possíveis). Um único lançamento de dados é simulado usando a semente de jogo gerada. Se o número lançado corresponder a uma das seleções do usuário, o usuário vence.
func rollDice(seed string, selectedNumbers []int64) (int64, bool, error) {
if len(selectedNumbers) < 1 || len(selectedNumbers) > 5 {
return 0, false, fmt.Errorf("incorrect number of selected numbers: %d", len(selectedNumbers))
}
bigSeed, ok := new(big.Int).SetString(seed, 16)
if !ok {
return 0, false, fmt.Errorf("failed to convert seed to big.Int")
}
// Simulate dice roll: number from 1 to 6
dice := new(big.Int).Mod(bigSeed, big.NewInt(6))
diceFace := dice.Int64() + 1
for _, num := range selectedNumbers {
if num == diceFace {
return diceFace, true, nil
}
}
return diceFace, false, nil
}
No jogo de Minas, o objetivo é descobrir o máximo de tiles possível sem bater em uma mina. A colocação de minas é derivada deterministicamente usando uma semente de jogo criptograficamente segura. Isso garante equidade e transparência, permitindo ao usuário verificar que o tabuleiro do jogo não foi manipulado.
Entradas:
seed: uma string hexadecimal de 64 caracteres derivada da combinação de ClientSeed, ServerSeed e Nonce usando HMAC-SHA256.
numberOfMines: o número de minas para colocar no tabuleiro.
maxCells: número total de células no tabuleiro (por exemplo, 25 para uma grade 5x5).
func generateMines(seedHex string, numberOfMines int) ([]int64, error) {
if numberOfMines > 25 {
return nil, fmt.Errorf("invalid number of mines: %d", numberOfMines)
}
var mines []int64
used := make(map[int]bool)
i := 0
for len(mines) < numberOfMines {
mac := hmac.New(sha256.New, []byte(seedHex))
mac.Write([]byte(fmt.Sprintf("mine-%d", i)))
sum := mac.Sum(nil)
val := binary.BigEndian.Uint32(sum)
pos := int(val%25) + 1
if !used[pos] {
used[pos] = true
mines = append(mines, int64(pos))
}
i++
}
return mines, nil
}Se numberOfMines = 3 e maxCells = 25, a função retornará deterministicamente 3 índices de célula únicos (de 1 a 25) onde as minas são colocadas.
No jogo de Plataformas, o resultado de cada rodada é determinado por um hash público e verificável que faz parte de uma cadeia hash publicada anteriormente.
Cada rodada usa um hash da cadeia para determinar a "última plataforma" — a plataforma que desaparecerá no final do jogo.
func generateLastPlatform(hash string) (int64, error) {
h, err := hex.DecodeString(hash)
if err != nil {
return 0, err
}
number := binary.BigEndian.Uint64(h[:8])
rng := rand.New(rand.NewSource(int64(number)))
randomNumber := rng.Intn(25) + 1 // Platforms are numbered 1 through 25
return int64(randomNumber), nil
}
Durante o jogo, os jogadores ficam em pé em diferentes plataformas. A plataforma que cai é selecionada aleatoriamente a partir das plataformas disponíveis, garantindo que nunca seja a mesma que a anterior.
Em Foguete, cada resultado de rodada (multiplicador de queda) é determinado usando um hash público da cadeia hash pré-gerada (única por jogo). O resultado é calculado usando uma função determinística que converte o hash em um multiplicador.
func generateMultiplier(hash string) (float64, error) {
const N = 40
bigH, ok := new(big.Int).SetString(hash, 16)
if !ok {
return 0, fmt.Errorf("failed to convert seed to big.Int")
}
mod := new(big.Int).Mod(bigH, big.NewInt(N))
if mod.Cmp(big.NewInt(0)) == 0 {
return 1, nil
}
if len(hash) < 13 {
return 0, fmt.Errorf("hash too short")
}
h13Str := hash[:13]
bigH13, ok := new(big.Int).SetString(h13Str, 16)
if !ok {
return 0, fmt.Errorf("failed to convert first 13 hex digits to big.Int")
}
// Compute 100 * (2^52 - h) / (2^52 - h)
e := new(big.Int).Lsh(big.NewInt(1), 52) // 2^52
hundred := big.NewInt(100)
hundredE := new(big.Int).Mul(hundred, e)
numerator := new(big.Int).Sub(hundredE, bigH13)
denom := new(big.Int).Sub(e, bigH13)
numFloat := new(big.Float).SetInt(numerator)
denomFloat := new(big.Float).SetInt(denom)
ratio, _ := new(big.Float).Quo(numFloat, denomFloat).Float64()
floored := math.Floor(ratio)
result := floored / 100.0
return result, nil
}
Cada jogador recebe o hash antes da rodada e pode verificar independentemente que o resultado corresponde à fórmula publicada, garantindo total transparência e equidade comprovável.
Em Batalha, um jogador é selecionado como vencedor com base no tamanho proporcional de sua aposta para o total. Isso significa que apostas maiores resultam em maiores chances de vitória.
Usamos um mecanismo de seleção determinístico e comprovadamente justo:
1. Um hash de jogo público é usado para gerar um número pseudo-aleatório no intervalo [0.0, 1.0).
2. Cada jogador é atribuído a um segmento do intervalo proporcional à sua aposta (por exemplo, uma aposta de 20% cobre 20% do intervalo).
3. O número aleatório determina o segmento vencedor, e portanto, o vencedor.
func pickWinner(hash string, chances []float64) (int, error) {
if len(chances) == 0 {
return -1, fmt.Errorf("zero participants")
}
total := 0.0
for _, chance := range chances {
if chance < 0 {
return -1, fmt.Errorf("chance cannot be negative")
}
total += chance
}
if total < 99.99 || total > 100.01 {
return -1, fmt.Errorf("sum of chances must be 100%%, got: %.2f%%", total)
}
randomValue, err := deterministicFloatFromHash(hash)
if err != nil {
return -1, fmt.Errorf("random generation failed: %w", err)
}
cumulative := 0.0
for i, chance := range chances {
cumulative += chance / 100.0
if randomValue < cumulative {
return i, nil
}
}
return -1, fmt.Errorf("failed to pick winner")
}
func deterministicFloatFromHash(hash string) (float64, error) {
bytes, err := hex.DecodeString(hash)
if err != nil {
return 0, err
}
if len(bytes) < 8 {
return 0, fmt.Errorf("hash too short")
}
num := binary.BigEndian.Uint64(bytes[:8])
return float64(num) / float64(math.MaxUint64), nil
}