diff --git a/frontend/src/components/match/MatchTeamTable.vue b/frontend/src/components/match/MatchTeamTable.vue
index a1fd0a9b..c3a9180f 100644
--- a/frontend/src/components/match/MatchTeamTable.vue
+++ b/frontend/src/components/match/MatchTeamTable.vue
@@ -7,6 +7,8 @@ import {useMatchStateStore} from "@/store/matchState";
import formatDuration from "format-duration";
import {teams} from "@/helpers";
import type {Team} from "@/proto/ssl_gc_common";
+import {Referee_Stage} from "@/proto/ssl_gc_referee_message";
+import {computed} from "vue";
const store = useMatchStateStore()
@@ -30,6 +32,12 @@ const nextYellowCardDue = (team: Team) => {
}
return 0
}
+const isShootout = computed(() => {
+ return store.matchState.stage === Referee_Stage.PENALTY_SHOOTOUT
+})
+const penaltyAttempts = (team: Team) => {
+ return store.matchState.shootoutState?.numberOfAttempts?.[team] || 0
+}
@@ -79,6 +87,17 @@ const nextYellowCardDue = (team: Team) => {
+
+
+
+
+ {{ penaltyAttempts(team) }}
+
+
+ Number of penalty attempts
+
+
+
diff --git a/frontend/src/proto/ssl_gc_state.ts b/frontend/src/proto/ssl_gc_state.ts
index b9e07939..3078be97 100644
--- a/frontend/src/proto/ssl_gc_state.ts
+++ b/frontend/src/proto/ssl_gc_state.ts
@@ -258,6 +258,12 @@ export interface State_TeamStateEntry {
export interface ShootoutState {
nextTeam?: Team;
+ numberOfAttempts?: { [key: string]: number };
+}
+
+export interface ShootoutState_NumberOfAttemptsEntry {
+ key: string;
+ value: number;
}
export const YellowCard = {
@@ -558,12 +564,39 @@ export const State_TeamStateEntry = {
export const ShootoutState = {
fromJSON(object: any): ShootoutState {
- return { nextTeam: isSet(object.nextTeam) ? teamFromJSON(object.nextTeam) : Team.UNKNOWN };
+ return {
+ nextTeam: isSet(object.nextTeam) ? teamFromJSON(object.nextTeam) : Team.UNKNOWN,
+ numberOfAttempts: isObject(object.numberOfAttempts)
+ ? Object.entries(object.numberOfAttempts).reduce<{ [key: string]: number }>((acc, [key, value]) => {
+ acc[key] = Number(value);
+ return acc;
+ }, {})
+ : {},
+ };
},
toJSON(message: ShootoutState): unknown {
const obj: any = {};
message.nextTeam !== undefined && (obj.nextTeam = teamToJSON(message.nextTeam));
+ obj.numberOfAttempts = {};
+ if (message.numberOfAttempts) {
+ Object.entries(message.numberOfAttempts).forEach(([k, v]) => {
+ obj.numberOfAttempts[k] = Math.round(v);
+ });
+ }
+ return obj;
+ },
+};
+
+export const ShootoutState_NumberOfAttemptsEntry = {
+ fromJSON(object: any): ShootoutState_NumberOfAttemptsEntry {
+ return { key: isSet(object.key) ? String(object.key) : "", value: isSet(object.value) ? Number(object.value) : 0 };
+ },
+
+ toJSON(message: ShootoutState_NumberOfAttemptsEntry): unknown {
+ const obj: any = {};
+ message.key !== undefined && (obj.key = message.key);
+ message.value !== undefined && (obj.value = Math.round(message.value));
return obj;
},
};
diff --git a/internal/app/engine/process_continue_next_action.go b/internal/app/engine/process_continue_next_action.go
index c0ff767f..0bb0a70f 100644
--- a/internal/app/engine/process_continue_next_action.go
+++ b/internal/app/engine/process_continue_next_action.go
@@ -331,6 +331,17 @@ func (e *Engine) randomTeam() state.Team {
func suggestEndOfMatch(currentState *state.State) bool {
goalsY := int(*currentState.TeamInfo(state.Team_YELLOW).Goals)
goalsB := int(*currentState.TeamInfo(state.Team_BLUE).Goals)
+
+ if *currentState.Stage == state.Referee_PENALTY_SHOOTOUT {
+ attempts := currentState.ShootoutState.NumberOfAttempts[state.Team_BLUE.String()] +
+ currentState.ShootoutState.NumberOfAttempts[state.Team_YELLOW.String()]
+
+ if attempts < 10 || attempts%2 == 1 {
+ return false
+ }
+ return goalsY != goalsB
+ }
+
if *currentState.Stage != state.Referee_POST_GAME &&
(goalsY >= 10 || goalsB >= 10) && math.Abs(float64(goalsY-goalsB)) > 1 {
return true
diff --git a/internal/app/state/ssl_gc_state.pb.go b/internal/app/state/ssl_gc_state.pb.go
index 56af0e57..3fdf5c27 100644
--- a/internal/app/state/ssl_gc_state.pb.go
+++ b/internal/app/state/ssl_gc_state.pb.go
@@ -948,7 +948,8 @@ type ShootoutState struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- NextTeam *Team `protobuf:"varint,1,opt,name=next_team,json=nextTeam,enum=Team" json:"next_team,omitempty"`
+ NextTeam *Team `protobuf:"varint,1,opt,name=next_team,json=nextTeam,enum=Team" json:"next_team,omitempty"`
+ NumberOfAttempts map[string]int32 `protobuf:"bytes,2,rep,name=number_of_attempts,json=numberOfAttempts" json:"number_of_attempts,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
}
func (x *ShootoutState) Reset() {
@@ -990,6 +991,13 @@ func (x *ShootoutState) GetNextTeam() Team {
return Team_UNKNOWN
}
+func (x *ShootoutState) GetNumberOfAttempts() map[string]int32 {
+ if x != nil {
+ return x.NumberOfAttempts
+ }
+ return nil
+}
+
var File_ssl_gc_state_proto protoreflect.FileDescriptor
var file_ssl_gc_state_proto_rawDesc = []byte{
@@ -1191,15 +1199,24 @@ var file_ssl_gc_state_proto_rawDesc = []byte{
0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
0x79, 0x12, 0x1f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x09, 0x2e, 0x54, 0x65, 0x61, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, 0x6c,
- 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, 0x08, 0x10, 0x10, 0x11, 0x22, 0x33, 0x0a, 0x0d,
- 0x53, 0x68, 0x6f, 0x6f, 0x74, 0x6f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x22, 0x0a,
- 0x09, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
- 0x32, 0x05, 0x2e, 0x54, 0x65, 0x61, 0x6d, 0x52, 0x08, 0x6e, 0x65, 0x78, 0x74, 0x54, 0x65, 0x61,
- 0x6d, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
- 0x52, 0x6f, 0x62, 0x6f, 0x43, 0x75, 0x70, 0x2d, 0x53, 0x53, 0x4c, 0x2f, 0x73, 0x73, 0x6c, 0x2d,
- 0x67, 0x61, 0x6d, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f,
- 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61,
- 0x74, 0x65,
+ 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, 0x08, 0x10, 0x10, 0x11, 0x22, 0xcc, 0x01, 0x0a,
+ 0x0d, 0x53, 0x68, 0x6f, 0x6f, 0x74, 0x6f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x22,
+ 0x0a, 0x09, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0e, 0x32, 0x05, 0x2e, 0x54, 0x65, 0x61, 0x6d, 0x52, 0x08, 0x6e, 0x65, 0x78, 0x74, 0x54, 0x65,
+ 0x61, 0x6d, 0x12, 0x52, 0x0a, 0x12, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x6f, 0x66, 0x5f,
+ 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24,
+ 0x2e, 0x53, 0x68, 0x6f, 0x6f, 0x74, 0x6f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x4e,
+ 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x45,
+ 0x6e, 0x74, 0x72, 0x79, 0x52, 0x10, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x41, 0x74,
+ 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x1a, 0x43, 0x0a, 0x15, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72,
+ 0x4f, 0x66, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
+ 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
+ 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05,
+ 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x3f, 0x5a, 0x3d, 0x67,
+ 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x52, 0x6f, 0x62, 0x6f, 0x43, 0x75,
+ 0x70, 0x2d, 0x53, 0x53, 0x4c, 0x2f, 0x73, 0x73, 0x6c, 0x2d, 0x67, 0x61, 0x6d, 0x65, 0x2d, 0x63,
+ 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
+ 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x65,
}
var (
@@ -1215,7 +1232,7 @@ func file_ssl_gc_state_proto_rawDescGZIP() []byte {
}
var file_ssl_gc_state_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
-var file_ssl_gc_state_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+var file_ssl_gc_state_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_ssl_gc_state_proto_goTypes = []interface{}{
(Command_Type)(0), // 0: Command.Type
(GameState_Type)(0), // 1: GameState.Type
@@ -1230,59 +1247,61 @@ var file_ssl_gc_state_proto_goTypes = []interface{}{
(*State)(nil), // 10: State
(*ShootoutState)(nil), // 11: ShootoutState
nil, // 12: State.TeamStateEntry
- (*GameEvent)(nil), // 13: GameEvent
- (*durationpb.Duration)(nil), // 14: google.protobuf.Duration
- (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp
- (Team)(0), // 16: Team
- (Referee_Stage)(0), // 17: Referee.Stage
- (*geom.Vector2)(nil), // 18: Vector2
- (Division)(0), // 19: Division
- (MatchType)(0), // 20: MatchType
+ nil, // 13: ShootoutState.NumberOfAttemptsEntry
+ (*GameEvent)(nil), // 14: GameEvent
+ (*durationpb.Duration)(nil), // 15: google.protobuf.Duration
+ (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp
+ (Team)(0), // 17: Team
+ (Referee_Stage)(0), // 18: Referee.Stage
+ (*geom.Vector2)(nil), // 19: Vector2
+ (Division)(0), // 20: Division
+ (MatchType)(0), // 21: MatchType
}
var file_ssl_gc_state_proto_depIdxs = []int32{
- 13, // 0: YellowCard.caused_by_game_event:type_name -> GameEvent
- 14, // 1: YellowCard.time_remaining:type_name -> google.protobuf.Duration
- 13, // 2: RedCard.caused_by_game_event:type_name -> GameEvent
- 13, // 3: Foul.caused_by_game_event:type_name -> GameEvent
- 15, // 4: Foul.timestamp:type_name -> google.protobuf.Timestamp
+ 14, // 0: YellowCard.caused_by_game_event:type_name -> GameEvent
+ 15, // 1: YellowCard.time_remaining:type_name -> google.protobuf.Duration
+ 14, // 2: RedCard.caused_by_game_event:type_name -> GameEvent
+ 14, // 3: Foul.caused_by_game_event:type_name -> GameEvent
+ 16, // 4: Foul.timestamp:type_name -> google.protobuf.Timestamp
0, // 5: Command.type:type_name -> Command.Type
- 16, // 6: Command.for_team:type_name -> Team
+ 17, // 6: Command.for_team:type_name -> Team
1, // 7: GameState.type:type_name -> GameState.Type
- 16, // 8: GameState.for_team:type_name -> Team
- 15, // 9: Proposal.timestamp:type_name -> google.protobuf.Timestamp
- 13, // 10: Proposal.game_event:type_name -> GameEvent
+ 17, // 8: GameState.for_team:type_name -> Team
+ 16, // 9: Proposal.timestamp:type_name -> google.protobuf.Timestamp
+ 14, // 10: Proposal.game_event:type_name -> GameEvent
7, // 11: ProposalGroup.proposals:type_name -> Proposal
2, // 12: TeamInfo.yellow_cards:type_name -> YellowCard
3, // 13: TeamInfo.red_cards:type_name -> RedCard
- 14, // 14: TeamInfo.timeout_time_left:type_name -> google.protobuf.Duration
+ 15, // 14: TeamInfo.timeout_time_left:type_name -> google.protobuf.Duration
4, // 15: TeamInfo.fouls:type_name -> Foul
- 15, // 16: TeamInfo.requests_bot_substitution_since:type_name -> google.protobuf.Timestamp
- 15, // 17: TeamInfo.requests_timeout_since:type_name -> google.protobuf.Timestamp
- 15, // 18: TeamInfo.requests_emergency_stop_since:type_name -> google.protobuf.Timestamp
- 17, // 19: State.stage:type_name -> Referee.Stage
+ 16, // 16: TeamInfo.requests_bot_substitution_since:type_name -> google.protobuf.Timestamp
+ 16, // 17: TeamInfo.requests_timeout_since:type_name -> google.protobuf.Timestamp
+ 16, // 18: TeamInfo.requests_emergency_stop_since:type_name -> google.protobuf.Timestamp
+ 18, // 19: State.stage:type_name -> Referee.Stage
5, // 20: State.command:type_name -> Command
6, // 21: State.game_state:type_name -> GameState
- 14, // 22: State.stage_time_elapsed:type_name -> google.protobuf.Duration
- 14, // 23: State.stage_time_left:type_name -> google.protobuf.Duration
- 15, // 24: State.match_time_start:type_name -> google.protobuf.Timestamp
+ 15, // 22: State.stage_time_elapsed:type_name -> google.protobuf.Duration
+ 15, // 23: State.stage_time_left:type_name -> google.protobuf.Duration
+ 16, // 24: State.match_time_start:type_name -> google.protobuf.Timestamp
12, // 25: State.team_state:type_name -> State.TeamStateEntry
- 18, // 26: State.placement_pos:type_name -> Vector2
+ 19, // 26: State.placement_pos:type_name -> Vector2
5, // 27: State.next_command:type_name -> Command
- 14, // 28: State.current_action_time_remaining:type_name -> google.protobuf.Duration
- 13, // 29: State.game_events:type_name -> GameEvent
+ 15, // 28: State.current_action_time_remaining:type_name -> google.protobuf.Duration
+ 14, // 29: State.game_events:type_name -> GameEvent
8, // 30: State.proposal_groups:type_name -> ProposalGroup
- 19, // 31: State.division:type_name -> Division
- 16, // 32: State.first_kickoff_team:type_name -> Team
- 20, // 33: State.match_type:type_name -> MatchType
- 15, // 34: State.ready_continue_time:type_name -> google.protobuf.Timestamp
+ 20, // 31: State.division:type_name -> Division
+ 17, // 32: State.first_kickoff_team:type_name -> Team
+ 21, // 33: State.match_type:type_name -> MatchType
+ 16, // 34: State.ready_continue_time:type_name -> google.protobuf.Timestamp
11, // 35: State.shootout_state:type_name -> ShootoutState
- 16, // 36: ShootoutState.next_team:type_name -> Team
- 9, // 37: State.TeamStateEntry.value:type_name -> TeamInfo
- 38, // [38:38] is the sub-list for method output_type
- 38, // [38:38] is the sub-list for method input_type
- 38, // [38:38] is the sub-list for extension type_name
- 38, // [38:38] is the sub-list for extension extendee
- 0, // [0:38] is the sub-list for field type_name
+ 17, // 36: ShootoutState.next_team:type_name -> Team
+ 13, // 37: ShootoutState.number_of_attempts:type_name -> ShootoutState.NumberOfAttemptsEntry
+ 9, // 38: State.TeamStateEntry.value:type_name -> TeamInfo
+ 39, // [39:39] is the sub-list for method output_type
+ 39, // [39:39] is the sub-list for method input_type
+ 39, // [39:39] is the sub-list for extension type_name
+ 39, // [39:39] is the sub-list for extension extendee
+ 0, // [0:39] is the sub-list for field type_name
}
func init() { file_ssl_gc_state_proto_init() }
@@ -1421,7 +1440,7 @@ func file_ssl_gc_state_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_ssl_gc_state_proto_rawDesc,
NumEnums: 2,
- NumMessages: 11,
+ NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/internal/app/statemachine/change_command.go b/internal/app/statemachine/change_command.go
index d764e92b..e8e74d5c 100644
--- a/internal/app/statemachine/change_command.go
+++ b/internal/app/statemachine/change_command.go
@@ -63,6 +63,7 @@ func (s *StateMachine) processChangeNewCommand(newState *state.State, newCommand
if *newState.Stage == state.Referee_PENALTY_SHOOTOUT &&
newState.ShootoutState != nil {
if *newCommand.Command.Type == state.Command_NORMAL_START {
+ newState.ShootoutState.NumberOfAttempts[newState.ShootoutState.NextTeam.String()]++
*newState.ShootoutState.NextTeam = newState.ShootoutState.NextTeam.Opposite()
} else if *newState.GameState.Type == state.GameState_STOP {
forTeam := *newState.ShootoutState.NextTeam
diff --git a/internal/app/statemachine/change_stage.go b/internal/app/statemachine/change_stage.go
index 5c68157c..945fe6a9 100644
--- a/internal/app/statemachine/change_stage.go
+++ b/internal/app/statemachine/change_stage.go
@@ -62,6 +62,10 @@ func (s *StateMachine) proceedStage(newState *state.State, newStage state.Refere
if newStage == state.Referee_PENALTY_SHOOTOUT {
newState.ShootoutState = &state.ShootoutState{
NextTeam: newState.FirstKickoffTeam,
+ NumberOfAttempts: map[string]int32{
+ state.Team_BLUE.String(): 0,
+ state.Team_YELLOW.String(): 0,
+ },
}
}
diff --git a/proto/ssl_gc_state.proto b/proto/ssl_gc_state.proto
index 41438a1f..92782230 100644
--- a/proto/ssl_gc_state.proto
+++ b/proto/ssl_gc_state.proto
@@ -121,4 +121,5 @@ message State {
message ShootoutState {
optional Team next_team = 1;
+ map number_of_attempts = 2;
}