diff --git a/bbreplay/replay.py b/bbreplay/replay.py index cbc7ae8..bd8dcb5 100644 --- a/bbreplay/replay.py +++ b/bbreplay/replay.py @@ -257,11 +257,7 @@ def _process_kickoff(self, cmds, log_entries, board): next(cmds) cmd = cmds.peek() - ball_bounces = True - for event in self._process_kickoff_event(cmds, log_entries, board): - yield event - if isinstance(event, Action) and event.action == ActionType.CATCH: - ball_bounces = event.result != ActionResult.SUCCESS + yield from self._process_kickoff_event(cmds, log_entries, board) board.kickoff() @@ -270,9 +266,30 @@ def _process_kickoff(self, cmds, log_entries, board): target_half = kickoff_cmd.position.y // half_pitch_length landed_half = ball_dest.y // half_pitch_length - touchback = board.get_ball_position().is_offpitch() or target_half != landed_half + ball_position = board.get_ball_position() + touchback = ball_position.is_offpitch() or target_half != landed_half + if not touchback: + under_ball = board.get_position(ball_position) + else: + under_ball = None + ball_carrier = board.get_ball_carrier() - if ball_bounces: + if ball_carrier: + # Already caught + pass + elif under_ball: + # We don't use `_process_catch` here because we can't reroll kickoff catches but the process + # method uses helpers that assume a failed action rerolls or declines a reroll + catch_entry = next(log_entries) + validate_log_entry(catch_entry, CatchEntry, board.receiving_team) + if catch_entry.result == ActionResult.SUCCESS: + board.set_ball_carrier(under_ball) + yield Action(under_ball, ActionType.CATCH, catch_entry.result, board) + else: + yield Action(under_ball, ActionType.CATCH, catch_entry.result, board) + yield from self._process_ball_movement(cmds, log_entries, board) + else: + # No-one to caught it if touchback: _ = next(log_entries) else: @@ -284,8 +301,6 @@ def _process_kickoff(self, cmds, log_entries, board): yield Bounce(old_position, ball_dest, log_entry.direction, board) if ball_dest.is_offpitch(): touchback = True - # else - # TODO: Handle no bounce when it gets caught straight away (but not via "High Kick" event) if touchback: cmd = next(cmds) @@ -394,6 +409,7 @@ def _process_turn(self, cmds, log_entries, expected_team, board): next(cmds) cmd = cmds.peek() + # FIXME: Cmd ends up as `None` but we get an infinite loop if we return if cmd.team != expected_team: # Next command should be from the *other* team. If it's not then something odd happened log_entry = log_entries.peek() diff --git a/dvc.lock b/dvc.lock index 1a2a597..520079d 100644 --- a/dvc.lock +++ b/dvc.lock @@ -4,8 +4,8 @@ stages: cmd: python3 metrics.py -o metrics/metrics.json -p metrics/plots.json data/ deps: - path: bbreplay/ - md5: ccd2944cfcfda74cf3623a466913efc9.dir - size: 253530 + md5: 08eb26111675333ebf70008ff100783b.dir + size: 254347 nfiles: 14 - path: data/ md5: 8f4f4d470b87ccc84345c3d35a283af3.dir @@ -16,8 +16,8 @@ stages: size: 3388 outs: - path: metrics/metrics.json - md5: f56c4c2db42aff5cd1ed00475d2a9252 - size: 5275 + md5: 2aac44f254e4f7ca69bcb4c1df77a96f + size: 5274 - path: metrics/plots.json - md5: 28131053b0fd11416298f2854e74e302 - size: 1753 + md5: c960b6c4c5fb39c8753cd4070f0111f0 + size: 1738 diff --git a/metrics/metrics.json b/metrics/metrics.json index 7cc1b43..4707a33 100644 --- a/metrics/metrics.json +++ b/metrics/metrics.json @@ -1,8 +1,8 @@ { "total_commands": 140177, - "total_processed": 44326, - "total_unprocessed": 95851, - "weighted_proportion": 0.4046040189978087, + "total_processed": 44354, + "total_unprocessed": 95823, + "weighted_proportion": 0.4048177708776801, "results": { "data/Replay_2021-04-05_11-35-42.db": { "commands": 42, @@ -174,9 +174,9 @@ }, "data/Replay_2021-04-11_10-04-51.db": { "commands": 4517, - "events": 788, - "processed": 4489, - "unprocessed": 28 + "events": 795, + "processed": 4517, + "unprocessed": 0 } } } \ No newline at end of file diff --git a/metrics/plots.json b/metrics/plots.json index d0ed438..2bb2b59 100644 --- a/metrics/plots.json +++ b/metrics/plots.json @@ -73,7 +73,7 @@ "score": 0.8461538461538461 }, { - "score": 0.9938011954837281 + "score": 1.0 }, { "score": 1.0 diff --git a/tests/test_replay__kickoff.py b/tests/test_replay__kickoff.py index 4848aec..7ff2eef 100644 --- a/tests/test_replay__kickoff.py +++ b/tests/test_replay__kickoff.py @@ -15,8 +15,8 @@ def test_normal_kick(board): PreKickoffCompleteCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value]) ]) log_entries = iter_([ - KickDirectionLogEntry(TeamType.AWAY.name, "1", ScatterDirection.S.value), - KickDistanceLogEntry(TeamType.AWAY.name, "1", 1), + KickDirectionLogEntry(TeamType.AWAY, "1", ScatterDirection.S.value), + KickDistanceLogEntry(TeamType.AWAY, "1", 1), KickoffEventLogEntry(KickoffEvent.CHEERING_FANS.value), BounceLogEntry(ScatterDirection.N.value) ]) @@ -64,8 +64,8 @@ def test_normal_kick_very_sunny(board): PreKickoffCompleteCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value]) ]) log_entries = iter_([ - KickDirectionLogEntry(TeamType.AWAY.name, "1", ScatterDirection.S.value), - KickDistanceLogEntry(TeamType.AWAY.name, "1", 1), + KickDirectionLogEntry(TeamType.AWAY, "1", ScatterDirection.S.value), + KickDistanceLogEntry(TeamType.AWAY, "1", 1), KickoffEventLogEntry(KickoffEvent.CHANGING_WEATHER.value), WeatherLogEntry(Weather.VERY_SUNNY.name), BounceLogEntry(ScatterDirection.N.value), @@ -115,6 +115,119 @@ def test_normal_kick_very_sunny(board): assert not next(log_entries, None) +def test_catch_on_kick(board): + home_team, away_team = board.teams + player = home_team.get_player(0) + board.set_position(Position(8, 14), player) + replay = Replay(home_team, away_team, [], []) + cmds = iter_([ + SetupCompleteCommand(1, 0, TeamType.AWAY.value, 0, []), + SetupCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value, 0, 8, 14]), + SetupCompleteCommand(1, 0, TeamType.HOME.value, 0, []), + KickoffCommand(1, 0, TeamType.AWAY.value, 0, [8, 15]), + PreKickoffCompleteCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value]) + ]) + log_entries = iter_([ + KickDirectionLogEntry(TeamType.AWAY, "1", ScatterDirection.S.value), + KickDistanceLogEntry(TeamType.AWAY, "1", 1), + KickoffEventLogEntry(KickoffEvent.CHEERING_FANS.value), + CatchEntry(TeamType.HOME, "1", "2+", "2", ActionResult.SUCCESS) + ]) + events = replay._process_kickoff(cmds, log_entries, board) + + event = next(events) + assert isinstance(event, TeamSetupComplete) + assert event.team == TeamType.AWAY + + event = next(events) + assert isinstance(event, TeamSetupComplete) + assert event.team == TeamType.HOME + + event = next(events) + assert isinstance(event, SetupComplete) + + event = next(events) + assert isinstance(event, Kickoff) + assert event.target == Position(8, 15) + assert event.scatter_direction == ScatterDirection.S + assert event.scatter_distance == 1 + assert board.get_ball_position() == Position(8, 14) + + event = next(events) + assert isinstance(event, KickoffEventTuple) + assert event.result == KickoffEvent.CHEERING_FANS + + event = next(events) + assert isinstance(event, Action) + assert event.action == ActionType.CATCH + assert event.result == ActionResult.SUCCESS + + assert board.get_ball_carrier() == player + + assert not next(events, None) + assert not next(cmds, None) + assert not next(log_entries, None) + + +def test_failed_catch_on_kick(board): + home_team, away_team = board.teams + replay = Replay(home_team, away_team, [], []) + cmds = iter_([ + SetupCompleteCommand(1, 0, TeamType.AWAY.value, 0, []), + SetupCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value, 0, 8, 14]), + SetupCompleteCommand(1, 0, TeamType.HOME.value, 0, []), + KickoffCommand(1, 0, TeamType.AWAY.value, 0, [8, 15]), + PreKickoffCompleteCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value]) + ]) + log_entries = iter_([ + KickDirectionLogEntry(TeamType.AWAY, "1", ScatterDirection.S.value), + KickDistanceLogEntry(TeamType.AWAY, "1", 1), + KickoffEventLogEntry(KickoffEvent.CHEERING_FANS.value), + CatchEntry(TeamType.HOME, "1", "2+", "1", ActionResult.FAILURE), + BounceLogEntry(ScatterDirection.N.value) + ]) + events = replay._process_kickoff(cmds, log_entries, board) + + event = next(events) + assert isinstance(event, TeamSetupComplete) + assert event.team == TeamType.AWAY + + event = next(events) + assert isinstance(event, TeamSetupComplete) + assert event.team == TeamType.HOME + + event = next(events) + assert isinstance(event, SetupComplete) + + event = next(events) + assert isinstance(event, Kickoff) + assert event.target == Position(8, 15) + assert event.scatter_direction == ScatterDirection.S + assert event.scatter_distance == 1 + assert board.get_ball_position() == Position(8, 14) + + event = next(events) + assert isinstance(event, KickoffEventTuple) + assert event.result == KickoffEvent.CHEERING_FANS + + event = next(events) + assert isinstance(event, Action) + assert event.action == ActionType.CATCH + assert event.result == ActionResult.FAILURE + + event = next(events) + assert isinstance(event, Bounce) + assert event.scatter_direction == ScatterDirection.N + assert event.start_space == Position(8, 14) + assert event.end_space == Position(8, 15) + + assert board.get_ball_carrier() is None + + assert not next(events, None) + assert not next(cmds, None) + assert not next(log_entries, None) + + def test_touchback_for_off_pitch_kick(board): home_team, away_team = board.teams replay = Replay(home_team, away_team, [], []) @@ -127,8 +240,8 @@ def test_touchback_for_off_pitch_kick(board): TouchbackCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value, 0]) ]) log_entries = iter_([ - KickDirectionLogEntry(TeamType.AWAY.name, "1", ScatterDirection.SW.value), - KickDistanceLogEntry(TeamType.AWAY.name, "1", 6), + KickDirectionLogEntry(TeamType.AWAY, "1", ScatterDirection.SW.value), + KickDistanceLogEntry(TeamType.AWAY, "1", 6), KickoffEventLogEntry(KickoffEvent.CHEERING_FANS.value), BounceLogEntry(ScatterDirection.NW.value) ]) @@ -176,8 +289,8 @@ def test_touchback_for_off_pitch_bounce(board): TouchbackCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value, 0]) ]) log_entries = iter_([ - KickDirectionLogEntry(TeamType.AWAY.name, "1", ScatterDirection.SW.value), - KickDistanceLogEntry(TeamType.AWAY.name, "1", 1), + KickDirectionLogEntry(TeamType.AWAY, "1", ScatterDirection.SW.value), + KickDistanceLogEntry(TeamType.AWAY, "1", 1), KickoffEventLogEntry(KickoffEvent.CHEERING_FANS.value), BounceLogEntry(ScatterDirection.S.value) ]) @@ -231,8 +344,8 @@ def test_touchback_for_own_half_kick(board): TouchbackCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value, 0]) ]) log_entries = iter_([ - KickDirectionLogEntry(TeamType.AWAY.name, "1", ScatterDirection.S.value), - KickDistanceLogEntry(TeamType.AWAY.name, "1", 1), + KickDirectionLogEntry(TeamType.AWAY, "1", ScatterDirection.S.value), + KickDistanceLogEntry(TeamType.AWAY, "1", 1), KickoffEventLogEntry(KickoffEvent.CHEERING_FANS.value), BounceLogEntry(ScatterDirection.W.value) ]) @@ -280,8 +393,8 @@ def test_touchback_for_own_half_kick_other_direction(board): TouchbackCommand(1, 0, TeamType.HOME.value, 0, [TeamType.HOME.value, 0]) ]) log_entries = iter_([ - KickDirectionLogEntry(TeamType.AWAY.name, "1", ScatterDirection.N.value), - KickDistanceLogEntry(TeamType.AWAY.name, "1", 1), + KickDirectionLogEntry(TeamType.AWAY, "1", ScatterDirection.N.value), + KickDistanceLogEntry(TeamType.AWAY, "1", 1), KickoffEventLogEntry(KickoffEvent.CHEERING_FANS.value), BounceLogEntry(ScatterDirection.W.value) ])