open-match.dev/open-match@v1.8.1/examples/functions/golang/backfill/mmf/matchfunction.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package mmf provides a sample match function that uses the GRPC harness to set up 1v1 matches. 16 // This sample is a reference to demonstrate the usage of backfill and should only be used as 17 // a starting point for your match function. You will need to modify the 18 // matchmaking logic in this function based on your game's requirements. 19 package mmf 20 21 import ( 22 "fmt" 23 "time" 24 25 "log" 26 27 "google.golang.org/grpc" 28 "google.golang.org/protobuf/types/known/anypb" 29 "google.golang.org/protobuf/types/known/timestamppb" 30 "google.golang.org/protobuf/types/known/wrapperspb" 31 "open-match.dev/open-match/pkg/matchfunction" 32 "open-match.dev/open-match/pkg/pb" 33 ) 34 35 const ( 36 playersPerMatch = 2 37 openSlotsKey = "open-slots" 38 matchName = "backfill-matchfunction" 39 ) 40 41 // matchFunctionService implements pb.MatchFunctionServer, the server generated 42 // by compiling the protobuf, by fulfilling the pb.MatchFunctionServer interface. 43 type matchFunctionService struct { 44 grpc *grpc.Server 45 queryServiceClient pb.QueryServiceClient 46 port int 47 } 48 49 func (s *matchFunctionService) Run(req *pb.RunRequest, stream pb.MatchFunction_RunServer) error { 50 log.Printf("Generating proposals for function %v", req.GetProfile().GetName()) 51 52 var proposals []*pb.Match 53 profile := req.GetProfile() 54 pools := profile.GetPools() 55 56 for _, p := range pools { 57 tickets, err := matchfunction.QueryPool(stream.Context(), s.queryServiceClient, p) 58 if err != nil { 59 log.Printf("Failed to query tickets for the given pool, got %s", err.Error()) 60 return err 61 } 62 63 backfills, err := matchfunction.QueryBackfillPool(stream.Context(), s.queryServiceClient, p) 64 if err != nil { 65 log.Printf("Failed to query backfills for the given pool, got %s", err.Error()) 66 return err 67 } 68 69 matches, err := makeMatches(profile, p, tickets, backfills) 70 if err != nil { 71 log.Printf("Failed to generate matches, got %s", err.Error()) 72 return err 73 } 74 75 proposals = append(proposals, matches...) 76 } 77 78 log.Printf("Streaming %v proposals to Open Match", len(proposals)) 79 // Stream the generated proposals back to Open Match. 80 for _, proposal := range proposals { 81 if err := stream.Send(&pb.RunResponse{Proposal: proposal}); err != nil { 82 log.Printf("Failed to stream proposals to Open Match, got %s", err.Error()) 83 return err 84 } 85 } 86 87 return nil 88 } 89 90 // makeMatches tries to handle backfills at first, then it makes full matches, at the end it makes a match with backfill 91 // if tickets left 92 func makeMatches(profile *pb.MatchProfile, pool *pb.Pool, tickets []*pb.Ticket, backfills []*pb.Backfill) ([]*pb.Match, error) { 93 var matches []*pb.Match 94 newMatches, remainingTickets, err := handleBackfills(profile, tickets, backfills, len(matches)) 95 if err != nil { 96 return nil, err 97 } 98 99 matches = append(matches, newMatches...) 100 newMatches, remainingTickets = makeFullMatches(profile, remainingTickets, len(matches)) 101 matches = append(matches, newMatches...) 102 103 if len(remainingTickets) > 0 { 104 match, err := makeMatchWithBackfill(profile, pool, remainingTickets, len(matches)) 105 if err != nil { 106 return nil, err 107 } 108 109 matches = append(matches, match) 110 } 111 112 return matches, nil 113 } 114 115 // handleBackfills looks at each backfill's openSlots which is a number of required tickets, 116 // acquires that tickets, decreases openSlots in backfill and makes a match with updated backfill and associated tickets. 117 func handleBackfills(profile *pb.MatchProfile, tickets []*pb.Ticket, backfills []*pb.Backfill, lastMatchId int) ([]*pb.Match, []*pb.Ticket, error) { 118 matchId := lastMatchId 119 var matches []*pb.Match 120 121 for _, b := range backfills { 122 openSlots, err := getOpenSlots(b) 123 if err != nil { 124 return nil, tickets, err 125 } 126 127 var matchTickets []*pb.Ticket 128 for openSlots > 0 && len(tickets) > 0 { 129 matchTickets = append(matchTickets, tickets[0]) 130 tickets = tickets[1:] 131 openSlots-- 132 } 133 134 if len(matchTickets) > 0 { 135 err := setOpenSlots(b, openSlots) 136 if err != nil { 137 return nil, tickets, err 138 } 139 140 matchId++ 141 match := newMatch(matchId, profile.Name, matchTickets, b) 142 matches = append(matches, &match) 143 } 144 } 145 146 return matches, tickets, nil 147 } 148 149 // makeMatchWithBackfill makes not full match, creates backfill for it with openSlots = playersPerMatch-len(tickets). 150 func makeMatchWithBackfill(profile *pb.MatchProfile, pool *pb.Pool, tickets []*pb.Ticket, lastMatchId int) (*pb.Match, error) { 151 if len(tickets) == 0 { 152 return nil, fmt.Errorf("tickets are required") 153 } 154 155 if len(tickets) >= playersPerMatch { 156 return nil, fmt.Errorf("too many tickets") 157 } 158 159 matchId := lastMatchId 160 searchFields := newSearchFields(pool) 161 backfill, err := newBackfill(searchFields, playersPerMatch-len(tickets)) 162 if err != nil { 163 return nil, err 164 } 165 166 matchId++ 167 match := newMatch(matchId, profile.Name, tickets, backfill) 168 // indicates that it is a new match and new game server should be allocated for it 169 match.AllocateGameserver = true 170 171 return &match, nil 172 } 173 174 // makeFullMatches makes matches without backfill 175 func makeFullMatches(profile *pb.MatchProfile, tickets []*pb.Ticket, lastMatchId int) ([]*pb.Match, []*pb.Ticket) { 176 ticketNum := 0 177 matchId := lastMatchId 178 var matches []*pb.Match 179 180 for ticketNum < playersPerMatch && len(tickets) >= playersPerMatch { 181 ticketNum++ 182 183 if ticketNum == playersPerMatch { 184 matchId++ 185 186 match := newMatch(matchId, profile.Name, tickets[:playersPerMatch], nil) 187 matches = append(matches, &match) 188 189 tickets = tickets[playersPerMatch:] 190 ticketNum = 0 191 } 192 } 193 194 return matches, tickets 195 } 196 197 // newSearchFields creates search fields based on pool's search criteria. This is just example of how it can be done. 198 func newSearchFields(pool *pb.Pool) *pb.SearchFields { 199 searchFields := pb.SearchFields{} 200 rangeFilters := pool.GetDoubleRangeFilters() 201 202 if rangeFilters != nil { 203 doubleArgs := make(map[string]float64) 204 for _, f := range rangeFilters { 205 doubleArgs[f.DoubleArg] = (f.Max - f.Min) / 2 206 } 207 208 if len(doubleArgs) > 0 { 209 searchFields.DoubleArgs = doubleArgs 210 } 211 } 212 213 stringFilters := pool.GetStringEqualsFilters() 214 215 if stringFilters != nil { 216 stringArgs := make(map[string]string) 217 for _, f := range stringFilters { 218 stringArgs[f.StringArg] = f.Value 219 } 220 221 if len(stringArgs) > 0 { 222 searchFields.StringArgs = stringArgs 223 } 224 } 225 226 tagFilters := pool.GetTagPresentFilters() 227 228 if tagFilters != nil { 229 tags := make([]string, len(tagFilters)) 230 for _, f := range tagFilters { 231 tags = append(tags, f.Tag) 232 } 233 234 if len(tags) > 0 { 235 searchFields.Tags = tags 236 } 237 } 238 239 return &searchFields 240 } 241 242 func newBackfill(searchFields *pb.SearchFields, openSlots int) (*pb.Backfill, error) { 243 b := pb.Backfill{ 244 SearchFields: searchFields, 245 Generation: 0, 246 CreateTime: timestamppb.Now(), 247 } 248 249 err := setOpenSlots(&b, int32(openSlots)) 250 return &b, err 251 } 252 253 func newMatch(num int, profile string, tickets []*pb.Ticket, b *pb.Backfill) pb.Match { 254 t := time.Now().Format("2006-01-02T15:04:05.00") 255 256 return pb.Match{ 257 MatchId: fmt.Sprintf("profile-%s-time-%s-num-%d", profile, t, num), 258 MatchProfile: profile, 259 MatchFunction: matchName, 260 Tickets: tickets, 261 Backfill: b, 262 } 263 } 264 265 func setOpenSlots(b *pb.Backfill, val int32) error { 266 if b.Extensions == nil { 267 b.Extensions = make(map[string]*anypb.Any) 268 } 269 270 any, err := anypb.New(&wrapperspb.Int32Value{Value: val}) 271 if err != nil { 272 return err 273 } 274 275 b.Extensions[openSlotsKey] = any 276 return nil 277 } 278 279 func getOpenSlots(b *pb.Backfill) (int32, error) { 280 if b == nil { 281 return 0, fmt.Errorf("expected backfill is not nil") 282 } 283 284 if b.Extensions != nil { 285 if any, ok := b.Extensions[openSlotsKey]; ok { 286 var val wrapperspb.Int32Value 287 err := any.UnmarshalTo(&val) 288 if err != nil { 289 return 0, err 290 } 291 292 return val.Value, nil 293 } 294 } 295 296 return playersPerMatch, nil 297 }