Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Riskelo — un jeu de conquête où le lancer de dés est remplacé par une question de culture générale. iPhone, iPad, Mac.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Un Risk où le lancer de dés est remplacé par une question de culture générale. iPhone, iPad et Mac — une seule cible, un seul code. L'attaquant choisit une ou deux questions : ce sont ses dés. Il choisit aussi la catégorie — c'est lui qui pose la question, et c'est là qu'est son adresse. Le défenseur répond, dans le temps du sablier. Le dosage des questions se choisit à la mise en place — faciles, mêlées, corsées. C'est un réglage de jeu, au même titre que la culture de la machine.檢視完整 README ↗
Un Risk où le lancer de dés est remplacé par une question de culture générale.
iPhone, iPad et Mac — une seule cible, un seul code.
L'attaquant choisit une ou deux questions : ce sont ses dés. Il choisit aussi la catégorie — c'est lui qui pose la question, et c'est là qu'est son adresse. Le défenseur répond, dans le temps du sablier.
| ce que fait le défenseur | l'équivalent au dé | qui perd un homme |
|---|---|---|
| bonne réponse | dé supérieur | l'attaquant |
| mauvaise réponse | dé inférieur | le défenseur |
| temps écoulé | dé le plus bas | le défenseur |
Le dosage des questions se choisit à la mise en place — faciles, mêlées, corsées. C'est un réglage de jeu, au même titre que la culture de la machine.
Le temps écoulé est le fait d'un humain, et de lui seul : la machine répond toujours quelque chose. Elle laissait passer le temps une fois sur sept quand elle se trompait, et l'écran n'avait rien à montrer — un silence ne marque aucune proposition en rouge, on ne voyait donc que la bonne réponse en vert pendant que le verdict annonçait un temps écoulé. On y lisait une erreur de l'application. Le retirer ne coûtait rien : pour le moteur, un silence et une erreur ont la même conséquence exactement, et l'équilibre mesuré n'a pas bougé d'un point.
Une question = une paire de dés = une perte. C'est exactement la comparaison du Risk, et le pari est le même : deux questions, c'est deux pertes possibles de son propre côté.
L'égalité, au Risk, profite au défenseur. Elle n'a pas d'équivalent en classique : une question n'a pas de match nul. Le mode face à face, lui, lui en rend un — voir plus bas.
Deux règles tiennent la promesse « vous choisissez le terrain » : la banque ne sort jamais de la catégorie demandée — épuisée, elle recommence plutôt que de déborder sur un autre sujet — et la machine, quand c'est elle qui attaque, tire son terrain au sort pondéré par vos faiblesses connues, sans jamais s'y verrouiller. Viser à chaque fois votre point faible exact est le coup optimal et le plus mauvais de tous : dix fois le même sujet, et chaque duel ressemble au précédent.
Sans hasard, un joueur qui sait ne perd jamais sa place, quelle que soit l'armée en face — la partie ne finirait pas. Ce qui remplace la statistique, c'est le temps qui se resserre : chaque question subie par un même territoire, dans le même tour, raccourcit le sablier.
1ʳᵉ question : 15 s 4ᵉ : 7,1 s
2ᵉ : 11,7 s 5ᵉ et suivantes : 6 s
3ᵉ : 9,1 s
Presser une place finit par payer, comme au Risk — mais c'est le souffle du défenseur qui cède, pas le sort. Le sablier se remet à zéro entre deux tours.
Comme dans la boîte : une carte par territoire, plus deux jokers. On en tire une à la fin d'un tour où l'on a pris au moins une place — c'est ce qui récompense l'audace plutôt que l'attente. Trois cartes assorties — trois symboles identiques ou trois différents, le joker remplaçant n'importe lequel — s'échangent contre des hommes, et le barème monte à chaque échange de la partie : 4, 6, 8, 10, 12, 15, puis cinq de plus à chaque fois. Deux hommes de plus si l'une des cartes porte une place que vous tenez.
C'est le barème qui empêche une partie de s'enliser : plus elle dure, plus les échanges pèsent. Et garder ses cartes ne les fait pas prendre de la valeur — cela laisse seulement la valeur monter pour l'adversaire.
Celui qui achève un joueur prend ses cartes. C'est la règle du Risk, et elle a une raison qu'on ne voit qu'en la retirant : sans elle, la main du vaincu reste gelée là où elle est, et ces cartes-là sortent du jeu pour de bon — le paquet s'appauvrit à chaque élimination, silencieusement.
Le Risk oblige à redescendre sous cinq cartes sur-le-champ, hommes posés dans la foulée. Ici c'est la règle qui existe déjà qui s'en charge : on ne peut pas quitter la phase de renforts avec cinq cartes en main, et le surplus se solde donc à l'ouverture du tour suivant. Le seul écart avec la règle d'origine est le moment où les hommes arrivent, et il évite d'inventer une étape de placement au milieu d'un assaut — le seul endroit du jeu où l'on ne pose jamais rien.
Mesuré sur 300 parties : l'équilibre ne bouge pas (54/45 à culture égale), la partie s'allonge de deux ou trois questions, et la culture pèse un peu plus (67/33 au lieu de 62/38 pour cinq points d'écart).
Il faut tous les territoires, sans exception, au lieu de la part qui suffit d'ordinaire. Mesuré : l'équilibre est le même, mais la partie dure environ deux fois plus — 112 questions au lieu de 71 à deux joueurs, 235 au lieu de 133 à trois. Avec les cartes en plus et trois joueurs, elle peut dépasser six cents questions. C'est une partie de soirée entière, et c'est le but.
Chacun reçoit au départ une conquête que lui seul connaît, et la remplir gagne la partie sur-le-champ. Le seuil de territoires reste en jeu par-dessus : c'est l'autre porte, ouverte à tous, et c'est elle qui garantit qu'une partie finit même si toutes les conquêtes deviennent impossibles.
Le paquet se taille sur le plateau : « deux gros continents » et « trois petits » n'ont de sens que relativement à lui, et les nombres du Risk sont des parts du monde — 24 territoires sur 42, c'est 57 % ; 18 avec deux hommes, 43 % ; 12 avec trois, 29 %. Aucune carte ne demande plus de 60 % du plateau : une conquête doit rester un raccourci, jamais un détour.
| Plateau | Tenir tant de places | … à deux hommes | … à trois hommes |
|---|---|---|---|
| L'Anneau — 28 territoires | 16 | 12 | 8 |
| Europe — 38 territoires | 22 | 16 | 11 |
| Monde — 42 territoires | 24 | 18 | 12 |
S'y ajoutent les continents — sur le Monde, « l'Asie et l'Amérique du Nord », « l'Europe, l'Afrique et l'Océanie », et ainsi de suite — et, à trois joueurs et plus, une carte par camp : faire tomber tel camp, et de votre main. Si un tiers vous prend votre proie, la carte se retourne et devient une conquête de territoires, comme au Risk : sans cela on passerait la fin de la partie à ne plus pouvoir gagner.
La liste complète n'est écrite nulle part : le mode d'emploi de l'application l'engendre à partir du paquet, plateau par plateau. Une liste recopiée mentirait au premier continent qui change de taille.
La machine reçoit une conquête comme vous et peut gagner par elle, mais elle ne la poursuit pas : elle joue comme elle a toujours joué.
Un homme de plus toutes les N bonnes réponses dans un même thème — N se règle à la mise en place, et la règle se retire. Seul le défenseur répond : c'est donc celui qui tient sa place en sachant qui l'empoche.
Il paraissait devoir rattraper les écarts — le joueur qui recule est celui qu'on attaque, donc celui qui répond le plus. Mesuré, c'est le contraire : le mieux cultivé répond juste plus souvent par question subie, et cela l'emporte sur le nombre. Sur 500 parties, 0,75 contre 0,70 passe de 63/36 à 66/33 quand le seuil descend à 3 ; à culture égale, rien ne bouge. Cinq est un bon réglage — l'effet reste dans le bruit ; trois fait peser la culture davantage, ce qui peut être exactement ce qu'on veut.
Renforts (un par tranche de trois territoires, minimum trois, plus les bonus de continent) → attaques → un déplacement, puis le tour passe.
Victoire : tenir sa part de départ plus sept territoires. Sur ce plateau de 28 : 21 à deux joueurs, 17 à trois, 14 à quatre. La conquête intégrale ferait traîner la fin d'une heure pour désigner le même vainqueur.
Le mode classique a un défaut que la mesure ne voit pas et qu'on sent en
jouant : une seule main lance les dés. Le moteur ne touche que
knowledge[défenseur] ; la culture de l'attaquant ne sert donc à rien quand
il attaque. Elle est une armure, jamais une arme. Et l'attaquant passe son
propre tour à regarder l'autre réfléchir.
En face à face, les deux répondent à la même question. Trois règles, toutes empruntées au dé :
| ce qu'il advient | |
|---|---|
| un seul des deux sait | il emporte l'échange |
| les deux savent | le sablier tranche, l'égalité stricte au défenseur |
| aucun des deux ne sait | la place tient — c'est l'égalité du Risk, enfin représentée |
Le départage au sablier n'est pas un ornement : c'est lui qui tient
l'équilibre. Sans lui, l'attaquant ne l'emporterait que dans p × (1−p) des
échanges — 23 % à 65 % de bonnes réponses — et la partie se figerait, plus
personne ne pouvant prendre une place. Avec lui, la mesure donne 41 à 45 %
d'échanges gagnés par l'attaquant, contre 41 à 42 % en classique : la
fourchette du dé contre dé du Risk (41,7 %), et le mode ne dérange donc pas
l'équilibre du jeu. Aucune partie inachevée sur les trois plateaux.
La vitesse ne départage jamais deux réponses inégales — un ignorant rapide ne bat pas un savant lent. Elle ne tranche que ce que le Risk tranchait par le chiffre du dé, et cela représente 37 à 44 % des échanges ; 43 à 45 % se règlent sur la seule connaissance, 13 à 19 % sur l'ignorance des deux.
Deux conséquences non cherchées, et mesurées :
Au Risk, le défenseur choisit un ou deux dés ; en classique il ne décide de rien. En face à face il peut, avant de répondre, doubler l'enjeu : l'échange vaudra deux hommes au lieu d'un, dans le sens où il tombera.
Doubler n'est pas un coup de force, c'est un coup de hasard. Quand les deux savent, l'échange se joue au sablier — à pile ou face, pour deux hommes. Or le hasard sert celui qui est derrière et coûte à celui qui mène : à relancer dès qu'elle se sentait sûre, la machine forte doublait une fois sur quatre et perdait son rang contre la moyenne (39 à 49 % selon le plateau). Elle ne relance donc que lorsqu'elle sait et qu'elle a quelque chose à rattraper.
Une mise de deux ne rapporte jamais que ce que la pile d'en face peut payer : on ne prend pas à l'assaillant sa dernière garnison. Le défenseur qui double face à une pile de deux hommes ne gagne donc qu'un homme — c'est à lui d'y penser.
Le mode a d'abord été illisible, et pour une raison précise : quatre échanges sur dix se décident au sablier, et rien ne le montrait. On répondait juste, on perdait une place, et l'on ne pouvait que croire à une erreur du jeu.
La feuille du verdict affiche donc les deux réponses côte à côte, chacune avec son temps et une couronne sur celle qui l'emporte :
● Bleu ✗ Le Chili 8,9 s
● Rouge ✗ Le Chili 2,7 s ♛
Personne ne savait. Comme sur une égalité de dés, Khanat tient
et l'assaillant laisse un homme.
Trois autres corrections sont venues du même défaut :
Le défenseur répond d'abord, l'attaquant ensuite. L'ordre n'est pas indifférent : celui qui répond en second a eu le temps de réfléchir pendant que l'autre cherchait, et cet avantage revient donc à l'attaquant, qui perd déjà toutes les égalités. L'écran « passez l'appareil » sépare les deux réponses, et la première n'est jamais révélée — la montrer donnerait la solution à qui doit encore répondre. En réseau, la question ne se pose pas : chacun répond chez soi.
Le moteur est du Swift pur, sans interface : mille parties tournent en une seconde, machine contre machine. Tous les réglages viennent de là.
swiftc -O -parse-as-library -o /tmp/sim Sources/Engine/*.swift outils/simulation.swift && /tmp/sim
| réglage | valeur | ce qui l'a décidée |
|---|---|---|
| sablier de départ | 15 s | à 30 s le défenseur tenait 7 fois sur 10 : attaquer devenait une mauvaise affaire et il fallait 200 questions pour finir. À 15 s, l'attaquant gagne 48 % des duels — la fourchette du Risk (39 % à deux dés contre deux, 42 % à un contre un) |
| écart de victoire | 7 territoires | 65 questions à deux joueurs, 120 à trois, 160 à quatre |
| compensation de rang | +2 hommes à deux joueurs | sans elle, celui qui ouvre gagnait 61 % des parties ; avec, 51/48 sur 600 parties. À trois et plus, l'avantage se dilue de lui-même |
| audace de la machine | attaque à forces égales en dernier recours | deux prudentes s'enterraient face à face : 8 parties sur 800 ne finissaient jamais |
Équilibre mesuré (600 à 800 parties par ligne) :
2 joueurs, culture égale 51 / 48 aucune partie inachevée
3 joueurs, culture égale 34 / 30 / 35
4 joueurs, culture égale 23 / 24 / 25 / 27
0,75 contre 0,70 64 / 35 cinq points de culture décident
0,80 contre 0,60 91 / 9
La culture décide, et elle décide nettement : c'est le but.
Trois au choix : L'Anneau, un monde inventé de 28 territoires ; Europe, 38 territoires de l'Atlantique à la mer Noire ; Monde, les 42 territoires et six continents du Risk d'origine.
Les cartes réelles gardent la silhouette approximative du vrai : le bras scandinave qui monte, la péninsule ibérique au sud-ouest, la botte italienne ; l'Amérique du Nord en coin qui se resserre sur l'Amérique centrale, l'Asie qui occupe tout le nord-est, l'Océanie dans le sien.
Cela a demandé une chose que le damier ne permettait pas : les traversées. Sur des hexagones contigus, seules les cases qui se touchent sont voisines — c'est ce qui met les voisinages à l'abri de la faute, mais cela interdit les îles et les détroits, donc toute carte qui ressemble au monde. Elles sont donc déclarées à la main, une poignée par plateau : Alaska–Kamtchatka, Groenland–Islande, Congo–Brésil. Les tests vérifient qu'elles sont réciproques et qu'elles mènent quelque part, et le plateau les trace en pointillé — une liaison qu'on ne voit pas n'existe pas pour le joueur. Deux traits superposés, un large et sombre sous un pointillé clair : un seul trait pâle se perdait sur le fond, et l'on ne devinait pas qu'on pouvait passer.
Une longue traversée s'arque, et s'écarte du centre du plateau. L'Alaska et le Kamtchatka sont aux deux bouts de la carte : le trait droit qui les reliait passait au travers du Groenland, de l'Islande et de toute la Sibérie, et l'on ne pouvait pas dire ce qu'il reliait. Une route qui contourne le monde par le haut se lit d'un coup.
Sur le Monde, l'Océanie n'a qu'une seule porte — Siam–Indonésie. C'est l'Australie du Risk d'origine : imprenable une fois tenue, et elle pèse sur toute la partie. C'est un parti pris assumé, pas un oubli — le test l'admet pour un continent par plateau, et jamais deux, sans quoi la partie se jouerait à qui les prend. Les cartes réelles sont des hexagones posés sur la géographie et non des contours relevés : on garde le plan qui se retouche en déplaçant une lettre, et les tests qui vérifient chaque plateau du catalogue à la compilation — un plateau ajouté ne peut pas entrer sans les passer.
Le plateau se déplace et se rapproche sous le doigt, et se recentre d'une double touche. C'est ce qui permet d'en avoir de plus grands que l'écran : une carte du monde ne tient pas sur un téléphone à une taille où les noms se lisent, mais elle tient très bien si on peut la promener. Les noms réapparaissent d'eux-mêmes dès que la case vue est assez large.
L'Anneau : 28 territoires, 5 continents en cercle — Boréa, Ostmark, Méridia, Zéphyrie, Ponant, et retour. Aucun continent n'a une seule porte : celui qui n'a qu'une entrée devient imprenable et décide la partie à lui seul, c'est le défaut de l'Australie du Risk d'origine.
Les continents sont cernés d'un trait de leur couleur, celle-là même que porte leur pastille en bas de l'écran. Sans lui, ils étaient invisibles : une case porte la couleur de qui la tient, pas celle de la terre à laquelle elle appartient, et l'on ne pouvait pas voir ce qu'il restait à prendre pour toucher le bonus. Le trait est rentré vers l'intérieur de chaque case : deux continents voisins tracent chacun le sien, et l'on obtient le double trait des atlas plutôt qu'une couture.
Le plateau n'est pas saisi territoire par territoire — il est dessiné en
toutes lettres dans TestBoard.swift, et les voisinages s'en déduisent :
. A A A . .
. A A . B .
C C . B B B
C C C . B .
. C . . D D
. C E . D .
. E E E D D
. . E E . .
Le plateau se retouche en déplaçant une lettre. Les tests vérifient à chaque compilation qu'il reste d'un seul tenant, qu'aucun continent n'est coupé en deux et qu'aucun n'a moins de deux portes.
Les teintes étaient rabattues pour ne pas disputer la couleur du camp qui tient la case, et le trait était large : un bandeau éteint autour de chaque territoire, qu'on ne lisait plus comme une frontière.
Le marché s'est inversé — teintes vives, trait deux fois plus fin. Un filet vif se lit comme une frontière ; un bandeau vif se serait battu avec le remplissage. Les huit teintes sont toutes claires là où les camps sont sombres : c'est cet écart de valeur, et non la teinte, qui empêche de confondre un contour de continent avec la couleur d'un joueur.
Sources/Engine/ le jeu, sans une ligne de SwiftUI — il compile et tourne seul
Map · HexPlan · TestBoard la carte et son dessin
Questions · QuestionsFile la banque, lue des fichiers de Resources/
Combat · Rules le duel, et toutes les manettes
Game l'état complet et les seuls coups qui le changent
Bot · BotRunner l'adversaire, et le tour joué un geste à la fois
Random un tirage rejouable, pour que les tests tiennent
Sources/UI/ le plateau, le duel, les panneaux
Sources/App/ le point d'entrée
Sources/Resources/ le catalogue d'icônes, engendré — voir plus bas
Resources/Questions/ 2 400 questions, un fichier par thème, une ligne chacune
Tests/ 37 tests
outils/ la simulation et le dessin de l'icône, hors cible
Le moteur ne connaît ni le temps qui passe ni l'écran : il reçoit une réponse
et rend un compte rendu. Le sablier, l'attente avant que la machine réponde,
le temps qu'on laisse pour lire le résultat — tout cela est dans
GameSession, et c'est du théâtre.
Le tempo y est réglé en un endroit (Tempo). Quand c'est la machine qui
défend, l'attaquant a 4 à 9 secondes pour lire la question — le temps d'y
répondre dans sa tête, ce qui est tout son jeu dans un duel qu'il ne dispute
pas — puis 4,5 secondes sur le résultat : la bonne réponse, les dés
équivalents, et qui perd un homme. Une touche écourte l'une comme l'autre,
sans quoi soixante questions feraient soixante attentes imposées.
Lecture et verdict ne font qu'un seul moment, et donc une seule descente de la barre. Deux descentes — une par étape, avec un remplissage entre les deux — donnaient à croire qu'un second compte à rebours s'ouvrait pour la même question. De même, quand c'est un humain qui a répondu, la barre se fige où elle en était : ce qui lui restait de temps est une information, pas un décompte à rejouer.
Un hexagone du plateau, partagé entre les deux camps, et le point d'interrogation qui les départage — la règle en un glyphe. Les six voisins sont esquissés derrière, très en retrait : sans eux l'hexagone n'est qu'une forme, avec eux c'est une case au milieu d'un plateau. Ils s'effacent d'eux- mêmes aux petites tailles, où ils n'auraient été que du bruit.
Elle est dessinée en code, et non peinte : les couleurs sont exactement celles des camps sur le plateau, et les deux systèmes n'attendent pas la même chose — iOS veut un carré plein qu'il masque lui-même, macOS un galet arrondi posé dans un carré transparent, aux proportions de la grille d'Apple. Les onze images du catalogue se refont d'une commande :
swiftc -O -parse-as-library -o /tmp/icone outils/icone.swift && /tmp/icone
Six signaux, écrits plutôt qu'enregistrés : l'homme posé, l'échange gagné, l'échange perdu, l'ouverture, et la fin de la partie — d'un côté ou de l'autre. Aucun fichier à porter, aucune licence à vérifier, et l'on règle une note en changeant un chiffre. Trois quarts de seconde de son tiennent en vingt lignes de partition.
La fin sonne deux fois différemment, mais seulement là où l'appareil sait pour qui il sonne : en réseau, et seul contre la machine. À deux humains sur un même téléphone, « mon camp » est celui qui l'a ouvert et non celui qui vient de gagner — la fanfare pour tout le monde plutôt que la défaite au vainqueur une fois sur deux.
Une partition se relit, elle ne s'entend pas. L'outil rend les six tels que l'application les joue, et mesure la crête de chaque mélange : plusieurs notes ensemble s'additionnent, et au-delà de 1 le son se coupe au lieu de sonner.
swiftc -O -parse-as-library -o /tmp/ecoute Sources/UI/Sons.swift outils/ecoute.swift && /tmp/ecoute
Deux mille cinq cents questions dans Resources/Questions/ — un fichier par
thème, une ligne par question :
M | Quel fleuve traverse Le Caire ? | Le Nil | L'Euphrate | Le Jourdain | Le Niger
Un fichier déposé est un thème de plus. Les thèmes ne sont énumérés nulle
part dans le code : le dossier est lu au démarrage, et chaque fichier se
présente en tête, avec des lignes qui ouvrent par ! — des déclarations, et
non des commentaires, car un commentaire se perd sans conséquence quand une
déclaration manquante doit se voir :
! id | histoire
! nom | Histoire
! de | d'Histoire ← l'élision, qu'aucun calcul ne devine
! icone | building.columns
! teinte | 0.85 0.66 0.22
! rang | 1 ← sa place dans la grille
C'était un enum à six cas, et le nom, l'icône et la couleur de chacun vivaient
dans trois switch répartis dans le code. Un enum ne grandit pas après la
compilation : ajouter un thème demandait de rouvrir quatre fichiers. Le gain
tient sans rien vendre — le nom d'un thème se corrige là où sont ses questions
— et c'est ce qui rendra possible, le jour venu, un thème vendu à part.
L'identifiant d'une question se calcule sur son énoncé, et non sur sa place dans le fichier. Il était le rang : insérer une question en tête décalait les mille suivantes, et la mémoire de ce qui a déjà été posé se mettait à parler d'autres questions que celles qu'elle nommait. Rien ne plantait — les questions revenaient simplement plus tôt qu'elles n'auraient dû.
Le tirage trie sur cet identifiant avant de tirer. randomElement choisit un
rang et non une question : deux appareils qui rangent leurs questions dans un
ordre différent tiraient le même rang et posaient deux questions différentes.
L'ordre venait des fichiers, c'est-à-dire de ce que le système avait rendu en
premier — rien qui soit promis d'être le même sur un iPhone et sur un iPad.
Elles étaient écrites en dur, et c'était juste tant qu'elles tenaient en quelques dizaines : le compilateur en était le meilleur relecteur. Passé le millier, le calcul s'inverse — un littéral de mille éléments écroule le temps de compilation, une coquille exige une reconstruction, et le fichier n'est plus relisible. Le format n'est pas du JSON non plus : mille questions y feraient quatorze mille lignes.
Ce que le compilateur ne vérifie plus, les tests le vérifient — et mieux. Il n'a jamais su dire qu'un leurre était égal à la bonne réponse. Les tests, eux, refusent : une ligne mal formée, un énoncé en double dans un thème, un leurre vide ou répété, un texte trop long pour la feuille, un énoncé qui ne porte pas exactement une question.
Ce dernier contrôle vient d'une faute réelle, et il a fait son office : en écrivant les fournées à la main, j'ai laissé une vingtaine de fois mes propres hésitations dans le texte — « Quel jeu se joue avec des dominos… plutôt : combien de faces a un dé ? ». Rien ne plantait, et le joueur lisait une phrase absurde. C'est exactement la classe de faute qu'une relecture humaine laisse passer sur mille lignes et qu'un test attrape à tous les coups.
Une partie pose de cinquante à cent soixante questions, et un thème peut en brûler vingt-cinq dans une seule. Quatre cents par thème, c'est la banque qui tient des mois sans se répéter.
Vingt-trois thèmes aujourd'hui, six mille questions. Les six de culture générale à quatre cents ; « Rock 70-80 », quatre cents questions sur les groupes et les voix des années 1970 et 1980, ici et ailleurs ; et seize packs scolaires à deux cents — Histoire, Géographie, Français et SVT pour les quatre années du collège.
Les packs n'ont demandé aucune ligne de code : dix-sept fichiers déposés dans le dossier. Le rock vient en tête, parce qu'il parle à ceux qui ont déjà l'application ; les packs scolaires suivent, rangés par niveau, parce qu'un parent achète pour une année et non pour une matière.
Deux cents, et non cent : c'est mesuré. Vingt séries de cinq soirées d'affilée sur un seul thème, part de questions déjà vues :
| Questions dans le thème | 1re | 2e | 3e | 4e | 5e |
|---|---|---|---|---|---|
| 53 | 0 % | 81 % | 100 % | 100 % | 100 % |
| 117 | 0 % | 35 % | 76 % | 98 % | 100 % |
| 200 | 0 % | 2 à 15 % | 23 à 45 % | 45 à 66 % | 69 à 83 % |
| 400 | 0 % | 0 % | 7 % | 28 % | 40 % |
Une partie pose environ soixante questions. Un thème de cinquante joué seul est donc épuisé à la deuxième soirée — quatre questions sur cinq déjà vues. À deux cents, on a deux soirées propres et une troisième acceptable ; à quatre cents, quatre soirées. C'est ce tableau qui a fixé la taille des packs, et non l'inverse.
Un score de catégorie appartient à celui qui l'a fait, et sa couleur dit son niveau à lui : vert, il y répond bien ; rouge, il y trébuche. Partout — dans le dossier comme dans le panneau d'assaut.
Les deux écrans se contredisaient : le panneau d'assaut colorait du point de vue de l'attaquant (vert sur « 2/2 », parce qu'il fallait éviter ce terrain), le dossier du point de vue du joueur. Le même chiffre était vert d'un côté et rouge de l'autre, et l'on croyait l'application confuse sur ce qui est juste et ce qui ne l'est pas. Où frapper se dit maintenant autrement : une lunette marque le point faible — et seulement s'il en est un, c'est-à-dire moins d'une bonne réponse sur deux.
De même, le verdict d'un duel nomme celui qui a répondu. Quand vous attaquez, c'est l'adversaire qui répond : lire « bonne réponse » puis perdre un homme se prenait pour une erreur.
Sa culture et sa manœuvre sont deux choses distinctes : on peut être savant et jouer mal. Le premier adversaire jouait mal, et cela se voyait — il prenait une place avec dix hommes et en laissait un dedans, en terre ennemie. Trois lignes gourmandes, dont deux récompensaient exactement le mauvais comportement :
| ce qu'il faisait | conséquence |
|---|---|
| après une conquête, si sa base touchait encore un ennemi, avancer le minimum | des garnisons d'un homme semées partout |
| un renfort sur la case la plus menacée, recalculé à chaque homme | des renforts étalés sur tout le front |
| attaquer toute cible où il avait +1 | des piles usées jusqu'à un homme |
C'est le contraire de la doctrine du Risk : une grosse pile bat cinq petites.
La forte garde à sa base ce qu'il lui faut pour tenir et fait avancer tout le reste ; elle verse ses renforts sur une seule pointe après avoir colmaté ce qui tomberait au premier coup ; elle n'attaque plus quand sa pile descend à deux hommes ; elle vise le continent le plus proche d'être complet, tout en cherchant à briser celui que l'adversaire est sur le point de tenir ; et elle achève une place déjà entamée, dont le sablier s'est raccourci.
Mesuré sur une partie, l'écart de comportement est net :
conquêtes hommes avancés places à 1 homme exposées
facile 6 1 7
forte 12 8 2
Une ligne de la forte vaut à elle seule tout l'écart : une pile réduite à deux hommes n'attaque plus. Je l'ai desserrée pour voir — en la laissant ramasser une place sans défense — et elle est tombée de 51 % à 32 % sur l'Europe. Prendre une case avec sa dernière paire d'hommes, c'est précisément le défaut qu'on corrige.
La manœuvre ne suffisait pas à faire trois niveaux. En intercalant une manœuvre intermédiaire — elle tient ses places, mais ne concentre pas ses hommes — j'ai obtenu l'inverse de l'échelle attendue : la moyenne perdait 19 à 22 % contre la facile. La prudence sans la concentration est pire que rien : garder six hommes en arrière sans jamais former de pointe, c'est immobiliser une armée pour rien. La garnison seule ne vaut rien non plus (50 %) ; elle ne paie qu'accompagnée de la pointe.
Le vrai second axe n'était pas sur la carte, il était dans les questions. La machine choisit le thème du duel : le flair mesure à quel point elle vise la faiblesse de l'adversaire au lieu de tirer un thème au hasard. C'est là que les trois niveaux se séparent proprement :
| garnison | pointe | continent | sablier | flair | |
|---|---|---|---|---|---|
| Facile | — | — | — | — | 0 |
| Moyenne | oui | — | — | — | 1,2 |
| Forte | oui | oui | oui | oui | 3,0 |
600 parties par case, à culture strictement égale, chaque paire de rangs échangée pour annuler l'avantage d'ouvrir :
forte/moyenne moyenne/facile forte/facile
L'Anneau (28 territoires) 51 % 55 % 53 %
Europe (38 territoires) 50 % 55 % 54 %
Monde (42 territoires) 59 % 57 % 63 %
Chaque cran gagne sur les trois plateaux, et l'écart bout à bout dépasse chacun des deux pas : l'échelle est cohérente. Mais elle est douce sur les petits plateaux — 50 et 51 % entre forte et moyenne, autant dire un souffle. Il faut de la place pour que la manœuvre paie : sur vingt-huit cases une partie se règle en sept tours et c'est le quiz qui décide ; sur quarante-deux, tenir ses places et concentrer ses hommes se paient. J'ai vérifié que ce n'était pas le seuil de victoire qui récompensait l'étalement : en guerre totale, les petits plateaux donnent le même résultat.
Un mot sur ce que la mesure ne dit pas. Pour mesurer le flair, il fallait que les machines aient des faiblesses par thème ; je leur en ai donné de légères (± 0,16 sur une échelle de 0 à 1). Un humain en a de bien plus grandes — on est nul en sciences ou fâché avec les dates d'une tout autre façon. Contre vous, l'écart entre les trois niveaux sera donc plus large que dans ce tableau, et c'est bien le flair, plus que la manœuvre, que vous sentirez passer.
Le niveau se choisit à la mise en place, indépendamment de la culture de la machine : ce sont deux réglages distincts, et on peut affronter une érudite qui joue mal comme une ignorante qui manœuvre bien.
« Une pile réduite à deux hommes n'attaque plus » vaut tout l'écart de la forte en classique. En face à face, elle lui coûte quatorze points contre la moyenne. J'ai désactivé les traits de la forte un par un pour en avoir le cœur net :
Europe Monde
la forte, telle quelle 44 % 41 %
sans le flair 43 % 40 %
sans le continent 43 % 38 %
sans la pointe 40 % 36 %
sans la retenue 58 % 56 % ← le coupable
La raison tient à ce que vaut un échange. En classique il coûte un homme et n'en prend qu'un, si bien qu'une pile de deux ne finit jamais rien ; en face à face, celui qui sait emporte l'échange sec, et la relance peut en prendre deux d'un coup. La dernière paire d'hommes peut donc achever une place — s'en priver, c'est abandonner des conquêtes réelles. C'est la seule règle du jeu qui s'inverse selon le mode.
Le choix du thème change aussi : en classique la machine vise la faiblesse d'en face, en face à face elle vise l'écart entre sa réussite et celle de l'autre. Choisir l'ignorance de l'adversaire en s'y jetant soi-même, c'est se piéger avec lui — et l'échange se règle alors sur une égalité, qui appartient au défenseur.
600 parties par case, rangs échangés, à culture strictement égale :
classique face à face
culture plateau f/m m/f f/f f/m m/f f/f
0,70 L'Anneau 51 % 55 % 53 % 50 % 53 % 55 %
Europe 50 % 55 % 54 % 57 % 58 % 62 %
Monde 59 % 57 % 63 % 56 % 56 % 62 %
0,55 L'Anneau 41 % 53 % 45 % 46 % 53 % 47 %
Europe 28 % 53 % 31 % 55 % 54 % 57 %
Monde 40 % 55 % 47 % 59 % 56 % 59 %
L'échelle du mode classique est calibrée pour une machine cultivée et se retourne en dessous. À 0,55 de culture, la « forte » y devient un adversaire plus faible que la « moyenne » — 28 % sur l'Europe. La raison est la même que plus haut : quand le défenseur répond mal, attaquer est très rentable, et la discipline qui garde sa dernière paire d'hommes refuse alors des conquêtes faciles. Le face à face, où l'échange se gagne aussi par sa propre connaissance, ne souffre pas de ce retournement.
C'est un défaut connu et non corrigé : il faudrait rendre la retenue de la forte tributaire de ce que le défenseur a montré, et cela demande sa propre campagne de mesures.
Le jeu paraissait mou à l'appui, et la cause n'était pas là où on la cherche. Elle tenait à un double-appui posé sur le plateau pour recentrer la carte. Un geste à deux appuis oblige tous les appuis simples à l'attendre : le système ne peut pas savoir qu'une touche est simple avant que la fenêtre du double se soit refermée. Chaque case touchée payait donc environ un quart de seconde de retard, à chaque fois, pour un raccourci que personne n'utilisait — le bouton de recentrage, en bas à droite de la carte, faisait déjà la même chose. Il est parti.
Deux réglages l'accompagnent :
Enfin, ranger un instant dans la bibliothèque a quitté le fil principal. L'opération relit tout l'index, encode la partie et écrit deux fichiers ; elle se faisait au beau milieu de l'appui sur « Fin du tour », qui marquait donc un temps. Les rangements se suivent maintenant à la queue leu leu, en arrière-plan — chacun attend le précédent, parce que l'index se lit, se modifie et se réécrit : deux à la fois s'écraseraient.
Trois pièces, dans cet ordre : la consigne, le fil du tour, le bouton.
La consigne a la forme du bouton — même capsule, même largeur — mais pas son habit. La nuance compte ici plus qu'ailleurs, parce que cette même place est cliquable pendant l'annonce d'un assaut : deux voisins qui se ressemblent, dont un seul répond, se paient cher.
Premier essai : fond mat, texte éteint, aucune couleur. Assez sobre pour ne pas passer pour un bouton, et au point de ne plus se voir du tout. Elle a donc repris couleur sans reprendre l'habit : le bouton est plein, son texte blanc et gras ; la consigne est un voile teinté cerné d'un filet, et son texte reste de l'encre ordinaire. Trois tons, parce qu'elle dit trois choses différentes — la couleur du camp quand elle attend quelque chose de vous, le rouge quand quelque chose vous bloque, la couleur d'en face quand ce n'est pas à vous de jouer — et un signe qui va avec : une main qui touche, un triangle d'avertissement, une bulle de points de suspension.
Le fil du tour montre les trois temps — renforts, attaque, déplacement — et où l'on en est. La barre du haut nommait la phase en cours, ce qui répond à « où suis-je » mais jamais à « qu'est-ce qui vient après », qui est la question de celui qui découvre le jeu ; elle ne la nomme donc plus. L'occupation d'une place conquise n'est pas une quatrième étape, c'est un moment de l'attaque, et le jalon y reste. Et quand ce n'est pas votre tour il n'y a pas de bouton : le fil prend toute la barre et montre où la machine en est du sien.
Le bouton ne tient plus toute la largeur — il laisse la place au fil — et ses libellés ont raccourci, le fil nommant déjà l'étape : « À l'attaque », « Au déplacement », « Fin du tour ». Sa hauteur, elle, n'a pas bougé : quarante-quatre points, le plancher de ce qui se touche sans rater, et c'est le bouton le plus tapé de la partie.
Le fil se replie en trois temps selon la largeur — les trois mots, puis celui
de l'étape en cours seul, puis trois jalons nus. Le dernier repli tient sur
n'importe quel écran, ce qui est nécessaire : ViewThatFits retient sa
dernière proposition même quand elle déborde. Le bouton, lui, ne se comprime
jamais ; c'est au fil de céder.
Quand c'est elle qui attaque, la partie passe par la carte cinq secondes avant le duel : les deux places s'allument, une flèche part de l'une vers l'autre, et la barre du bas dit qui attaque quoi, avec combien de questions et sur quel terrain.
Sans cela, l'écran de duel recouvrait tout dès la déclaration : on répondait à une question sans jamais savoir d'où venait l'assaut ni ce qu'il visait. Une touche écourte l'annonce, comme partout ailleurs.
L'annonce est dite sous la carte et non par-dessus : les deux places concernées sont souvent en haut du plateau, et un bandeau flottant les aurait justement cachées.
Sur un grand plateau rapproché, la vue va chercher le combat : elle amène le milieu des deux places au centre de l'écran. Sans cela, l'assaut pouvait se dérouler entièrement hors champ. Sans effet si tout le plateau tient déjà — rien ne serait plus agaçant qu'une carte entièrement visible qui se met à glisser toute seule.
Le duel n'est plus un écran qui remplace la carte : c'est une feuille qui coulisse du bas, sans voile par-dessus le plateau. La question, le « prêt ? » et le bilan s'y posent tour à tour. On voit donc les hommes tomber pendant qu'on répond — c'était l'objet même du changement : un jeu de plateau se suit sur le plateau.
Trois conséquences, toutes nécessaires :
L'annonce d'ouverture ne passe pas par le même chemin que les autres : la partie est posée dans l'initialiseur, et une propriété qu'on installe ne déclenche pas sa propre surveillance. Le premier tour restait muet.
Elle survit à la fermeture de l'application : on la retrouve où on l'a laissée, sans rien avoir à faire. Un fichier dans Application Support, et non les réglages du système — une partie est un document. L'écriture est atomique : une coupure en plein enregistrement laisserait sinon un fichier à moitié écrit, c'est-à-dire une partie perdue en croyant la sauver.
Trois choix :
La barre du haut est posée en marge de sécurité et non dans la pile des vues. Dans la pile, la feuille du duel passait par-dessus elle — or cette feuille porte le geste « touchez pour continuer » sur toute sa surface, et elle avalait donc le bouton quitter pendant tout le tour de la machine. Il était impossible de sortir d'une partie tant que l'adversaire jouait.
L'enregistrement part du seul endroit d'où la partie peut changer — aucun coup ne peut donc l'oublier — et il est différé d'une demi-seconde, parce que pendant le tour de la machine les coups s'enchaînent plusieurs fois par seconde et que seul le dernier compte. L'application ajoute un enregistrement immédiat quand elle passe à l'arrière-plan : elle peut être arrêtée sans autre préavis.
La sauvegarde ci-dessus ne garde qu'un état, le dernier, et l'écrase à chaque coup. C'est ce qu'il faut pour reprendre, et exactement ce qu'il ne faut pas pour revenir : au moment où l'on se dit « c'est là que j'ai tout perdu », l'instant en question est effacé depuis longtemps.
La bibliothèque garde donc les parties passées et les instants où l'on peut y revenir. Trois choix de fabrication :
Revenir à un instant ouvre une branche neuve : la partie d'origine reste entière, sinon rejouer une fin effacerait la fin qu'on voulait garder.
Un détail que le test a attrapé et que l'œil n'aurait pas vu : turn compte
les tours de table, pas les tours de joueur. Ne garder qu'un instant par
turn aurait gardé un tour sur deux à deux joueurs, un sur quatre à quatre.
L'instant est donc classé par tour et par camp.
Jusqu'à quatre, un appareil par joueur. Un joueur ouvre la table, les autres la rejoignent.
Il y a trois façons de relier les appareils, et elles ne se valent pas :
chacune a une portée et chacune a un prix. Le jeu, lui, ne sait pas laquelle
est en service — c'est tout l'objet du protocole Fil, qui tient en six
fonctions parce que c'est tout ce que GameSession a jamais demandé.
| Portée | Ce qu'elle demande | Reprise après coupure | D'une soirée sur l'autre | |
|---|---|---|---|---|
La même pièce (Link) | quelques mètres | rien du tout | non — un appareil hors de portée est éteint | non |
Le loin (Relais) | le monde | un serveur, six lettres | oui, deux minutes | oui, une semaine |
Game Center (Arene) | le monde | un compte Apple | non — Apple ne la propose pas | non |
La même pièce reste le chemin par défaut, et l'ordre des trois à l'écran n'est pas neutre : elle ne dépend de personne, ni serveur ni compte, et c'est la seule dont on soit sûr qu'elle marchera encore dans dix ans.
On échange les coups, pas l'état. C'est la décision qui commande tout le reste, et elle n'est possible que parce que le moteur est reproductible : mêmes coups, même tirage au sort, même partie. Chaque appareil tient donc la partie entière et ne reçoit que ce qui la fait changer — quelques dizaines d'octets par coup au lieu des quarante kilo-octets de l'état.
Pour cela il a fallu que chaque coup existe comme une valeur (Action), et
que l'interface n'ait plus qu'une seule porte pour modifier la partie.
Aucun coup ne peut être oublié en chemin : il n'y a pas d'autre chemin.
Trois messages seulement. L'état complet une fois, à la connexion — plus sûr que de faire deviner la mise en place. Les coups ensuite. Et une demande de renvoi si les deux parties divergent : chaque coup voyage avec l'empreinte de la partie telle qu'elle doit être une fois joué, et celui qui reçoit compare. Une divergence ne se voit pas autrement — les deux écrans restent cohérents chacun de son côté, et ce sont deux parties différentes.
Qui héberge choisit le plateau, les règles et le nombre de joueurs, et les envoie avec la partie : plusieurs écrans de réglages seraient autant d'occasions de ne pas être d'accord. C'est lui aussi qui donne son rang à chacun, dans l'ordre d'arrivée, et à chacun le sien seulement. Sur chaque appareil, seul le joueur dont c'est le tour peut agir — et seul le défenseur peut répondre, où qu'il soit.
Au-delà de deux, l'hôte relaie. Rien ne garantit que deux invités se voient directement : chaque coup reçu d'un joueur est renvoyé aux autres. Le relais peut donc livrer deux fois le même coup si la liaison directe passe aussi — d'où le numéro d'ordre que chaque coup porte. Rejoué, il est reconnu et ignoré ; manquant, c'est qu'un coup s'est perdu, et l'on redemande la partie plutôt que de continuer sans lui. C'est le seul défaut que l'empreinte seule ne voyait pas.
Pas de machine dans une partie en réseau : un adversaire artificiel devrait être joué par tous les appareils à la fois, ce qui n'apporterait rien.
Riskelo n'a jamais demandé à personne de créer un compte, et ce n'est pas le loin qui va l'y obliger. Celui qui ouvre reçoit six lettres, il les envoie comme il envoie tout le reste — WhatsApp, SMS, Messages — et l'autre les tape.
Le code se dicte autant qu'il se colle : consonne, voyelle, consonne, voyelle, consonne, voyelle. « MARENO » se répète au téléphone à une grand-mère qui ne trouve pas le lien ; « X7KQ2V » se fait répéter trois fois et se tape de travers. Six cent quatorze mille codes possibles, ce qui suffit très largement pour des salons qui vivent le temps d'une partie.
Le zéro et la lettre O, le un et le I ne se distinguent pas à l'écrit : ils sont redressés plutôt que refusés. Renvoyer « ce code n'existe pas » à quelqu'un qui a tapé exactement ce qu'il voyait est la pire réponse possible.
Et le lien, quand il passe, dispense de taper quoi que ce soit : la page ouvre le jeu sur le bon salon. C'est le chemin ordinaire ; le code écrit en gros est ce qui reste quand le chemin ordinaire échoue — et il échoue, chez quelqu'un, un jour.
Il ne sait pas ce qu'est un territoire, ne compte les points de personne, ne
voit jamais une question ni une réponse. Il a des salons désignés par un code,
et il recopie des paquets dont il ignore le contenu — les mêmes Message,
exactement, que sur le fil de la même pièce.
C'est ce qui le rend anodin en cas de panne. Un serveur qui tiendrait la partie la perdrait en tombant ; celui-ci, en tombant, interrompt une liaison — et les appareils la rétablissent d'eux-mêmes, avec l'état que chacun garde de son côté.
C'est aussi ce qui le laisse tenir en trois cents lignes de JavaScript, sur le
forfait gratuit de Cloudflare. Le salon s'endort entre deux coups : un
joueur qui réfléchit quinze secondes ne coûte rien, puisque c'est le temps de
calcul qui se facture. Voir serveur/README.md.
Il ne reçoit aucun nom de joueur. Le pseudo passait dans l'adresse de connexion, ce qui était commode — le salon affichait « Marie » avant même que son appareil ait dit bonjour. Mais un paramètre d'adresse finit dans les journaux du serveur, et un prénom n'y a rien à faire : il voyage désormais dans un paquet que le serveur recopie sans savoir le lire. Le seul coût est un dixième de seconde où la liste dit « Joueur ».
Dans la même pièce, une liaison qui tombe est un appareil qu'on a éteint : il n'y a rien à attendre. Au loin, c'est un tunnel, un ascenseur, un appel qui arrive — et l'appareil revient trente secondes plus tard. Une partie qui s'arrêterait là-dessus ne finirait jamais.
La bonne surprise est que la reprise n'a demandé aucun message nouveau.
Celui qui revient dit « je ne suis plus à la même partie que vous » —
Message.perdu, qui existait déjà pour les divergences — et celui qui la tient
la renvoie entière, comme au premier jour. Il suffisait que le salon garde la
place au chaud, et que le fil rappelle tout seul en espaçant ses essais.
Les deux minutes de rappel automatique couvrent le tunnel et l'ascenseur, pas le lendemain soir. Or une partie de Riskelo ne tient pas dans une soirée, et c'est le même mécanisme qui la reprend — à trois pièces près :
GRACE_MS), et non plus deux
minutes. Ce qui attend est un point de rendez-vous, pas une partie ;RendezVous) : le code,
quel camp est le sien, où en était le compte des coups, et le camp de chaque
appareil. Sans ce dernier, celui qui héberge ne saurait plus à qui renvoyer
quel rang — et ne renverrait donc rien ;Relais.reprendreLeSalon).
Un code neuf ne servirait à rien : les autres n'ont que l'ancien. Le serveur
le reconnaît à son identifiant, ou refuse en le disant si le code est reparti
à quelqu'un d'autre depuis.Le salon peut très bien avoir été effacé entre les deux soirées : il renaît alors vide sous le même code, et cela ne change rien — il n'a jamais tenu la partie.
La troisième pièce a réparé au passage une panne qui n'avait rien à voir avec le lendemain. L'hôte qui se reconnectait après une coupure obtenait un salon neuf, sous un code neuf : il demandait à héberger, et le serveur, qui ne regardait pas le code qu'il proposait, lui en tirait un autre. Les autres restaient dans l'ancien salon à parler tout seuls, l'hôte dans le sien à attendre, et son écran ne bougeait plus. Rien ne le disait — le bandeau disparaissait, la liaison était parfaite : c'est le gel qu'on a vu au bout de quelques assauts, du côté de celui qui avait ouvert la partie.
Ce n'est pas la partie par correspondance, et le jeu ne le laisse pas croire : un duel se joue sablier en main, les deux appareils allumés en même temps. Ce qui se reprend est le rendez-vous.
C'est aussi ce qui décide si une partie va dans un tiroir, et
Fil.codeDeReprise porte la question. Une partie à plusieurs rangée sans de
quoi rouvrir son fil se rouvrait sans lui : les deux camps redevenaient
jouables sur un seul téléphone, des deux côtés à la fois, chacun jouant
l'adversaire de l'autre sans le savoir.
GameStore tenait une partie. Il en tient maintenant une d'ici, et une par
salon au loin — le code fait la clé : il est unique par partie, c'est déjà
ce que les joueurs s'échangent, et le serveur le garde une semaine de son côté.
Elles n'attendent pas la même chose, et c'est pour cela qu'elles ne peuvent pas partager un fichier. Celle d'ici attend qu'on rouvre l'application. Celles du loin attendent chacune leur monde : celle de Marie ne croise jamais celle de Paul, elles vont à leur rythme. Tout dans un seul fichier, chacune chassait les autres — ouvrir une partie contre la machine un soir de semaine effaçait celle qu'on avait commencée avec sa sœur, rendez-vous compris et sans rien dire.
Rien à changer côté serveur : chaque code est déjà un salon indépendant. Et l'identifiant de l'appareil ne pose pas de problème non plus — le salon ne vérifie les doublons qu'en son sein, donc le même téléphone peut tenir plusieurs salons. C'est ce qui sépare cette idée-ci de la panne du fantôme, qui était deux liaisons vers le même salon.
Chacune a sa porte, et c'est ce qui les distingue à l'écran. Le bouton vert de l'accueil rend la partie d'ici, et le plateau paraît aussitôt. Celles du loin s'alignent en tête de « Jouer à plusieurs ▸ Au loin » — contre qui, quel tour, depuis quand — parce qu'elles mènent à un salon où il faut attendre quelqu'un, ce qu'un bouton vert partagé ne pouvait pas dire.
Ce qui dormait dans les rangements d'avant déménage tout seul au premier regard, sous son code : la partie au loin unique avait déjà dormi à deux endroits, d'abord dans le tiroir d'ici avec son rendez-vous posé à côté.
Ce n'est toujours pas la partie par correspondance : rien ne prévient que c'est votre tour dans l'autre partie. Il faudrait des notifications, donc un serveur qui garde des jetons d'appareil — exactement ce que ce jeu ne fait pas.
Un garde est venu avec : on ne joue pas dans le vide. Sans lui, un coup joué pendant que la liaison est tombée est appliqué ici et n'arrive nulle part. L'appareil se remet d'aplomb au retour — l'hôte renvoie la partie, qui fait foi — mais le joueur aurait vu son coup s'effacer sous ses yeux, ce qui est la chose la plus inquiétante qu'un jeu puisse faire.
Apple fait gratuitement ce que le serveur fait pour zéro à cinq euros, et il ajoute ce qu'aucun code ne donnera jamais : une liste d'amis, et quelqu'un à qui jouer quand on n'a personne sous la main.
Il demande deux choses en échange, et l'écran les dit avant qu'on s'y engage, parce qu'elles se découvrent autrement au pire moment — quand deux personnes sont déjà installées pour jouer. Un compte, d'abord. Et pas de reprise : une partie en temps réel chez Apple ne se reconnecte pas.
Il n'y a pas d'hôte chez Apple non plus : quatre appareils se retrouvent dans une partie, et aucun n'est arrivé le premier. On le désigne donc sans rien se demander — le plus petit identifiant des quatre. Chaque appareil trie la même liste et trouve le même premier, sans qu'un seul paquet ait à circuler. Un vote aurait demandé un message, donc un désaccord possible, donc une panne de plus.
Le jeu au loin est l'endroit où une panne ne dit rien du tout. Une partie qui diverge reste cohérente de chaque côté : les deux écrans sont justes, les deux joueurs sont sûrs d'eux, et ce sont deux parties différentes.
serveur/essai.mjs — vingt-deux vérifications de bout en bout sur le
serveur, dont la coupure et le retour. Aucune attente n'y est un délai fixe :
la première version dormait trois cents millisecondes après chaque envoi et
passait, jusqu'à ce que la machine soit occupée ailleurs — onze
vérifications sur vingt et une se sont mises à échouer d'un coup, sur un
serveur qui n'avait pas changé d'une ligne.Tests/LoinTests.swift — le code, la mise en place commune aux trois fils,
et la reprise, sur un FilFactice qu'on coupe d'un booléen.Tests/SalonTests.swift — l'application contre le vrai serveur. C'est la
couture la plus risquée et la seule qu'un essai en mémoire ne peut pas
tenir : le nom des clés. Il a trouvé ce qu'il cherchait dès le premier
passage — l'hôte n'annonçait jamais son code, l'écran marchant par un autre
chemin que l'état.C'était l'étape la plus muette de tout le jeu. On touchait le nom d'un appareil dans la liste — et l'écran ne changeait pas d'un pixel. Pas de tourniquet, pas de message : l'invitation partait, expirait vingt secondes plus tard sans que le système prévienne personne, et la liste restait la liste. On croyait que l'appui n'avait pas été pris.
Trois choses manquaient, et aucune n'était dans le réseau :
rejoindre envoyait l'invitation sans
rien changer à l'état du lien. Il y a désormais un .invite, et l'écran le
montre.didNotStartBrowsingForPeers et didNotStartAdvertisingPeer n'étaient pas
implémentés : quand iOS refusait d'ouvrir le réseau local, il le disait, et
personne n'écoutait.Le jeu à deux appareils marchait. L'extension à quatre a été validée en simulation, et jamais sur du vrai matériel — et c'est là qu'elle a cessé de marcher.
Une seule ligne portait la différence :
case .connected:
if !self.jHeberge { self.fermerLaTable() } // l'hôte, lui, continue
À deux appareils, l'hôte cessait naturellement d'annoncer dès que le second arrivait : la table était pleine. Pour en accueillir trois, il fallait qu'il continue — et il a continué jusqu'au lancement, c'est-à-dire pendant toute la négociation de la session. Une annonce qui se poursuit par-dessus une liaison en cours d'établissement la brouille sur du vrai matériel : le pair passe « en cours », puis retombe « non relié », sans erreur et sans message. Deux simulateurs sur la même machine ne le voient pas.
L'hôte connaît désormais le nombre d'appareils qu'il attend et s'arrête dès que la table est pleine. À deux, c'est exactement le comportement d'avant ; à quatre, il accueille jusqu'au troisième invité puis se tait.
MCPeerID était refait à chaque démarrage :
private let moi = MCPeerID(displayName: Link.nomDeLAppareil) // à chaque fois
C'est précisément ce qu'Apple demande de ne pas faire. Un MCPeerID doit être
archivé et réutilisé : le système garde trace des appareils qu'il a vus, et un
téléphone qui se présente sous une identité neuve à chaque lancement finit par
en accumuler des dizaines pour lui seul. La liaison marche la première fois,
puis échoue — sans erreur, sans message, et sans que rien n'ait changé entre
les deux essais.
C'est la signature exacte de « ça a fonctionné après l'installation, et plus jamais depuis ». Elle est désormais gardée dans les réglages et refaite dans un seul cas : si le nom de l'appareil a changé.
Corollaire pratique : une fois les identités accumulées, il faut redémarrer les appareils pour que le système oublie les anciennes. Installer la correction ne suffit pas à effacer ce qui traîne déjà.
Une régression que j'ai fabriquée en voulant corriger autre chose, et qu'il faut garder écrite parce qu'elle est facile à refaire.
En ajoutant l'état « connexion en cours », j'ai ajouté du même geste :
case .notConnected:
if case .invite = self.state { self.state = .refuse(nom) } // faux
Cela paraît évident — « non relié » pendant une invitation, c'est un refus.
C'est faux. MCSessionDelegate annonce cet état au fil de la négociation,
pour un pair qui n'a encore jamais été relié et qui va l'être une seconde plus
tard. Conclure là tuait des liaisons qui allaient aboutir : le symptôme est
passé de « les appareils se relient mais la partie ne démarre pas » à « les
appareils ne se relient plus du tout », et c'était moi.
Seul le délai conclut, désormais. Et il est plus long que l'invitation elle-même : relancer par-dessus une invitation encore vivante en enverrait deux à la fois.
Une fausse piste, notée ici parce qu'elle est tentante. Devant une liaison qui n'aboutit pas, on pense à couper le Wi-Fi pour forcer le Bluetooth : la documentation d'Apple parle des deux, et beaucoup de box bloquent le trafic direct entre appareils tout en laissant passer la découverte.
C'est le contraire qu'il faut faire. MultipeerConnectivity s'appuie sur le Wi-Fi, en réseau ou en pair à pair, et couper le Wi-Fi supprime toute découverte : les deux appareils cherchent indéfiniment sans jamais se voir. Le Wi-Fi doit être allumé des deux côtés — pas nécessairement sur le même réseau, mais allumé.
La panne la plus longue à trouver, parce que tout le reste marchait : les deux appareils se voyaient, la table était ouverte, l'autorisation accordée — et l'invitation restait sans réponse.
invitationHandler doit être appelé dans le rappel lui-même. Il ne l'était
pas : la réponse passait par un saut vers le fil principal.
Task { @MainActor in
invitationHandler(true, self.session) // trop tard
}
Une invitation à laquelle on répond en différé peut être tenue pour sans
réponse. Celui qui invitait voyait « n'a pas répondu » pendant que la table
était grande ouverte en face. La session est donc désormais lisible depuis
n'importe quel fil — MCSession le supporte, c'est la seule raison pour
laquelle la copie est sûre — et le rappel répond sur-le-champ.
C'est le prix d'un moteur @MainActor : tout ce qui vient du système arrive
ailleurs, et l'on prend l'habitude de sauter vers le fil principal sans se
demander si l'appelant peut attendre. Ici, il ne pouvait pas.
Il y a une cause plus sournoise encore, et elle n'est pas dans le code :
l'application doit déclarer le réseau local dans son Info.plist
(NSLocalNetworkUsageDescription et NSBonjourServices). Sans ces deux
entrées, iOS ne demande jamais l'autorisation, ne l'accorde donc jamais, et
refuse la liaison — mais la découverte, elle, continue de fonctionner par le
Bluetooth. Les appareils se voient parfaitement et ne se relient jamais.
Aucune compilation n'échoue, aucun code n'est fautif, et rien nulle part ne
nomme la cause.
Ce n'est pas une hypothèse : deux dossiers de compilation ont coexisté, et
l'un contenait une version bâtie sans ces entrées. Le fichier est engendré par
xcodegen — une compilation lancée depuis Xcode sans l'avoir régénéré prend
ce qui traîne sur le disque.
Deux tests interrogent donc maintenant le paquet lui-même : les deux
entrées sont là, et les services Bonjour déclarés correspondent au nom du
service dans Link. Les séparer serait une panne muette de plus, et rien ne
les reliait.
Un signe simple pour trancher, sur l'appareil : Réglages → Confidentialité et sécurité → Réseau local. Si Riskelo n'y figure pas du tout, ce n'est pas un refus — c'est que l'application n'a jamais demandé, et donc que la version installée ne déclare rien.
La cause de loin la plus fréquente est l'autorisation « réseau local ». Elle se demande une seule fois, au premier essai, et si l'on répond « Ne pas autoriser » elle ne se redemande jamais : les appareils continuent de se voir — la découverte passe par le Bluetooth — mais la liaison ne s'établit plus. L'écran d'échec donne donc le chemin exact : Réglages → Confidentialité et sécurité → Réseau local → Riskelo.
Le jeu à plusieurs appareils a une panne qui ne ressemble pas à ce qu'elle est : les deux appareils se voient, se relient, l'un affiche « Tout le monde est là » — et rien ne démarre. On cherche du côté du réseau, qui va très bien.
La cause est ailleurs : la partie arrive et ne se lit pas. Elle voyage en
JSON, et si les deux appareils n'exécutent pas la même version du jeu, celui
qui reçoit ne sait pas décoder ce qu'on lui envoie. Message.lire rendait
alors nil, la vue ignorait le message, et l'écran continuait d'annoncer « en
attente du lancement… » — c'est-à-dire un mensonge : il n'y avait plus rien à
attendre.
Trois corrections, dont aucune ne change le protocole :
Message.data et Message.lire ne mangent plus leurs erreurs : elles
les écrivent. Un message qui ne part pas, ou qui ne se lit pas, laisse
désormais une trace.Le tout est gardé par des tests qui font voyager sur le fil une partie de chaque plateau dans chaque mode, un assaut en cours avec sa mise doublée, et chacun des onze coups du jeu — en vérifiant à chaque fois que l'empreinte survient identique de l'autre côté.
Le numéro de dialecte a d'abord été posé à l'intérieur du message :
case partie(GameState, votreRang: PlayerID, numero: Int, dialecte: Int) // trop tard
Ce qui paraît suffisant et ne l'est pas. Pour lire ce numéro, il faut d'abord décoder le message — c'est-à-dire réussir exactement ce qu'un désaccord de version fait échouer. Le garde-fou était derrière la porte qu'il devait garder : il ne parlait que si tout allait déjà bien.
Le numéro est désormais devant, dans une enveloppe dont la forme est le seul contrat que toutes les versions à venir doivent tenir :
{ "dialecte": 3, "message": { … } }
Un décodeur ne réclame que les clés qu'il connaît, et celui de l'en-tête n'en connaît qu'une. Le dialecte se lit donc même quand le message qu'il enveloppe est écrit dans une langue entièrement inconnue — le cas réel, puisque deux versions différentes, ce sont deux formes de message différentes.
Message.lire rend maintenant trois issues au lieu d'une valeur ou de nil,
parce que « je n'ai pas compris » ne dit pas pourquoi, et que les trois ne
demandent pas la même chose au joueur :
| issue | ce que c'est | ce qu'on lui dit |
|---|---|---|
.message | tout va bien | rien |
.autreDialecte(n) | une autre version du jeu — n est le numéro qu'elle annonce, nil si elle est antérieure à l'enveloppe et n'en annonce aucun | « installez la même version sur les deux » |
.illisible | le bon dialecte, et le contenu ne passe pas | c'est un défaut du jeu : le numéro n'a pas été monté quand la forme d'un message a changé |
Au passage, une faute que l'ancienne écriture cachait : le salon comptait pour un désaccord de version tout ce qui n'était pas la partie. Un coup arrivé une fraction de seconde trop tôt affichait donc « Versions différentes » à deux appareils parfaitement d'accord.
La panne la plus instructive de toutes, parce qu'elle ne ressemblait à aucune des précédentes et qu'aucun réglage ne l'expliquait.
Un iPad hébergeait, un iPhone rejoignait. L'iPhone voyait la table, touchait son nom, et vingt secondes plus tard : « l'iPad n'a pas répondu ». La console de l'iPhone disait pourquoi :
nw_resolver … Query fired: did not receive all answers in time
for 5929a740-…-79f76966b86c.local.:49238
Il trouvait le service et ne parvenait jamais à en résoudre l'adresse. Tout le
reste était sain, et un troisième appareil l'a prouvé : depuis un Mac sur
le même Wi-Fi, la table de l'iPad se laissait trouver et résoudre
instantanément — dns-sd -B _riskelo-jeu._tcp, puis dns-sd -L, puis
dns-sd -G jusqu'à 192.168.1.142. C'est le geste qui a tout débloqué :
devant deux appareils qui s'accusent l'un l'autre, un témoin extérieur tranche.
Puis on a inversé les rôles. L'iPhone hébergeant, l'iPad rejoignant : relié en onze secondes.
La liaison était asymétrique, et le jeu imposait justement la direction bouchée. L'hôte annonçait, l'invité cherchait et invitait ; ce sens-là ne passait pas, l'autre était grand ouvert, et rien dans le jeu ne permettait de l'emprunter.
Les deux appareils sont désormais symétriques :
r=h pour une table, r=i pour qui en cherche
une ;c=<nom de l'hôte>) ;Le rôle absent est traité comme une table : autrefois, seules les tables s'annonçaient.
C'est arrivé, et c'est la cause réelle d'une soirée entière : trois copies du
projet cohabitaient sur le Bureau — riskelo, riskelo copie,
riskelo ligrh — toutes avec le même PRODUCT_BUNDLE_IDENTIFIER. Sur un
iPhone, la dernière installée écrase les autres, et rien à l'écran ne dit
laquelle est en place.
L'une d'elles était l'ancêtre d'avant toutes les corrections de ce chapitre : pas d'identité gardée, pas de réponse immédiate à l'invitation, pas de déclaration de réseau local dans son paquet, pas de dialecte. Un ⌘R dans la mauvaise fenêtre d'Xcode réinstallait toute la panne d'un coup.
La leçon ne tient pas dans le code : un seul dossier de projet. Les autres s'archivent hors du Bureau, ou changent d'identifiant. Le signe qui trahit la situation est visible sans rien ouvrir :
for x in ~/Library/Developer/Xcode/DerivedData/Riskelo-*; do
/usr/libexec/PlistBuddy -c 'Print :WorkspacePath' "$x/info.plist"
done
Plus d'une ligne, et l'on ne sait plus ce qui tourne sur les téléphones.
La règle du jeu ne tenait nulle part : elle était dans ce fichier, c'est-à-dire
dans un endroit où aucun joueur ne va. Elle est maintenant dans
l'application — Sources/UI/ManuelView.swift, quatorze chapitres — et elle
s'ouvre de deux endroits : le bouton Mode d'emploi de l'accueil, et le point
d'interrogation de la barre du haut pendant une partie. C'est le second qui
compte : la règle d'un jeu de plateau se consulte au moment où l'on hésite, pas
avant de commencer. Elle se pose par-dessus le plateau sans rien interrompre.
Le texte des chapitres est écrit une fois, sous forme de données — une
suite de blocs typés, jamais de vues — et les chiffres qu'il cite sont ceux de
Rules. Un réglage qui bouge là-bas se corrige ici, à un seul endroit.
Le même texte est en HTML dans docs/, avec quatre autres pages : la page
d'accueil, l'assistance, la politique de confidentialité et les conditions
d'utilisation. C'est le site, servi par GitHub Pages depuis ce dossier —
https://boboul-cloud.github.io/riskelo/. Apple exige deux de ces URL
(assistance, confidentialité) et n'accepte pas qu'elles renvoient sur un
formulaire vide.
soumission/ porte le reste : les métadonnées prêtes à coller, les réponses au
questionnaire de confidentialité, les tailles de captures d'écran et les notes
pour le relecteur. Deux points y sont traités parce qu'ils font tomber des
soumissions : aucune marque de jeu de société n'apparaît nulle part — ni
dans les mots-clés, ni dans la description — et le jeu à plusieurs appareils
est annoncé comme demandant deux appareils, faute de quoi un relecteur qui ne
peut pas l'essayer refuse la version.
Le fichier .xcodeproj n'est pas versionné : il se refait de project.yml,
qui est la seule description du projet.
xcodegen generate
xcodebuild -scheme Riskelo -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test