[#_lesson_28_online] == Lektion 28: Online image::learn/28-online.png[Die fertige Lektion, 480] In Lektion zwölf hast du einen Highscore gespeichert. Am nächsten Morgen ist er immer noch da, und dafür ist ein Highscore größtenteils da, aber es ist ein Highscore auf einem einzigen Rechner. Diese Lektion bringt einen Punktestand auf eine Bestenliste, die andere Leute sehen können. Es ist auch die Lektion über das Scheitern. Alles andere, was du geschrieben hast, scheitert aus Gründen, die du finden kannst: Eine Datei fehlt, eine Zahl ist falsch, eine Funktion ist falsch geschrieben. Das Netzwerk scheitert aus Gründen, die niemand im Raum sehen kann. In einem Gebäude, in dem du nie warst, wurde ein Kabel herausgezogen, und dein Spiel muss weiterlaufen. Das meiste von dem, was folgt, handelt davon. [#_what_the_service_is] === Was der Dienst ist Singe kann mit einem *Master-Dienst* sprechen, einem Server, der drei Dinge verwaltet: * ein *Konto*, das zum Rechner gehört und nicht zu einem einzelnen Spiel; * einen *Katalog* von Spielen zum Herunterladen und Aktualisieren; * *Highscore-Bestenlisten*, eine oder mehrere pro Spiel. Der Standarddienst ist `master.singeengine.com`, und ein Betreiber, der seinen eigenen betreibt, richtet den Rechner woandershin aus. Der Abschnitt über den Online-Dienst im Handbuch beschreibt beide Seiten. Jetzt der Teil, den die Leute überspringen. Der Dienst ist *kein* Mehrspielerdienst: Es gibt keine Möglichkeit, dass zwei Spieler im selben Spiel sind. Er ist keine Cloud für Spielstände; der Spielstand aus Lektion zwölf bleibt auf dem Rechner. Er ist kein Kopierschutz, und nichts darin hindert irgendwen daran, dein Spiel zu spielen. Er ist keine Pflicht: Ein Spiel, das sich nie anmeldet, läuft genau so, wie es immer gelaufen ist, und das ist der einzige vernünftige Standard für etwas, das eine funktionierende Verbindung braucht. Die Bestenliste ist also ein Extra. Behalte den lokalen Highscore aus Lektion zwölf, zeig ihn weiter an, und lass die Online-Bestenliste das sein, was manchmal da ist. [#_signing_in_happens_once_and_not_in_your_game] === Anmelden passiert einmal, und nicht in deinem Spiel Das Konto gehört zum Rechner. Es wird im mitgelieferten Menü angelegt und angemeldet, hinter der Taste, die mit `INPUT_SERVICE` belegt ist, im Tool namens *Online Account*. Das Token, das es dabei bekommt, wird in `master.dat` im obersten Datenordner geschrieben, eine Ebene über dem eigenen Datenordner jedes Spiels, also reicht jedes Spiel auf dem Rechner seine Punkte unter derselben Anmeldung ein. Dein Spiel fragt nie nach einer E-Mail-Adresse und nie nach einem Passwort. Es gibt keine Funktion dafür, und das ist Absicht: Ein Spieler, der ein Passwort in ein Spiel tippt, das er heruntergeladen hat, kann nicht wissen, wohin es geht. Als was ein Spieler auf einer Bestenliste erscheint, ist ein *Handle*: drei bis zwanzig Zeichen aus Buchstaben, Ziffern, Bindestrichen und Unterstrichen, beginnend und endend mit einem Buchstaben oder einer Ziffer. Keine Leerzeichen, keine Punkte und kein `@`, sodass ein Handle nicht einmal wie eine E-Mail-Adresse aussehen kann. Es wird getrennt vom Konto geführt, es ist das Einzige an einem Spieler, das andere Spieler sehen, und es wird einmal gewählt -- ein Betreiber kann eins ändern, ein Spieler nicht, denn eine Bestenliste, deren Namen sich ständig ändern, ist eigentlich niemandes Geschichte. Solange kein Handle gewählt ist, werden Punktestände abgelehnt, denn ein Punktestand, auf dem kein Name steht, steht in keinem sinnvollen Sinn auf einer Bestenliste. Das Tool Online Account zeigt den Namen, unter dem dieser Rechner einreicht, und dein Spiel liest ihn mit `scorePlayerName()`, das `nil` antwortet, wenn es keinen gibt. Gut zu wissen, bevor es dich verwirrt: Ein Punktestand, hinter dem kein Handle steht, wird vom *Dienst* abgelehnt, nicht von `scoreSubmit`, also erfährt dein Spiel nichts davon. Wenn `scorePlayerName()` `nil` ist, sag das auf dem Bildschirm, solange noch jemand da ist, der es lesen kann. [#_your_game_needs_an_id] === Dein Spiel braucht eine ID Eine Bestenliste muss unter irgendetwas geführt werden. Dieses Etwas ist `GAME_ID` im Eintrag deines Spiels in `games.dat`, der Datei, die du in Lektion dreizehn angelegt hast: [source,lua] ---- GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10" ---- Es ist eine UUID, in genau dieser Form: acht hexadezimale Ziffern, drei Gruppen zu vier, dann zwölf, getrennt durch Bindestriche. Erzeug dir eine, wie du willst, und füg sie ein; es ist egal, welche du bekommst, nur darf sie sich nie wieder ändern. Der Eintrag zu `singeGetGameId` in der Referenz erklärt, warum nichts anderes akzeptiert wird. `singeGetGameId()` gibt sie zurück, oder `nil`, wenn der Eintrag keine hat. Ein Spiel ohne ID kann gespielt, aber in keine Rangliste aufgenommen werden, und vorher zu fragen ist der Weg, keine Bestenliste anzubieten, die nie funktionieren kann: [source,lua] ---- if singeGetGameId() then -- showLeaderboardButton ist deine eigene Funktion, keine von Singe. showLeaderboardButton() end ---- [#_the_six_calls] === Die sechs Aufrufe Alles, was ein Spiel mit dem Dienst macht, sind sechs Funktionen, und sie kommen aus `Singe/Framework.singe`, also ist das `dofile` oben in deinem Skript hier keine Kür: [source,lua] ---- dofile("Singe/Framework.singe") ---- `scoreBegin()` sagt, dass eine Partie beginnt. `scoreSubmit(value)` stellt einen Punktestand in die Warteschlange. `scoreBoard(onDone)` holt die Bestenliste. `scorePlayerName()` ist das Handle. `scoreWaiting()` ist die Zahl der Punktestände, die noch nicht hinausgegangen sind. Und `scoreUpdate()` muss einmal pro Frame laufen, aus `onOverlayUpdate`, sonst wird keiner der anderen je fertig. Der letzte ist der ganze Trick dieser Lektion, darum wird er zweimal gesagt. Nichts hier spricht mit dem Netzwerk, wenn du es aufrufst. `scoreSubmit` schreibt in eine Warteschlange und kehrt sofort zurück. `scoreBoard` merkt sich, was du willst, und kehrt sofort zurück. Die eigentliche Arbeit passiert Stück für Stück in `scoreUpdate`, zwischen deinen Frames, und darum ruckelt dein Spiel nie, während die Leitung langsam ist. Lass `scoreUpdate` weg, und nichts geht kaputt, nichts meldet einen Fehler, und nie passiert irgendetwas. [source,lua] ---- function onOverlayUpdate() scoreUpdate() overlayClear() return OVERLAY_UPDATED end ---- [#_posting_a_score] === Einen Punktestand einreichen Am Ende eines Spiels: [source,lua] ---- if scoreSubmit(score) then fetchBoard() else saying = "Kept here only: " .. whyNotPosted() end ---- `scoreSubmit` antwortet `false`, wenn es kein Konto gibt oder das Spiel keine `GAME_ID` hat, und `true`, wenn der Punktestand in die Warteschlange gegangen ist. Beachte, was das `else` *nicht* tut: Es wirft den Punktestand nicht weg, es entschuldigt sich nicht lang und breit, und es hält den Spieler nicht davon ab, ein neues Spiel zu starten. Es sagt eine kurze Zeile, denn der Spieler hat nichts falsch gemacht und kann es von hier aus nicht beheben. Die Warteschlange ist der Grund, warum das sicher ist. Ein Punktestand in der Warteschlange wird auf die Festplatte geschrieben, also übersteht er das Ausschalten des Rechners, und er wird dem Dienst übergeben, sobald es das nächste Mal eine Verbindung gibt -- eine Stunde später oder nächsten Dienstag. Darum landet auch ein Automat in einem Raum mit schlechtem WLAN am Ende auf der Bestenliste. Sei ehrlich zu dir, was die Grenze dieses Versprechens angeht: Die Warteschlange hält Punktestände für einen Rechner, der ein Konto *hat*. Hat sich nie jemand angemeldet, antwortet `scoreSubmit` `false`, und der Punktestand wird nicht für später aufgehoben, weil es niemanden gibt, für den man ihn aufheben könnte. `scoreWaiting()` sagt dir, wie viele noch in der Warteschlange sind, und diese Zahl irgendwo zu zeigen ist eine Freundlichkeit: Sie ist der Unterschied zwischen „der Dienst ist kaputt“ und „drei deiner Punktestände warten darauf, dass das Internet zurückkommt“. `scoreBegin()` verdient einen eigenen Absatz. Ruf es auf, wenn eine Partie beginnt, und der Dienst notiert den Moment nach seiner eigenen Uhr. Der Punktestand, der folgt, trägt mit sich, wie lange die Partie gedauert hat, gemessen auf dem Server, und das ist die einzige Zahl in einer Einreichung, die nicht durch den Rechner des Spielers gegangen ist. Aufgrund dieser Zahl wird nichts abgelehnt: Sie wird dem, der den Dienst betreibt, neben dem Punktestand gezeigt, und ein Mensch entscheidet. Ein Rechner, der offline war, als die Partie begann, zeichnet keine Zeit auf, und das ist ehrlich, nicht kaputt. Und wo wir schon ehrlich sind: Ein eingereichter Punktestand ist eine *Behauptung*. Das Spiel läuft auf dem Computer des Spielers, also kann nichts, was der Client tut, mehr daraus machen, und ein geheimer Schlüssel, der in deinem Spiel mitgeliefert wird, sähe nur nach Sicherheit aus. Der Dienst zeichnet auf, wer was behauptet hat, begrenzt, wie schnell Punktestände ankommen können, und markiert die wilden, damit ein Mensch sie sich ansieht. Entwirf deine Bestenliste mit diesem Wissen. [#_reading_the_board_back] === Die Bestenliste zurücklesen `scoreBoard` ist die erste Funktion in diesem Buch, die eine Funktion als Argument nimmt. [source,lua] ---- scoreBoard(function(ok, result) ... end) ---- Du hast in Lektion fünf Funktionen geschrieben und ihnen Namen gegeben. Diese hier hat keinen Namen: Sie wird dort geschrieben, wo sie gebraucht wird, direkt an `scoreBoard` übergeben und später aufgerufen -- vielleicht Sekunden später -- wenn die Antwort ankommt. Das ist wieder ein *Callback*, dieselbe Idee wie `onOverlayUpdate`, nur dass du diesmal bestimmst, wer ihn aufruft. `ok` ist `true` oder `false`. Ist es `false`, ist `result` ein kurzer String, der sagt, warum. Ist es `true`, ist `result` eine Tabelle mit zwei Teilen: * `result.top`, eine Liste von Zeilen, jede mit einem `name` und einem `value`; * `result.standing`, mit dem eigenen `value` dieses Spielers, seinem `rank` und der Zahl der `players` auf der Bestenliste -- dieser Teil kann fehlen, wenn der Spieler nichts darauf hat. Zwischen dem Aufruf und dem Callback läuft dein Spiel weiter und zeichnet. Diese Lücke ist echt, und der Spieler kann sie sehen, also füll sie mit etwas. Das Skript unten setzt eine Textzeile auf `"Asking the service..."`, bevor es aufruft, und der Callback ersetzt diese Zeile entweder durch die Bestenliste oder durch den Grund, warum es keine gibt. Ein Bildschirm, der vier Sekunden lang nichts sagt, sieht kaputt aus; ein Bildschirm, der sagt, was er tut, ist nur langsam. [#_the_catalogue] === Der Katalog Das Dritte, was der Dienst tut, ist, Spiele aufzulisten. Das Tool *Get Games* im Menü zeigt alles, was der Dienst anbietet, markiert, was auf diesem Rechner schon installiert ist, und lädt es herunter, aktualisiert oder entfernt es. Ein Download wird mit der Prüfsumme verglichen, die der Katalog veröffentlicht hat, bevor er irgendetwas ersetzt, also schlägt eine Übertragung, die beschädigt ankommt, fehl, statt ein Spiel zu installieren, das nicht läuft. Ein Spiel, das der Dienst zurückgezogen hat, funktioniert weiter und wird als installiert, aber nicht mehr angeboten angezeigt: Es bekommt nur keine Updates mehr, denn wer ein Spiel hat, der hat es. Dein Spiel ruft nichts davon auf, und das sollte es auch nicht. Der Katalog ist für dich als Autor eines Spiels wichtig, nicht als Schreiber eines Skripts: Dorthin kommt deine `.game`-Datei, wenn du sie veröffentlichst, und das ist Lektion dreißig, und er ist ein weiterer Grund, warum deine `games.dat` eine `GAME_ID` braucht, die sich nie ändert. Falls du die Liste doch einmal aus einem Skript heraus willst: `Singe/Master.singe` ist das Modul, das unter alledem liegt, und es lohnt sich, es einmal zu lesen. [#_what_the_service_knows_about_a_player] === Was der Dienst über einen Spieler weiß Wenn du ein Spiel auslieferst, das Punktestände einreicht, übergibst du die Daten von jemand anderem an einen Server, und das ist deine Verantwortung, auch wenn du den Server nicht geschrieben hast. Also sag es klar und deutlich, im Info-Bildschirm oder in der Liesmich-Datei deines Spiels: * Der Dienst speichert eine *E-Mail-Adresse* und ein Passwort für das Konto. Mit der Adresse wird ein verlorenes Passwort wiederhergestellt, und für nichts anderes. Sie wird anderen Spielern nie gezeigt und kann nachträglich nicht geändert werden. * Das *Handle* ist das, was auf Bestenlisten erscheint. Es wird absichtlich vom Konto getrennt geführt, damit andere Spieler nie eine Adresse zu sehen bekommen, und es wird mit einer Liste von Namen, die niemand beanspruchen sollte, und von Wörtern abgeglichen, die nicht auf einen Bildschirm in einem öffentlichen Raum gehören. * Ein *Punktestand* trägt seinen Wert, die Bestenliste, auf der er steht, die Zeit, die der Dienst aufgezeichnet hat, und das Handle. Das ist alles. * Der Katalog weiß, *welche Spiele* auf dem Rechner *installiert sind* und in welcher Version, denn so bietet er dir ein Update an. Was nichts davon tut, ist, einem Spieler von Spiel zu Spiel zu folgen, irgendetwas zu verkaufen oder in den Rechner nach etwas zu greifen, das du nicht geschickt hast. Dein Spiel sollte der Liste nichts hinzufügen. Wenn du einem Punktestand zusätzliche Informationen anhängst -- `scoreSubmit` nimmt dafür ein optionales drittes Argument -- dann sollten sie nur vom Spiel handeln. [#_the_script] === Das Skript Ein Reaktionstest: Warte auf das Wort, drück den Knopf, und dein Punktestand ist, was von tausend übrig bleibt. Es ist ein kleines Spiel, und der Rest davon ist der Online-Teil. Leg einen Ordner namens `reaction` an und schreib das hier in `reaction.singe` darin. Gib dem Ordner eine `games.dat` wie die aus Lektion dreizehn, die `reaction/reaction.singe` nennt, mit der Zeile `GAME_ID` von oben in ihrem Eintrag. [source,lua] ---- dofile("Singe/Framework.singe") local WAIT_MIN = 1000 local WAIT_MAX = 3000 local PERFECT = 1000 local state = "attract" local goAt = 0 local score = 0 local best = saveGet("best", 0) local saying = "Button 1 to play." local top = nil local standing = nil local function whyNotPosted() if not singeGetGameId() then return "this game has no GAME_ID." end return "this machine is not signed in." end local function fetchBoard() top = nil standing = nil saying = "Asking the service..." scoreBoard(function(ok, result) if not ok then saying = "No board: " .. tostring(result) return end top = result.top standing = result.standing if #top == 0 then saying = "The board is empty. You could be first." else saying = "" end end) end local function finish(reaction) score = PERFECT - reaction if score < 0 then score = 0 end if score > best then best = score saveSet("best", best) end state = "result" if scoreSubmit(score) then fetchBoard() else saying = "Kept here only: " .. whyNotPosted() end end local function play() state = "waiting" goAt = singeGetTicks() + math.random(WAIT_MIN, WAIT_MAX) score = 0 top = nil standing = nil saying = "" scoreBegin() end function onInputPressed(what) if what ~= SWITCH_BUTTON1 then return end if state == "waiting" then state = "result" score = 0 saying = "Too soon. Nothing posted." elseif state == "go" then finish(singeGetTicks() - goAt) else play() end end function onOverlayUpdate() scoreUpdate() if state == "waiting" and singeGetTicks() >= goAt then state = "go" end overlayClear() overlayPrint(2, 0, "REACTION TEST") if state == "attract" then overlayPrint(2, 2, "Button 1 to play.") elseif state == "waiting" then overlayPrint(2, 2, "Wait for it...") elseif state == "go" then overlayPrint(2, 2, "NOW!") else overlayPrint(2, 2, "You scored " .. score .. ". Button 1 plays again.") end overlayPrint(2, 4, "Best on this machine: " .. best) overlayPrint(2, 5, "Posting as: " .. (scorePlayerName() or "nobody yet")) overlayPrint(2, 6, "Waiting to send: " .. scoreWaiting()) overlayPrint(2, 8, saying) if top then for place, row in ipairs(top) do if place > 5 then break end overlayPrint(2, 9 + place, place .. ". " .. row.name .. " " .. row.value) end end if standing then overlayPrint(2, 16, "You are " .. standing.rank .. " of " .. standing.players .. ".") end return OVERLAY_UPDATED end ---- Starte es mit `Singe -R reaction`. Auf einem Rechner, der nie angemeldet war, läuft das Spiel, es behält deinen besten Punktestand und sagt `Kept here only: this machine is not signed in.` Das ist der wichtige Durchlauf, und es ist der, den die meisten deiner Spieler haben werden. [#_what_just_happened_23] === Was gerade passiert ist [source,lua] ---- local top = nil local standing = nil ---- Zwei Variablen, die meistens `nil` sind. `nil` heißt „hier ist nichts“, und das steht in einer Variablen, bevor irgendetwas hineingelegt wird. Diese beiden enthalten die Bestenliste, wenn es eine gibt, und `nil`, wenn nicht: während die Anfrage unterwegs ist, nach einem Fehlschlag und vor dem ersten Spiel. Der Zeichencode fragt `if top then` und zeichnet nichts, wenn nichts da ist, und das ist die Form von fast jedem Stück Netzwerkcode, das du schreiben wirst. [source,lua] ---- saying = "Asking the service..." scoreBoard(function(ok, result) ---- Die Reihenfolge ist wichtig. Die Textzeile wird *vor* dem Aufruf gesetzt, denn der Aufruf kehrt sofort zurück, und der Callback läuft vielleicht erst nach Sekunden. Wenn du den Text nur im Callback setzt, zeigt der Bildschirm bis zur Antwort die Meldung des letzten Spiels, und das ist genau die falsche Meldung im genau falschen Moment. [source,lua] ---- if not ok then saying = "No board: " .. tostring(result) return end ---- Der Fehlschlag kommt zuerst, und er ist kurz. `tostring` macht aus allem, was zurückkam, Text, damit das Zusammenfügen mit `..` nicht fehlschlagen kann; `result` ist ein String, wenn `ok` falsch ist, aber eine Zeile Zeichencode, die dem Typ von etwas vertraut, das sie von einem Server bekommen hat, ist eine Zeile, die auf ihren Absturz wartet. Der Spieler sieht einen Satz, und das Spiel läuft weiter. [source,lua] ---- if scoreSubmit(score) then fetchBoard() else saying = "Kept here only: " .. whyNotPosted() end ---- Dass `scoreSubmit` `false` antwortet, ist kein Fehler. Es ist eine Tatsache über diesen Rechner, und die beiden Tatsachen, die es bedeuten kann, sollte man auseinanderhalten, und genau das tut `whyNotPosted`: Eine fehlende `GAME_ID` ist *dein* Fehler, und ein nicht angemeldeter Rechner ist niemandes Fehler. Der lokale beste Punktestand wurde schon gespeichert, zwei Zeilen darüber, bevor irgendetwas davon versucht wurde. Erst speichern, dann einreichen, immer. [source,lua] ---- overlayPrint(2, 6, "Waiting to send: " .. scoreWaiting()) ---- Die Warteschlange, auf dem Bildschirm. Auf einem Rechner mit Verbindung steht hier für immer `0`, und niemand bemerkt es. Auf einem Rechner ohne Verbindung steigt die Zahl, und der Spieler weiß, dass das Spiel seine Punktestände aufbewahrt, statt sie zu fressen. [source,lua] ---- function onOverlayUpdate() scoreUpdate() ---- Erste Zeile des Frames, vor allem anderen. Setz es dorthin, wo kein `if` es überspringen kann, denn ein `scoreUpdate`, das nur auf dem Game-Over-Bildschirm läuft, ist eine Warteschlange, die sich nur auf dem Game-Over-Bildschirm leert. [#_try_it_28] === Probier es aus . *Zieh den Stecker.* Trenn den Rechner vom Netzwerk, spiel ein paar Spiele und beobachte die Zahl bei `Waiting to send`. Steck ihn wieder ein und sieh zu, wie sie sinkt, ohne dass du etwas tust. . *Lösch die ID.* Nimm `GAME_ID` aus deiner `games.dat` heraus und starte es noch einmal. Die Meldung ändert sich. Setz sie wieder ein. . *Zeig mehr von der Bestenliste.* Die Schleife hört nach fünf Zeilen auf. Das Overlay ist standardmäßig achtzehn Zeichenzeilen hoch, also rechne aus, wie viele du wirklich unterbringst, und verschieb die Zeile mit `standing`, wenn nötig. . *Gib der Bestenliste einen Namen.* `scoreSubmit(score, "reaction")` und `scoreBoard(onDone, "reaction")` benutzen eine Bestenliste mit diesem Namen statt der, die nach deinem Skript benannt ist. Reich aus demselben Spiel auf zwei Bestenlisten ein -- sagen wir, eine für den Punktestand und eine für die schnellste einzelne Reaktion -- und lass Knopf 2 umschalten, welche gezeigt wird. . *Mach das Warten sichtbar.* Zeichne zwischen dem Aufruf von `scoreBoard` und dem Moment, in dem der Callback läuft, einen Punkt, der sich bewegt. Dann finde heraus, wie lange das bei deiner Verbindung wirklich dauert und ob es sich gelohnt hat, ihn zu zeichnen. [#_break_it_on_purpose_27] === Mach es absichtlich kaputt Nimm die erste Zeile aus `onOverlayUpdate` heraus, sodass der Aufruf `scoreUpdate()` fehlt, und spiel ein Spiel. Nichts passiert. Kein Fehler, keine Meldung, keine Bestenliste, und die Zahl bei `Waiting to send` steigt und bleibt für immer oben. Das Spiel läuft einwandfrei. Das ist die schlimmste Art von Bug, und es lohnt sich, ihr einmal absichtlich zu begegnen: ein fehlender Schritt, der Stille erzeugt statt einer Beschwerde. Es gibt keine Zeilennummer, die du dir ansehen kannst, denn nichts ist schiefgegangen -- du hast um Arbeit gebeten und ihr dann nie eine Gelegenheit gegeben, zu passieren. Wenn etwas in Singe, das einen Callback nimmt, dich nie zurückruft, ist die erste Frage immer, ob das, was es antreibt, in jedem Frame läuft. Der andere Fehler, dem du begegnen wirst, ist lauter. Setz `scoreUpdate()` wieder ein, lösch oben die Zeile mit `dofile` und starte es: ---- Error executing function 'onOverlayUpdate': reaction/reaction.singe:89: attempt to call a nil value (global 'scoreUpdate') stack traceback: reaction/reaction.singe:89: in function 'onOverlayUpdate' ---- Zeile 89 ist das `scoreUpdate()`, das du wieder eingesetzt hast, eine Zeile weiter oben, jetzt wo das `dofile` fehlt. Lies wie immer die erste Zeile: die Funktion, die die Engine aufgerufen hat, die Datei, die Zeile und die Beschwerde. Wieder `nil` -- im Namen `scoreUpdate` steckt nichts, weil die Datei, die ihn definiert, nie geladen wurde. Alle sechs dieser Aufrufe wohnen in `Singe/Framework.singe`. [#_what_you_learned_28] === Was du gelernt hast * Der Dienst verwaltet ein Konto, einen Katalog und Highscore-Bestenlisten. Er ist kein Mehrspielerdienst, keine Cloud für Spielstände und nie Pflicht. * Das Konto gehört zum Rechner und wird in den Service-Tools des Menüs angemeldet. Ein Spiel fragt nie nach einem Passwort. * Eine Bestenliste wird unter `GAME_ID` in `games.dat` geführt, einer UUID, die sich nie ändern darf. `singeGetGameId()` liest sie. * `scoreSubmit` stellt einen Punktestand in eine Warteschlange auf der Festplatte und kehrt sofort zurück; die Warteschlange übersteht das Ausschalten des Rechners. * `scoreUpdate()` muss in jedem Frame laufen, sonst wird nichts von dem, was der Dienst tut, je fertig. * `scoreBoard` übergibt seine Antwort an eine Funktion, die du schreibst und die später läuft. Zeichne in der Zwischenzeit etwas. * Jeder Netzwerkaufruf kann fehlschlagen, und ein Fehlschlag ist kein Fehler. Sag eine kurze Zeile und spiel weiter. * Ein eingereichter Punktestand ist eine Behauptung, und der Dienst behandelt ihn auch so. * Behalte den lokalen Highscore. Er ist der, der immer funktioniert. [#_next_time_28] === Beim nächsten Mal Ein Highscore ist eine einzige Zahl, und der Spielstand aus Lektion zwölf ist der richtige Ort dafür. Lektion neunundzwanzig ist für alles andere, was ein Spiel aufbewahren will: jede Runde, die irgendwer spielt, die Level, die du mitlieferst, und die Fragen, die du beiden stellen willst. Sie nimmt diesen Reaktionstest und gibt ihm eine Datenbank.