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
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import '../../../services/capability_axis.dart';
import '../../../services/capability_service.dart';
import '../../models/career_generation_inputs.dart';
import '../../models/challenge.dart';
import '../../../models/posture.dart';
import '../../models/level_milestone.dart';
import '../../models/phrase_bank.dart';
import '../../models/posture_unlock.dart';
import '../../models/session_length_choice.dart';
import '../../models/specialization.dart';
import '../../models/unlock_key.dart';
Expand Down Expand Up @@ -521,6 +523,13 @@ class CareerSessionGenerator {
/// Quand un défi est passé, un step breath de countdown + un step défi
/// sont insérés vers 60 % du temps planifié (cf. spec § 4.3).
ChallengeInputs challenge = ChallengeInputs.none,

/// Active les postures imposées + breaks scénarisés (issue #77, flag
/// debug `debug.scripted_breaks`). `false` = posture `free`, aucun
/// break (comportement historique). En PR3 seule la posture initiale
/// est tirée ; l'insertion des breaks suit. Cf. spec
/// `specs/scripted_breaks.md`.
bool scriptedBreaks = false,
}) {
// Invariants `milestones` : on ne peut pas les déplacer dans le
// constructeur de `MilestonePlan` car `.placement` n'est pas
Expand Down Expand Up @@ -577,8 +586,10 @@ class CareerSessionGenerator {
lengthChoice: lengthChoice,
intense: intense,
encoreChainIndex: encoreChainIndex,
scriptedBreaks: scriptedBreaks,
);
_initScratchpad(unlockedKeys: unlockedKeys, clearPatternBuffer: true);
_initialPose = _pickInitialPose(unlockedKeys);
// Mode "Session bâclée" : 6 min par défaut, intense tout du long. Floor
// d'intensité appliqué au tirage de difficulté + on saute l'intro douce
// et la pré-finition. Une durée explicite reste prioritaire (cas de la
Expand Down Expand Up @@ -1473,6 +1484,23 @@ class CareerSessionGenerator {
ctx.stamina = s;
}

/// Posture imposée au démarrage de la séance courante. Tirée en début de
/// `generate()` (où `unlockedKeys` est typé `Set<UnlockKey>`) et lue par
/// `_assembleResult`. `free` tant que le flag debug `scriptedBreaks` est
/// off.
Posture _initialPose = Posture.free;

/// Tire la posture imposée au démarrage (issue #77). [Posture.free] tant
/// que le flag debug `scriptedBreaks` est off ; sinon tirage uniforme
/// parmi les postures débloquées (`free` incluse dans le pool). Bas niveau
/// / rien de débloqué ⇒ `free` de toute façon. Déterministe sous seed via
/// `_rng`.
Posture _pickInitialPose(Set<UnlockKey> unlockedKeys) {
if (!_config.scriptedBreaks) return Posture.free;
final available = availablePostures(unlockedKeys);
return available[_rng.nextInt(available.length)];
}

/// Construit le [CareerGenerationResult] final à partir des accumulateurs
/// `ctx.steps` / `ctx.profile` et du curseur `time`. Tronque le profil à la
/// durée effective (= `time + 2`), assemble la [Session] avec toutes ses
Expand Down Expand Up @@ -1534,6 +1562,7 @@ class CareerSessionGenerator {
noStats: ctx.noStats,
challenges: challenges,
challengeTriggerTimes: challengeTriggerTimes,
initialPose: _initialPose,
),
staminaProfile: trimmedProfile,
overloadAxis: _config.overloadAxis,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class SessionConfig {
this.lengthChoice,
this.intense = false,
this.encoreChainIndex = 0,
this.scriptedBreaks = false,
});

// Bornes globales
Expand Down Expand Up @@ -100,6 +101,11 @@ class SessionConfig {
/// `intense` est actif sur l'encore (cf. `generate()`).
final int encoreChainIndex;

/// Active les postures imposées + breaks scénarisés (issue #77, flag debug
/// `debug.scripted_breaks`). `false` = comportement historique (posture
/// `free`, aucun break). Cf. spec `specs/scripted_breaks.md`.
final bool scriptedBreaks;

// ─── Méthodes dérivées (pures, lisent uniquement les fields ci-dessus) ───

/// True si le mode est exclu :
Expand Down
88 changes: 88 additions & 0 deletions rhythm_coach/test/posture_generation_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'package:beat_bitch/career/models/career_generation_inputs.dart';
import 'package:beat_bitch/career/models/phrase_bank.dart';
import 'package:beat_bitch/career/services/generation/career_session_generator.dart';
import 'package:beat_bitch/models/posture.dart';
import 'package:flutter_test/flutter_test.dart';

List<PhraseEntry> _p(List<String> t) =>
t.map((s) => PhraseEntry(text: s)).toList();

PhraseBank _bank() => PhraseBank(
byMode: {
for (final m in SessionMode.values)
m: {
'soft': _p(['s']),
'medium': _p(['m']),
'hard': _p(['h']),
'finale': _p(['f']),
},
},
congrats: _p(['bravo']),
intros: _p(['intro']),
);

void main() {
group('Posture initiale — generate()', () {
test('flag off → free, même avec des postures débloquées', () {
final gen = CareerSessionGenerator(seed: 7);
final result = gen.generate(
level: 12,
bank: _bank(),
durationSeconds: 18 * 60,
unlockedKeys: UnlockKey.values.toSet(),
// scriptedBreaks: false (défaut)
);
expect(result.session.initialPose, Posture.free);
});

test('flag on mais rien de débloqué → free', () {
final gen = CareerSessionGenerator(seed: 7);
final result = gen.generate(
level: 3,
bank: _bank(),
durationSeconds: 12 * 60,
unlockedKeys: const {},
scriptedBreaks: true,
);
expect(result.session.initialPose, Posture.free);
});

test('flag on + postures débloquées → posture du set disponible', () {
final gen = CareerSessionGenerator(seed: 7);
final unlocked = {
UnlockKey.postureKneeling,
UnlockKey.postureAllFours,
};
final result = gen.generate(
level: 12,
bank: _bank(),
durationSeconds: 18 * 60,
unlockedKeys: unlocked,
scriptedBreaks: true,
);
expect(
result.session.initialPose,
anyOf(Posture.free, Posture.kneeling, Posture.allFours),
);
});

test('tirage déterministe sous seed identique', () {
final unlocked = {
UnlockKey.postureSitting,
UnlockKey.postureKneeling,
UnlockKey.postureAllFours,
};
Posture run() => CareerSessionGenerator(seed: 99)
.generate(
level: 12,
bank: _bank(),
durationSeconds: 18 * 60,
unlockedKeys: unlocked,
scriptedBreaks: true,
)
.session
.initialPose;
expect(run(), run());
});
});
}
Loading