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#

  • Install Golang 1.16^ on your computer

Create Your project#

Every Open Match service should be in its own project. Create a new folder named match-function 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.

Implement Match Function base structure#

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.

main.go (Override All)
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 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

We will use gRPC to query the core and send back the proposals.

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

main.go (Add to file)
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

main.go (Replace)
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

main.go (Replace)
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.16 for its base image.

Create a file named Dockerfile and write this inside.

FROM golang:1.16
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"]

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 .

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 Match Function. You also created a Docker image that can be used later.