Skip to content

Commit e561fb5

Browse files
committed
[refactoring,bugfix] Rework ball placement procedure
1 parent 4e5053f commit e561fb5

24 files changed

+395
-283
lines changed

config/ssl-game-controller.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ game:
1616
multiple-placement-failures: 5
1717
auto-ref-proposal-timeout: 5s
1818
default-division: DivA
19-
lack-of-progress-free-kick-timeout:
19+
free-kick-time:
2020
DivA: 5s
2121
DivB: 10s
22-
lack-of-progress-timeout: 10s
22+
general-time: 10s
23+
ball-placement-time: 30s
2324
normal:
2425
half-duration: 5m
2526
half-time-duration: 5m

internal/app/config/config.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,20 @@ type Geometry struct {
3030

3131
// Game holds configs that are valid for the whole game
3232
type Game struct {
33-
YellowCardDuration time.Duration `yaml:"yellow-card-duration"`
34-
DefaultDivision Division `yaml:"default-division"`
35-
Normal Special `yaml:"normal"`
36-
Overtime Special `yaml:"overtime"`
37-
TeamChoiceTimeout time.Duration `yaml:"team-choice-timeout"`
38-
DefaultGeometry map[Division]*Geometry `yaml:"default-geometry"`
39-
MultipleCardStep int `yaml:"multiple-card-step"`
40-
MultipleFoulStep int `yaml:"multiple-foul-step"`
41-
MultiplePlacementFailures int `yaml:"multiple-placement-failures"`
42-
MaxBots map[Division]int `yaml:"max-bots"`
43-
AutoRefProposalTimeout time.Duration `yaml:"auto-ref-proposal-timeout"`
44-
LackOfProgressFreeKickTimeout map[Division]time.Duration `yaml:"lack-of-progress-free-kick-timeout"`
45-
LackOfProgressTimeout time.Duration `yaml:"lack-of-progress-timeout"`
33+
YellowCardDuration time.Duration `yaml:"yellow-card-duration"`
34+
DefaultDivision Division `yaml:"default-division"`
35+
Normal Special `yaml:"normal"`
36+
Overtime Special `yaml:"overtime"`
37+
TeamChoiceTimeout time.Duration `yaml:"team-choice-timeout"`
38+
DefaultGeometry map[Division]*Geometry `yaml:"default-geometry"`
39+
MultipleCardStep int `yaml:"multiple-card-step"`
40+
MultipleFoulStep int `yaml:"multiple-foul-step"`
41+
MultiplePlacementFailures int `yaml:"multiple-placement-failures"`
42+
MaxBots map[Division]int `yaml:"max-bots"`
43+
AutoRefProposalTimeout time.Duration `yaml:"auto-ref-proposal-timeout"`
44+
FreeKickTime map[Division]time.Duration `yaml:"free-kick-time"`
45+
GeneralTime time.Duration `yaml:"general-time"`
46+
BallPlacementTime time.Duration `yaml:"ball-placement-time"`
4647
}
4748

4849
// Network holds configs for network communication
@@ -110,8 +111,9 @@ func DefaultControllerConfig() (c Controller) {
110111
c.Game.MultipleFoulStep = 3
111112
c.Game.MultiplePlacementFailures = 5
112113
c.Game.AutoRefProposalTimeout = 5 * time.Second
113-
c.Game.LackOfProgressTimeout = time.Second * 10
114-
c.Game.LackOfProgressFreeKickTimeout = map[Division]time.Duration{DivA: time.Second * 5, DivB: time.Second * 10}
114+
c.Game.GeneralTime = time.Second * 10
115+
c.Game.FreeKickTime = map[Division]time.Duration{DivA: time.Second * 5, DivB: time.Second * 10}
116+
c.Game.BallPlacementTime = time.Second * 30
115117

116118
c.Game.Normal.HalfDuration = 5 * time.Minute
117119
c.Game.Normal.HalfTimeDuration = 5 * time.Minute

internal/app/config/testdata/config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
timeFromVision: false
12
network:
23
publish-address: 224.5.23.1:10003
34
vision-address: 224.5.23.2:10006
@@ -15,6 +16,11 @@ game:
1516
multiple-placement-failures: 5
1617
auto-ref-proposal-timeout: 5s
1718
default-division: DivA
19+
free-kick-time:
20+
DivA: 5s
21+
DivB: 10s
22+
general-time: 10s
23+
ball-placement-time: 30s
1824
normal:
1925
half-duration: 5m
2026
half-time-duration: 5m

internal/app/controller/engine.go

Lines changed: 127 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,11 @@ func (e *Engine) Tick(delta time.Duration) {
6363
if e.State.MatchTimeStart.After(time.Unix(0, 0)) {
6464
e.State.MatchDuration = e.TimeProvider().Sub(e.State.MatchTimeStart)
6565
}
66-
if e.State.LackOfProgressDeadline.After(time.Unix(0, 0)) {
67-
e.State.LackOfProgressTimeRemaining = e.RemainingLackOfProgressTime()
66+
if e.State.CurrentActionDeadline.After(time.Unix(0, 0)) {
67+
e.State.CurrentActionTimeRemaining = e.State.CurrentActionDeadline.Sub(e.TimeProvider())
6868
}
6969
}
7070

71-
func (e *Engine) RemainingLackOfProgressTime() time.Duration {
72-
return e.State.LackOfProgressDeadline.Sub(e.TimeProvider())
73-
}
74-
7571
func (e *Engine) updateTimes(delta time.Duration) {
7672
if e.countStageTime() {
7773
e.State.StageTimeElapsed += delta
@@ -123,26 +119,33 @@ func (e *Engine) SendCommand(command RefCommand, forTeam Team) {
123119
}
124120

125121
if command.ContinuesGame() {
122+
// reset game events
126123
if len(e.State.GameEvents) > 0 {
127124
e.State.GameEvents = []*GameEvent{}
128125
}
126+
// reset game event proposals
129127
if len(e.State.GameEventProposals) > 0 {
130128
e.State.GameEventProposals = []*GameEventProposal{}
131129
}
130+
// reset ball placement pos and follow ups
132131
e.State.PlacementPos = nil
133132
e.State.NextCommand = CommandUnknown
134133
e.State.NextCommandFor = TeamUnknown
135134

136-
if command != CommandKickoff && command != CommandPenalty {
137-
e.State.LackOfProgressTimeRemaining = e.config.LackOfProgressTimeout
138-
if command == CommandIndirect || command == CommandDirect {
139-
e.State.LackOfProgressTimeRemaining = e.config.LackOfProgressFreeKickTimeout[e.State.Division]
140-
}
141-
e.State.LackOfProgressDeadline = e.TimeProvider().Add(e.State.LackOfProgressTimeRemaining)
135+
// update current action timeout
136+
if command == CommandIndirect || command == CommandDirect {
137+
e.setCurrentActionTimeout(e.config.FreeKickTime[e.State.Division])
138+
} else if command != CommandKickoff && command != CommandPenalty {
139+
e.setCurrentActionTimeout(e.config.GeneralTime)
142140
}
143141
}
144142
}
145143

144+
func (e *Engine) setCurrentActionTimeout(timeout time.Duration) {
145+
e.State.CurrentActionTimeRemaining = timeout
146+
e.State.CurrentActionDeadline = e.TimeProvider().Add(e.State.CurrentActionTimeRemaining)
147+
}
148+
146149
func (e *Engine) AddGameEvent(gameEvent GameEvent) {
147150
e.State.GameEvents = append(e.State.GameEvents, &gameEvent)
148151
e.LogGameEvent(gameEvent)
@@ -172,57 +175,57 @@ func (e *Engine) CommandForEvent(event *GameEvent) (command RefCommand, forTeam
172175
return
173176
}
174177

175-
forTeam = event.ByTeam().Opposite()
176-
177-
switch event.Type {
178-
case
179-
GameEventBallLeftFieldTouchLine,
180-
GameEventAimlessKick,
181-
GameEventBotKickedBallTooFast,
182-
GameEventBotDribbledBallTooFar,
183-
GameEventAttackerDoubleTouchedBall,
184-
GameEventAttackerInDefenseArea,
185-
GameEventAttackerTouchedKeeper,
186-
GameEventKickTimeout,
187-
GameEventKeeperHeldBall,
188-
GameEventPlacementFailedByTeamInFavor:
178+
if e.State.bothTeamsCanPlaceBall() && e.State.ballPlacementFailedBefore() && event.Type.resultsFromBallLeavingField() {
179+
// A failed placement will result in an indirect free kick for the opposing team.
189180
command = CommandIndirect
190-
case
191-
GameEventBallLeftFieldGoalLine,
192-
GameEventIndirectGoal,
193-
GameEventPossibleGoal,
194-
GameEventChippedGoal,
195-
GameEventDefenderInDefenseAreaPartially,
196-
GameEventAttackerTooCloseToDefenseArea,
197-
GameEventBotTippedOver,
198-
GameEventBotCrashUnique,
199-
GameEventBotPushedBot,
200-
GameEventBotHeldBallDeliberately:
201-
command = CommandDirect
202-
case
203-
GameEventGoal:
204-
command = CommandKickoff
205-
case
206-
GameEventBotCrashDrawn,
207-
GameEventNoProgressInGame:
208-
command = CommandForceStart
209-
case
210-
GameEventDefenderInDefenseArea,
211-
GameEventMultipleCards:
212-
command = CommandPenalty
213-
case
214-
GameEventBotInterferedPlacement,
215-
GameEventDefenderTooCloseToKickPoint,
216-
GameEventPlacementFailedByOpponent:
217-
command, err = e.LastGameStartCommand()
218-
default:
219-
err = errors.Errorf("Unhandled game event: %v", e.State.GameEvents)
220-
}
181+
forTeam = event.ByTeam()
182+
} else {
183+
forTeam = event.ByTeam().Opposite()
184+
switch event.Type {
185+
case
186+
GameEventBallLeftFieldTouchLine,
187+
GameEventAimlessKick,
188+
GameEventBotKickedBallTooFast,
189+
GameEventBotDribbledBallTooFar,
190+
GameEventAttackerDoubleTouchedBall,
191+
GameEventAttackerInDefenseArea,
192+
GameEventAttackerTouchedKeeper,
193+
GameEventKickTimeout,
194+
GameEventKeeperHeldBall:
195+
command = CommandIndirect
196+
case
197+
GameEventBallLeftFieldGoalLine,
198+
GameEventIndirectGoal,
199+
GameEventPossibleGoal,
200+
GameEventChippedGoal,
201+
GameEventDefenderInDefenseAreaPartially,
202+
GameEventAttackerTooCloseToDefenseArea,
203+
GameEventBotTippedOver,
204+
GameEventBotCrashUnique,
205+
GameEventBotPushedBot,
206+
GameEventBotHeldBallDeliberately:
207+
command = CommandDirect
208+
case
209+
GameEventGoal:
210+
command = CommandKickoff
211+
case
212+
GameEventBotCrashDrawn,
213+
GameEventNoProgressInGame:
214+
command = CommandForceStart
215+
case
216+
GameEventDefenderInDefenseArea,
217+
GameEventMultipleCards:
218+
command = CommandPenalty
219+
default:
220+
err = errors.Errorf("Unhandled game event: %v", e.State.GameEvents)
221+
}
221222

222-
if e.State.Division == config.DivA && command.IsFreeKick() && !e.State.TeamState[forTeam].CanPlaceBall {
223-
// in division A, if the team in favor can not place the ball (because of too many failures), free kicks are awarded to the other team
224-
forTeam = forTeam.Opposite()
225-
command = CommandIndirect
223+
if e.State.Division == config.DivA && // For division A
224+
!e.State.TeamState[forTeam].CanPlaceBall && // If team in favor can not place the ball
225+
event.Type.resultsFromBallLeavingField() { // event is caused by the ball leaving the field
226+
// All free kicks that were a result of the ball leaving the field, are awarded to the opposing team.
227+
forTeam = forTeam.Opposite()
228+
}
226229
}
227230

228231
if command.NeedsTeam() && forTeam.Unknown() {
@@ -234,18 +237,35 @@ func (e *Engine) CommandForEvent(event *GameEvent) (command RefCommand, forTeam
234237
return
235238
}
236239

237-
func (e *Engine) LastGameStartCommand() (RefCommand, error) {
238-
for i := len(e.UiProtocol) - 1; i >= 0; i-- {
239-
event := e.UiProtocol[i]
240-
if event.Type == UiProtocolCommand {
241-
cmd := RefCommand(event.Name)
242-
switch cmd {
243-
case CommandPenalty, CommandKickoff, CommandIndirect, CommandDirect:
244-
return cmd, nil
245-
}
240+
func (g GameEventType) resultsFromBallLeavingField() bool {
241+
switch g {
242+
case
243+
GameEventBallLeftFieldTouchLine,
244+
GameEventBallLeftFieldGoalLine,
245+
GameEventAimlessKick,
246+
GameEventIndirectGoal,
247+
GameEventPossibleGoal,
248+
GameEventChippedGoal:
249+
return true
250+
}
251+
return false
252+
}
253+
254+
func (s *State) bothTeamsCanPlaceBall() bool {
255+
return s.TeamState[TeamYellow].CanPlaceBall && s.TeamState[TeamBlue].CanPlaceBall
256+
}
257+
258+
func (s *State) noTeamCanPlaceBall() bool {
259+
return !s.TeamState[TeamYellow].CanPlaceBall && !s.TeamState[TeamBlue].CanPlaceBall
260+
}
261+
262+
func (s *State) ballPlacementFailedBefore() bool {
263+
for _, gameEvent := range s.GameEvents {
264+
if gameEvent.Type == GameEventPlacementFailedByTeamInFavor {
265+
return true
246266
}
247267
}
248-
return "", errors.New("No last game start command found.")
268+
return false
249269
}
250270

251271
func (e *Engine) Process(event Event) error {
@@ -613,23 +633,20 @@ func (e *Engine) processGameEvent(event *GameEvent) error {
613633
e.State.TeamState[team].Goals++
614634
}
615635

636+
if event.Type == GameEventBotInterferedPlacement {
637+
// reset ball placement timer
638+
e.setCurrentActionTimeout(e.config.BallPlacementTime)
639+
}
640+
616641
e.State.PlacementPos = e.BallPlacementPos()
617642

618-
if e.State.GameState() != GameStateHalted && !event.IsSkipped() && !event.IsSecondary() {
619-
if event.Type == GameEventPossibleGoal || event.Type == GameEventPlacementFailedByOpponent || e.allTeamsFailedPlacement() {
620-
e.SendCommand(CommandHalt, "")
621-
} else if e.State.PlacementPos != nil && (event.ByTeam() == TeamBlue || event.ByTeam() == TeamYellow) {
622-
teamInFavor := event.ByTeam().Opposite()
623-
if e.State.TeamState[teamInFavor].CanPlaceBall {
624-
e.SendCommand(CommandBallPlacement, teamInFavor)
625-
} else if e.State.TeamState[teamInFavor.Opposite()].CanPlaceBall {
626-
e.SendCommand(CommandBallPlacement, teamInFavor.Opposite())
627-
} else if e.State.GameState() != GameStateStopped {
628-
e.SendCommand(CommandStop, "")
629-
}
630-
} else if e.State.GameState() != GameStateStopped {
631-
e.SendCommand(CommandStop, "")
632-
}
643+
if e.State.GameState() == GameStateHalted {
644+
log.Printf("Warn: Received a game event while halted: %v", event)
645+
} else if event.Type == GameEventDefenderTooCloseToKickPoint {
646+
// stop the game and let bots move away from the ball first. The autoRef will continue the game afterwards
647+
e.SendCommand(CommandStop, "")
648+
} else if !event.IsSkipped() && !event.IsSecondary() {
649+
e.placeBall(event)
633650
} else if e.State.AutoContinue && event.IsContinueGame() {
634651
e.Continue()
635652
}
@@ -638,6 +655,30 @@ func (e *Engine) processGameEvent(event *GameEvent) error {
638655
return nil
639656
}
640657

658+
func (e *Engine) placeBall(event *GameEvent) {
659+
teamInFavor := event.ByTeam().Opposite()
660+
if e.State.PlacementPos == nil || teamInFavor.Unknown() || e.State.noTeamCanPlaceBall() {
661+
// placement not possible, human ref must help out
662+
e.SendCommand(CommandHalt, "")
663+
return
664+
} else if e.State.Division == config.DivB && // For division B
665+
!e.State.TeamState[teamInFavor].CanPlaceBall { // If team in favor can not place the ball
666+
// Rule: [...] the team is allowed to bring the ball into play, after the ball was placed by the opposing team.
667+
e.SendCommand(CommandBallPlacement, teamInFavor.Opposite())
668+
} else if e.State.bothTeamsCanPlaceBall() && e.State.ballPlacementFailedBefore() && event.Type.resultsFromBallLeavingField() {
669+
// Rule: A failed placement will result in an indirect free kick for the opposing team.
670+
e.SendCommand(CommandBallPlacement, teamInFavor.Opposite())
671+
} else if e.State.Division == config.DivA && // For division A
672+
!e.State.TeamState[teamInFavor].CanPlaceBall && // If team in favor can not place the ball
673+
event.Type.resultsFromBallLeavingField() {
674+
// Rule: All free kicks that were a result of the ball leaving the field, are awarded to the opposing team.
675+
e.SendCommand(CommandBallPlacement, teamInFavor.Opposite())
676+
} else {
677+
e.SendCommand(CommandBallPlacement, teamInFavor)
678+
}
679+
e.setCurrentActionTimeout(e.config.BallPlacementTime)
680+
}
681+
641682
func (e *Engine) allTeamsFailedPlacement() bool {
642683
possibleFailures := 0
643684
if e.State.TeamState[TeamYellow].CanPlaceBall {

0 commit comments

Comments
 (0)