Architektura
Jedna symulacja, dwa miejsca wykonania
Ta sama deterministyczna symulacja liczy grę na serwerze i – z wyprzedzeniem – w przeglądarce. Serwer ma ostatnie słowo, klient przewiduje, a w trybie solo serwer działa lokalnie w karcie.
2.1 Komponenty
Klient to aplikacja three.js: renderuje miasto, HUD i broń, zbiera wejście gracza i przewiduje skutki własnych ruchów. Nie podejmuje jednak decyzji o trafieniach, obrażeniach ani o tym, co zrobi horda – to zadanie symulacji.
Symulacja (pakiet sim) jest deterministyczna: ten sam stan i te same wejścia dają co do bitu ten sam wynik. Dzięki temu jeden kod działa w trzech rolach – na serwerze w chmurze, w przeglądarce jako predykcja i w trybie solo jako LocalServer w karcie, bez sieci.
Backend stoi na Cloudflare: statyczny klient na Pages, API na Workers, pokoje co-op i ich checkpointy w Durable Objects, kafle i gotowe areny w R2. Pakiet protocol pilnuje, żeby klient i serwer mówiły tym samym językiem.
2.2 Mecz co-op z predykcją
Zanim gracz wejdzie do pokoju, klient i serwer porównują wersję protokołu i defsHash – skrót wszystkich definicji rozgrywki (broni, przeciwników, tramwajów). Niezgodna para jest odrzucana jeszcze przed powitaniem. Lepszy czytelny komunikat niż mecz, w którym dwie strony liczą inne obrażenia.
W trakcie gry klient nie czeka na serwer: od razu przewiduje własny ruch tym samym kodem symulacji. Gdy przychodzi snapshot, porównuje go z przewidywaniem, cofa się do potwierdzonego stanu i odtwarza niepotwierdzone wejścia. Pozostałych graczy i boty interpoluje.
Przy opóźnieniu 150 ms z wahaniami 30 ms różnica między predykcją a serwerem w testach tramwajów wynosiła 0 m; pozycje zdalnych encji – do 1 mm.
2.3 Areny z OpenStreetMap
Każda arena to okrąg o promieniu 600 m wokół rynku prawdziwego miasta. Generator czyta kafle z danymi OpenStreetMap i zapisuje trzy pliki: JSON z opisem areny, PRE z danymi wstępnie policzonymi i GEO z geometrią i colliderami.
Generator ma wersję GEN – skrót reguł, które wpływają na wynik. Zmiana reguły zmienia GEN, a pliki trafiają do nowego prefiksu w R2, więc stara i nowa wersja nigdy się nie mieszają. Testy mutacyjne sprawdzają w obie strony: zmiana reguły musi zmienić wersję, a zmiana, która na wynik nie wpływa – nie.
Generacja jest deterministyczna. W ostatniej dostawie 20 z 20 aren miało identyczne sumy SHA-256 we wszystkich trzech plikach w dwóch niezależnych przebiegach. Największa arena po kompresji zajmuje 853 KB – poniżej limitu 1 MB.
| Arena (seed 1) | Trasy spełniające warunki | Tramwaje | Przystanki na wybranych trasach |
|---|---|---|---|
| Wrocław, Rynek | 46 | 3 | 3 |
| Łódź, Plac Wolności | 24 | 3 | 5 |
| Kraków, Rynek Główny | 20 | 3 | 4 |
| Warszawa, Stare Miasto | 4 | 3 | 0 |
| Skierniewice, Rynek | 0 | 0 | 0 |
2.4 Tramwaj jako funkcja czasu
Tramwaj jest w grze dużym, ruchomym obiektem, który potrafi zepchnąć gracza i zadać obrażenia. Wysyłanie jego pozycji w każdym snapshocie byłoby proste – i kosztowne. Zamiast tego położenie tramwaju jest czystą funkcją danych areny, jej seeda i numeru ticku. Serwer i klient importują tę samą implementację, więc obie strony wiedzą, gdzie jest tramwaj, bez jednego bajtu w sieci.
Rozkład nie zależy od graczy, jakości grafiki ani zegara ściennego. Snapshot i checkpoint nie zawierają stanu tramwajów – stan ma tylko gracz (np. czas do kolejnego trafienia), więc po wznowieniu meczu tramwaj jest dokładnie tam, gdzie powinien.
Korytarz ostrzegawczy (D5) to świetlny pas na jezdni, rysowany z przyszłych pozycji tramwaju na najbliższe 3 s – też z tej samej funkcji czasu. Pierwsza wersja przeszła testy, ale z góry była niewidoczna; jak to wykryto i naprawiono, opisuje studium TRAM-1, a porównanie przed/po jest w sekcji 6.
Kontakt z tramwajem: wypchnięcie zawsze, obrażenia 5 HP tylko gdy tramwaj jedzie szybciej niż 0,5 m/s i najwyżej raz na przejazd. Postój przy peronie jest bezpieczny.



