/// 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);
}
