Skip to content

Fix/duree seance defis - #336

Merged
bbstudioapp merged 13 commits into
developfrom
fix/duree-seance-defis
Aug 12, 2026
Merged

Fix/duree seance defis#336
bbstudioapp merged 13 commits into
developfrom
fix/duree-seance-defis

Conversation

@bbstudioapp

Copy link
Copy Markdown
Owner

No description provided.

L'horloge de timeline est gelée pendant tout un défi (choix assumé : le
défi ne consomme pas de temps de séance). C'est pourtant elle que l'écran
de fin affichait et que `_finish` créditait au temps de jeu cumulé — un
défi de dix minutes ne rapportait donc ni point de spécialisation, ni
déblocage de coach, ni badge d'endurance, et la séance était rapportée
plus courte que ce que la joueuse venait de jouer.

`playedSeconds` porte désormais le temps réellement passé en séance,
pauses exclues. Le gel de la timeline est inchangé : il continue de
piloter le contenu.
Le générateur réserve après chaque trigger de défi une enveloppe
`breath + nominalDurationSeconds` où il ne pose aucun contenu, puisque le
défi y sera joué. Cette enveloppe était prise sur la durée demandée, et
le runtime la retirait *encore* de la durée de séance
(`_excisChallengeFromSession`) : la joueuse la payait deux fois. Une
séance « Moyenne » (1500 s) avec les 3 défis du retour utilisateur — dont
un d'endurance à 530 s — ne rendait plus que ~980 s de contenu.

`GenerationContext.challengeReserveSeconds` cumule ces fenêtres : elles
décalent d'autant la fin de la boucle main et sont retranchées de
`progress`, si bien que le contenu et la courbe de difficulté restent
ceux de la durée demandée. La séance dure donc le temps demandé plus le
temps des défis.
`challenges` était découpée sur le curseur de file, qui avance aussi
quand un défi est écarté faute de place, alors que
`challengeTriggerTimes` ne reçoit une entrée que pour les défis
insérés. Le contrôleur indexe les deux listes par la même position :
dès qu'un défi était écarté, `_updateChallengePhase` sortait des
bornes dans le ticker principal, où une exception ne s'auto-annule
pas — gel de séance ou plantage.

Le générateur ne livre plus que les défis réellement insérés, et la
boucle d'armement se borne à la plus courte des deux listes.

Cas structurel : les trigger times sont répartis à 20/50/80 % du
budget sans tenir compte de la taille du défi, donc un défi de 530 s
tiré en dernier n'a jamais la place — d'où le désalignement.
Un défi ne s'arme que si l'horloge traverse sa fenêtre
`[trigger, trigger + 13 s)` pendant qu'un tick tourne. La reprise
après un fail saute à la prochaine section : quand le fail tombait
pile dans cette fenêtre, le saut passait par-dessus et le défi
disparaissait sans un mot — N défis demandés au menu, N-1 joués.

Le saut de reprise est désormais borné à l'ouverture de la fenêtre du
prochain défi encore armable ; quand elle est déjà ouverte, on ne
saute pas du tout (le défi s'arme au tick suivant et gèle l'horloge
lui-même).
Le défi ne prend rien à la séance, il s'y ajoute : une « Moyenne » à
3 défis annonçait « 25 min » pour ~35 min vécues. La durée affichée
sous le sélecteur porte désormais la mention quand les défis sont
activés — « 25 min + le temps des défis » — dans les 4 langues.

La mention va sur la ligne pleine largeur et non dans les cartes du
sélecteur : à 5 cartes sur un écran de 360 dp il reste ~42 dp de
texte par carte, où « Herausforderungen » ne rentre pas.
Le contrôle de place était devenu auto-contradictoire : depuis que
l'enveloppe d'un défi est reportée sur `genUntil`, insérer un défi ne
coûte plus de contenu, donc `remaining = genUntil - time` est invariant
sous l'insertion — le test réclamait une place que l'insertion crée
elle-même. Un défi de 530 s tiré en dernier n'entrait jamais : une
séance sur trois perdait son plus gros défi, sans un mot.

Contrepartie assumée et annoncée : la séance dure la durée choisie plus
le temps des défis. Mesuré sur 200 tirages, une « Bâclée » (360 s
nominal, 456 s générés sans défi) avec le trio du retour utilisateur
passe de 516 s à 1059 s — 17,6 min médianes. Le taux d'insertion est de
100 % sur 4800 générations croisant 10 niveaux, 4 paliers et l'ordre de
tirage.
Puisque tous les défis entrent désormais, la joueuse doit savoir au
moment où elle active le réglage que sa séance dépassera le format
choisi. La note complète le texte du réglage dans les 4 langues.

La mention portée par la ligne de durée tenait sur deux lignes avec un
libellé d'exemple mais pas avec les vrais : « Zeit für
Herausforderungen » poussait la carte à trois lignes sur les 5 paliers
allemands. Formulation allemande resserrée, et le test porte désormais
sur les libellés réels de tous les paliers — dont « Überraschung », qui
n'est pas un nombre.
La borne défensive de la boucle d'armement empêche le plantage si
`challenges` et `challengeTriggerTimes` divergeaient à nouveau, mais
elle abandonnait le défi orphelin en silence complet : personne ne
l'aurait su avant qu'une joueuse ne le signale — ce qui vient
d'arriver.

