Läpinäkyvyyden ja todennettavan reiluuden varmistamiseksi olemme luoneet jokaista moninpeliä varten 10 miljoonan SHA256-tiivisteen ketjun. Luominen alkaa palvelimen salaisuudesta, ja SHA256:n tulos syötetään takaisin algoritmiin toistuvasti 10 miljoonaa kertaa.
Platform-pelin viimeisen tiivisteen SHA256-arvo on: eeb4d340bfaed702601b9a08faba2f8088c4810c3e3a883293accaf25053b883
Crash-pelin viimeisen tiivisteen SHA256-arvo on: c46f0705f6ba4df891ce44accda8f89f5a6fa8b987eb7c7b445280cf6cabfbc6
Battle-pelin viimeisen tiivisteen SHA256-arvo on: 63cb1eb64ec4eb4de02f6bad85e42365d08f37497a1b7e907bd0b9183f27e1b3
Julkaisemalla sen täällä estämme vaihtoehtoisen SHA256-ketjun valitsemisen. Pelipalvelin käy nyt tätä tiivisteketjua läpi käänteisessä järjestyksessä ja käyttää arvoja pelitulosten laskemiseen todistettavasti reilulla tavalla.
1. Salainen alkuperäinen tiiviste: Jokaisella moninpelityypillä on oma yksityinen alkuperäinen tiivisteensä, jonka vain palvelin tuntee ja joka muodostaa yksilöllisen tiivisteketjun juuren.
2. Tiivisteketjun luominen: Luomme salaisesta alkuperäisestä tiivisteestä 10 000 000 tiivisteen ketjun soveltamalla SHA256-algoritmia toistuvasti.
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. Käyttäminen käänteisessä järjestyksessä:Paljastamme ketjun viimeisen tiivisteen julkisesti. Jokaisella pelikierroksella tiivisteitä käytetään käänteisessä järjestyksessä: Kierros 1 käyttää arvoa hash[10_000_000] Kierros 2 käyttää arvoa hash[9_999_999] ja niin edelleen…”
4. Tämän rakenteen ansiosta tulevia tiivisteitä on mahdotonta väärentää, mutta kuka tahansa voi varmentaa kaikki aikaisemmat tiivisteet.
Ennakoimattomuus: Tulevia pelituloksia ei voi tietää eikä niihin voi vaikuttaa edes palvelin.
Todennettavuus: Pelaajat voivat varmentaa minkä tahansa aikaisemman kierroksen eheyden käyttämällä ainoastaan julkisesti paljastettua viimeistä tiivistettä.
Pelien eristäminen: Jokaisella pelityypillä on oma ketjunsa, mikä estää pelien välisen manipuloinnin.
Yksinpelien todennettavan reiluuden varmistamiseksi alustamme käyttää tulossiemenen luomiseen järjestelmää, joka perustuu kolmeen pääkomponenttiin:
1. Asiakassiemen: Tämä on käyttäjän hallitsema siemen. Se luodaan automaattisesti käyttäjän rekisteröityessä, mutta käyttäjä voi vaihtaa sitä milloin tahansa. Asiakassiemenen vaihtaminen käynnistää uuden palvelinsiemenen luomisen.
2. Palvelinsiemen: Palvelin luo tämän käyttämällä sisäisiä parametreja, joita ei paljasteta käyttäjälle. Se sidotaan yksilöllisesti nykyiseen asiakassiemeneen. Jokaista asiakassiementä vastaa oma palvelinsiemen. Kun käyttäjä vaihtaa asiakassiemenen, edellinen palvelinsiemen paljastetaan varmentamista varten.
3. Nonce-arvo: Yksilöllinen luku, joka perustuu panoksen aikaleimaan millisekunteina. Se varmistaa, että myös toistuvat toiminnot tuottavat erilaiset lopputulokset.
Nämä kolme arvoa yhdistetään ja tiivistetään HMAC-SHA256-algoritmilla lopullisen pelisiemenen luomiseksi. Pelin lopputulos määritetään tämän siemenen avulla.
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
}
Palvelin toimittaa nykyisen palvelinsiemenen tiivistetyn version ennen panoksen asettamista.
Kun käyttäjä vaihtaa asiakassiemenensä, aikaisemmin käytetty palvelinsiemen paljastetaan.
Näin käyttäjät voivat varmistaa, että kaikki edellisellä palvelinsiemenellä luodut tulokset olivat yhdenmukaisia ja reiluja.
Dice-pelissä käyttäjä valitsee yhdestä viiteen numeroa kuudesta mahdollisesta nopan sivusta. Luodun pelisiemenen avulla simuloidaan yksi nopanheitto. Jos heitetty numero vastaa jotakin käyttäjän valinnoista, käyttäjä voittaa.
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
}
Mines-pelin tavoitteena on paljastaa mahdollisimman monta ruutua osumatta miinaan. Miinojen sijainnit johdetaan deterministisesti kryptografisesti turvallisen pelisiemenen avulla. Tämä takaa reiluuden ja läpinäkyvyyden sekä antaa käyttäjälle mahdollisuuden varmistaa, ettei pelilautaa ole manipuloitu.
Syötteet:
seed: 64 merkin pituinen heksadesimaalimerkkijono, joka johdetaan ClientSeed-, ServerSeed- ja Nonce-arvojen yhdistelmästä HMAC-SHA256-algoritmilla.
numberOfMines: pelilaudalle sijoitettavien miinojen määrä.
maxCells: pelilaudan ruutujen kokonaismäärä (esimerkiksi 25 ruutua 5 × 5 -ruudukossa).
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
}Jos numberOfMines = 3 ja maxCells = 25, funktio palauttaa deterministisesti kolme yksilöllistä ruutuindeksiä väliltä 1–25, joihin miinat sijoitetaan.
Platforms-pelissä jokaisen kierroksen tulos määritetään julkisella ja todennettavalla tiivisteellä, joka kuuluu aikaisemmin julkaistuun tiivisteketjuun.
Jokaisella kierroksella ketjusta otettavan tiivisteen avulla määritetään ”viimeinen alusta” eli alusta, joka katoaa pelin päättyessä.
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
}
Pelaamisen aikana pelaajat seisovat eri alustoilla. Putoava alusta valitaan satunnaisesti saatavilla olevista alustoista siten, ettei se koskaan ole sama kuin edellisellä kerralla pudonnut alusta.
Rocket-pelissä jokaisen kierroksen tulos eli romahduskerroin määritetään ennalta luodusta ja kullekin pelille yksilöllisestä tiivisteketjusta otettavan julkisen tiivisteen avulla. Lopputulos lasketaan deterministisellä funktiolla, joka muuntaa tiivisteen kertoimeksi.
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
}
Jokainen pelaaja saa tiivisteen ennen kierrosta ja voi itsenäisesti varmistaa, että tulos vastaa julkaistua kaavaa. Tämä takaa täydellisen läpinäkyvyyden ja todennettavan reiluuden.
Battle-pelissä yksi pelaaja valitaan voittajaksi hänen panoksensa ja kokonaispotin välisen suhteellisen osuuden perusteella. Suurempi panos antaa siis suuremman voittomahdollisuuden.
Käytämme determinististä ja todistettavasti reilua valintamekanismia:
1. Julkisen pelitiivisteen avulla luodaan näennäissatunnainen luku väliltä [0.0, 1.0).
2. Jokaiselle pelaajalle määritetään hänen panoksensa suhteellista osuutta vastaava osa lukuvälistä. Esimerkiksi 20 %:n panos kattaa 20 % lukuvälistä.
3. Satunnaisluku määrittää voittavan osan ja siten voittajan.
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
}