ArenaSim bir maç simülasyonu: kadronu kurarsın, motor maçı oynar, sonuç çıkar.
Temel gereksinim şuydu: aynı tohumla oynanan maç, her yerde aynı sonucu vermeli. Sunucuda üretilen maç, tarayıcıda tekrar oynatıldığında bit bit aynı olmalı. Yoksa “arşivden izle” olamaz, doğrulama olamaz, adalet olamaz.
Bir doğrulama aracı yazdım: sunucudaki maçı ve tarayıcıdaki maçı karşılaştır, fark varsa kırmızı yak.
Kırmızı yandı.
İlk şüpheliler
- Tohum doğru geçiyor mu? Evet, iki tarafta da aynı sayı.
- Girdi verisi aynı mı? Evet, aynı kadro, aynı parametreler.
- Olay sırası mı kayıyor? Hayır, ilk olay bile farklıydı.
Üçüncüsü önemli bir ipucuydu: sapma birikmiyordu, en baştan vardı.
Suçlu
Kodda tek bir yerde dilin yerleşik rastgele fonksiyonu kalmıştı. Bir eşitlik bozma durumunda kullanılıyordu; küçük, masum, gözden kaçmış.
Onu da tohumlu üretece bağladım — ve hâlâ tutmuyordu.
“Aynı tohum aynı diziyi verir” cümlesi eksik. Doğrusu: aynı tohum, aynı algoritmada aynı diziyi verir.
Sunucu tarafındaki dilin yerleşik üreteci ile tarayıcının üreteci farklı algoritmalar. Aynı tohumla tamamen farklı diziler üretiyorlar. İkisi de “doğru” çalışıyor.
Motor kendi matematiğini taşır
Çözüm dilin sunduğuna güvenmeyi bırakmaktı. Motor artık kendi üretecini taşıyor — her iki tarafta satır satır aynı yazılmış, otuz iki bitlik bir xorshift:
function next(s) {
s ^= s << 13; s >>>= 0;
s ^= s >>> 17;
s ^= s << 5; s >>>= 0;
return s;
}
Üç kaydırma, üç XOR. Kriptografik değil — olması da gerekmiyor. Gereken tek şey her yerde aynı olması.
>>> 0 ifadeleri kritik: her adımdan sonra sonucu işaretsiz 32 bite sabitliyor. Bu olmadan taşma davranışı platformdan platforma değişebiliyor ve sapma tam oradan giriyor.
Determinizmi bozan diğer üç şey
1. Kararsız sıralama
Eşit değerli iki öğe sıralandığında hangisinin önce geleceği garanti değildi. Çözüm: her karşılaştırmaya bir ikincil anahtar eklemek. Eşitlikte kimlik numarasına bakılıyor, böylece sıralama tam belirlenmiş hale geliyor.
2. Kayan noktalı eşitlik
Fark 10⁻¹⁶ mertebesinde. Ama o fark bir eşik karşılaştırmasına girerse iki taraf farklı dala gider ve maç oradan itibaren tamamen ayrışır.
Çözüm iki kademeli: toplama sırası sabitlendi, eşik karşılaştırmaları tolerans ile yapılıyor.
3. Nesne kimliği
Bir yerde nesneler bir eşleme yapısında tutuluyor ve üzerinden dönülüyordu. Sıra ekleme sırasına, o da ağ isteklerinin dönüş sırasına bağlıydı.
Ortamdan gelen hiçbir şey hesaba girmemeli: bellek adresi, nesne kimliği, saat, ekleme sırası.
Doğrulama aracı olmasaydı
Bu hataların hiçbirini elle bulamazdım. Maçlar ikisinde de makul görünüyordu. Farklı skorlar, farklı olaylar — ama ikisi de inandırıcı.
Doğrulama aracı olmasa, farkı ancak bir kullanıcı “izlediğim maç oynadığımdan farklıydı” dediğinde öğrenirdim. Ve o noktada nedenini bulmam imkânsız olurdu.
Determinizm bir niyet değil, bir kısıttır. Ve ancak her derlemede otomatik kontrol ediliyorsa vardır.
Ne öğrendim
Bir sistem “her yerde aynı davranmalı” diyorsa, dilin sunduğu kolaylıkların çoğu kullanılamaz. Rastgele, sıralama, kayan nokta, koleksiyon sırası — hepsi ortama bağlı ve hiçbiri bunu sana söylemiyor.
Kendi matematiğini taşımak fazladan iş gibi görünüyor. Alternatifi, hangi platformda neden farklı davrandığını asla öğrenememek.
Aynı kısıt ışık şovu motorunda farklı bir kılıkta karşıma çıktı — ve bütün simülasyonlarımın ortak kuralı oldu.