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  }