lib/sportradar/api/basketball/game.rb
module Sportradar
module Api
module Basketball
class Game < Data
attr_accessor :response, :id, :title, :home_id, :away_id, :score, :status, :coverage, :time_zone, :scheduled, :venue, :broadcast, :clock, :duration, :attendance, :team_stats, :player_stats, :changes, :media_timeouts
attr_accessor :period
@all_hash = {}
# def self.new(data, **opts)
# existing = @all_hash[data['id']]
# if existing
# existing.update(data, **opts)
# existing
# else
# @all_hash[data['id']] = super
# end
# end
# def self.all
# @all_hash.values
# end
def initialize(data, **opts)
@response = data
@api = opts[:api]
# @season = opts[:season]
@updates = {}
@changes = {}
@score = {}
@team_stats = {}
@player_stats = {}
@scoring_raw = Scoring.new(data, game: self)
@teams_hash = {}
@periods_hash = {}
@home_points = nil
@away_points = nil
@home_id = nil
@away_id = nil
@id = data['id']
update(data, **opts)
end
def timeouts
{}
end
def tied?
@score[away_id].to_i == @score[home_id].to_i
end
def points(team_id)
team_id.is_a?(Symbol) ? @score[@team_ids[team_id]].to_i : @score[team_id].to_i
end
def stats(team_id)
team_id.is_a?(Symbol) ? @team_stats[@team_ids[team_id]].to_i : @team_stats[team_id].to_i
end
def scoring
@scoring_raw.scores
end
def update_score(score)
@score.merge!(score)
end
def update_stats(team, stats)
@team_stats.merge!(team.id => stats.merge!(team: team))
end
def update_player_stats(player, stats)
@player_stats.merge!(player.id => stats.merge!(player: player))
end
def parse_score(data)
update_score(data.dig('home', 'id') => data.dig('home', 'points').to_i)
update_score(data.dig('away', 'id') => data.dig('away', 'points').to_i)
end
def clock_seconds
return unless @clock
m,s = @clock.split(':')
m.to_i * 60 + s.to_i
end
def update(data, source: nil, **opts)
# via pbp
@status = data['status'] if data['status']
@coverage = data['coverage'] if data['coverage']
@home_id = data['home_team'] || data.dig('home', 'id') if data['home_team'] || data.dig('home', 'id')
@away_id = data['away_team'] || data.dig('away', 'id') if data['away_team'] || data.dig('away', 'id')
@home_points = data['home_points'].to_i if data['home_points']
@away_points = data['away_points'].to_i if data['away_points']
@scheduled = Time.parse(data["scheduled"]) if data["scheduled"]
@venue = Venue.new(data['venue']) if data['venue']
@broadcast = Broadcast.new(data['broadcast']) if !data['broadcast'].to_h.empty?
@home = team_class.new(data['home'], api: api, game: self) if data['home']
@away = team_class.new(data['away'], api: api, game: self) if data['away']
@title = data['title'] || @title || (home && away && "#{home.full_name} vs #{away.full_name}")
@time_zone = data.dig("time_zones", "venue") if data.dig("time_zones", "venue")
@duration = data['duration'] if data['duration']
@clock = data['clock'] if data['clock']
@attendance = data['attendance'] if data['attendance']
@lead_changes = data['lead_changes'] if data['lead_changes']
@times_tied = data['times_tied'] if data['times_tied']
@team_ids = { home: @home_id, away: @away_id}
update_score(@home_id => @home_points.to_i) if @home_points
update_score(@away_id => @away_points.to_i) if @away_points
parse_score(data['scoring']) if data['scoring']
@scoring_raw.update(data, source: source)
create_data(@teams_hash, data['team'], klass: team_class, api: api, game: self) if data['team']
end
def home
@teams_hash[@home_id] || @home
end
def away
@teams_hash[@away_id] || @away
end
def leading_team_id
return nil if score.values.uniq.size == 1
score.max_by(&:last).first
end
def leading_team
@teams_hash[leading_team_id] || (@away_id == leading_team_id && away) || (@home_id == leading_team_id && home)
end
def team(team_id)
@teams_hash[team_id]
end
def assign_home(team)
@home_id = team.id
@teams_hash[team.id] = team
end
def assign_away(team)
@away_id = team.id
@teams_hash[team.id] = team
end
def box
@box ||= get_box
end
def pbp
if !future? && periods.empty?
get_pbp
end
@pbp ||= periods
end
def plays
periods.flat_map(&:plays)
end
def plays_by_type(play_type, *types)
if types.empty?
plays.grep(Play.subclass(play_type.delete('_')))
else
play_classes = [play_type, *types].map { |type| Play.subclass(type.delete('_')) }
plays.select { |play| play_classes.any? { |klass| play.kind_of?(klass) } }
end
end
def summary
@summary ||= get_summary
end
alias :events :plays
def periods
@periods_hash.values
end
# tracking updates
def remember(key, object)
@updates[key] = object&.dup
end
def not_updated?(key, object)
@updates[key] == object
end
def changed?(key)
@changes[key]
end
def check_newness(key, new_object)
@changes[key] = !not_updated?(key, new_object)
remember(key, new_object)
end
# url paths
def path_base
"games/#{ id }"
end
def path_box
"#{ path_base }/boxscore"
end
def path_pbp
"#{ path_base }/pbp"
end
def path_summary
"#{ path_base }/summary"
end
# status helpers
def realtime_state
if future?
'Scheduled'
elsif finished?
'Final'
elsif postponed?
'Postponed'
elsif halftime?
'Halftime'
else
clock_display
end
end
def halftime?
status == 'halftime' || clock == '00:00' && quarter == 2
end
def clock_display
if clock && period
"#{clock} #{period_display}"
end
end
def period_display_long
if period > 5
"Overtime #{period - 4}"
elsif period == 5
'Overtime'
else
"#{Sportradar.ordinalize_period(period)} Quarter"
end
end
def period_display
if period > 5
"#{period - 4}OT"
elsif period == 5
'OT'
else
"#{period}Q"
end
end
def postponed?
'postponed' == status
end
def unnecessary?
'unnecessary' == status
end
def cancelled?
['unnecessary', 'postponed'].include? status
end
def future?
['scheduled', 'delayed', 'created', 'time-tbd', 'if-necessary'].include? status
end
def started?
['inprogress', 'halftime', 'delayed'].include? status
end
def finished?
['complete', 'closed'].include? status
end
def completed?
'complete' == status
end
def closed?
'closed' == status
end
# data retrieval
def get_box
data = api.get_data(path_box).to_h
ingest_box(data)
end
def ingest_box(data)
update(data, source: :box)
@period = data.delete(period_name).to_i
check_newness(:box, @clock)
data
end
def queue_pbp
url, headers, options, timeout = api.get_request_info(path_pbp)
{url: url, headers: headers, params: options, timeout: timeout, callback: method(:ingest_pbp)}
end
def get_pbp
data = api.get_data(path_pbp).to_h
ingest_pbp(data)
end
def ingest_pbp(data)
period_name = 'periods'
update(data, source: :pbp)
period_data = if data[period_name] && !data[period_name].empty?
@period = data[period_name].last['sequence'].to_i
pers = data[period_name]
pers.is_a?(Array) && (pers.size == 1) ? pers[0] : pers
else
@period = nil
[]
end
if data['overtime']
extra_periods = data['overtime'].is_a?(Hash) ? [data['overtime']] : data['overtime']
period_data.concat(extra_periods)
end
set_pbp(period_data)
@pbp = @periods_hash.values
check_newness(:pbp, plays.last&.updated)
check_newness(:clock, @clock)
check_newness(:score, @score)
data
end
def get_summary
data = api.get_data(path_summary).to_h
ingest_summary(data)
end
def queue_summary
url, headers, options, timeout = api.get_request_info(path_summary)
{url: url, headers: headers, params: options, timeout: timeout, callback: method(:ingest_summary)}
end
def ingest_summary(data)
update(data, source: :summary)
@period = data.delete(period_name).to_i
check_newness(:box, @clock)
check_newness(:score, @score)
data
end
def set_pbp(data)
create_data(@periods_hash, data, klass: period_class, api: api, game: self)
@plays = nil # to clear empty array empty
@periods_hash
end
# @abstract Subclass is expected to implement #period_class
# @!method period_class
# The class used for game periods
# @abstract Subclass is expected to implement #period_name
# @!method period_name
# The string name used for game periods
# @abstract Subclass is expected to implement #api
# @!method api
# The base for the requests needed for a subclass
end
end
end
end