Une ligne, une seule fois par séance : la boucle tourne à chaque tick.
… les stats

Décision produit : l'horloge qui décide de la fin de séance ignore le
temps passé en défi — la joueuse joue la durée de contenu qu'elle a
choisie — tandis que le chiffre rendu en fin de séance et le temps
crédité aux statistiques comptent tout, défis compris.

C'est ce que fait `297abe4` au milieu d'un lot de correctifs ; ce test
énonce l'invariant pour lui-même. Il casse sur les deux neutralisations
(crédit ramené sur l'horloge gelée : 7 s au lieu de 21 ; panneau de fin
recâblé sur `elapsedSeconds`).

Le panneau de fin est privé à `session_screen.dart` et une séance ne
tourne pas sous horloge simulée : à défaut de lire le chiffre rendu, la
clause d'affichage verrouille la ligne qui l'alimente.
L'ampleur d'un défi « durée » vaut `comfort × kChallengeOverloadFactor` et
n'avait aucun plafond — contrairement aux défis de vitesse (bornés par le
BPM maximum du moteur) et de profondeur (bornés par le nombre de crans).
Or `comfort` ne fait que monter avec la pratique : sur le profil du retour
utilisateur 0.6.1 une Bâclée annoncée 6 minutes en durait 15, et rien ne
bornait la suite (73 minutes à `comfort = 3000`).

Le plafond est dérivé du palier : la part `kChallengesShareOfFormat` de la
durée annoncée, divisée par le nombre d'événements que le palier planifie.
Comme un palier ne demande jamais plus de défis qu'il ne planifie
d'événements, la somme des défis d'une séance ne peut plus dépasser la
moitié de la durée annoncée, quel que soit le format et quel que soit
l'état du catalogue de milestones.

Le tutoriel (5 s) et les défis exploratoires (5-30 s) passent très en
dessous du plus petit plafond (180 s) : le plafond ne mord que sur les
axes durée d'un profil déjà entraîné. Tronquer la cible tronque aussi la
valeur créditée au profil, qui passe alors sous le `comfort` — la
régulation y est neutre, ni le record ni la cible ne bougent. La mesure
n'est pas faussée, elle est rendue optionnelle : « JE TIENS ENCORE »
reste ouverte pour la reprendre, sur décision de la joueuse.

Le paramètre est requis plutôt que défaillant : un appelant qui l'oublie
ne doit pas retomber silencieusement sur l'ancien comportement sans borne.
… redit le temps

Le sous-titre annonçait « un défi vers 60 % de la séance » alors que le
palier en pose jusqu'à quatre, répartis entre 20 % et 80 % de la séance.
Faux dans les quatre langues, et faux juste à côté d'une phrase qu'on
venait de rendre exacte. Le nombre annoncé est désormais celui des
paliers, avec une garde qui casse si un palier planifie plus d'événements
que les textes n'en annoncent.

La mention courte allemande avait perdu le mot « Zeit » en se resserrant :
« 12 Min. + Herausforderungen » se lit comme une énumération — durée et
défis compris — c'est-à-dire l'inverse de ce qu'elle veut dire, quand le
français, l'anglais et l'espagnol gardent tous les trois le temps.

Aucune formulation gardant `Herausforderung` en entier ne tient : mesurées
aux 5 paliers × 4 largeurs, « Herausforderungszeit », « Zeit für
Herausforderungen » et « zzgl. Herausforderungen » débordent toutes à
trois lignes — le mot seul occupe déjà une ligne complète. « Challenge-Zeit »
tient partout et garde les deux notions ; « Challenge » se lit sans effort
comme le synonyme du toggle affiché juste au-dessus.

Le test de mise en page porte maintenant sur les quatre largeurs : le
resserrement d'un texte allemand se relit sur toute la gamme visée, pas
seulement sur l'écran le plus étroit.
…le source

La clause d'affichage se verrouillait sur la ligne qui alimente le panneau
de fin, faute de pouvoir lire le chiffre rendu : le panneau est privé à
`session_screen.dart` et `Stopwatch` n'est pas simulé par `flutter_test`.
Elle se lit maintenant sur le texte que l'écran affiche vraiment.

Deux points rendaient le montage possible. `debugFinishSuccess()` clôt une
séance par arithmétique en passant par le même chemin de fin que le jeu
réel, et un bouton de debug l'appelle déjà depuis l'écran. Et une boucle
qui avance les deux horloges ensemble — le temps du mur sous `runAsync`
pour le `Stopwatch`, l'horloge simulée par `pump` pour le ticker — fait
tourner une séance dans un test de widget.

Le défi, lui, se joue en temps réel : c'est le gel de la timeline pendant
le défi qui fait diverger les deux horloges, et sans divergence les deux
câblages rendraient le même chiffre. Le test coûte 16 s et casse bien sur
la neutralisation : « Durée : 7 s » au lieu des 15 s jouées.
@bbstudioapp
bbstudioapp merged commit 17f8d41 into develop Aug 12, 2026
5 checks passed
@bbstudioapp
bbstudioapp deleted the fix/duree-seance-defis branch August 12, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant