Rundungsfehler und Skalierungsartefakte

Warum 91 die Antwort war

WebKit vs. Blink

Kurzversion: Die Antwort ist 91, ungerade und weit weg von 16.

Die lange Geschichte fängt damit an, dass ich in einem Geschäft ein physisches Spiel (ja die gibt es immer noch) mit dem Namen Rubik's Race sah, bei dem es darum geht, eine erwürfelte Vorlage von Farbkacheln eines 3x3-Felds auf einem 5x5-Feld nachzubauen und dabei schneller zu sein, als der Gegenspieler. Das Spiel für Zwei wird wie eines dieser Schiebepuzzles mit den rot-weißen Kacheln aus den 80er Jahren des letzten Jahrtausends gespielt.

Schiebepuzzles gefielen mir immer schon, eine neue Programmierfingerübung hatte ich auch gesucht, also ging ich frisch ans Werk. Grundsätzlich wollte ich CSS-Grids mit benannten Reihen und Spalten verwenden, um das Spielfeld aufzubauen. Ein Feld soll frei bleiben, die umgebenden Kacheln werden hervorgehoben und können angetippt werden, um sie in das freie Feld zu verschieben, wobei intern der Kachel einfach ein neuer Platz im Grid über die Veränderung der Reihe oder Spalte zugewiesen wird. Ein Schiebepuzzle alleine war mir aber zu langweilig und das kompetitive Element von Rubik's Race gefiel mir gut, daher wollte ich etwas Ähnliches und doch Anderes machen.

Ziel soll es sein, ein 5x5 Referenzfeld auf seinem 5x5 Spielfeld nachzubauen, wobei jeder Spieler das selbe Referenzfeld aber auch die selbe Startvorgabe für sein Spielfeld bekommen soll, damit es für alle fair ist. Nun wollte ich aber keine Client-Server-Geschichte daraus machen, um client states abgleichen zu können, stattdessen behalf ich mich der Eigenheiten von Pseudozufallszahlengeneratoren (PRNG), die man mit einem Startwert (seed) vorbehandeln kann, worauf diese immer die selben Zahlenfolgen ausspucken. Wenn also ein kleiner Gnom allen Spielern gleichzeitig den selben Startwert zusteckte, würden diese sowohl dasselbe Referenz- wie auch Spielfeld generiert bekommen. Der Startwert heißt in diesen Fall timestamp auf die Minute genau, was bedeutet, dass alle Spieler, die in der selben Minute ihren Browser neu laden die selben Bedingungen vorfinden. Nachdem praktisch alle Smartphones und dergleichen Zeitserver-synchronisiert sind, sollte das grob- und fein-granular genug sein, um auch in der Praxis brauchbar zu sein.

Das Spiel war recht schnell fertig gebaut bis es mir dämmerte, dass es unheimlich praktisch wäre, wenn das Spiel dem Spieler auch kommunizieren könnte, sobald Referenz-, und Spielfeld deckungsgleich sind. Ich hatte aber beim Design des Spiels datenmodellmäßig gespart, das heißt meine visuelle Repräsentation des Spielfelds ist die einzige Repräsentation, es gibt kein Datenmodell mit Kacheln und Koordinaten dahinter. Deshalb wollte ich für die Felder basierend auf den Elementen im DOM jeweils einen Hash-Wert berechnen und nach jeder Änderung des Spielfelds vergleichen.

Der erste Anlauf war aber eine Rechnung ohne Wirt, denn einfach nur die Elemente im DOM durchzuiterieren und diese Reihenfolge mit der Reihenfolge des Referenzfelds zu vergleichen offenbart einen peinlichen Denkfehler. Es legen nämlich beim Spielen des Spiels die Kacheln mitunter weite Strecken zurück und werden durcheinandergewürfelt. Rein optisch mag dann links oben eine rote Kachel liegen, aber innen drin kann an der ersten Stelle im DOM irgendetwas liegen, weil die CSS-Grid-Reihe und -Spalte die Positionierung bestimmt und nicht die Reihenfolge im DOM. Nach dem Sortieren und Filtern der Kacheln in der Hash-Funktion war das Spiel fertig.

Das Spiel wurde ausprobiert, Links wurden verschickt, sogar auf der Apple Watch lässt sich das Ding spielen, wenn auch mit sehr spitzen Fingern. Ein paar Tage später wurde ich dann auf etwas aufmerksam, was der Grund für diesen Artikel ist und zu dessen Illustration das obige Bild dient. Worum geht es, wie kommt es dazu und was sieht man dort?

Ich verwende einen ResizeObserver, um dafür zu sorgen, dass das Spiel auf egal welchem Bildschirm immer genau 90% der Höhe oder Breite der sichbaren Zeichenfläche des Browsers einnimmt. Ohne diese Skalierung wäre das Spiel immer gleich groß, beziehungsweise eben so groß wie es wäre, wenn jede Kachel genau 2rem mal 2rem misst und das Root-Element 16 Pixel hoch ist. Solange man das Spiel also nicht auf einem kleinen Phone spielt oder gar auf der Watch wird es hochskaliert. Jetzt zeigt sich, dass die WebKit render engine (Safari auf macOS, iOS, iPadOS) damit überhaupt keine Probleme hat. Die Blink render engine (Chrome, Chromium, Opera, neuer Edge) beim Referenzfeld mit dem Hochskalieren von kleinen Größen nicht zurecht kommt.

Die schnelle Abhilfe in diesem Fall ist es, das Root-Element zu vergrößern. Also vergrößerte ich und vergrößerte...

Blink bei 90 und 91px

...schließlich bei 91px sah es gut aus, denn bei 90px waren die Zwischenräume zwischen den Kacheln des Referenzfelds immer noch wobbelig.

Daher: Die Antwort ist 91, ungerade und weit weg von 16.

Und jetzt viel Spaß mit Slider Race.

Erstellt: 13.12.2020Aktualisiert: 19.11.2021
v10.1.0 - © 2024?