Skip to content

referee: end the penalty shootout only after both teams took the same number of kicks - #154

Open
oxsyai wants to merge 1 commit into
rcsoccersim:masterfrom
foxsyai:upstream-penalty-shootout-equal-kicks
Open

oxsyai wants to merge 1 commit into
rcsoccersim:masterfrom
foxsyai:upstream-penalty-shootout-equal-kicks

Conversation

@oxsyai

@oxsyai oxsyai commented Sep 13, 2026

Copy link
Copy Markdown

Bug

PenaltyRef::penalty_check_score() (src/referee.cpp) ends the shootout with

if (M_pen_nr_taken > 2 * (penMaxExtraKicks() + penNrKicks()))

The sudden-death branch below it (> 2 * nr, only on even counts, only when the score differs) never triggers at 2 * (nr + extra) when the teams are level, so nothing else stops the extra kick.

Second, smaller case: with pen_nr_kicks=0 and pen_max_extra_kicks=0 the score check only runs after a kick, so both teams still took one kick each before the decision.

Fix

if ( M_pen_nr_taken % 2 == 0
     && M_pen_nr_taken >= 2 * ( penMaxExtraKicks() + penNrKicks() )
     && teamLeft().penaltyPoint() == teamRight().penaltyPoint() )
  • % 2 == 0 and >=: evaluated only after both teams completed the same number of attempts, ending after exactly nr + extra kicks each (10 with the defaults).
  • The score-equality term is required: this branch is checked before the winner branch. Without it, a shootout that is 4-3 after the last pair would go to the coin toss / draw instead of naming the winner. With it, the existing branches name the winner as before (verified below with the old binary as control).
  • Zero-kicks guard in startPenaltyShootout(): when pen_nr_kicks + pen_max_extra_kicks <= 0 call penalty_check_score() right away, which with 0 kicks taken and 0-0 goes straight to the draw / coin toss.

No behaviour change for a shootout decided before the exhaustion point, and no change to the "cannot win any more" early termination.

pen_max_extra_kicks=6 is not needed to get 11 kicks per team with this fix: nr + extra is now exactly the number of kicks per team, so 5 + 6 gives 11 each, 5 + 5 gives 10 each.

Verification (headless, this branch vs. master, same harness)

Harness: one rcssserver (auto_mode, synch_mode, penalty_shoot_outs=on, pen_nr_kicks=2, pen_max_extra_kicks=2, pen_dist_x=10, player_rand=ball_rand=kick_rand=0), 5 field players + 1 goalie per side as plain UDP clients, 2 x 2 s halves ending 0-0. Each taker runs to the ball during penalty_setup, the referee snaps it to 2 m at the setup timeout, then it dashes and kicks to the far post (scores) unless told to miss. Play modes are the referee messages heard by the clients; "Score:" lines are the server's own stderr.

master (unpatched), all kicks score

attempts taken: LEFT 4 (scored 4, missed 0), RIGHT 5 (scored 5, missed 0)
  Score: *4-4  after 8 penalties.
  Score:  4-5* after 9 penalties.
  Final score: 4-5
... 746:penalty_ready_r 749:penalty_taken_r 755:penalty_score_r 755:penalty_draw 756:time_over

RIGHT (kicked first) took a 9th kick, LEFT never answered, referee said penalty_draw with a 4-5 penalty score.

this branch, all kicks score

attempts taken: LEFT 4 (scored 4, missed 0), RIGHT 4 (scored 4, missed 0)
  Score:  4-4* after 8 penalties.
  Final score: 4-4
... 667:penalty_ready_r 670:penalty_taken_r 676:penalty_score_r 676:penalty_draw 677:time_over

Ends after 8 kicks, equal attempts, the second team's last kick (penalty_score_r at 676) is recorded.

this branch, pen_random_winner=on

attempts taken: LEFT 4, RIGHT 4
  Score: *4-4  after 8 penalties.
  Left team has won the coin toss!

this branch, LEFT misses its 4th kick (score differs after the last pair)

attempts taken: LEFT 4 (scored 3, missed 1), RIGHT 4 (scored 4, missed 0)
  Score:  3-4* after 8 penalties.
  Final score: 3-4
... 606:penalty_miss_l ... 687:penalty_score_r 687:penalty_winner_r 688:time_over

penalty_winner_r at 8, not a draw; the master control run on the same scenario also gives penalty_winner_r after 8, so the winner path is unchanged.

zero kicks configured (pen_nr_kicks=0, pen_max_extra_kicks=0)

master:       40:penalty_setup_l 100:penalty_ready_l 103:penalty_taken_l 109:penalty_score_l 109:penalty_draw
              Score: *1-0  after 1 penalties. / Final score: 1-0
this branch:  40:penalty_onfield_l 40:penalty_setup_l 40:penalty_draw 40:time_over
              Score: *0-0  after 0 penalties. / Final score: 0-0

Built with CMake (Release) on Ubuntu 24.04, Boost 1.83.

Release path (not done here)

🤖 Generated with Claude Code

Fixes the off-by-one left by #126 (the branch reorder kept the strict >). The same change was verified headlessly with scripted clients at pen_nr_kicks=2, pen_max_extra_kicks=2: before, the shootout ended 4-5 after 9 kicks with penalty_draw; after, it ends 4-4 after 8 with both teams' kicks recorded, a missed kick still yields penalty_winner, and pen_nr_kicks=0 with pen_max_extra_kicks=0 goes straight to the draw/coin toss.

… number of kicks

PenaltyRef::penalty_check_score() ended the shootout with
`M_pen_nr_taken > 2 * (pen_max_extra_kicks + pen_nr_kicks)`, i.e. after
2 * (nr + extra) + 1 kicks: the team that kicked first took one attempt
more than the other, the second team's last kick was never taken, and the
referee then announced penalty_draw (or a coin toss) on an unequal number
of attempts. With the defaults (5 + 5) that is 11 kicks against 10.

The exhaustion check now runs only after an even number of kicks, at
`>= 2 * (nr + extra)`, and only while the teams are level; when the score
differs after the last pair the existing branches name the winner as
before. With 5 + 5 the shootout now ends after exactly 10 kicks each.

Also handle pen_nr_kicks = 0 and pen_max_extra_kicks = 0: the score check
only runs after a kick, so both teams used to take one kick each before
the draw/coin toss. startPenaltyShootout() now decides right away.

Reported upstream to rcsoccersim/rcssserver by the FoxLeague owner
(unmerged); the same condition is in rcssserver master and 19.0.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 048f871)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants