From dd937d3752346476cba6d075d40b4b7461dae9ff Mon Sep 17 00:00:00 2001 From: Nicolai Ommer Date: Thu, 6 Jul 2023 15:31:06 +0200 Subject: [PATCH] Implement shootout procedure --- .../src/components/match/MatchTeamTable.vue | 19 +++ frontend/src/proto/ssl_gc_state.ts | 35 ++++- .../engine/process_continue_next_action.go | 11 ++ internal/app/state/ssl_gc_state.pb.go | 121 ++++++++++-------- internal/app/statemachine/change_command.go | 1 + internal/app/statemachine/change_stage.go | 4 + proto/ssl_gc_state.proto | 1 + 7 files changed, 140 insertions(+), 52 deletions(-) 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 +} 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; }