Skip to main content

Match Function

Open Match Match Function Edgegap's tutorial (basic)#

The Match Function is the core of the decision. It's this service that creates game proposals. It makes its decisions from profile's configuration. In this service, you can code any logic that will generate your match proposals

Prerequisites#

Create a new Go project#

Every Open Match service should be in its own project. Create a new folder for the OpenMatch match-function component.

In this folder, run the following command.

go mod init matchfunction

And create a file named main.go where the core logic of the component will be.

Create a gRPC server#

The first thing that we need for our Match Function is a gRPC server that will listen for a call from Open Match Core.

We will need two libraries for this step. You can install them with

go get google.golang.org/grpc
go get open-match.dev/open-match/pkg/pb

Now, in your main.go file, you can add the following code. This will create the gPRC server.

package main
import (
"fmt"
"log"
"net"
"google.golang.org/grpc"
"open-match.dev/open-match/pkg/pb"
)
const (
matchFunctionServePort = 50502
matchName = "basics-match-function"
// Link inside kubernetes
openMatchQueryService = "open-match-query:50503"
)
func main() {
log.Println("Starting Edgegap's Match Function!")
server := grpc.NewServer()
pb.RegisterMatchFunctionServer(server, nil)
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", matchFunctionServePort))
if err != nil {
log.Fatalf("TCP net listener initialization failed for port %v, got %s", 50502, err.Error())
}
log.Printf("TCP net listener initialized for port %v", matchFunctionServePort)
err = server.Serve(ln)
if err != nil {
log.Fatalf("gRPC serve failed, got %s", err.Error())
}
}

You can try it by starting your app with go run ./main.go. You should see

2021/01/26 12:27:50 Starting Edgegap's Match Function!
2021/01/26 12:27:50 TCP net listener initialized for port 50502

Create the Match Function#

The strength of Open Match comes from the customization of Match Function and Director services. We will now create the Match Function. This one aims to fetch Open Match tickets based on profiles. Match Function can be very complicated, but we will keep it simple and implement a basic Match Function in this tutorial.

The Match Function will fetch all the tickets that match the profile received in the request. The tickets will be categorized by pool's name (Profiles contain pools). Then, we will go through each pool and create game proposals from the tickets in the pool. When the proposals are all created, we will send them back to Open Match Core via gRPC

Below is the code for the Match Function. It should be placed in the main.go file under the main function.

type basicMatchFunction struct {
queryServiceClient pb.QueryServiceClient
}
func (bmf *basicMatchFunction) 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(), bmf.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) {
ticketsPerPoolPerMatch := 2
var matches []*pb.Match
count := 0
for {
insufficientTickets := false
matchTickets := []*pb.Ticket{}
for pool, tickets := range poolTickets {
if len(tickets) < ticketsPerPoolPerMatch {
// This pool is completely drained out. Stop creating matches.
insufficientTickets = true
break
}
// Remove the Tickets from this pool and add them to the match proposal.
matchTickets = append(matchTickets, tickets[0:ticketsPerPoolPerMatch]...)
poolTickets[pool] = tickets[ticketsPerPoolPerMatch:]
}
if insufficientTickets {
break
}
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,
})
count++
}
return matches, nil
}

You will have to change your dependencies. They should look like this

import (
"fmt"
"log"
"net"
"time"
"google.golang.org/grpc"
"open-match.dev/open-match/pkg/matchfunction"
"open-match.dev/open-match/pkg/pb"
)

Also, you will have to update the main function to create the Open Match Query service connection and make the basicMatchFunction

func main() {
log.Println("Starting Edgegap's Match Function!")
// Create Open Match Query service connection
conn, err := grpc.Dial(
openMatchQueryService, grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("Error while creating gRPC connection, err: %v", err.Error())
}
queryServiceClient := pb.NewQueryServiceClient(conn)
// Creating basicMatchFunction
basicMatchFunction := basicMatchFunction{
queryServiceClient: queryServiceClient,
}
server := grpc.NewServer()
pb.RegisterMatchFunctionServer(server, &basicMatchFunction) // IMPORTANT Register MatchFunction
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", matchFunctionServePort))
if err != nil {
log.Fatalf("TCP net listener initialization failed for port %v, got %s", 50502, err.Error())
}
log.Printf("TCP net listener initialized for port %v", matchFunctionServePort)
err = server.Serve(ln)
if err != nil {
log.Fatalf("gRPC serve failed, got %s", err.Error())
}
}

It will be hard to test this service in a standalone context. We will see it in action when all the services (Front end, Director, and Match Function) will be ready.

Containerize your service#

Containerization is powerful. You can learn more about it on Edgegap's documentation or Docker. Our container, we will use golang:1.15 for its base image.

Create a file named Dockerfile and write this inside.

FROM golang:1.15
COPY . /go/src/app
WORKDIR /go/src/app
RUN go get -d -v /go/src/app
RUN go install -v /go/src/app
WORKDIR /go/bin
CMD ["matchfunction"]

On your local repository#

You can now build your image using.

docker build -t edgegap-om-tuto-match-function:tuto .

You can run it using this command, but without all the other services, it will not work.

docker run -it --name om-tuto-match-function edgegap-om-tuto-match-function:tuto .

On an external repository#

You can now build your image using. (If you use Edgegap's harbor, <REPOSITORY_MAME> is harbor.edgegap.net/<YOUR_PROJECT>)

docker build -t <REPOSITORY_MAME>/edgegap-om-tuto-match-function:tuto .

You can run it using

docker run -it --name om-tuto-match-function <REPOSITORY_MAME>/edgegap-om-tuto-match-function:tuto

Now push your image on your Docker repository. (You may have to use Docker login)

docker push <REPOSITORY_MAME>/edgegap-om-tuto-match-function:tuto

Conclusion#

You now have a basic Open Match Match Function service that can be called by Open Match Core using gRPC protocol. This is not production-ready, however you should be able to grasp the basics of Open Match Front End. You also created a Docker image and pushed it to your repository.