Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions rhythm_coach/lib/career/services/career_level_gates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ class CareerLevelGates {
static int defaultMaxDepthIndex() => Position.full.index;

/// Plafond de profondeur dérivé du `CapabilityProfile` (Phase 19.7).
/// Lit `rhythm.depth_max.comfort` (en cran, double), l'arrondit et le
/// borne dans `[mid, full]`.
/// Lit `rhythm.depth_max.comfort` (en cran, double), l'arrondit, ajoute
/// **un cran de sonde** tant que le `best` prouvé est plus profond, et
/// borne le tout dans `[mid, full]`.
///
/// Plancher à `mid` (= idx 2) : les basics enseignent déjà la
/// progression jusqu'à `mid` (cf. `intro_deeper_basics`, `intro_hold_mid`,
Expand All @@ -107,7 +108,25 @@ class CareerLevelGates {
if (profile == null) return defaultMaxDepthIndex();
final comfort = profile.comfortOf(CapabilityAxis.rhythmDepthMax);
if (comfort == null) return defaultMaxDepthIndex();
// Sonde vers le `best` prouvé : quand la profondeur déjà atteinte est plus
// profonde que la cible courante (compétence rabaissée par un tap-out
// imputé ou par le decay), on repropose **un cran** au-dessus du comfort.
// Sans ça le tirage reste borné au comfort, donc `reached` ne dépasse
// jamais `comfort`, donc le régulateur n'a jamais l'overshoot qui le ferait
// remonter : descente facile, remontée verrouillée. Jamais au-delà du
// `best` — on ne sonde que du territoire déjà prouvé.
//
// Écrit en ternaire, et **pas** `min(rounded + 1, best.round())` : les
// deux formules ne coïncident que tant que `best >= comfort`. Sur un
// profil incohérent (`best < comfort`, atteignable par un import de
// profil, qui écrit les deux clés sans passer par le régulateur), le
// `min` rabaisserait le cap SOUS le comfort — l'inverse de ce qu'on
// corrige. C'est le pendant du `max(comfort, …)` explicite de
// `capability_clamps.dart`, ici porté par la forme. Le test `cap(3, 2)`
// de `capability_depth_comfort_vs_best_test.dart` sépare les deux.
final rounded = comfort.round();
return rounded.clamp(Position.mid.index, Position.full.index);
final best = profile.bestOf(CapabilityAxis.rhythmDepthMax) ?? comfort;
final probe = best.round() > rounded ? rounded + 1 : rounded;
return probe.clamp(Position.mid.index, Position.full.index);
}
}
43 changes: 28 additions & 15 deletions rhythm_coach/lib/career/services/generation/capability_clamps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,17 @@ class CapabilityClamps implements CapabilityClampSurface {
static const double kIntenseComfortBoost = 1.30;

/// Bonus de cran de profondeur appliqué à `rhythmDepthMax` quand
/// `intense=true` (séance d'escalade). Cumulé avec le +1 cran de
/// surcharge si l'axe est aussi `overloadAxis`. Le clamp `to <=
/// `intense=true` (séance d'escalade). Cumulé avec le +1 cran accordé en
/// amont par [capabilityCapFor] (surcharge ou sonde). Le clamp `to <=
/// maxDepthIndex` (Position.full) et les milestones acquittées bornent
/// en amont via `_maxDepthIndex`/`_milestoneRhythmCeilingIdx`, donc
/// "+1 cran" ne fait jamais sauter une milestone non débloquée.
static const int kIntenseDepthCranBonus = 1;

/// Plafond effectif (= le plus contraignant) d'un axe de capacité pour
/// la génération en cours : minimum de `comfort` (éventuellement
/// **surchargé** si c'est [overloadAxis] de la séance, et **boosté**
/// **surchargé** si c'est [overloadAxis] de la séance, **sondé d'un cran
/// vers le `best` prouvé** pour `rhythmDepthMax`, et **boosté**
/// par `kIntenseComfortBoost` si la séance est `intense`) et du plafond
/// figé sur un FAIL de cette session ([ceilings], §6 — qui plafonne
/// *même* l'axe surchargé : pas de re-fail dans la même séance).
Expand All @@ -189,18 +190,30 @@ class CapabilityClamps implements CapabilityClampSurface {
final p = config.capProfile;
if (p == null) return null;
var comfort = p.comfortOf(axis);
if (comfort != null && axis == config.overloadAxis) {
if (axis == CapabilityAxis.rhythmDepthMax) {
// Profondeur = cran discret : on autorise +1 cran, et seulement si
// la confiance au cran courant est là (cf. asymétries §5).
// « Humiliation l'autorise » + « milestone d'unlock acquittée »
// sont déjà garantis par `_maxDepthIndex` (qui borne `to` en amont).
if (p.stateOf(axis).successRate >= CapabilityRegulator.kDepthCranGate) {
comfort = comfort + 1;
}
} else {
comfort = comfort * config.overloadFactor;
}
if (comfort != null && axis == CapabilityAxis.rhythmDepthMax) {
// Profondeur = cran discret, deux régimes distincts pour le +1 cran :
//
// - **Escalade** (la profondeur EST l'axe surchargé et la confiance au
// cran courant est là) : le cran peut dépasser le `best`. C'est ce
// qui fait grandir le `best` — comportement d'origine, inchangé.
// - **Sonde** (tout le reste) : le cran est borné par le `best` déjà
// prouvé. On ne pousse pas au-delà de ce qu'elle a tenu, donc pas
// besoin d'attendre que l'axe gagne la loterie de surcharge ni que
// `successRate` repasse le seuil — deux conditions qui verrouillaient
// la remontée : tant que le clamp rabat `to` au comfort, `reached` ne
// dépasse jamais le comfort, donc le régulateur ne voit jamais
// l'overshoot qui le ferait remonter.
//
// `max(comfort, …)` : garde-fou pour un profil où `best < comfort`
// (état persisté incohérent) — la sonde ne doit jamais RABAISSER le cap.
// « Milestone d'unlock acquittée » reste garanti en amont par
// `_maxDepthIndex` / `milestoneRhythmCeilingIdx`.
final best = p.bestOf(axis) ?? comfort;
final escalating = axis == config.overloadAxis &&
p.stateOf(axis).successRate >= CapabilityRegulator.kDepthCranGate;
comfort = escalating ? comfort + 1 : max(comfort, min(comfort + 1, best));
} else if (comfort != null && axis == config.overloadAxis) {
comfort = comfort * config.overloadFactor;
}
// Boost intense (Supplier / Encore) : cumulatif avec la surcharge
// de l'axe. Pour `rhythmDepthMax` (cran discret), bonus exprimé en
Expand Down
Loading
Loading