[{"data":1,"prerenderedAt":1004},["ShallowReactive",2],{"page:\u002Fde\u002Fposts\u002Fcode_n_shit\u002Fhalfword":3,"translations:\u002Fde\u002Fposts\u002Fcode_n_shit\u002Fhalfword":985,"series:de:halfword":994},{"id":4,"title":5,"author":6,"body":7,"date":969,"description":970,"draft":971,"extension":972,"image":973,"listed":974,"meta":975,"navigation":974,"part":976,"path":977,"seo":978,"series":979,"stem":980,"tags":981,"theme":979,"toc":974,"__hash__":984},"content_de\u002Fde\u002Fposts\u002Fcode_n_shit\u002Fhalfword.md","Ich habe ein Partyspiel gemacht. Alle bekommen dasselbe Wort. Fast alle.","slnt_opp",{"type":8,"value":9,"toc":958},"minimark",[10,32,35,40,46,107,118,122,125,129,132,151,154,165,168,241,244,249,252,259,262,284,287,294,301,304,308,331,334,428,443,450,454,461,468,611,614,624,772,775,782,785,789,792,798,816,820,827,831,838,865,868,872,877,884,891,894,898,901,905,908,914,917,928,932,935,951,954],[11,12,13,14,18,19,22,23,27,28,31],"p",{},"Meine Freunde und ich spielen ein Wortspiel namens Undercover. Falls du es nicht kennst: Die meisten am Tisch bekommen dasselbe geheime Wort, sagen wir ",[15,16,17],"em",{},"Kaffee",". Ein paar Spieler bekommen ein fast gleiches Wort, ",[15,20,21],{},"Tee",", und niemand sagt ihnen, dass sie die Außenseiter sind: Sie sind die ",[24,25,26],"strong",{},"Undercover",". Und ein Spieler, ",[24,29,30],{},"Mr. White",", bekommt gar kein Wort. Reihum beschreiben alle ihr Wort, ohne es zu sagen, dann wählt der Tisch jemanden raus. Zu vage, und du wirkst verdächtig. Zu genau, und Mr. White erfährt das Wort.",[11,33,34],{},"Dafür haben wir eine andere App benutzt. Ich wollte ein schöneres Spiel, ohne Werbung und mit Wortpaaren, die generiert werden, statt aus einer festen Liste zu stammen. Die ersten beiden Wünsche erfüllt Halfword. Die generierten Paare kommen noch.",[36,37,39],"h2",{"id":38},"was-drin-ist","Was drin ist",[11,41,42,45],{},[24,43,44],{},"Halfword"," ist Undercover für ein Handy, das reihum geht:",[47,48,49,56,62,68,74,80,86,92,98,104],"ul",{},[50,51,52,55],"li",{},[24,53,54],{},"4 bis 20 Spieler, ein Handy,"," iPhone oder Android. Kein Konto, kein Internet, keine Werbung, kein Tracking. Namen, Spiele und Punkte bleiben auf dem Handy.",[50,57,58,61],{},[24,59,60],{},"Das Austeilen:"," Alle halten gedrückt, um ihre Karte aufzudecken, lesen sie allein und verdecken sie wieder, bevor sie weitergeben. Eine Karte lässt sich nochmal zeigen, aber der ganze Tisch sieht, dass es passiert ist.",[50,63,64,67],{},[24,65,66],{},"Verdeckte Rollen, richtig gemacht."," Bürger und Undercover bekommen genau dieselbe Karte, nur mit einem anderen Wort darauf, also erfährt niemand, was er ist. Nur Mr. White weiß Bescheid, und der Tisch erfährt die Rolle eines Spielers, wenn er raus ist. Ein erwischter Mr. White darf einmal das Wort der Bürger raten, und mit einem richtigen Tipp schnappt er sich den Sieg.",[50,69,70,73],{},[24,71,72],{},"Drei Arten abzustimmen,"," jederzeit umschaltbar, sogar mitten in der Abstimmung: eine echte geheime Wahl, während das Handy reihum geht, Handzeichen, die der Gastgeber eintippt, oder nur das Ergebnis.",[50,75,76,79],{},[24,77,78],{},"Acht Sonderrollen"," zum Dazumischen: die Göttin der Gerechtigkeit, die Verliebten, der Rächer, die Duellanten, der Geist, Mr. Meme, der Bumerang und der Spaßvogel.",[50,81,82,85],{},[24,83,84],{},"Tischregeln:"," mit Mr. White spielen oder ohne ihn, und festlegen, ob die Undercover mit ihm gejagt werden oder für den Tisch spielen.",[50,87,88,91],{},[24,89,90],{},"Punkte für einen ganzen Abend,"," in einer Rangliste. Mehr dazu weiter unten.",[50,93,94,97],{},[24,95,96],{},"Sechs Sprachen:"," Englisch, Deutsch, Spanisch, Russisch, Belarussisch und Ukrainisch, mit je 150 Wortpaaren, eigens für jede Sprache geschrieben, nie übersetzt. Die Sprache der App und die der Wörter stellt man getrennt ein, sodass eine gemischte Runde die App in der einen Sprache lesen und in der anderen spielen.",[50,99,100,103],{},[24,101,102],{},"Erst die Form, dann die Farbe:"," ein Kreis für die Bürger, ein schiefes Dreieck für den Undercover, ein leeres Quadrat für Mr. White, damit die Farbe nie der einzige Hinweis ist.",[50,105,106],{},"Mitten im Spiel anhalten und später genau da weitermachen, wo man aufgehört hat.",[11,108,109,110,117],{},"Halfword ist für iPhone und Android erschienen; die Links zu beiden Stores stehen auf ",[111,112,116],"a",{"href":113,"rel":114},"https:\u002F\u002Fhalfword.slnt-opp.xyz",[115],"nofollow","halfword.slnt-opp.xyz",".",[119,120],"gallery",{":images":121},"[{\"src\":\"\u002Fimages\u002Fhalfword\u002Fhome.jpg\",\"alt\":\"Heute am Tisch: wer vorne liegt, und das nächste Spiel nur einen Fingertipp entfernt\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Freveal-coffee.jpg\",\"alt\":\"Chens Karte: coffee (Kaffee)\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Freveal-tea.jpg\",\"alt\":\"Darios Karte ist dieselbe Karte, mit tea (Tee) darauf\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Freveal-mr-white.jpg\",\"alt\":\"Greta ist Mr. White, und nur sie erfährt es\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fvoting.jpg\",\"alt\":\"Drei Arten abzustimmen, hier per Handzeichen\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fcascade.jpg\",\"alt\":\"Sonderrollen: Chen und Fatima waren die Verliebten, also sind beide raus\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Ftable-wins.jpg\",\"alt\":\"Der Tisch gewinnt, und alle, die noch dabei sind, bekommen den Anteil\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fleaderboard.jpg\",\"alt\":\"Die Punkte summieren sich über den Abend\"}]",[11,123,124],{},"Jetzt der spaßige Teil: die Dinge, die am meisten Tüftelei gekostet haben.",[36,126,128],{"id":127},"punkte-auf-gleitender-skala","Punkte auf gleitender Skala",[11,130,131],{},"Die Punkte zählen über einen ganzen Abend, und genau das macht sie knifflig. Ein Sieg als Mr. White sollte sich wie ein Sieg anfühlen, aber den Spieler nicht für den Rest des Abends uneinholbar an die Spitze setzen.",[11,133,134,135,138,139,142,143,146,147,150],{},"In der Wertung, bei der ich gelandet bin, stecken zwei Begriffe. Die Bürger, ",[24,136,137],{},"der Tisch",", spielen um einen ",[24,140,141],{},"Anteil",". Die andere Seite, ",[24,144,145],{},"die Gejagten"," (standardmäßig Mr. White und die Undercover), spielt um einen ",[24,148,149],{},"Einsatz",". Beide hängen davon ab, wie viele Spieler der Tisch verloren hat, gezählt in Schritten, die mit der Größe des Tisches wachsen.",[11,152,153],{},"Hier der Kern, gekürzt aus dem Docstring des Skripts, das die Zahlen ausgesucht hat:",[155,156,161],"pre",{"className":157,"code":159,"language":160},[158],"language-text","step  = (n + 2) \u002F\u002F 6       1 at 4-9 players, 2 at 10-15, 3 at 16-20\nlost  = table seats out when the game ends, by any cause\nk     = lost \u002F\u002F step\nshare = max(1, 6 - k \u002F\u002F 2)   table win: to each table seat still in\nstake = 9 + min(2, k)        a right guess, or the hunted outlasting the table\n","text",[162,163,159],"code",{"__ignoreMap":164},"",[11,166,167],{},"Bei 4 bis 9 Spielern ergibt das:",[169,170,171,199],"table",{},[172,173,174],"thead",{},[175,176,177,181,184,187,190,193,196],"tr",{},[178,179,180],"th",{},"Verlorene Spieler des Tisches",[178,182,183],{},"0",[178,185,186],{},"1",[178,188,189],{},"2",[178,191,192],{},"3",[178,194,195],{},"4",[178,197,198],{},"5",[200,201,202,221],"tbody",{},[175,203,204,208,211,213,215,217,219],{},[205,206,207],"td",{},"Anteil, für jeden vom Tisch, der noch dabei ist",[205,209,210],{},"6",[205,212,210],{},[205,214,198],{},[205,216,198],{},[205,218,195],{},[205,220,195],{},[175,222,223,226,229,232,235,237,239],{},[205,224,225],{},"Einsatz, für jeden Gejagten, der ihn bekommt",[205,227,228],{},"9",[205,230,231],{},"10",[205,233,234],{},"11",[205,236,234],{},[205,238,234],{},[205,240,234],{},[11,242,243],{},"Ein Tisch, der unterwegs zwei der eigenen Leute verliert, gewinnt also je 5 statt 6, und ein Mr. White, der so lange durchhält und dann das Wort nennt, nimmt 11. Geteilt wird nie, und wer raus ist, bekommt weder Anteil noch Einsatz, auch wenn die eigene Seite gewinnt (außer Mr. White mit seinem Tipp: Wenn er rät, ist er ja schon raus; die Duellanten und der Spaßvogel bringen ihre eigenen Punkte obendrauf mit).",[245,246],"halfword-art",{":items":247,":narrow":189,"caption":248},"[{\"src\":\"\u002Fimages\u002Fhalfword\u002Fillo_win_civilians.png\",\"w\":160,\"h\":96,\"alt\":\"Fünf cyanfarbene Kreise unter einem Strahlenkranz\",\"label\":\"Der Tisch gewinnt: der Anteil\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fillo_win_hunted.png\",\"w\":160,\"h\":96,\"alt\":\"Die selbstzufriedene Hälfte zwischen einem riesigen rosa Dreieck und dem Quadrat von Mr. White\",\"label\":\"Die Gejagten halten durch: der Einsatz\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fillo_win_mrwhite.png\",\"w\":160,\"h\":96,\"alt\":\"Das Quadrat von Mr. White unter einer bernsteinfarbenen Sprechblase\",\"label\":\"Mr. White nennt das Wort: der Einsatz\"}]","Drei Spielausgänge, wie der Ergebnisbildschirm sie zeichnet",[11,250,251],{},"Die Zahlen sind nicht geraten. Ein Simulationsskript, Python mit nur der Standardbibliothek, spielt das Spiel als exakte Markow-Kette über die Abstimmungszustände durch. Ein Gleichstand kommt in einer von zehn Runden vor, die Abstimmung trifft einen Mr. White, der noch im Spiel ist, öfter als jeden anderen, ein Hinweis, der leicht danebenliegt, macht einen Undercover etwa doppelt so verdächtig wie einen Bürger, und ein erwischter Mr. White rät mit jeder Runde etwas öfter richtig. Jede dieser Annahmen wird über einen ganzen Bereich variiert (243 Kombinationen), denn die Antwort soll nicht von meinen Vermutungen darüber abhängen, wie Leute abstimmen.",[11,253,254,255,258],{},"Dann spielt es unter jedem Punkteschema ",[24,256,257],{},"5.000 Abende mit je neun Spielen",", auch unter dem aus der App, mit der wir früher gespielt haben (10 Punkte für Mr. White, wenn er gewann, in jedem Spiel 2 für alle, die noch im Spiel waren, 3 oder 5 für die Überlebenden, wenn sie gewannen), und stellt die Frage, auf die es über einen Abend ankommt: Wenn jemand das erste Spiel allein für die Gejagten gewinnt, wie oft holt ihn den ganzen Abend niemand mehr ein?",[11,260,261],{},"Bei sechs Spielern:",[47,263,264,271,278],{},[50,265,266,267,270],{},"die alte App: ",[24,268,269],{},"18 %"," der Abende;",[50,272,273,274,277],{},"der erste Plan (der Anteil sinkt mit jedem Schritt um eins, ein Einsatz von 8 bis 12): ",[24,275,276],{},"20 %",", schlechter als die alte App;",[50,279,280,281,117],{},"das Schema oben: ",[24,282,283],{},"17 %",[11,285,286],{},"Das sieht nicht nach viel aus, bis man die andere Seite sieht. Im selben Modell ist ein Platz als Mr. White fast doppelt so viel wert wie einer als Bürger (das 1,92-Fache an Punkten pro Spiel), und ein Sieg der Gejagten ist so viel wert wie 4,8 durchschnittliche Spiele eines Bürgers. Ein Sieg als Mr. White wird also spürbar belohnt, und bei sechs Spielern entscheidet er den Abend nicht öfter als in der alten App.",[11,288,289,290,293],{},"Das Skript geht außerdem jede erreichbare Stellung durch und sucht nach jemandem, der ",[24,291,292],{},"davon profitieren würde, den eigenen Teamkollegen rauszuwählen",". Mit den endgültigen Zahlen ist das niemand, abgesehen von den Duellanten, die genau dafür gebaut sind, und einigen seltenen Sonderfällen an den größten Tischen. Diese Prüfung hat ein paar gut klingende Ideen erledigt. Den Einsatz unter den Gewinnern aufzuteilen etwa heißt, dass Mr. White davon profitiert, wenn sein eigener Undercover rausfliegt: in 5.950 der 6.071 geprüften Stellungen. Auch die alten „2 Punkte für alle, die noch leben“ sind rausgeflogen.",[11,295,296,297,300],{},"Und dabei hat sich eine Regel ergeben, die anderswo wichtig ist: ",[24,298,299],{},"Keine Punkteregel schaut auf einen Stimmzettel."," Eine frühere Version der Regeln gab einen Punkt, wenn man für die richtige Person stimmte, und dafür muss man wissen, wer wen gewählt hat. Ohne diesen Punkt kann die geheime Wahl wirklich geheim sein: Wer wen gewählt hat, bleibt in der Engine und im Protokoll des Spiels auf dem Handy, weil das Nachspielen es braucht, und nichts auf irgendeinem Screen kann es je zeigen.",[11,302,303],{},"Und die Frage lässt sich billig neu stellen: Die Punktetabelle kommt in unter einer Sekunde heraus, die ganze Studie mit kleineren Stichproben in etwa zwei Minuten und der volle Lauf in vier bis fünf.",[36,305,307],{"id":306},"eine-codebasis-zwei-sprachen","Eine Codebasis, zwei Sprachen",[11,309,310,311,316,317,322,323,326,327,330],{},"Halfword ist einmal geschrieben, in Swift und SwiftUI. Die Android-Version kommt von ",[111,312,315],{"href":313,"rel":314},"https:\u002F\u002Fskip.dev",[115],"Skip",", aber nicht so, wie es bei ",[111,318,321],{"href":319,"rel":320},"https:\u002F\u002Fslnt-opp.xyz\u002Fposts\u002Fcode_n_shit\u002Ffare_enough",[115],"Fare Enough"," sein wird. Fare Enough nutzt Skips Fuse-Modus, der Swift nativ für Android kompiliert. Halfword nutzt ",[24,324,325],{},"Skip Lite",": Das Swift wird in Kotlin-Quellcode ",[15,328,329],{},"übersetzt",", und Android baut den. Der Preis: Man kann nur das Swift verwenden, das Skip versteht, und manches davon bedeutet als Kotlin etwas anderes.",[11,332,333],{},"Deshalb laufen die Tests jedes Moduls zweimal, in zwei Spalten: als Swift auf dem Mac und als Kotlin auf der JVM, weil Skip auch die Tests transpiliert. Eine Kostprobe dessen, was die Android-Spalte gefunden hat:",[47,335,336,363,388,406,416],{},[50,337,338,344,345,347,348,350,351,354,355,358,359,362],{},[24,339,340,343],{},[162,341,342],{},"Int"," hat auf Android 32 Bit."," Aus Swifts ",[162,346,342],{}," wird Kotlins ",[162,349,342],{},", ",[162,352,353],{},"Int.max"," ist dort also 2.147.483.647, und ein Überlauf bringt das Programm nicht wie in Swift zum Absturz: Der Wert springt still ins Negative. Bei einem Überlaufbug stürzt die eine Seite ab, und die andere liefert ein falsches Ergebnis. Alles, was so groß werden kann, ist ",[162,356,357],{},"Int64",", und gewollter Überlauf wird ",[162,360,361],{},"&+"," geschrieben.",[50,364,365,368,369,372,373,376,377,380,381,384,385,387],{},[24,366,367],{},"Vorzeichenlose Literale überleben nicht."," Mit ",[162,370,371],{},"a: UInt64"," kompiliert ",[162,374,375],{},"a &* 3"," in Kotlin nicht, ",[162,378,379],{},"a &* UInt64(3)"," schon. ",[162,382,383],{},"UInt64(bitPattern:)"," kompiliert gar nicht. Deshalb ist der Zufallsgenerator des Spiels SplitMix64 auf vorzeichenbehaftetem ",[162,386,357],{},", mit den Konstanten als negative Bitmuster geschrieben.",[50,389,390,393,394,397,398,401,402,405],{},[24,391,392],{},"Sortieren nur strikt."," ",[162,395,396],{},"sorted { $0 \u003C= $1 }"," ist in Swift kein Problem. Auf Android wirft Javas TimSort ",[15,399,400],{},"„Comparison method violates its general contract!“",". Die Regel lautet also: ein striktes ",[162,403,404],{},"\u003C",", und bei Gleichheit entscheidet die Platznummer.",[50,407,408,411,412,415],{},[24,409,410],{},"Gleich ist nicht überall gleich."," In Swift ist ein vorkomponiertes é gleich einem e mit nachfolgendem kombinierendem Akzent, in Kotlin nicht, und ",[162,413,414],{},"\"👍\".count"," ist auf dem iPhone 1 und auf Android 2. Das wird zum echten Problem, wenn Mr. White seinen Tipp eingibt. Deshalb läuft jeder Tipp durch eine Normalisierungstabelle: erst alles kleingeschrieben, dann jedes Zeichen abgebildet (ä auf a, ß auf ss, ein kombinierender Akzent auf nichts), sodass die Buchstaben jedes Wortpakets auf beiden Plattformen gleich normalisiert werden. „Kaese“ zählt auch als „Käse“, und im Russischen zählt „ё“ als „е“.",[50,417,418,393,424,427],{},[24,419,420,423],{},[162,421,422],{},"rounded()"," rundet auf Android halbe Werte zur geraden Zahl.",[162,425,426],{},"(10.5).rounded()"," ist auf iOS 11 und auf Android 10. Das wird unten bei der Pixelart wichtig.",[11,429,430,431,434,435,438,439,442],{},"Mein Favorit: ",[162,432,433],{},"#expect(a == 2 && b == 1)"," wird zu ",[162,436,437],{},"expectEqual(a == 2 && b, 1)"," transpiliert. Das letzte ",[162,440,441],{},"=="," wird vom ganzen Ausdruck abgetrennt, und mit den passenden Typen kompiliert das und prüft das Falsche. Jede dieser Regeln ist aufgeschrieben, samt der Art, wie sie geprüft wurde, und es sind Dutzende.",[11,444,445,446,117],{},"Android kam auf Umwegen. Die erste Version der Engine, der Wortpakete und des Speicherns bestand jeden Test auf beiden Seiten. Dann ging meinem Mac bei parallelen Builds der Speicher aus, und ich habe Android pausiert, während die iPhone-Version erschien. Als Android zurückkam, bestanden die Tests jedes Moduls in beiden Spalten. Inzwischen ist Halfword bei Google Play. Wie die Android-Screens ohne Emulator geprüft wurden, steht in ",[111,447,449],{"href":448},"\u002Fde\u002Fposts\u002Fcode_n_shit\u002Fhalfword_android_rendering","einem eigenen Beitrag",[36,451,453],{"id":452},"ein-spiel-das-man-nachspielen-kann","Ein Spiel, das man nachspielen kann",[11,455,456,457,460],{},"Die Regeln stecken in einem Modul namens ",[162,458,459],{},"GameCore",", und das ist der strengste Code der App: Er importiert nichts außer der Swift-Standardbibliothek. Kein Foundation, keine Uhr, kein I\u002FO.",[11,462,463,464,467],{},"Ein Spiel ist ein Reducer. Jede Eingabe ist eine Aktion, und ",[162,465,466],{},"apply"," ist der einzige Weg, etwas zu ändern:",[155,469,473],{"className":470,"code":471,"language":472,"meta":164,"style":164},"language-swift shiki shiki-themes monokai","var game = try Game(seats: seats, config: config, pair: pair, rng: rng)\n_ = try game.apply(.startGame)        \u002F\u002F deals the cards\n\u002F\u002F … the reveals, the clues, the vote is called …\nlet events = try game.apply(.castBallot(voter: 2, target: 5))\n\u002F\u002F Screens draw game.snapshot: no words, no living player's role,\n\u002F\u002F never a secret ballot's target.\n","swift",[162,474,475,525,547,553,599,605],{"__ignoreMap":164},[476,477,480,484,488,491,494,498,501,504,507,510,513,516,519,522],"span",{"class":478,"line":479},"line",1,[476,481,483],{"class":482},"s8I7P","var",[476,485,487],{"class":486},"sCdxs"," game ",[476,489,490],{"class":482},"=",[476,492,493],{"class":482}," try",[476,495,497],{"class":496},"sYf5A"," Game",[476,499,500],{"class":486},"(",[476,502,503],{"class":496},"seats",[476,505,506],{"class":486},": seats, ",[476,508,509],{"class":496},"config",[476,511,512],{"class":486},": config, ",[476,514,515],{"class":496},"pair",[476,517,518],{"class":486},": pair, ",[476,520,521],{"class":496},"rng",[476,523,524],{"class":486},": rng)\n",[476,526,528,531,533,535,538,540,543],{"class":478,"line":527},2,[476,529,530],{"class":486},"_ ",[476,532,490],{"class":482},[476,534,493],{"class":482},[476,536,537],{"class":486}," game.",[476,539,466],{"class":496},[476,541,542],{"class":486},"(.startGame)        ",[476,544,546],{"class":545},"snpHw","\u002F\u002F deals the cards\n",[476,548,550],{"class":478,"line":549},3,[476,551,552],{"class":545},"\u002F\u002F … the reveals, the clues, the vote is called …\n",[476,554,556,559,562,564,566,568,570,573,576,578,581,584,587,589,592,594,596],{"class":478,"line":555},4,[476,557,558],{"class":482},"let",[476,560,561],{"class":486}," events ",[476,563,490],{"class":482},[476,565,493],{"class":482},[476,567,537],{"class":486},[476,569,466],{"class":496},[476,571,572],{"class":486},"(.",[476,574,575],{"class":496},"castBallot",[476,577,500],{"class":486},[476,579,580],{"class":496},"voter",[476,582,583],{"class":486},": ",[476,585,189],{"class":586},"s7s5_",[476,588,350],{"class":486},[476,590,591],{"class":496},"target",[476,593,583],{"class":486},[476,595,198],{"class":586},[476,597,598],{"class":486},"))\n",[476,600,602],{"class":478,"line":601},5,[476,603,604],{"class":545},"\u002F\u002F Screens draw game.snapshot: no words, no living player's role,\n",[476,606,608],{"class":478,"line":607},6,[476,609,610],{"class":545},"\u002F\u002F never a secret ballot's target.\n",[11,612,613],{},"Eine abgelehnte Aktion wirft einen Fehler und ändert nichts, weil sie auf einer Kopie läuft. Der Snapshot, aus dem die Screens zeichnen, enthält bis zum Spielende keine Geheimnisse, also kann ein Screen eine Rolle nicht einmal aus Versehen verraten. Ein Test vertauscht zwei geheime Stimmen und prüft, dass sich nichts Sichtbares ändert.",[11,615,616,617,620,621,623],{},"Alles Zufällige (wer der Undercover ist, wer welche Sonderrolle bekommt, wer anfängt, wer diese Runde nur mit Gesten beschreiben muss) kommt aus ",[24,618,619],{},"einem"," Generator mit Seed, in einer festgeschriebenen Reihenfolge. Es ist SplitMix64, wegen der Kotlin-Regeln oben auf ",[162,622,357],{},":",[155,625,627],{"className":470,"code":626,"language":472,"meta":164,"style":164},"public mutating func next() -> Int64 {\n    state = state &+ SeededRNG.gamma\n    var z = state\n    z = (z ^ SeededRNG.shiftRightLogical(z, 30)) &* SeededRNG.mix1\n    z = (z ^ SeededRNG.shiftRightLogical(z, 27)) &* SeededRNG.mix2\n    return z ^ SeededRNG.shiftRightLogical(z, 31)\n}\n",[162,628,629,657,672,685,719,745,766],{"__ignoreMap":164},[476,630,631,634,637,641,645,648,651,654],{"class":478,"line":479},[476,632,633],{"class":482},"public",[476,635,636],{"class":482}," mutating",[476,638,640],{"class":639},"sOx1s"," func",[476,642,644],{"class":643},"sHkqI"," next",[476,646,647],{"class":486},"() ",[476,649,650],{"class":482},"->",[476,652,653],{"class":639}," Int64",[476,655,656],{"class":486}," {\n",[476,658,659,662,664,667,669],{"class":478,"line":527},[476,660,661],{"class":486},"    state ",[476,663,490],{"class":482},[476,665,666],{"class":486}," state ",[476,668,361],{"class":482},[476,670,671],{"class":486}," SeededRNG.gamma\n",[476,673,674,677,680,682],{"class":478,"line":549},[476,675,676],{"class":482},"    var",[476,678,679],{"class":486}," z ",[476,681,490],{"class":482},[476,683,684],{"class":486}," state\n",[476,686,687,690,692,695,698,701,704,707,710,713,716],{"class":478,"line":555},[476,688,689],{"class":486},"    z ",[476,691,490],{"class":482},[476,693,694],{"class":486}," (z ",[476,696,697],{"class":482},"^",[476,699,700],{"class":486}," SeededRNG.",[476,702,703],{"class":496},"shiftRightLogical",[476,705,706],{"class":486},"(z, ",[476,708,709],{"class":586},"30",[476,711,712],{"class":486},")) ",[476,714,715],{"class":482},"&*",[476,717,718],{"class":486}," SeededRNG.mix1\n",[476,720,721,723,725,727,729,731,733,735,738,740,742],{"class":478,"line":601},[476,722,689],{"class":486},[476,724,490],{"class":482},[476,726,694],{"class":486},[476,728,697],{"class":482},[476,730,700],{"class":486},[476,732,703],{"class":496},[476,734,706],{"class":486},[476,736,737],{"class":586},"27",[476,739,712],{"class":486},[476,741,715],{"class":482},[476,743,744],{"class":486}," SeededRNG.mix2\n",[476,746,747,750,752,754,756,758,760,763],{"class":478,"line":607},[476,748,749],{"class":482},"    return",[476,751,679],{"class":486},[476,753,697],{"class":482},[476,755,700],{"class":486},[476,757,703],{"class":496},[476,759,706],{"class":486},[476,761,762],{"class":586},"31",[476,764,765],{"class":486},")\n",[476,767,769],{"class":478,"line":768},7,[476,770,771],{"class":486},"}\n",[11,773,774],{},"Seine Ausgaben stimmen auf beiden Plattformen mit Vignas veröffentlichten Referenzwerten überein. Und weil Swifts Dictionaries in zufälliger Reihenfolge iterieren und Kotlins in Einfügereihenfolge, läuft für eine Entscheidung nie eine Schleife über ein Dictionary oder ein Set: Die Plätze werden nach ihrer Nummer abgegangen.",[11,776,777,778,781],{},"Der Lohn: ",[24,779,780],{},"Ein gespeichertes Spiel ist nur sein Seed, sein Setup und seine Liste von Aktionen."," Die App speichert nach jeder Aktion. Wenn ein Spiel zum ersten Mal gespeichert wird, und noch einmal am Spielende, spielt die Speicherschicht es aus genau dem nach, was sie gleich schreiben wird, und lehnt ein Spiel ab, das nicht identisch herauskommt. Wenn also jemand einen Anruf annimmt oder dem Handy mitten in der geheimen Wahl der Akku ausgeht, öffnet sich das Spiel genau dort wieder, wo es war.",[11,783,784],{},"Die Tests verlassen sich stark darauf. Tausend zufällige Spiele werden je zweimal gespielt und aus ihren gespeicherten Aktionen nachgespielt, und alle drei Durchläufe müssen bei jedem Ereignis übereinstimmen. Hundert davon werden zu einer festgeschriebenen Zahl zusammengefaltet, die iOS und Android beide liefern müssen. Und ein Test spielt einen ganzen Abend mit vier Spielen über das Modell der App, so wie die Screens es tun, und beendet und startet die App dabei sechsmal neu: mitten in der Stichwahl nach einem Gleichstand, mitten in einer geheimen Wahl, gerade als Mr. White seinen Tipp eingeben will. Jedes Mal muss sie genau richtig zurückkommen.",[36,786,788],{"id":787},"pixel-ohne-bilddateien","Pixel ohne Bilddateien",[11,790,791],{},"Halfword verwendet insgesamt sechzehn Farben. Jede Grafik darin beginnt als Textdatei, ein Zeichen pro Pixel. Hier das Symbol des Undercovers in 16 × 16 (Kopf gekürzt):",[155,793,796],{"className":794,"code":795,"language":160},[158],"name: role_undercover_16\nsize: 16x16\noutline: full\n---\nframe 0\n................\n..........K.....\n.........KRK....\n........KRRK....\n.......KRRRrK...\n......KRRRRrK...\n.....KRRRRRrK...\n....KRRRRRRrK...\n...KRRRRRRRrrK..\n..KRRRRRRRRrrK..\n.KRRRRRRRRRrrK..\n..KKRRRRRRRrrK..\n....KKKKRRRRrrK.\n........KKKKrrK.\n............KK..\n................\n",[162,797,795],{"__ignoreMap":164},[11,799,800,803,804,807,808,811,812,815],{},[162,801,802],{},"K"," ist die Kontur, ",[162,805,806],{},"R"," Rosa, ",[162,809,810],{},"r"," der dunklere Ton davon. Ein kleines Tool, ",[162,813,814],{},"pixelkit.py"," (wieder Python mit nur der Standardbibliothek), prüft jede Datei gegen die Grafikregeln und exportiert die PNGs: Größen in Vielfachen von 8, nur Farben aus der Palette, das Maskottchen in seinen vier Farben gezeichnet und mit genau 8 Bildern pro Sekunde animiert, und keine Rollenfarbe auf der Karte, die jeder Spieler sieht.",[245,817],{":items":818,":narrow":210,"caption":819},"[{\"src\":\"\u002Fimages\u002Fhalfword\u002Frole_undercover_16.png\",\"w\":16,\"h\":16,\"alt\":\"Das Symbol des Undercovers: ein schiefes rosa Dreieck\"}]","Dasselbe Symbol, jeder Pixel 6 × 6 groß gezeichnet",[11,821,822,823,826],{},"Das Maskottchen ist ",[24,824,825],{},"die Hälfte"," (auf Englisch: the Half): ein kleiner Sprite, dessen rechte Seite nie ganz angekommen ist, die Pixel lösen sich erst in ein Dithering und dann in nichts auf. Sie hat sechs Stimmungen. Diese vier laufen in Schleife:",[245,828],{":items":829,":narrow":189,"caption":830,":wide":192},"[{\"src\":\"\u002Fimages\u002Fhalfword\u002Fhalf_idle_sheet.png\",\"w\":32,\"h\":32,\"frames\":4,\"alt\":\"Die Hälfte, wartend\",\"label\":\"wartend\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fhalf_peek_sheet.png\",\"w\":32,\"h\":32,\"frames\":2,\"alt\":\"Die Hälfte, spähend\",\"label\":\"spähend\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fhalf_suspicious_sheet.png\",\"w\":32,\"h\":32,\"frames\":4,\"alt\":\"Die Hälfte, skeptisch\",\"label\":\"skeptisch\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fhalf_smug_sheet.png\",\"w\":32,\"h\":32,\"frames\":4,\"alt\":\"Die Hälfte, selbstzufrieden\",\"label\":\"selbstzufrieden\"}]","Die Hälfte: wartend, spähend, skeptisch und selbstzufrieden, mit 8 Bildern pro Sekunde",[11,832,833,834,837],{},"Für die Darstellung gilt eine strenge Regel: ",[24,835,836],{},"nur ganzzahlige Maßstäbe",", 2×, 3×, 4× oder 6×, ein Maßstab pro Screen, nie geglättet, nie gedreht. Auf dem iPhone ist das einfach, weil ein Punkt genau 2 oder 3 Pixel sind. Auf Android nicht. Ein verbreitetes Handy hat die Dichte 2,625, also ergibt „2× in dp“ manche Pixel, die 5 Gerätepixel breit sind, und manche mit 6, und Pixelart fällt auseinander, wenn ihre Pixel nicht gleich groß sind.",[11,839,840,841,844,845,848,849,852,853,856,857,860,861,864],{},"Schlimmer noch: SkipUIs ",[162,842,843],{},"Image"," kann auf Android überhaupt keine Pixelart zeichnen. ",[162,846,847],{},".interpolation(.none)"," tut dort nichts, und jede Bitmap wird bilinear gefiltert. Deshalb liefert die App keinen einzigen Sprite als Bild aus. Sie liefert dieselben Zeilen aus Palettenzeichen und zeichnet jeden Sprite als SwiftUI-Shapes: ein ",[162,850,851],{},"Path"," aus Rechtecken ganzer Pixel pro Farbe. Auf Android ist ein Quellpixel dann genau ",[162,854,855],{},"n × n"," Gerätepixel, mit ",[162,858,859],{},"n = round(scale × density)",". Dieses ",[162,862,863],{},"round"," ist ausdrücklich als „halb von null weg“ geschrieben, weil Kotlin, wie oben gesagt, 10,5 auf 10 abrunden würde.",[11,866,867],{},"Auch die Karten tragen die Regeln. Bürger und Undercover dürfen nicht wissen, was sie sind, also teilen sie sich eine Karte, ohne Rollenfarbe darauf. Nur die Karte von Mr. White ist anders, ein leeres Quadrat, und sie ist genauso dunkel wie die anderen, damit ihr Leuchten ihn nicht über den Tisch hinweg verrät:",[245,869],{":items":870,":narrow":189,"caption":871},"[{\"src\":\"\u002Fimages\u002Fhalfword\u002Fcard_back.png\",\"w\":96,\"h\":64,\"alt\":\"Die Kartenrückseite: eine bernsteinfarbene Sprechblase, die nach rechts ausblendet\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fcard_word.png\",\"w\":96,\"h\":64,\"alt\":\"Die Wortkarte: eine ganze bernsteinfarbene Sprechblase\"},{\"src\":\"\u002Fimages\u002Fhalfword\u002Fcard_mrwhite.png\",\"w\":96,\"h\":64,\"alt\":\"Die Karte von Mr. White: ein leeres Quadrat\"}]","Die Kartenrückseite, die Karte für alle mit einem Wort und die von Mr. White",[873,874,876],"h3",{"id":875},"das-c-das-wie-eine-klammer-aussah","Das C, das wie eine Klammer aussah",[11,878,879,880,883],{},"Titel und die Sprüche der Hälfte sind in ",[24,881,882],{},"Micro 5"," gesetzt, einer Open-Source-Pixelschrift. Sie hat zwei Schwächen: Ihr großes C ist zwei Pixel breit und hat rechtwinklige Ecken, also liest sich „Chen“ wie „[hen“ und der englische Titel „Count the votes“ wie „[ount the votes“. Ihr G mit einer abgeschnittenen Ecke liest sich wie ein D: Aus „Greta“ wird „Dreta“. Die Titel doppelt so groß zu machen, hat die Buchstaben größer gemacht, nicht anders.",[11,885,886,887,890],{},"Also liefert die App ein gepatchtes Micro 5 aus. Ein Skript enthält die neuen Zeichnungen und baut die gepatchte Schrift aus der Upstream-Datei neu, jedes Mal mit denselben Bytes: Es tauscht diese Glyphen aus (C und G, ihre Formen mit Akzenten und die typografischen Anführungszeichen) und baut dann die Tabellen, Bounding Boxes und Prüfsummen der Schrift von Hand neu, nur mit der Standardbibliothek. Es behält ",[24,888,889],{},"jede Zeichenbreite",", weil die App Namen in Schriftpixeln misst, um sicherzugehen, dass sie in eine Titelzeile passen („Marie-Christine“ hat genau 52, die Grenze), und ein breiteres C hätte diese Grenze verschoben. Micro 5 hat keinen Reserved Font Name, also darf die geänderte Kopie unter der OFL ihren Namen behalten.",[11,892,893],{},"Das G ist repariert: „Greta“ liest sich als Greta. Das C ist besser, aber nicht repariert: In voller Größe sieht es immer noch ein wenig wie eine runde Klammer aus, „(hen“. Ein dritter Pixel würde das heilen und „Marie-Christine“ einen Pixel über die Grenze schieben.",[245,895],{":items":896,":narrow":192,"caption":897,":wide":195},"[{\"src\":\"\u002Fimages\u002Fhalfword\u002Fmicro5-c-and-g.png\",\"w\":59,\"h\":78,\"alt\":\"Chen, Count the votes und Greta, jeweils im originalen Micro 5 und dann in der gepatchten Schrift\"}]","Das originale Micro 5, dann das gepatchte; die Striche markieren die neu gezeichneten Buchstaben",[11,899,900],{},"Die Überschriften dieses Beitrags sind in dieser gepatchten Schrift gesetzt.",[36,902,904],{"id":903},"zuerst-gezeichnet","Zuerst gezeichnet",[11,906,907],{},"Bevor die App eine Zeile Code hatte, hatte sie eine Spezifikation, visuelle Richtlinien, ihre Grafiken und ein Design-Canvas voller Screens. Die Richtlinien legten die Regeln fest, auf die sich dieser Beitrag schon gestützt hat (sechzehn Farben, ganzzahlige Maßstäbe, erst die Form, dann die Farbe), dazu zwei weitere: Pixelart für die Inhalte und native Bedienelemente für alles, was man antippt; und jede Textfarbe muss gegen ihren Hintergrund WCAG AA bestehen.",[11,909,910,911,913],{},"Die Art-Direction-Notizen sind beim Zeichnen nach diesen Regeln entstanden, und sie korrigieren sie mit Messwerten. Die Richtlinien schlugen amber-dk für bernsteinfarbenen Text im hellen Theme vor; auf paper, dem hellen Hintergrund, misst es 3,29:1 und fällt für Fließtext durch, also ist farbiger Text im hellen Theme green-dk oder rose-dk, und Bernstein gibt es dort nur als Fläche. Helle Schrift (paper) auf rose kommt auf 3,11:1, deshalb sind die Beschriftungen auf rosa Buttons dunkel. Die Bemessung in Gerätepixeln, die oben beschrieben ist, hat als eine dieser Notizen angefangen: Das Android-Snippet der Richtlinien selbst hat Sprites in dp bemessen. Und sobald Kreis, Dreieck und Quadrat den Rollen gehören, darf kein Icon sie verwenden: Ein dreieckiges Warnschild würde sich wie ",[15,912,26],{}," lesen, also ist das Warnsymbol ein nacktes „!“, und der Timer ist eine Sanduhr, kein Zifferblatt. Wo Notizen und Richtlinien sich widersprechen, gewinnen die Notizen.",[11,915,916],{},"Das Canvas hat 44 Boards. Die meisten sind Handy-Screens mit 390 × 844 Punkten, vom Splashscreen bis zur Rangliste, ein paar davon noch einmal im hellen Theme. Der Rest legt die Grundlagen fest: die Palette mit ihren gemessenen Kontrasten, die sechs Zustände des Maskottchens, die Icons, die Rollengrafiken, die Illustrationen und das App-Icon. Jedes Screen-Board erzählt denselben Abend, mit denselben acht Spielern, Kaffee gegen Tee, Dario als Undercover, Greta als Mr. White und Punkten, die sich von Board zu Board aufaddieren. Jeder Screen gibt seinen Pixelmaßstab an, und ein Lint prüft jedes Bild darauf gegen diesen Maßstab.",[11,918,919,920,923,924,927],{},"Jeder Screen, der ein Board hat, wurde danach gebaut. Die Fixtures der App, Zustände, die sie für Tests und Screenshots direkt beim Start öffnet, spielen denselben Abend nach, und jede Fixture trägt den Namen des Boards, nach dem sie gebaut wurde, ",[162,921,922],{},"Round-Voting"," oder ",[162,925,926],{},"Resolve-Cascade",", sodass man einen Screenshot neben seine Zeichnung halten kann. Die Store-Screenshots, auch die in diesem Beitrag, kommen aus denselben Fixtures. Manche Boards hinken den Regeln inzwischen hinterher; bis sie neu gezeichnet sind, gelten die Screenshots als Referenz.",[36,929,931],{"id":930},"das-wars","Das war’s",[11,933,934],{},"Halfword spricht sechs Sprachen, funktioniert offline, läuft auf iPhone und Android und verlangt von niemandem ein Konto.",[47,936,937,944],{},[50,938,939,940,943],{},"Herunterladen: ",[111,941,116],{"href":113,"rel":942},[115],", dort gibt es die Links zum App Store und zu Google Play",[50,945,946,947],{},"Ideen, Bugs, ein Wortpaar, das zu leicht ist: ",[111,948,950],{"href":949},"mailto:info@slnt-opp.xyz","info@slnt-opp.xyz",[11,952,953],{},"Und wenn Mr. White sich an deinem Tisch trotzdem den ganzen Abend schnappt, sag mir Bescheid. Dafür habe ich ein Skript.",[955,956,957],"style",{},"html pre.shiki code .s8I7P, html code.shiki .s8I7P{--shiki-default:#F92672}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sYf5A, html code.shiki .sYf5A{--shiki-default:#66D9EF}html pre.shiki code .snpHw, html code.shiki .snpHw{--shiki-default:#88846F}html pre.shiki code .s7s5_, html code.shiki .s7s5_{--shiki-default:#AE81FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sOx1s, html code.shiki .sOx1s{--shiki-default:#66D9EF;--shiki-default-font-style:italic}html pre.shiki code .sHkqI, html code.shiki .sHkqI{--shiki-default:#A6E22E}",{"title":164,"searchDepth":527,"depth":527,"links":959},[960,961,962,963,964,967,968],{"id":38,"depth":527,"text":39},{"id":127,"depth":527,"text":128},{"id":306,"depth":527,"text":307},{"id":452,"depth":527,"text":453},{"id":787,"depth":527,"text":788,"children":965},[966],{"id":875,"depth":549,"text":876},{"id":903,"depth":527,"text":904},{"id":930,"depth":527,"text":931},"2026-10-01","Halfword ist das Wortspiel Undercover für ein Handy, das reihum geht: Pixelart, acht Sonderrollen und Punkte, die sich über einen ganzen Abend summieren. Es ist für iPhone und Android erschienen. Hier steht, was es dafür gebraucht hat: per Simulation austarierte Punkte, eine Swift-Codebasis für zwei Plattformen, eine Engine, die jedes Spiel nachspielen kann, Pixelart ohne Bilddateien und Screens, die gezeichnet waren, bevor sie programmiert wurden.",false,"md","\u002Fimages\u002Fhalfword\u002Fcover.png",true,{},null,"\u002Fde\u002Fposts\u002Fcode_n_shit\u002Fhalfword",{"title":5,"description":970},"halfword","de\u002Fposts\u002Fcode_n_shit\u002Fhalfword",[162,472,982,983],"ios","games","BrEK0y4qtMMg7JLzp5NGaIA0HmYLq6dQ42i_e9GW5OQ",[986,990],{"locale":987,"path":988,"title":989},"en","\u002Fposts\u002Fcode_n_shit\u002Fhalfword","I made a party game. Everyone gets the same word. Almost.",{"locale":991,"path":992,"title":993},"ru","\u002Fru\u002Fposts\u002Fcode_n_shit\u002Fhalfword","Я сделал игру для компании. У всех одно и то же слово. Почти.",{"slug":979,"title":995,"path":996,"chapters":997},"Wie Halfword entsteht","\u002Fde\u002Fseries\u002Fhalfword",[998,999],{"path":977,"title":5,"description":970,"date":969,"image":973,"draft":971,"series":979,"part":976,"n":479},{"path":448,"title":1000,"description":1001,"date":1002,"image":1003,"draft":971,"series":979,"part":976,"n":527},"Ich habe meine Android-Screens ohne Emulator getestet. Alle 114, in vier Sprachen.","Der Android-Emulator und ein Haufen Gradle-Builds haben meinen Mac mit in die Tiefe gerissen, also zeichnet jetzt die JVM die Android-Screens von Halfword: Robolectric startet die echte App, Roborazzi speichert die Bilder, 456 Stück in vier Sprachen in 2 Minuten 19 Sekunden. Wie das funktioniert, was es gefunden hat und was es dir nicht verraten kann.","2026-10-02","\u002Fimages\u002Fhalfword_android\u002Fcover.jpg",1791333556892]