diff --git a/skill-based/.DS_Store b/skill-based/.DS_Store new file mode 100644 index 0000000..fea4bd7 Binary files /dev/null and b/skill-based/.DS_Store differ diff --git a/skill-based/Dockerfile b/skill-based/Dockerfile new file mode 100644 index 0000000..435fd6b --- /dev/null +++ b/skill-based/Dockerfile @@ -0,0 +1,24 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM golang:1.15.2 as builder + +WORKDIR /go/src/open-match.dev/open-match-ecosystem/skill-based +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o matchfunction . + +FROM gcr.io/distroless/static:nonroot +WORKDIR /app/ +COPY --from=builder --chown=nonroot /go/src/open-match.dev/open-match-ecosystem/skill-based/matchfunction /app/ + +ENTRYPOINT ["/app/matchfunction"] diff --git a/skill-based/main.go b/skill-based/main.go new file mode 100644 index 0000000..c11e3c3 --- /dev/null +++ b/skill-based/main.go @@ -0,0 +1,37 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main defines a sample match function that uses the GRPC harness to set up +// the match making function as a service. This sample is a reference +// to demonstrate the usage of the GRPC harness and should only be used as +// a starting point for your match function. You will need to modify the +// matchmaking logic in this function based on your game's requirements. +package main + +import ( + "open-match.dev/open-match/examples/functions/golang/skill-based/mmf" +) + +// This tutorial implenents a basic Match Function that is hosted in the below +// configured port. You can also configure the Open Match QueryService endpoint +// with which the Match Function communicates to query the Tickets. + +const ( + queryServiceAddress = "om-query.open-match.svc.cluster.local:50503" // Address of the QueryService endpoint. + serverPort = 50502 // The port for hosting the Match Function. +) + +func main() { + mmf.Start(queryServiceAddress, serverPort) +} diff --git a/skill-based/mmf/matchfunction.go b/skill-based/mmf/matchfunction.go new file mode 100644 index 0000000..040a93e --- /dev/null +++ b/skill-based/mmf/matchfunction.go @@ -0,0 +1,142 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mmf + +import ( + "fmt" + "log" + "math" + "sort" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/any" + + "open-match.dev/open-match/pkg/matchfunction" + "open-match.dev/open-match/pkg/pb" +) + +// This match function fetches all the Tickets for all the pools specified in +// the profile. It uses a configured number of tickets from each pool to generate +// a Match Proposal. It continues to generate proposals till one of the pools +// runs out of Tickets. +const ( + matchName = "skill-matchfunction" + ticketsPerPoolPerMatch = 4 +) + +// Run is this match function's implementation of the gRPC call defined in api/matchfunction.proto. +func (s *MatchFunctionService) Run(req *pb.RunRequest, stream pb.MatchFunction_RunServer) error { + // Fetch tickets for the pools specified in the Match Profile. + log.Printf("Generating proposals for function %v", req.GetProfile().GetName()) + + poolTickets, err := matchfunction.QueryPools(stream.Context(), s.queryServiceClient, req.GetProfile().GetPools()) + if err != nil { + log.Printf("Failed to query tickets for the given pools, got %s", err.Error()) + return err + } + + // Generate proposals. + proposals, err := makeMatches(req.GetProfile(), poolTickets) + if err != nil { + log.Printf("Failed to generate matches, got %s", err.Error()) + return err + } + + log.Printf("Streaming %v proposals to Open Match", len(proposals)) + // Stream the generated proposals back to Open Match. + for _, proposal := range proposals { + if err := stream.Send(&pb.RunResponse{Proposal: proposal}); err != nil { + log.Printf("Failed to stream proposals to Open Match, got %s", err.Error()) + return err + } + } + + return nil +} + +func makeMatches(p *pb.MatchProfile, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) { + // Create a colletion to hold match proposals + var matches []*pb.Match + count := 0 + for { + insufficientTickets := false + // Create a collection to hold tickets selected for a match + matchTickets := []*pb.Ticket{} + for pool, tickets := range poolTickets { + // Set flag if there are not enough tickets to create a match + if len(tickets) < ticketsPerPoolPerMatch { + insufficientTickets = true + break + } + // Sort the tickets based on skill + sort.Slice(tickets, func(i, j int) bool { + return tickets[i].SearchFields.DoubleArgs["mmr"] < tickets[j].SearchFields.DoubleArgs["mmr"] + }) + + // Remove the Tickets from this pool and add to the match proposal. + matchTickets = append(matchTickets, tickets[0:ticketsPerPoolPerMatch]...) + poolTickets[pool] = tickets[ticketsPerPoolPerMatch:] + } + + if insufficientTickets { + break + } + // Compute the match quality/score + matchQuality := computeQuality(matchTickets) + evaluationInput, err := ptypes.MarshalAny(&pb.DefaultEvaluationCriteria{ + Score: matchQuality, + }) + + if err != nil { + log.Printf("Failed to marshal DefaultEvaluationCriteria, got %s.", err.Error()) + return nil, fmt.Errorf("Failed to marshal DefaultEvaluationCriteria, got %w", err) + } + // Output the match quality for our sanity + log.Printf("Quality for the generated match is %v", matchQuality) + // Create a match proposal + matches = append(matches, &pb.Match{ + MatchId: fmt.Sprintf("profile-%v-time-%v-%v", p.GetName(), time.Now().Format("2006-01-02T15:04:05.00"), count), + MatchProfile: p.GetName(), + MatchFunction: matchName, + Tickets: matchTickets, + Extensions: map[string]*any.Any{ + "evaluation_input": evaluationInput, + }, + }) + + count++ + } + + return matches, nil +} + +// Compute the quality as a difference in the highest and lowest player skill levels. This can be used to determine if the match is outside a given skill differential +func computeQuality(tickets []*pb.Ticket) float64 { + quality := 0.0 + high := math.Inf(-1) + low := math.Inf(1) + for _, ticket := range tickets { + if high < ticket.SearchFields.DoubleArgs["mmr"] { + high = ticket.SearchFields.DoubleArgs["mmr"] + } + if low > ticket.SearchFields.DoubleArgs["mmr"] { + low = ticket.SearchFields.DoubleArgs["mmr"] + } + } + quality = high - low + + return quality +} diff --git a/skill-based/mmf/matchfunction_test.go b/skill-based/mmf/matchfunction_test.go new file mode 100644 index 0000000..5e825d1 --- /dev/null +++ b/skill-based/mmf/matchfunction_test.go @@ -0,0 +1,42 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mmf + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + "open-match.dev/open-match/pkg/pb" +) + +func TestCheckQuality(t *testing.T) { + t.Run("no tickets", func(t *testing.T) { + q := computeQuality(nil) + require.Equal(t, math.Inf(-1), q) + }) + t.Run("one ticket", func(t *testing.T) { + q := computeQuality([]*pb.Ticket{ + { + SearchFields: &pb.SearchFields{ + DoubleArgs: map[string]float64{ + "mmr": 3, + }, + }, + }, + }) + require.Equal(t, 0.0, q) + }) +} diff --git a/skill-based/mmf/server.go b/skill-based/mmf/server.go new file mode 100644 index 0000000..9ea14b6 --- /dev/null +++ b/skill-based/mmf/server.go @@ -0,0 +1,62 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mmf + +import ( + "fmt" + "log" + "net" + + "google.golang.org/grpc" + "open-match.dev/open-match/pkg/pb" +) + +// MatchFunctionService implements pb.MatchFunctionServer, the server generated +// by compiling the protobuf, by fulfilling the pb.MatchFunctionServer interface. +type MatchFunctionService struct { + grpc *grpc.Server + queryServiceClient pb.QueryServiceClient + port int +} + +// Start creates and starts the Match Function server and also connects to Open +// Match's queryService. This connection is used at runtime to fetch tickets +// for pools specified in MatchProfile. +func Start(queryServiceAddr string, serverPort int) { + // Connect to QueryService. + conn, err := grpc.Dial(queryServiceAddr, grpc.WithInsecure()) + if err != nil { + log.Fatalf("Failed to connect to Open Match, got %s", err.Error()) + } + defer conn.Close() + + mmfService := MatchFunctionService{ + queryServiceClient: pb.NewQueryServiceClient(conn), + } + + // Create and host a new gRPC service on the configured port. + server := grpc.NewServer() + pb.RegisterMatchFunctionServer(server, &mmfService) + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", serverPort)) + if err != nil { + log.Fatalf("TCP net listener initialization failed for port %v, got %s", serverPort, err.Error()) + } + + log.Printf("TCP net listener initialized for port %v", serverPort) + err = server.Serve(ln) + if err != nil { + log.Fatalf("gRPC serve failed, got %s", err.Error()) + } +}