Provably Fair
Aby zapewnić przejrzystość i sprawdzalną uczciwość, wygenerowaliśmy łańcuch 10 milionów skrótów SHA256 dla każdej gry wieloosobowej, zaczynając od tajemnego klucza serwera, który wielokrotnie zasilał wyjście SHA256 wstecz na siebie 10 milionów razy
SHA256 ostatecznego skrótu dla Platform to: eeb4d340bfaed702601b9a08faba2f8088c4810c3e3a883293accaf25053b883
SHA256 ostatecznego skrótu dla Crash to: c46f0705f6ba4df891ce44accda8f89f5a6fa8b987eb7c7b445280cf6cabfbc6
SHA256 ostatecznego skrótu dla Battle to: 63cb1eb64ec4eb4de02f6bad85e42365d08f37497a1b7e907bd0b9183f27e1b3
Publikując to tutaj, uniemożliwiamy możliwość wyboru alternatywnego łańcucha SHA256. Teraz serwer gry odtwarzauje ten łańcuch skrótów w odwrotnej kolejności, używając tych wartości do obliczenia wyników gry w sposób sprawdzalnie uczciwy.
1. Tajny Skrót Początkowy: Każdy typ gry wieloosobowej ma swój prywatny skrót początkowy (znany tylko serwerowi), stanowiący pierwiastek unikalnego łańcucha skrótów.
2. Generowanie Łańcucha Skrótów: Od początkowego tajnego skrótu generujemy łańcuch 10 000 000 skrótów poprzez wielokrotne zastosowanie SHA256.
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. Konsumpcja Wstecz: Ujawniamy ostateczny skrót w łańcuchu publicznie. Każda runda gry konsumuje skróty w odwrotnej kolejności: Runda 1 używa hash[10_000_000], Runda 2 używa hash[9_999_999], itd...
4. Ten projekt uniemożliwia fałszowanie przyszłych skrótów, a jednocześnie umożliwia każdemu zweryfikowanie wszystkich poprzednich skrótów.
Nieprzewidywalność: Przyszłe wyniki gry nie mogą być znane ani na nie wpływać nawet serwer.
Weryfikowalność: Gracze mogą zweryfikować integralność dowolnej poprzedniej rundy, używając tylko publicznie ujawnionego ostatecznego skrótu.
Izolacja Gry: Każdy typ gry ma swój łańcuch, zapobiegając manipulacji między grami.
Aby zapewnić sprawdzalną uczciwość w grach jednoosobowych, nasza platforma wykorzystuje system oparty na trzech głównych komponentach do wygenerowania ziarna wyniku:
1. Ziarno Klienta: To ziarno kontrolowane przez użytkownika. Jest automatycznie generowane przy rejestracji użytkownika, ale użytkownik może je zmienić w dowolnym momencie. Zmiana ziarna klienta powoduje wygenerowanie nowego ziarna serwera.
2. Ziarno Serwera: Jest generowane przez serwer przy użyciu wewnętrznych parametrów, które nie są ujawniane użytkownikowi. Jest jednoznacznie powiązane z bieżącym ziarnem klienta. Dla każdego ziarna klienta istnieje odpowiednie ziarno serwera. Gdy użytkownik zmienia ziarno klienta, poprzednie ziarno serwera jest ujawniane w celach weryfikacji.
3. Nonce: Unikalna liczba oparta na znaczniku czasowym zakładu (w milisekundach). Zapewnia, że nawet powtórzone działania dają różne wyniki.
Te trzy wartości są łączone i haszowane za pomocą HMAC-SHA256 w celu wygenerowania ostatecznego ziarna gry, które jest następnie używane do określenia wyniku gry.
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
}
Serwer zapewnia haszowaną wersję bieżącego ziarna serwera przed dokonaniem jakiegokolwiek zakładu.
Po zmianie ziarna klienta przez użytkownika poprzednio używane ziarno serwera jest ujawniane.
Pozwala to użytkownikom zweryfikować, że wszystkie wyniki wygenerowane z poprzednim ziarnem serwera były spójne i uczciwe.
W grze w kości użytkownik wybiera od 1 do 5 liczb (z 6 możliwych stron). Losowanie pojedynczej kostki jest symulowane za pomocą wygenerowanego ziarna gry. Jeśli wylosowana liczba odpowiada jednemu z wyborów użytkownika, użytkownik wygrywa.
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
}
W grze Miny celem jest odkrycie jak największej liczby pól bez trafienia w minę. Umieszczenie min jest determinowany przez kryptograficznie bezpieczne ziarno gry. Zapewnia to uczciwość i przejrzystość, pozwalając użytkownikowi zweryfikować, że plansza gry nie została zmanipulowana.
Wejścia:
seed: ciąg szesnastkowy o 64 znakach pochodzący z kombinacji ClientSeed, ServerSeed i Nonce przy użyciu HMAC-SHA256.
numberOfMines: liczba min do umieszczenia na planszy.
maxCells: całkowita liczba komórek na planszy (np. 25 dla siatki 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
}Jeśli numberOfMines = 3 i maxCells = 25, funkcja deterministycznie zwróci 3 unikalne indeksy komórek (od 1 do 25), w których umieszczone są miny.
W grze Platformy wynik każdej rundy jest określany przez publiczny, weryfikowalny skrót, który jest częścią wcześniej opublikowanego łańcucha skrótów.
Każda runda używa skrótu z łańcucha do określenia "ostatniej platformy" — platformy, która zniknie na koniec gry.
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
}
Podczas gry gracze stoją na różnych platformach. Platforma, która spada, jest losowo wybierana z dostępnych platform, zapewniając, że nigdy nie jest taka sama jak wcześniej upadła.
W Rakiecie każdy wynik rundy (mnożnik awarii) jest określany za pomocą publicznego skrótu z wstępnie wygenerowanego łańcucha skrótów (unikalny na grę). Wynik jest obliczany za pomocą funkcji deterministycznej, która konwertuje skrót na mnożnik.
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
}
Każdy gracz otrzymuje skrót przed rundą i może niezależnie zweryfikować, że wynik odpowiada opublikowanej formule, zapewniając pełną przejrzystość i sprawdzalną uczciwość.
W Bitwie jeden gracz jest wybierany jako zwycięzca na podstawie proporcjonalnej wielkości ich zakładu do całej puli. Oznacza to, że większe zakłady skutkują wyższą szansą wygrania.
Używamy deterministycznego i sprawdzalnie uczciwego mechanizmu wyboru:
1. Publiczny skrót gry jest używany do wygenerowania liczby pseudolosowej w zakresie [0.0, 1.0).
2. Każdemu graczowi jest przypisany segment zakresu proporcjonalny do jego zakładu (np. zakład 20% obejmuje 20% zakresu).
3. Liczba losowa określa segment zwycięski, a zatem zwycięzcę.
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
}