Introduce architettura multi-sport con catalogo, engine scoring e overlay.
Catalogo sport in YAML, board implicito, engine per volley/basket/timed/racket/timer/generic, regia e app Android allineate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,13 +9,16 @@ class Match < ApplicationRecord
|
||||
|
||||
validates :opponent_name, presence: true
|
||||
validates :sets_to_win, numericality: { greater_than: 0, less_than: 6 }
|
||||
validates :sport_key, presence: true
|
||||
validates :opponent_primary_color, format: { with: Brandable::HEX_COLOR }, allow_blank: true
|
||||
validate :scoring_rules_shape, if: -> { scoring_rules.present? }
|
||||
validate :sport_key_known
|
||||
validate :overlay_kind_allowed
|
||||
validate :scoring_rules_shape
|
||||
validate :opponent_logo_file_type, if: -> { opponent_logo_file.attached? }
|
||||
|
||||
# category: campionato / descrizione torneo (facoltativo, es. "Serie C").
|
||||
before_validation :normalize_sport_key
|
||||
before_validation :inherit_sport_from_team, on: :create
|
||||
|
||||
# Partite ancora da giocare in diretta (orario nel futuro).
|
||||
scope :scheduled_for_live, -> {
|
||||
where.not(scheduled_at: nil).where("scheduled_at >= ?", Time.zone.now)
|
||||
}
|
||||
@@ -78,8 +81,6 @@ class Match < ApplicationRecord
|
||||
opponent_primary_color.presence || DEFAULT_OPPONENT_COLOR
|
||||
end
|
||||
|
||||
# Hub app coach: dirette da riprendere o partite programmate future.
|
||||
# Le bozze «Avversario» abbandonate (senza sessione) non compaiono.
|
||||
def coach_hub_visible?
|
||||
active = active_stream_session
|
||||
return true if active&.resumable?
|
||||
@@ -90,8 +91,72 @@ class Match < ApplicationRecord
|
||||
scheduled_upcoming?
|
||||
end
|
||||
|
||||
def effective_board_type
|
||||
Sports::Catalog.board_for(sport_key)
|
||||
end
|
||||
|
||||
def effective_overlay_kind
|
||||
kind = overlay_kind.presence || Sports::Catalog.overlay_for(sport_key)
|
||||
allowed = Sports::Catalog.allowed_overlays_for(sport_key)
|
||||
allowed.include?(kind) ? kind : Sports::Catalog.overlay_for(sport_key)
|
||||
end
|
||||
|
||||
def effective_scoring_rules
|
||||
merged = Sports::Catalog.default_rules_for(sport_key).deep_merge(normalized_scoring_rules)
|
||||
merged["sets_to_win"] = sets_to_win if merged["sets_to_win"].blank? && sets_to_win.present?
|
||||
merged
|
||||
end
|
||||
|
||||
def sport_label
|
||||
Sports::Catalog.find_optional(sport_key)&.dig(:label) || sport_key.to_s.humanize
|
||||
end
|
||||
|
||||
# Alias legacy API/spec (colonna rinominata in sport_key).
|
||||
def sport
|
||||
sport_key
|
||||
end
|
||||
|
||||
def sport=(value)
|
||||
self.sport_key = Sports::Catalog.normalize_key(value)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_sport_key
|
||||
self.sport_key = Sports::Catalog.normalize_key(sport_key)
|
||||
end
|
||||
|
||||
def inherit_sport_from_team
|
||||
self.sport_key = team.sport_key if sport_key.blank? && team.present?
|
||||
end
|
||||
|
||||
def sport_key_known
|
||||
return if sport_key.blank?
|
||||
return if Sports::Catalog.find_optional(sport_key)
|
||||
|
||||
errors.add(:sport_key, "non valido")
|
||||
end
|
||||
|
||||
def overlay_kind_allowed
|
||||
return if overlay_kind.blank?
|
||||
|
||||
unless Sports::OverlayKind.valid?(overlay_kind)
|
||||
errors.add(:overlay_kind, "non valido")
|
||||
return
|
||||
end
|
||||
|
||||
allowed = Sports::Catalog.allowed_overlays_for(sport_key)
|
||||
errors.add(:overlay_kind, "non consentito per questo sport") unless allowed.include?(overlay_kind)
|
||||
end
|
||||
|
||||
def normalized_scoring_rules
|
||||
return {} unless scoring_rules.is_a?(Hash)
|
||||
|
||||
scoring_rules.each_with_object({}) do |(k, v), h|
|
||||
h[k.to_s] = v
|
||||
end
|
||||
end
|
||||
|
||||
def opponent_logo_file_type
|
||||
return if opponent_logo_file.content_type.in?(%w[image/png image/jpeg image/webp])
|
||||
|
||||
@@ -99,17 +164,8 @@ class Match < ApplicationRecord
|
||||
end
|
||||
|
||||
def scoring_rules_shape
|
||||
rules = scoring_rules.is_a?(Hash) ? scoring_rules : {}
|
||||
%w[points_per_set points_deciding_set min_point_lead].each do |key|
|
||||
val = rules[key] || rules[key.to_sym]
|
||||
next if val.blank?
|
||||
|
||||
unless val.is_a?(Integer) || val.to_s.match?(/\A\d+\z/)
|
||||
errors.add(:scoring_rules, "#{key} non valido")
|
||||
next
|
||||
end
|
||||
int_val = val.to_i
|
||||
errors.add(:scoring_rules, "#{key} deve essere positivo") if int_val < 1
|
||||
end
|
||||
Sports::RulesSchema.validate!(effective_board_type, normalized_scoring_rules)
|
||||
rescue ArgumentError => e
|
||||
errors.add(:scoring_rules, e.message)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user