Teknik
Dolphin Beyond: én deterministisk motor bag to produkter
Jeg byggede et lille arcade-spil oven på en ren, deterministisk simulation — og brugte så simulationen som fundament for alt andet: ens vilkår på tværs af enheder, tests der kører uden browser, snydesikre ranglister og to vidt forskellige udgaver, der deler den samme motor.

- Motor
- Ren TypeScript, kører uden browser
- Tidsskridt
- Fast — deterministisk
- Tilfældighed
- PRNG med fast seed
- Udgaver
- iPhone (touch) + web (tastatur)
- Mod snyd
- Serveren genafspiller
- Grafik
- Ét Canvas2D-lag, 60 fps
Dolphin Beyond er et fysikbaseret arcade-spil: byg fart under vandet, spring op gennem overfladen, kæd tricks sammen, og lad farten bære dig fra havet helt ud i rummet. Hele spillet bygger på én idé: ét håndgreb, én akse. Du styrer kun vinklen på dyk og udspring — fart, højde, tricks og score følger af, hvor præcist du dykker og springer. Men det her handler ikke om selve spillet. Det handler om den lille kerne, som alt det andet hviler på.
Motorens kontrakt
step(state, input) => nextState // ren funktion, ingen sideeffekter seed + samme række af input => præcis samme forløb, hver gang
Den rene motor
Al spillogik er almindelig TypeScript. Der er hverken React, Firebase eller canvas inde i motoren — den kan køre helt uden browser, direkte i Node. Laget, der tegner spillet, må kun læse tilstanden; det skriver aldrig til den. Den foranderlige tilstand lever inde i selve spil-løkken, mens React kun tager sig af menuer, konto og rammen udenom og henter tilstanden med lav frekvens. Grænsen er skarp med vilje: motoren ved ikke, at der findes en skærm.
Determinisme som en hård regel
Reglen er enkel: samme seed og samme række af input giver præcis det samme forløb — hver gang, for altid. Det kræver disciplin tre steder. Al tilfældighed kommer fra én pseudotilfældig generator med et fast seed. Fysikken kører i faste tidsskridt — ikke ud fra tiden mellem billeder — og er koblet fri af de 60 billeder i sekundet, som Canvas2D-laget tegner, så en langsom telefon og en hurtig computer regner fuldstændig ens. Og motoren kigger aldrig på et rigtigt ur: “nu” er bare et tal, den får udleveret. Når de tre ting holder, falder resten på plads af sig selv — gemte genafspilninger, spøgelser at kappes med, daglige seeds, som alle spiller på samme vilkår, og et automatisk tjek (CI), der slår fejl, hvis jeg kommer til at ændre fysikken.
Genafspilninger og balancetest
At teste sådan et spil plejer at handle om, hvorvidt det “føles rigtigt”. Determinismen gør det målbart. En testcase gemmer sit seed, et øjebliksbillede af justeringsværdierne, rækken af input og det forventede resultat — score og statistik. En simulator spiller den igennem og kræver, at resultatet er præcis det samme. Vil jeg bevidst ændre på følelsen, bliver den berørte testcase godkendt på ny, med en begrundelse skrevet ned, så intet ændrer sig i det skjulte. Og spørgsmål som “er dét trick for stærkt?” svarer jeg ikke på ud fra en mavefornemmelse — jeg lader bots spille tusindvis af seedede runder og ser på, hvordan scorerne fordeler sig.
To produkter, én motor
Den samme kerne — som tager ét skridt ad gangen, optager og kan afspille igen — driver to vidt forskellige udgaver. Det, der adskiller dem, er styringen, kameraets synsfelt og præsentationen — og hvilken rangliste scoren havner på. Ikke motoren:
iPhone
TouchPortræt, bygget som native app med Capacitor. Ét håndgreb styrer aksen — vinklen på dyk og udspring — sammen med haptik og login via Apple.
Web
TastaturLandskab, direkte i browseren. Samme akse, men styret på tastatur — og med sin egen rangliste, så en touch-score og en tastatur-score aldrig stilles op mod hinanden. Det ville ikke være rimeligt.
Serveren har det sidste ord
Fordi motoren er ren, kan serveren også køre den. Når en score skal på en rangliste, eller en belønning skal udbetales, afspiller en server-funktion (en Cloud Function) den indsendte række af input gennem præcis den samme motor og regner resultatet efter. Vi tror aldrig bare på den score, en enhed sender ind. Determinismen er altså ikke kun til test — den er selve værnet mod snyd: en score gælder kun, hvis den kan genskabes ud fra sine input.
Ens vilkår på alle skærme
Telefoner har vidt forskellige skærmformater, og i et spil, hvor højden er en fordel, bliver det et retfærdighedsproblem. Derfor låser motoren et fast lodret synsfelt: en høj iPhone Pro Max ser ikke mere af spilverdenen opad end en lille SE og kan altså ikke score højere på hardwaren alene. Den ekstra strimmel i siderne på bredere skærme er rent pynt. Der findes en test af skærmformater, der beviser det: de samme input giver den samme score, uanset om det er en SE, en Pro Max eller en iPad i højformat.
Reglerne gælder også for agenterne
Som soloudvikler læner jeg mig i hverdagen op ad AI-kodeagenter — men de arbejder under præcis de samme regler: en agent må gerne foreslå nye justeringer eller finpudse følelsen, men motoren skal blive ren, og både simuleringstests og typetjek skal være grønne, før noget overhovedet må kaldes færdigt.
Stakken, kort
Bevidst kedelig: Vite, React, TypeScript og Tailwind på nettet, pakket til iPhone med Capacitor og med Firebase på serversiden til verificerede scores — den samme stak, jeg bruger i mine andre apps. Alt bliver tegnet på ét enkelt Canvas2D-lag, og fysikken har jeg skrevet fra bunden uden et fysikbibliotek. Justeringsværdierne ligger i en tabel, som motoren læser ved hvert skridt, så jeg kan finpudse følelsen uden at røre ved selve logikken.
Det var determinismen, der gjorde forskellen: fra et spil, der bare “føltes rigtigt på min egen telefon”, til noget jeg kan udgive, teste og stole på — i alle udgaver, på alle skærme.
Vil du prøve det? Dolphin Beyond kan spilles gratis direkte i browseren på dolphinbeyond.com — og findes til iPhone.

Om forfatteren
Halfdan Harring
Udvikler og app-bygger fra Aarhus. Bygger hjemmesider og webapps gennem JegUdvikler.dk, laver egne apps på AppsMedHalfdan.dk og holder foredrag. Læs min historie.