Day28 Apex 模拟配对实作

昨天我们已经初步了解了,Apex 这款游戏的玩法与配对机制,今天我们将基於 Open-Match 配对框架,来实作看看 Apex 的配对过程。我们将透过两种模式、多个角色、多个区间与不同级分,来简单模拟一下,配对可能会需要注意的地方。

部署范例

配对目标 (MatchProfile

重点在於我们在划分 MatchProfile 与其 Pools 的过程,同时也是设定了我们想要的配对目标 ,藉由细分 MatchProfile 的内容,可以让我们获得更多不同类别的匹配池 Pools

  • 一般场

    • 完全不分阶级等级
    req := &pb.FetchMatchesRequest{
    			Config: &pb.FunctionConfig{
    				Host: "om-function.open-match-demo.svc.cluster.local",
    				Port: 50502,
    				Type: pb.FunctionConfig_GRPC,
    			},
    			Profile: &pb.MatchProfile{
    				Name: "3v3_normal_battle_royale",
    				Pools: []*pb.Pool{
    					{
    						Name: "3v3_normal_battle_royale",
    						StringEqualsFilters: []*pb.StringEqualsFilter{
    							{
    								StringArg: "mode",
    								Value:     "3v3_normal_battle_royale",
    							},
    						},
    					},
    				},
    			},
    		}
    
  • 排位场

    • 高级场
    • 中级场
    • 新手场
    req := &pb.FetchMatchesRequest{
    			Config: &pb.FunctionConfig{
    				Host: "om-function.open-match-demo.svc.cluster.local",
    				Port: 50502,
    				Type: pb.FunctionConfig_GRPC,
    			},
    			Profile: &pb.MatchProfile{
    				Name: "3v3_rank_battle_royale",
    				Pools: []*pb.Pool{
    					{
    						Name: "3v3_rank_low",
    						StringEqualsFilters: []*pb.StringEqualsFilter{
    							{
    								StringArg: "mode",
    								Value:     "3v3_rank_battle_royale",
    							},
    						},
    						DoubleRangeFilters: []*pb.DoubleRangeFilter{
    							{
    								DoubleArg: "score",
    								Min:       0,
    								Max:       3500,
    							},
    						},
    					},
    					{
    						Name: "3v3_rank_mid",
    						StringEqualsFilters: []*pb.StringEqualsFilter{
    							{
    								StringArg: "mode",
    								Value:     "3v3_rank_battle_royale",
    							},
    						},
    						DoubleRangeFilters: []*pb.DoubleRangeFilter{
    							{
    								DoubleArg: "score",
    								Min:       3400,
    								Max:       7300,
    							},
    						},
    					},
    					{
    						Name: "3v3_rank_high",
    						StringEqualsFilters: []*pb.StringEqualsFilter{
    							{
    								StringArg: "mode",
    								Value:     "3v3_rank_battle_royale",
    							},
    						},
    						DoubleRangeFilters: []*pb.DoubleRangeFilter{
    							{
    								DoubleArg: "score",
    								Min:       7200,
    								Max:       15000,
    							},
    						},
    					},
    				},
    			},
    		}
    

配对逻辑 (MMF

我们将在 MatchFunction 实作大多数的配对细节,包含 3人一组、不同 MatchProfile 将使用不同的 func、同阶级 pool 才能组队、同队伍不能有相同角色等等,这些逻辑全部都汇整於我们的 MMF 中。我们可以依照我们逻辑的重要性,切分成下列流程:

依 MatchProfile 进行主模式切分

func makeMatches(p *pb.MatchProfile, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
	var matches []*pb.Match
	
	//一般场
	nm, err := normalMatch(p, poolTickets)
	if err != nil {
		log.Println(err)
		return matches, err
	}

	matches = append(matches, nm...)

	//牌位场
	rm, err := rankMatch(p, poolTickets)
	if err != nil {
		log.Println(err)
		return matches, err
	}

	matches = append(matches, rm...)

	return matches, nil
}

func normalMatch(p *pb.MatchProfile, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
	var matches []*pb.Match
	if p.Name != "3v3_normal_battle_royale" {
		return nil, nil
	}

	//下略
}

func rankMatch(p *pb.MatchProfile, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
	var matches []*pb.Match

	if p.Name != "3v3_rank_battle_royale" {
		return matches, nil
	}
	//下略
}

再依 Pool 捞取近似性质玩家

func rankTeam(p *pb.MatchProfile, poolName string, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
	matches := []*pb.Match{}
	team := &pb.Match{}
	roleInTeam := []string{}
	count := 0

	if tickets, ok := poolTickets[poolName]; ok {
		for j := range tickets {
			if len(team.Tickets) < 3 {
				//check deduplicated role
				if stringInArr(tickets[j].SearchFields.StringArgs["role"], roleInTeam) {
					continue
				}

				team.Tickets = append(team.Tickets, tickets[j])
				roleInTeam = append(roleInTeam, tickets[j].SearchFields.StringArgs["role"])

				if len(team.Tickets) == 3 {
					// Compute the match quality/score
					matchQuality := computeQuality(team.Tickets)
					evaluationInput, err := ptypes.MarshalAny(&pb.DefaultEvaluationCriteria{
						Score: matchQuality,
					})
					if err != nil {
						return nil, err
					}

					team.MatchId = fmt.Sprintf("profile-%v-time-%v-%d", poolName, time.Now().Format("2006-01-02T15:04:05.00"), rankMatchIDCreator.Generate().Int64()+int64(count))
					team.MatchFunction = rankMatchName
					team.MatchProfile = p.GetName()
					team.Extensions = map[string]*any.Any{
						"evaluation_input": evaluationInput,
					}

					matches = append(matches, team)
					team = &pb.Match{}
					roleInTeam = []string{}
					count++
				}
			}
		}
	}

	return matches, nil
}

提供 Match Quality Value

再有 overlapping 的情况下,计算出配对品质,提供 evaluator 选择出最适合的配对

func computeQuality(tickets []*pb.Ticket) float64 {
	quality := 0.0
	high := 0.0
	low := tickets[0].SearchFields.DoubleArgs["score"]
	for _, ticket := range tickets {
		if high < ticket.SearchFields.DoubleArgs["score"] {
			high = ticket.SearchFields.DoubleArgs["score"]
		}
		if low > ticket.SearchFields.DoubleArgs["score"] {
			low = ticket.SearchFields.DoubleArgs["score"]
		}
	}
	quality = high - low

	return quality
}

Result

确认人数、角色、级距等结果,是否符合我们的预期

Normal

{
  "director": {
    "Status": "Sleeping",
    "LatestMatches": [
      {
        "match_id": "profile-3v3_normal_battle_royale-time-2021-10-06T06:24:20.67-1445636026520702976",
        "match_profile": "3v3_normal_battle_royale",
        "match_function": "3v3_normal_battle_royale_matchfunction",
        "tickets": [
          {
            "id": "c5ek1emjgom43kqfsacg",
            "search_fields": {
              "double_args": {
                "avg_dmg": 66,
                "avg_kd": 0.47,
                "level": 44,
                "rank": 2,
                "score": 1515,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_normal_battle_royale",
                "role": "caustic",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445635647426859008"
              }
            },
            "create_time": {
              "seconds": 1633501370,
              "nanos": 449469400
            }
          },
          {
            "id": "c5ek246jgom43kqfsam0",
            "search_fields": {
              "double_args": {
                "avg_dmg": 35,
                "avg_kd": 0.27,
                "level": 51,
                "rank": 1,
                "score": 894,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_normal_battle_royale",
                "role": "bang",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445636007352668160"
              }
            },
            "create_time": {
              "seconds": 1633501456,
              "nanos": 182299100
            }
          },
          {
            "id": "c5ek246jgom43kqfsamg",
            "search_fields": {
              "double_args": {
                "avg_dmg": 101,
                "avg_kd": 0,
                "level": 41,
                "rank": 1,
                "score": 892,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_normal_battle_royale",
                "role": "valk",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445636007067455488"
              }
            },
            "create_time": {
              "seconds": 1633501456,
              "nanos": 182849200
            }
          }
        ],
        "extensions": {
          "evaluation_input": {
            "type_url": "type.googleapis.com/openmatch.DefaultEvaluationCriteria",
            "value": "CQAAAAAAeINA"
          }
        }
      }
    ]
  },
  "uptime": 1169
}

Rank

{
  "director": {
    "Status": "Sleeping",
    "LatestMatches": [
      {
        "match_id": "profile-3v3_rank_low-time-2021-10-06T06:22:38.73-1445635598345179136",
        "match_profile": "3v3_rank_battle_royale",
        "match_function": "3v3_rank_battle_royale_matchfunction",
        "tickets": [
          {
            "id": "c5ek196jgom43kqfsaa0",
            "search_fields": {
              "double_args": {
                "avg_dmg": 66,
                "avg_kd": 0.06,
                "level": 132,
                "rank": 0,
                "score": 814,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_rank_battle_royale",
                "role": "crypto",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445635553034047488"
              }
            },
            "create_time": {
              "seconds": 1633501348,
              "nanos": 82612800
            }
          },
          {
            "id": "c5ek14ujgom43kqfsa7g",
            "search_fields": {
              "double_args": {
                "avg_dmg": 172,
                "avg_kd": 0.39,
                "level": 443,
                "rank": 2,
                "score": 1597,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_rank_battle_royale",
                "role": "caustic",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445635483068862464"
              }
            },
            "create_time": {
              "seconds": 1633501331,
              "nanos": 316996500
            }
          },
          {
            "id": "c5ek1aejgom43kqfsac0",
            "search_fields": {
              "double_args": {
                "avg_dmg": 57,
                "avg_kd": 0.24,
                "level": 352,
                "rank": 1,
                "score": 345,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_rank_battle_royale",
                "role": "gibby",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445635577381982208"
              }
            },
            "create_time": {
              "seconds": 1633501353,
              "nanos": 782109500
            }
          }
        ],
        "extensions": {
          "evaluation_input": {
            "type_url": "type.googleapis.com/openmatch.DefaultEvaluationCriteria",
            "value": "CQAAAAAAkJNA"
          }
        }
      }
    ]
  },
  "uptime": 1068
}

完整范例


<<:  近似最短路径 (2)

>>:  day21 开分支,浅谈kotlin paging3 with flow

why
杂谈    

[Day 02] - Mongo DB环境建置

第二天,首先我打算先把Mongo DB环境建起来 为了方便,就用docker在local部属 Mon...

GitHub Action 实作持续交付 - 部署至 Azure App Service

可以 ASP.NET Core 网站部署的环境相当多,包含 IIS, Nginx, App serv...

Day 0x8 - WebHook Api 建立( part 1 )

0x1 API 需求 在发出建立订单 - 取得虚拟帐号的请求後,若付款完成会呼叫 BackendUR...

Day5:深入认识 Coroutine

这几天我们把 Coroutine 神秘的面纱好像掀开了一点,知道他是用来解决非同步程序的问题,也我们...

Day4|【Git】用户名称与信箱- Git的初始设定与 config

💡 开始使用 Git 之前,我们需要先设定使用者名称及电子邮件地址。 为什麽需要设定用户名称及 E-...