beneggett/sportradar-api

View on GitHub
lib/sportradar/api/baseball/game.rb

Summary

Maintainability
F
3 days
Test Coverage
module Sportradar
  module Api
    module Baseball
      class Game < Data
        attr_accessor :response, :id, :title, :home_id, :away_id, :score, :status, :coverage, :scheduled, :venue, :broadcast, :duration, :attendance, :team_stats, :player_stats, :changes, :lineup

        attr_reader :inning, :half, :outs, :bases, :pitchers, :final, :rescheduled, :inning_over
        attr_reader :outcome, :count
        DEFAULT_BASES = { '1' => nil, '2' => nil, '3' => nil }

        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)
          @lineup         = Lineup.new(data, game: self)
          @teams_hash     = {}
          @innings_hash   = {}
          @home_runs      = nil
          @away_runs      = nil
          @home_id        = nil
          @away_id        = nil
          @outcome        = Outcome.new(data, game: self)
          @count          = {}
          @pitchers       = {}
          @bases          = { '1' => nil, '2' => nil, '3' => nil }

          @id = data['id']

          update(data, **opts)
        end
        def lineup
          @lineup ||= Lineup.new({}, game: self)
        end

        def timeouts
          {}
        end

        def period
          @innings
        end

        def tied?
          @score[away_id].to_i == @score[home_id].to_i
        end
        # def runs(team_id)
        #   summary_stat(team_id, 'runs')
        # end
        def hits(team_id)
          @scoring_raw.hits(team_id)
        end
        def errors(team_id)
          @scoring_raw.errors(team_id)
        end
        def runs(team_id)
          team_id.is_a?(Symbol) ? @score[@team_ids[team_id]] : @score[team_id]
        end
        def summary_stat(team_id, stat_name)
          scoring.dig(team_id, stat_name)
        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)
          home_id = data.dig('home', 'id')
          away_id = data.dig('away', 'id')
          rhe = {
            'runs'    => { home_id => data.dig('home', 'runs'), away_id => data.dig('away', 'runs')},
            'hits'    => { home_id => data.dig('home', 'hits'), away_id => data.dig('away', 'hits')},
            'errors'  => { home_id => data.dig('home', 'errors'), away_id => data.dig('away', 'errors')},
          }
          @scoring_raw.update(rhe, source: :rhe)
          update_score(home_id => data.dig('home', 'runs'))
          update_score(away_id => data.dig('away', 'runs'))
        end

        def parse_pitchers(data)
          pitchers = {
            'starting'  => { home_id => data.dig('home', 'starting_pitcher'), away_id => data.dig('away', 'starting_pitcher')},
            'probable'  => { home_id => data.dig('home', 'probable_pitcher'), away_id => data.dig('away', 'probable_pitcher')},
            'current'   => { home_id => data.dig('home', 'current_pitcher'),  away_id => data.dig('away', 'current_pitcher')},
          }
          @pitchers.merge!(pitchers) do |key, current_val, merge_val|
            current_val.merge(merge_val) { |k, cur, mer| (mer || cur) }
          end
        end

        def update(data, source: nil, **opts)
          @response.merge!(data)
          # via pbp
          @status       = data['status']                if data['status']
          @coverage     = data['coverage']              if data['coverage']
          @day_night    = data['day_night']             if data['day_night']
          @game_number  = data['game_number']           if data['game_number']
          @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_runs    = data['home_runs'].to_i      if data['home_runs']
          # @away_runs    = data['away_runs'].to_i      if data['away_runs']

          @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.new(data['home'] || data.dig('scoring', 'home'), api: api, game: self) if data['home'] || data.dig('scoring', 'home')
          @away         = Team.new(data['away'] || data.dig('scoring', 'away'), api: api, game: self) if data['away'] || data.dig('scoring', 'away')
          @title        = data['title'] || @title || (home && away && "#{home.full_name} vs #{away.full_name}")

          @duration     = data['duration']              if data['duration']
          @attendance   = data['attendance']            if data['attendance']

          @final        = data['final']                 if data['final']
          @rescheduled  = data['rescheduled']           if data['rescheduled']

          @team_ids     = { home: @home_id, away: @away_id}

          update_bases(data)
          parse_pitchers(data) if data['home'] && data['away']

          lineup.update(data, source: source) unless source == :pbp
          if data['scoring']
            parse_score(data['scoring'])
          elsif data.dig('home', 'hits')
            parse_score(data)
          end
          @scoring_raw.update(data, source: source)
          if data['outcome']
            @outcome.update(data, source: nil)
            @count.merge!(@outcome.count || {})
          end
          create_data(@teams_hash, data['team'], klass: Team, api: api, game: self) if data['team']
        end

        # def update_from_team(id, data)
        # end

        def update_bases(data)
          if data.is_a?(Sportradar::Api::Baseball::Error)
            puts data.inspect
            return
          end
          @bases = if data.respond_to?(:runners)
            hash = Array(data.runners).map { |runner| [runner.ending_base.to_s, runner.id] if !runner.out }.compact.to_h
            DEFAULT_BASES.merge(hash)
          elsif (runners = data.dig('outcome', 'runners'))
            hash = runners.map { |runner| [runner['ending_base'].to_s, runner['id']] if !runner['out'] }.compact.to_h
            DEFAULT_BASES.merge(hash)
          else # probably new inning, no runners
            DEFAULT_BASES.dup
          end
        rescue => e
          puts data.inspect
          raise e
        end
        def advance_inning
          @inning_over = false
          return unless count['outs'] == 3
          if count['inning'] >= 9
            if count['inning_half'] == 'T' && leading_team_id == home.id
              return
            elsif count['inning_half'] == 'B' && !tied?
              return
            end
          end
          @inning_over = true
          @bases = DEFAULT_BASES.dup
          half, inn = if count['inning_half'] == 'B'
            ['E', count['inning']]
          elsif count['inning_half'] == 'T'
            ['M', count['inning']]
          else
            [nil, 1]
          end
          @count = {
            'balls'       => 0,
            'strikes'     => 0,
            'outs'        => 0,
            'inning'      => inn,
            'inning_half' => half,
          }
        end

        def extract_count(data) # extract from pbp
          recent_pitches = pitches.last(10)
          last_pitch = recent_pitches.reverse_each.detect(&:count)
          return unless last_pitch
          update_bases(last_pitch)
          @count.merge!(last_pitch.count)
          hi = last_pitch.at_bat.event.half_inning
          @count.merge!('inning' => hi.number.to_i, 'inning_half' => hi.half)
          advance_inning
        end

        def home
          @teams_hash[@home_id] || @home
        end

        def away
          @teams_hash[@away_id] || @away
        end

        def leading_team_id
          return nil if tied?
          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? && innings.empty?
            get_pbp
          end
          @pbp ||= innings
        end

        def events
          innings.flat_map(&:events)
        end

        def at_bats
          events.map(&:at_bat).compact
        end

        def pitches
          at_bats.flat_map(&:pitches)
        end

        def summary
          @summary ||= get_summary
        end

        def innings
          @innings_hash.values
        end
        def half_innings
          innings.flat_map(&:half_innings)
        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

        # status helpers

        def realtime_state(full_word: false)
          if future?
            'Scheduled'
          elsif delayed?
            'Delayed'
          elsif finished?
            'Final'
          elsif postponed?
            'Postponed'
          else
            full_word ? inning_word : inning_short
          end
        end

        def inning_abbr
          if !count.empty?
            inning_half = self.count['inning_half']
            inning = self.count['inning']
            "#{inning_half || 'T'}#{(inning || 1)}"
          end
        end

        def inning_short
          if !count.empty?
            inning_half = self.count['inning_half']
            inning = self.count['inning']
            "#{half_short} #{ordinalize_inning(inning || 1)}" # TODO remove AS dependency
          end
        end

        def inning_word
          if !count.empty?
            inning_half = self.count['inning_half']
            inning = self.count['inning']
            "#{half_word} #{ordinalize_inning(inning || 1)}"
          end
        end

        def ordinalize_inning(i)
          Sportradar.ordinalize_period(i)
        end

        def half_word
          {
            'B' => 'Bottom',
            'T' => 'Top',
            'M' => 'Middle',
            'E' => 'End',
          }.freeze[self.count['inning_half']]
        end

        def half_short
          {
            'B' => 'Bot',
            'T' => 'Top',
            'M' => 'Mid',
            'E' => 'End',
          }.freeze[self.count['inning_half']]
        end

        def clock
          half_short
        end

        def postponed?
          'postponed' == status
        end

        def unnecessary?
          'unnecessary' == status
        end

        def cancelled?
          ['unnecessary', 'postponed'].include? status
        end

        def delayed?
          ['wdelay', 'delayed'].include? status
        end

        def future?
          ['scheduled', 'delayed', 'created', 'time-tbd'].include? status
        end

        def started?
          ['inprogress', 'wdelay', 'delayed'].include? status
        end

        def finished?
          ['complete', 'closed'].include? status
        end

        def completed?
          'complete' == status
        end

        def closed?
          'closed' == status
        end

        # url path helpers
        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

        # data retrieval

        def get_box
          data = api.get_data(path_box)
          ingest_box(data)
        end

        def ingest_box(data)
          data = data['game']
          update(data, source: :box)
          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);
          ingest_pbp(data)
        end

        def ingest_pbp(data)
          data = data['game']
          update(data, source: :pbp)
          innings = data['innings'].each { |inning| inning['id'] = "#{data['id']}-#{inning['number']}" }
          create_data(@innings_hash, innings, klass: Inning, api: api, game: self) if data['innings']
          extract_count(data)
          lineup.update(data, source: :pbp)
          # update lineups
          check_newness(:pitches, pitches.last&.id)
          check_newness(:events, events.last&.description)
          check_newness(:score, @score)
          @pbp = @innings_hash.values
          data
        # rescue => e
        #   binding.pry
        end

        def get_summary
          data = api.get_data(path_summary)
          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)
          data = data['game']
          update(data, source: :summary)
          @inning = data.delete('inning').to_i
          check_newness(:box, @clock)
          check_newness(:score, @score)
          data
        end

        def set_pbp(data)
          create_data(@innings_hash, data, klass: inning_class, api: api, game: self)
          @innings_hash
        end

        def api
          @api ||= Sportradar::Api::Baseball::Mlb::Api.new
        end

        def sim!
          @api = api.sim!
          self
        end

      end
    end
  end
end

__END__

# mlb = Sportradar::Api::Baseball::Mlb::Hierarchy.new
# res = mlb.get_schedule;
# g = mlb.games.first
# g = Sportradar::Api::Baseball::Game.new('id' => "8cd71519-429f-4461-88a2-8a0e134eb89b")
g = Sportradar::Api::Baseball::Game.new('id' => "9d0fe41c-4e6b-4433-b376-2d09ed39d184");
g = Sportradar::Api::Baseball::Game.new('id' => "fe9f37fd-6848-4a32-a999-9655044b7319");
res = g.get_pbp;
res = g.get_summary;
res = g.get_box # probably not as useful as summary


mlb = Sportradar::Api::Baseball::Mlb::Hierarchy.new
res = mlb.get_daily_summary;
g = mlb.games[8];
g.count
g.get_pbp;
g.count

# mlb = Sportradar::Api::Baseball::Mlb::Hierarchy.new;
# res = mlb.get_daily_summary;
# g = mlb.games.sort_by(&:scheduled).first;
g = Sportradar::Api::Baseball::Game.new('id' => "8731b56d-9037-44d1-b890-fa496e94dc10");
res = g.get_pbp;
res = g.get_summary;
g.pitchers