/// Pure scoring math: placement points, line-clear bonuses, combo streaks. library; /// Base score for clearing [lines] rows/columns simultaneously. /// 1 -> 100, 2 -> 300, 3 -> 600, 4 -> 1000: simultaneous clears are /// superlinearly rewarded. int lineClearBase(int lines) => 100 * lines + 50 * (lines - 1) * lines; /// Multiplier applied to clear score at combo [streak], capped at streak 8 /// (x5.0) so late-game scores stay bounded. double comboMultiplier(int streak) => 1 + 0.5 * (streak > 8 ? 8 : streak); /// Combo streak with one dry-move grace before reset. class ComboState { const ComboState({required this.streak, required this.dryMoves}); static const initial = ComboState(streak: 0, dryMoves: 0); final int streak; final int dryMoves; /// One dry move keeps the streak alive (grace); the second resets it. ComboState advance({required bool cleared}) { if (cleared) return ComboState(streak: streak + 1, dryMoves: 0); if (dryMoves + 1 >= 2) return initial; return ComboState(streak: streak, dryMoves: dryMoves + 1); } } class ScoreDelta { const ScoreDelta({required this.points, required this.combo}); final int points; final ComboState combo; } /// Computes the score delta for one placement and the resulting combo state. ScoreDelta scorePlacement({ required int cellsPlaced, required int linesCleared, required ComboState combo, }) { final next = combo.advance(cleared: linesCleared > 0); var points = cellsPlaced; if (linesCleared > 0) { points += (lineClearBase(linesCleared) * comboMultiplier(next.streak)) .round(); } return ScoreDelta(points: points, combo: next); }