Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skill Based Matchfunction #11

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added skill-based/.DS_Store
Binary file not shown.
24 changes: 24 additions & 0 deletions skill-based/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
37 changes: 37 additions & 0 deletions skill-based/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
142 changes: 142 additions & 0 deletions skill-based/mmf/matchfunction.go
Original file line number Diff line number Diff line change
@@ -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
}
42 changes: 42 additions & 0 deletions skill-based/mmf/matchfunction_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
62 changes: 62 additions & 0 deletions skill-based/mmf/server.go
Original file line number Diff line number Diff line change
@@ -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())
}
}