Skip to content

Commit

Permalink
Re #22: Handle catch from kickoff
Browse files Browse the repository at this point in the history
Fixes #22

`Replay_2021-04-11_10-04-51.db` isn't quite perfect yet
because of #23
  • Loading branch information
IBBoard committed Feb 5, 2022
1 parent 17d83b2 commit bcabdd2
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 34 deletions.
34 changes: 25 additions & 9 deletions bbreplay/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 6 additions & 6 deletions dvc.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
12 changes: 6 additions & 6 deletions metrics/metrics.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
}
}
}
2 changes: 1 addition & 1 deletion metrics/plots.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"score": 0.8461538461538461
},
{
"score": 0.9938011954837281
"score": 1.0
},
{
"score": 1.0
Expand Down
137 changes: 125 additions & 12 deletions tests/test_replay__kickoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
])
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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, [], [])
Expand All @@ -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)
])
Expand Down Expand Up @@ -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)
])
Expand Down Expand Up @@ -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)
])
Expand Down Expand Up @@ -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)
])
Expand Down

0 comments on commit bcabdd2

Please sign in to comment.