GridWars
Kare kare bir tahtada geçen, 1v1 tarayıcı strateji oyunu: toprak altın getirir, altın ordu kurar.

Problem
Travian/Tribal Wars ruhunu kısa maçlara indiren bir düello oyunu. İki oyuncu simetrik bir tahtada tur tur asker üretir, hareket eder ve savaşır. Rakibin kalesini düşüren ya da tur sınırında daha çok kare tutan kazanır.
Rolüm
Ürün sahibi, mimar ve geliştirici: ürün yönünü ve kararları belirledim, oyunu (motor, sunucu, istemci, denge) geliştirdim. Kararlar ADR kaydında tutuldu.
Dil, çatı ve kütüphaneler
- Dil ve depo
- TypeScript ^5.9.2
- Node.js >=20
- pnpm workspaces (pnpm@12.5.1)
- Turborepo ^2.5.5
- İstemci
- React ^18.3.1
- Vite ^6.0.7
- three.js ^0.186.1
- postprocessing ^6.39.5
- @colyseus/sdk 0.18.4
- Web Audio API
- Web Worker
- Sunucu
- @colyseus/core 0.18.17
- @colyseus/ws-transport 0.18.4
- @colyseus/schema 5.0.34
- tsx ^4.19.2
- node:crypto (HMAC-SHA256)
- Test ve araçlar
- Vitest ^2.1.9
- node:test
- puppeteer-core ^25.12.0 (e2e)
Mimari
- Monorepo dört paket: shared (tipler, denge config'i, ağ protokolü), engine (saf kural motoru), server (Colyseus oda), client (React + three.js).
- Kural yalnız engine'de yaşar; sunucu ve istemci aynı fonksiyonları (produce, resolveMove, endTurn) çağırır, kuralı yeniden yazmaz.
- Sunucu otoriteli DuelRoom: istemci yalnız emir gönderir; oda doğrular, motoru çalıştırır, her koltuğa sis filtreli snapshot yollar.
- Sahne tek yönlü: oyun durumu three.js sahnesine yansıtılır, sahnede oyun kuralı yoktur. HUD ve menüler React'te.
- Denge sayıları packages/shared/src/config.ts içinde veri olarak tutulur; self-play simülasyon betikleriyle taranır.
Veri yapıları
- GameState: hücre dizisi (Cell), oyuncular (PlayerState: altın), tur, faz, takım başına sis hafızası (explored), rngState, olay listesi (GameEvent).
- Cell, originalOwner alanını tutar; ev toprağı savunma bonusu yalnız sahip = başlangıç sahibi iken uygulanır, geri alınan karede uygulanmaz.
- Order birleşim tipi: ProduceOrder, MoveOrder, DeployOrder. Ağda team alanı istemciden alınmaz, koltuktan yazılır (ClientOrder).
- Ağ protokolü Colyseus'tan bağımsız tiplerdir: OrderMsg, EndTurnMsg, SnapshotMsg, MatchEndMsg, RejectMsg; PROTOCOL_VERSION uyuşmazsa katılım reddedilir.
- CombatReport: taraflar, ham güçler, uygulanan bonuslar, şans zarları, kayıplar ve kazanan; istemcideki savaş sinematiği bu rapordan oynatılır.
Yöntem ve desenler
Tohumlu determinizm
Şans faktörü mulberry32 tabanlı tohumlu PRNG ile; durum state.rngState'te taşınır. Aynı tohum ve aynı emirler aynı sonucu verir; sıralamalar yerel ayardan bağımsızdır (compareCodeUnits).
Öngörülemez sunucu tohumu
Oda 128 bit gizli sırdan HMAC-SHA256 ile başlangıç tohumunu ve her tur için yeni RNG durumunu türetir; gözlenen zarlardan sonraki turlar tahmin edilemez.
Sunucuda sis filtreleme
redactForTeam görünmeyen karelerdeki orduyu, rakip altınını, tohumu, rakip hafızasını ve yürüyüş yollarını gizler. Sızıntı testi 200 tohumlu rastgele durumda iki takımı denetler; fonksiyon idempotenttir.
Katmanlı emir doğrulama
Önce sanitizeOrder yalnız şekli denetler (tip, sınır, prototip kirletme), sonra motor kuralı karar verir. Sıra, bayat tur ve hız sınırı kontrol edilir; görünmeyen duruma bağlı red yalnız ILLEGAL_ORDER döner.
Sunucu saati ve tekrar oynatma
Tur zamanlayıcısı sunucuda; süre dolunca sunucu turu bitirir, 3 boş tur terk sayılır, kopan oyuncuya 60 sn tanınır. Gizli sır + emir defteri replayMatch ile aynı maçı yeniden üretir.
Ölçüme dayalı performans
Puppeteer ile ölçüm betiği; kod bölme, AI'nin Web Worker'a taşınması, istek üzerine çizim, BatchedMesh/instancing ile birleştirme ve cihaza göre otomatik kalite kademesi.
Zorluklar ve çözümler
Zorluk Sisli oyunda istemci tam durumu tutarsa rakip bilgisi devtools ile okunabilir.
Çözüm Sunucu tam durumu tutar, her koltuğa yalnız redactForTeam görünümünü yollar; tohum her durumda 0'lanır, sis altındaki kale HP'si son görülen değerle gösterilir.
Zorluk Şans faktörü istendi, ama replay, test ve sunucu otoritesi için determinizm gerekiyordu.
Çözüm Tohumlu PRNG ile şans; luckRange config'te (0 verilirse tamamen deterministik). Determinizm testi aynı tohum ve emirlerde aynı durum ve raporu doğrular.
Zorluk three.js sahnesi ilk yüklemeyi ağırlaştırıyor ve boştayken bile her karede çiziyordu.
Çözüm three ayrı chunk'a alındı ve tembel yüklendi; istek üzerine çizim eklendi; ortam animasyonu yüksek kalitede 30 fps, düşükte durağan.
Zorluk AI hesabı ana iş parçacığında çalışıyordu; Worker'a taşırken kararların aynı kalması gerekiyordu.
Çözüm AI Web Worker'a taşındı, hata veya 5 sn zaman aşımında ana iş parçacığına düşer. Structured clone sınırında kararların birebir aynı olduğu testle sabitlendi.
Ölçülebilir sonuçlar
- İlk yüklenen JS (brotli) 403,3 KB'tan 71,5 KB'a indi (-%82).
- Maç boştayken ana iş parçacığı yükü 72'den 43 ms/sn'ye indi (-%40, 1x CPU).
- Maç sonu çizim çağrısı 167'den 78'e indi (-%53); maç başında 103'ten 61'e.
- 60 turluk otomatik oyun ölçümü: 60 fps, p95 kare süresi 16,7 ms (headless Chrome, Apple Silicon GPU).
Sık sorulanlar
Neden otoriteli sunucu?
Sisli bir strateji oyununda istemciye güvenilemez. Sunucu tam durumu tutar, her emri doğrular, motoru kendisi çalıştırır ve her oyuncuya yalnız görebileceğini yollar. Saat de sunucudadır, istemci saati hiç okunmaz.
Neden Colyseus?
Oda tabanlı düello için oda yaşam döngüsü, eşleşme, yeniden bağlanma ve hız sınırı hazır geliyor; ham ws'e göre daha az elle iş, Nakama'ya göre daha hafif. Protokol tipleri Colyseus'tan bağımsız tutuldu, taşıma katmanı değişebilir.
Neden PixiJS'ten three.js'e geçildi?
Ürün sahibi 2.5D görünümü basit buldu ve masa üstü diorama hissi istedi. Babylon.js daha ağır bulundu. Sahne yalnız durumu yansıttığı için motor ve kurallar değişmeden render katmanı değiştirildi.
En zor teknik kısım neydi?
Sis bilgisini sızdırmadan senkron yapmak: ordu, altın, tohum, yürüyüş yolu, destek konumu, istatistik gibi birçok kanal ayrı ayrı kapatıldı. Red kodları bile bilgi vermemeli. 200 rastgele durumla sızıntı testi ve HMAC ile tur başına tohum bunu güvenceye aldı.
Teknik ayrıntılar proje deposundaki README, tasarım belgeleri ve karar kayıtlarından derlenmiştir.